Browse Source

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

shizhongqi 2 months ago
parent
commit
dce9f2cfee

+ 31 - 2
app-ghs/controllers/ConsoleController.php

@@ -242,6 +242,28 @@ class ConsoleController extends BaseController
             ["name" => "收款流水", "img" => "ghs/home/kcyjs.png", "url" => "/admin/order/scanPay"],
             ["name" => "收款码", "img" => "ghs/home/shop.png", "url" => "/admin/cg/code"]
         ];
+        $menuV3 = [
+            ["name" => "商城码", "img" => "ghs/home/shop3.png", "url" => "/admin/home/mall"],
+            ["name" => "改价", "img" => "ghs/home/gj.png", "url" => "/admin/changePrice/list2"],
+            ["name" => "花材", "img" => "ghs/home/hcgl.png", "url" => "/admin/item/list2"],
+            ["name" => "配送片区", "img" => "ghs/home/kcyjs.png", "url" => "/admin/order/orderListByDist"],
+            ["name" => "改库存", "img" => "ghs/home/kcyjs.png", "url" => "/admin/changePrice/changeStock2"],
+            ["name" => "采购", "img" => "ghs/home/icon_ghs.png", "url" => "/pagesPurchase/supplier"],
+            ["name" => "采购记录", "img" => "ghs/home/icon_caigou.png", "url" => "/pagesPurchase/order"],
+            ["name" => "调拨入库", "img" => "ghs/home/dbrk.png", "url" => "/pagesStorehouse/allot/allotPut"],
+            ["name" => "计算器", "img" => "ghs/home/kcyjs.png", "url" => "/admin/tools/calculator"],
+            ["name" => "买花", "img" => "ghs/home/icon_buy_item.png", "url" => "buyGhs"],
+            ["name" => "买花记录", "img" => "ghs/home/icon_buy_record.png", "url" => "buyRecord", 'remindNum' => 0],
+            ["name" => "调拨出库", "img" => "ghs/home/dbck.png", "url" => "/pagesStorehouse/allot/allotEx"],
+            ["name" => "支出", "img" => "ghs/home/kcyjs.png", "url" => "/admin/expend/list"],
+            ["name" => "员工", "img" => "ghs/home/yggl.png", "url" => "/admin/staff/list"],
+            ["name" => "库存预警", "img" => "ghs/home/kcyjs.png", "url" => "/admin/item/warning"],
+            ["name" => "盘点", "img" => "ghs/home/pandian.png", "url" => "/pagesStorehouse/inventory/list"],
+            ["name" => "报损", "img" => "ghs/home/kcyjs.png", "url" => "/admin/breakage/list"],
+            ["name" => "拆散", "img" => "ghs/home/kcyjs.png", "url" => "/admin/part/list"],
+            ["name" => "收款流水", "img" => "ghs/home/kcyjs.png", "url" => "/admin/order/scanPay"],
+            ["name" => "收款码", "img" => "ghs/home/shop.png", "url" => "/admin/cg/code"]
+        ];
         $menuIconMap = [
             '商城码' => 'mall_code',
             '改价' => 'modify_price',
@@ -263,7 +285,6 @@ class ConsoleController extends BaseController
             '损耗' => 'breakage',
             '拆散' => 'break_up',
             '收款流水' => 'gather_record',
-            '收款流水' => 'gather_record',
             '收款码' => 'gather_code',
             '商城' => 'mall_code',
             '花材(简)' => 'item',
@@ -278,6 +299,14 @@ class ConsoleController extends BaseController
                 $menuItem['icon'] = $menuIconMap[$item['name']] ?? 'item';
                 $menu[] = $menuItem;
             }
+        }elseif($version == 4) {
+            $menu = [];
+            foreach ($menuV3 as $item) {
+                $menuItem = $item;
+                unset($menuItem['img']);
+                $menuItem['icon'] = $menuIconMap[$item['name']] ?? 'item';
+                $menu[] = $menuItem;
+            }
         }
 
         //查看财务的权限
@@ -313,4 +342,4 @@ class ConsoleController extends BaseController
         util::success(['dict' => $dict, 'shop' => $shop]);
     }
 
-}
+}

+ 18 - 7
app-ghs/controllers/ItemController.php

@@ -493,6 +493,18 @@ class ItemController extends BaseController
         } elseif ($requestType == 'part') {
             $where['delStatus'] = 0;
             $where['status'] = 1;
+        } elseif ($requestType == 'warning') {
+            $where['delStatus'] = 0;
+            unset($where['status']);
+            if ($lookStock == 1) {
+                $where['stock>'] = 0;
+            }
+            if ($lookStock == 2) {
+                $where['stock'] = 0;
+            }
+            if ($onStock == 1) {
+                $where['onStock>'] = 0;
+            }
         } else {
             util::fail('没有定义的请求方式');
         }
@@ -558,7 +570,11 @@ class ItemController extends BaseController
         }
 
         $field = '*';
-        $result = ProductClass::getList($field, $where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC]);
+        if ($requestType == 'warning') {
+            $result = ProductClass::warningList($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC]);
+        } else {
+            $result = ProductClass::getList($field, $where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC]);
+        }
         $list = $result['list'] ?? [];
         if ($requestType == 'hasStockList') {
             util::fail('无效方式');
@@ -589,12 +605,7 @@ class ItemController extends BaseController
         } elseif ($requestType == 'cg') {
             $list = ProductClass::cgItemGroup($list, $level);
         } elseif ($requestType == 'warning') {
-            util::fail('无效方式');
-//            $itemInfoData = Product::find()->where("mainId={$this->mainId}")
-//                ->andWhere('`stock`+`onStock`<`stockWarning`')
-//                ->andWhere("delStatus=0")
-//                ->select($field)->asArray()->all();
-//            $itemInfoData = ProductClass::warningGroup($itemInfoData, $level);
+            $list = ProductClass::itemListGroup($list, $level);
         } else {
             util::fail('没有数据');
         }

+ 2 - 2
app-ghs/controllers/MainController.php

@@ -251,7 +251,7 @@ class MainController extends BaseController
             $showAd = 0;
         }
         //是否展示终身版套餐
-        $lifelong = 0;
+        $lifelong = 1;
 
         $deadline = $sj['deadline'] ?? '';
         $relation = isset($this->shopAdmin) && !empty($this->shopAdmin) ? $this->shopAdmin->attributes : [];
@@ -286,4 +286,4 @@ class MainController extends BaseController
         util::success($data);
     }
 
-}
+}

+ 3 - 2
app-ghs/controllers/NoticeController.php

@@ -63,7 +63,7 @@ class NoticeController extends PublicController
             if ($capitalType == dict::getDict('capitalType', 'xhRenew', 'id')) {
                 noticeUtil::push("有客户付款成功:" . $orderSn . ' ' . $totalFee, '15280215347');
             }
-
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
@@ -157,6 +157,7 @@ class NoticeController extends PublicController
             }
             //请不要修改
             echo "success";
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
@@ -257,7 +258,7 @@ class NoticeController extends PublicController
             }
             $transaction->commit();
             echo "success";
-            util::end();
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());

+ 6 - 6
app-ghs/controllers/ProductController.php

@@ -399,11 +399,11 @@ class ProductController extends BaseController
             'hideItem' => 'id,py,cover,name,classId,itemId,price,skPrice,hjPrice,stock,onStock,cost,avCost,addPrice,hjAddPrice,skMore,limitBuy',
             'changePrice' => 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,hjPrice,discountPrice,hjDiscountPrice,skDiscountPrice,skMore,hjAddPrice,variety,price,stock,onStock,presell,frontHide,reachNum,reachNumDiscount,limitBuy',
             'changeStock' => 'id,py,cover,name,cost,itemId,status,classId,addPrice,skPrice,discountPrice,skMore,variety,price,stock,frontHide,limitBuy',
-            'kd' => 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,hjDiscountPrice,skDiscountPrice,discountPrice,stock,weight,stockWarning,presell,ratioType,smallUnit,smallRatio,bigUnit,ratio,frontHide,reachNum,reachNumDiscount,limitBuy',
-            'book' => 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,hjDiscountPrice,skDiscountPrice,discountPrice,stock,weight,stockWarning,presell,ratioType,smallUnit,smallRatio,bigUnit,ratio,frontHide,limitBuy',
-            'itemList' => 'id,py,cover,name,cost,avCost,itemId,classId,skPrice,variety,price,hjPrice,discountPrice,hjDiscountPrice,skDiscountPrice,stock,onStock,actualSold,status,ratio,ratioType,presell,frontHide,auth,reachNum,reachNumDiscount,addPrice,hjAddPrice,skMore,limitBuy',
+            'kd' => 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,hjDiscountPrice,skDiscountPrice,discountPrice,stock,weight,stockWarning,presell,ratioType,smallUnit,smallRatio,bigUnit,ratio,frontHide,reachNum,reachNumDiscount,limitBuy,itemRemark',
+            'book' => 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,hjDiscountPrice,skDiscountPrice,discountPrice,stock,weight,stockWarning,presell,ratioType,smallUnit,smallRatio,bigUnit,ratio,frontHide,limitBuy,itemRemark',
+            'itemList' => 'id,py,cover,name,cost,avCost,itemId,classId,skPrice,variety,price,hjPrice,discountPrice,hjDiscountPrice,skDiscountPrice,stock,onStock,actualSold,status,ratio,ratioType,presell,frontHide,auth,reachNum,reachNumDiscount,addPrice,hjAddPrice,skMore,limitBuy,itemRemark',
             'cgStaffList' => 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,discountPrice,hjDiscountPrice,skDiscountPrice,stock,onStock,actualSold,status,ratio,ratioType,presell,frontHide,auth,reachNum,reachNumDiscount,cgStaffId,cgStaffName,limitBuy',
-            'outList' => 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,bigUnit,smallUnit,ratio,ratioType,discountPrice,skMore,variety,price,stock,presell,status,limitBuy',
+            'outList' => 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,bigUnit,smallUnit,ratio,ratioType,discountPrice,skMore,variety,price,stock,presell,status,limitBuy,itemRemark',
             'stopList' => 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,bigUnit,smallUnit,ratio,ratioType,discountPrice,skMore,variety,price,stock,presell,status,delStatus,limitBuy',
             'removeList' => 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,bigUnit,smallUnit,ratio,ratioType,discountPrice,skMore,variety,price,stock,presell,status,limitBuy',
             'check' => 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,stockWarning,presell,ratioType,smallUnit,bigUnit,ratio,frontHide,limitBuy',
@@ -411,7 +411,7 @@ class ProductController extends BaseController
             'part' => 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,stockWarning,presell,ratioType,smallUnit,bigUnit,ratio,limitBuy',
             'stockOut' => 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,stockWarning,presell,ratioType,smallUnit,bigUnit,ratio,smallRatio,frontHide,limitBuy',
             'cg' => 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,presell,ratioType,smallUnit,bigUnit,ratio,weight,frontHide,limitBuy',
-            'warning' => 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,discountPrice,stock,onStock,actualSold,presell,bigUnit,stockWarning,status,frontHide,limitBuy',
+            'warning' => 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,discountPrice,stock,onStock,actualSold,presell,bigUnit,stockWarning,status,frontHide,limitBuy',
         ];
 
         if (!isset($fieldMap[$requestType])) {
@@ -1818,4 +1818,4 @@ class ProductController extends BaseController
         util::complete('修改成功');
     }
 
-}
+}

+ 1 - 1
app-ghs/web/index.php

@@ -20,7 +20,7 @@ function dd($out)
 {
 	echo '<pre>';
 	var_dump($out);
-	Yii::$app->end();
+	exit();
 }
 
 $application = new yii\web\Application($config);

+ 7 - 3
app-hd/controllers/NoticeController.php

@@ -256,7 +256,7 @@ class NoticeController extends PublicController
                     }
                 }
             }
-
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
@@ -506,7 +506,7 @@ class NoticeController extends PublicController
                     }
                 }
             }
-
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
@@ -634,6 +634,7 @@ class NoticeController extends PublicController
                     }
                 }
             }
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
@@ -706,6 +707,7 @@ class NoticeController extends PublicController
                 noticeUtil::push($text, '15280215347');
             }
             echo '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
@@ -885,6 +887,7 @@ class NoticeController extends PublicController
                 }
 
             }
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
@@ -980,10 +983,11 @@ class NoticeController extends PublicController
             }
             //请不要修改
             echo "success";
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
         }
     }
 
-}
+}

+ 1 - 1
app-hd/web/index.php

@@ -20,7 +20,7 @@ function dd($out)
 {
 	echo '<pre>';
 	print_r($out);
-	Yii::$app->end();
+	exit();
 }
 
 $application = new yii\web\Application($config);

+ 1 - 1
app-mall/config/main.php

@@ -33,7 +33,7 @@ return [
             ],
         ],
         'errorHandler' => [
-            'errorAction' => 'site/error',
+            'errorAction' => 'main/error',
         ],
         'urlManager' => [
             'enablePrettyUrl' => true,

+ 27 - 4
app-mall/controllers/MainController.php

@@ -2,12 +2,35 @@
 
 namespace mall\controllers;
 
-use bizMall\ad\services\AdService;
-use bizMall\goods\services\CategoryService;
-use bizMall\goods\services\GoodsCategoryService;
 use common\components\util;
+use Yii;
+use yii\web\HttpException;
 
 class MainController extends BaseController
 {
+    public $guestAccess = ['index', 'error'];
 
-}
+    /**
+     * 默认首页:供健康检查、域名根路径访问,避免 defaultRoute 404
+     */
+    public function actionIndex()
+    {
+        util::success([
+            'service' => 'app-mall',
+            'status' => 'ok',
+        ]);
+    }
+
+    /**
+     * 统一 JSON 错误输出(替代不存在的 site/error)
+     */
+    public function actionError()
+    {
+        $exception = Yii::$app->errorHandler->exception;
+        if ($exception instanceof HttpException) {
+            util::error($exception->statusCode, $exception->getMessage());
+        }
+        $message = $exception ? $exception->getMessage() : '服务器错误';
+        util::fail($message);
+    }
+}

+ 35 - 0
app-mall/controllers/MobileController.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace mall\controllers;
+
+use biz\shop\classes\ShopClass;
+use common\components\util;
+use Yii;
+
+class MobileController extends BaseController
+{
+    public $guestAccess = ['index'];
+
+    /**
+     * 门店入口:/account/{shopId} 美化 URL,与 query 参数 account 行为一致
+     */
+    public function actionIndex($account = 0)
+    {
+        $account = (int)$account;
+        if ($account <= 0) {
+            $account = (int)Yii::$app->request->get('account', 0);
+        }
+        if ($account <= 0) {
+            util::fail('缺少门店编号');
+        }
+        $shop = ShopClass::getById($account, true);
+        if (empty($shop)) {
+            util::fail('门店不存在');
+        }
+        util::success([
+            'account' => $account,
+            'shopId' => $account,
+            'shopName' => $shop->name ?? '',
+        ]);
+    }
+}

+ 6 - 2
app-mall/controllers/NoticeController.php

@@ -86,6 +86,7 @@ class NoticeController extends PublicController
             if (!empty($text)) {
                 noticeUtil::push($text, '15280215347');
             }
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
@@ -226,7 +227,7 @@ class NoticeController extends PublicController
                     ShopExtClass::skRechargeReport($recharge);
                 }
             }
-
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
@@ -295,6 +296,7 @@ class NoticeController extends PublicController
                 $shopExt = ShopExtClass::getByCondition(['shopId' => $shopId], true);
                 ShopExtClass::newWorkRemind($shopExt, $order);
             }
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
@@ -412,6 +414,7 @@ class NoticeController extends PublicController
             }
             //请不要修改
             echo "success";
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
@@ -544,10 +547,11 @@ class NoticeController extends PublicController
             }
             //请不要修改
             echo "SUCCESS";
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
         }
     }
 
-}
+}

+ 2 - 1
app-mall/controllers/TestController.php

@@ -105,10 +105,11 @@ class TestController extends BaseController
             }
             //请不要修改
             echo "SUCCESS";
+            exit();
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::error("失败原因:" . $e->getMessage());
         }
     }
 
-}
+}

BIN
app-mall/web/favicon.ico


BIN
app-mall/web/favicon.png


+ 1 - 1
app-mall/web/index.php

@@ -5,7 +5,7 @@ function dd($out)
 {
 	echo '<pre>';
 	print_r($out);
-	Yii::$app->end();
+	exit();
 }
 
 require(__DIR__ . '/../../vendor/autoload.php');

+ 2 - 0
app-mall/web/robots.txt

@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:

+ 2 - 2
app-pt/views/main/app.php

@@ -235,7 +235,7 @@
             }
             // 这里可以根据appType跳转到对应的下载链接
             const downloadUrls = {
-                'xiaohuabao': 'https://api.shop.hzghd.com/1291.apk', // 销花宝APP下载链接
+                'xiaohuabao': 'https://api.shop.hzghd.com/1307.apk', // 销花宝APP下载链接
                 'huazhanggui': 'https://api.shop.hzghd.com/238.apk', // 花掌柜APP下载链接
                 'huazhanggui_cashier': 'https://api.shop.hzghd.com/200.apk', // 花掌柜收银下载链接
                 'xiaohuabao_cashier': 'https://api.shop.hzghd.com/159.apk' // 销花宝收银下载链接
@@ -267,4 +267,4 @@
         });
     </script>
 </body>
-</html>
+</html>

+ 1 - 1
app-pt/web/index.php

@@ -5,7 +5,7 @@ function dd($out)
 {
 	echo '<pre>';
 	print_r($out);
-	Yii::$app->end();
+	exit();
 }
 
 require(__DIR__ . '/../../vendor/autoload.php');

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

@@ -392,6 +392,55 @@ class OrderCgClearClass extends BaseClass
         return $relationCount >= $expected;
     }
 
+    /**
+     * 检测结账单关系表问题,无问题返回 null
+     * @param bool $checkEmptyAmount 是否把 amount 为空/0 视为问题(抽样检查传 false)
+     * @return array|null ['reason'=>string, 'relationCount'=>int, 'expected'=>int]
+     */
+    public static function detectBackfillIssue($clear, $relationCount, array $emptyAmountClearIds, array $legacyGys2KmGysClearIds, $checkEmptyAmount = true)
+    {
+        $clearId = intval(is_object($clear) ? ($clear->id ?? 0) : ($clear['id'] ?? 0));
+        if ($clearId <= 0) {
+            return null;
+        }
+        $saleIds = trim(is_object($clear) ? ($clear->saleIds ?? '') : ($clear['saleIds'] ?? ''));
+        $purchaseIds = trim(is_object($clear) ? ($clear->purchaseIds ?? '') : ($clear['purchaseIds'] ?? ''));
+        if ($saleIds === '' && $purchaseIds === '') {
+            return null;
+        }
+        $expected = self::expectedRelationCountFromClear($clear);
+        $relationCount = intval($relationCount);
+        if (in_array($clearId, $legacyGys2KmGysClearIds, true)) {
+            return [
+                'reason' => 'gys2KmGys旧格式(orderId>0,cgId=0)',
+                'relationCount' => $relationCount,
+                'expected' => $expected,
+            ];
+        }
+        if ($checkEmptyAmount && in_array($clearId, $emptyAmountClearIds, true)) {
+            return [
+                'reason' => 'amount为空或0',
+                'relationCount' => $relationCount,
+                'expected' => $expected,
+            ];
+        }
+        if ($relationCount <= 0) {
+            return [
+                'reason' => '无关系行',
+                'relationCount' => 0,
+                'expected' => $expected,
+            ];
+        }
+        if ($expected > 0 && $relationCount < $expected) {
+            return [
+                'reason' => '关系行不足',
+                'relationCount' => $relationCount,
+                'expected' => $expected,
+            ];
+        }
+        return null;
+    }
+
     /**
      * 批量查询关系表已存在的 clearId(避免逐条 SELECT)
      * @param int[] $clearIds
@@ -686,4 +735,227 @@ class OrderCgClearClass extends BaseClass
         return array_values(array_unique(array_filter(array_map('intval', $arr))));
     }
 
+    /**
+     * xhOrderCgClear.amount 是否视为 0(含 NULL、0.00)
+     */
+    protected static function isZeroAmountSql($alias = '')
+    {
+        $prefix = $alias !== '' ? $alias . '.' : '';
+        return '(' . $prefix . 'amount IS NULL OR ' . $prefix . 'amount = 0 OR ' . $prefix . 'amount = \'0\' OR ' . $prefix . 'amount = \'0.00\')';
+    }
+
+    /**
+     * 步骤1:只从 xhOrderCgClear 查 amount=0 的行
+     * @return array[] id, clearId, orderId, cgId
+     */
+    public static function fetchZeroAmountRelationBatch($cursorId, $limit)
+    {
+        $relTable = self::$baseFile::tableName();
+        $sql = 'SELECT id, clearId, orderId, cgId FROM ' . $relTable
+            . ' WHERE ' . self::isZeroAmountSql();
+        $params = [];
+        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();
+    }
+
+    /**
+     * 步骤2 辅助:批量取 clearStyle(仅处理步骤1命中的 clearId)
+     * @param int[] $clearIds
+     * @return array<int,int> clearId => clearStyle
+     */
+    public static function getClearStyleMap(array $clearIds)
+    {
+        $clearIds = array_values(array_unique(array_filter(array_map('intval', $clearIds))));
+        if (empty($clearIds)) {
+            return [];
+        }
+        $rows = Yii::$app->db->createCommand(
+            'SELECT id, clearStyle FROM ' . Clear::tableName()
+            . ' WHERE id IN (' . implode(',', $clearIds) . ')'
+        )->queryAll();
+        $map = [];
+        foreach ($rows as $row) {
+            $map[intval($row['id'])] = intval($row['clearStyle'] ?? 0);
+        }
+        return $map;
+    }
+
+    /**
+     * 步骤2:反查本行 amount 应写入的值(源单 actPrice)
+     */
+    public static function resolveAmountForRelationRowDirect(array $row)
+    {
+        $clearStyle = intval($row['clearStyle'] ?? 0);
+        $orderId = intval($row['orderId'] ?? 0);
+        $cgId = intval($row['cgId'] ?? 0);
+        $gys2KmGys = intval(dict::getDict('clearStyle', 'gys2KmGys'));
+        $gys2Hd = intval(dict::getDict('clearStyle', 'gys2Hd'));
+        $hd2Gys = intval(dict::getDict('clearStyle', 'hd2Gys'));
+
+        if ($clearStyle === $gys2KmGys) {
+            $ghsCgId = $cgId > 0 ? $cgId : $orderId;
+            return self::amountFromGhsCgOrderId($ghsCgId);
+        }
+        if ($clearStyle === $gys2Hd && $orderId > 0) {
+            $amount = self::amountFromGhsOrderId($orderId);
+            if ($amount !== null) {
+                return $amount;
+            }
+        }
+        if ($clearStyle === $hd2Gys && $cgId > 0) {
+            $amount = self::amountFromHdPurchaseId($cgId);
+            if ($amount !== null) {
+                return $amount;
+            }
+        }
+        if ($orderId > 0) {
+            $amount = self::amountFromGhsOrderId($orderId);
+            if ($amount !== null) {
+                return $amount;
+            }
+        }
+        if ($cgId > 0) {
+            $amount = self::amountFromHdPurchaseId($cgId);
+            if ($amount !== null) {
+                return $amount;
+            }
+            return self::amountFromGhsCgOrderId($cgId);
+        }
+        return null;
+    }
+
+    protected static function amountFromGhsOrderId($orderId)
+    {
+        $orderId = intval($orderId);
+        if ($orderId <= 0) {
+            return null;
+        }
+        $order = OrderClass::getById($orderId);
+        return self::amountFromEntity($order);
+    }
+
+    protected static function amountFromHdPurchaseId($cgId)
+    {
+        $cgId = intval($cgId);
+        if ($cgId <= 0) {
+            return null;
+        }
+        $purchase = PurchaseClass::getById($cgId);
+        return self::amountFromEntity($purchase);
+    }
+
+    protected static function amountFromGhsCgOrderId($cgId)
+    {
+        $cgId = intval($cgId);
+        if ($cgId <= 0) {
+            return null;
+        }
+        $cg = PurchaseOrderClass::getById($cgId);
+        return self::amountFromEntity($cg);
+    }
+
+    protected static function amountFromEntity($entity)
+    {
+        if (empty($entity)) {
+            return null;
+        }
+        $amount = self::resolveDebtAmount($entity);
+        if (bccomp($amount, '0', 2) <= 0) {
+            return null;
+        }
+        return $amount;
+    }
+
+    /**
+     * 步骤1~3:xhOrderCgClear amount=0 → 反查应填金额 → 按 id 更新
+     * @param array[] $rows fetchZeroAmountRelationBatch 的结果
+     * @return array{updated:int,skipped:int,details:array[]}
+     */
+    public static function patchZeroAmountRows(array $rows, $dryRun = false, $logDetails = false)
+    {
+        if (empty($rows)) {
+            return ['updated' => 0, 'skipped' => 0, 'details' => []];
+        }
+
+        $clearIds = array_values(array_unique(array_filter(array_map('intval', array_column($rows, 'clearId')))));
+        $clearStyleMap = self::getClearStyleMap($clearIds);
+
+        $table = self::$baseFile::tableName();
+        $updated = 0;
+        $skipped = 0;
+        $details = [];
+
+        foreach ($rows as $row) {
+            $relId = intval($row['id'] ?? 0);
+            $clearId = intval($row['clearId'] ?? 0);
+            if ($relId <= 0) {
+                $skipped++;
+                continue;
+            }
+
+            $row['clearStyle'] = $clearStyleMap[$clearId] ?? 0;
+            $amount = self::resolveAmountForRelationRowDirect($row);
+            if ($amount === null) {
+                $skipped++;
+                if ($logDetails) {
+                    $details[] = [
+                        'id' => $relId,
+                        'clearId' => $clearId,
+                        'orderId' => intval($row['orderId'] ?? 0),
+                        'cgId' => intval($row['cgId'] ?? 0),
+                        'status' => 'skip',
+                        'reason' => '反查不到有效 actPrice',
+                    ];
+                }
+                continue;
+            }
+
+            if (!$dryRun) {
+                $affected = Yii::$app->db->createCommand(
+                    'UPDATE ' . $table . ' SET amount = :amount WHERE id = :id AND ' . self::isZeroAmountSql()
+                )->bindValues([
+                    ':amount' => $amount,
+                    ':id' => $relId,
+                ])->execute();
+                if ($affected > 0) {
+                    $updated++;
+                    if ($logDetails) {
+                        $details[] = [
+                            'id' => $relId,
+                            'clearId' => $clearId,
+                            'amount' => $amount,
+                            'status' => 'updated',
+                        ];
+                    }
+                } else {
+                    $skipped++;
+                    if ($logDetails) {
+                        $details[] = [
+                            'id' => $relId,
+                            'status' => 'skip',
+                            'reason' => '更新时 amount 已非 0',
+                        ];
+                    }
+                }
+            } else {
+                $updated++;
+                if ($logDetails) {
+                    $details[] = [
+                        'id' => $relId,
+                        'clearId' => $clearId,
+                        'orderId' => intval($row['orderId'] ?? 0),
+                        'cgId' => intval($row['cgId'] ?? 0),
+                        'amount' => $amount,
+                        'status' => 'would_update',
+                    ];
+                }
+            }
+        }
+        return ['updated' => $updated, 'skipped' => $skipped, 'details' => $details];
+    }
+
 }

+ 41 - 0
biz-ghs/product/classes/ProductClass.php

@@ -199,6 +199,33 @@ class ProductClass extends BaseClass
         return $list;
     }
 
+    /**
+     * 库存预警分页列表(与 get-item-list 共用 where 条件):stock + onStock < stockWarning
+     */
+    public static function warningList($where, $order = ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC])
+    {
+        $get = Yii::$app->request->get();
+        $page = isset($get['page']) ? intval($get['page']) : 1;
+        $pageSize = isset($get['pageSize']) && !empty($get['pageSize']) ? intval($get['pageSize']) : Yii::$app->params['pageSize'];
+        $offset = ($page - 1) * $pageSize;
+
+        $model = self::getModel();
+        $query = $model->conditionQuery($where)->andWhere('(`stock` + `onStock`) < `stockWarning`');
+        $count = (clone $query)->count();
+        $totalPage = $pageSize > 0 ? (int)ceil($count / $pageSize) : 0;
+        if (!empty($order)) {
+            $query->orderBy($order);
+        }
+        $list = $query->offset($offset)->limit($pageSize)->asArray()->all();
+
+        return [
+            'totalNum' => $count,
+            'totalPage' => $totalPage,
+            'moreData' => $totalPage > $page ? 1 : 0,
+            'list' => $list,
+        ];
+    }
+
     //盘点用到的数据组合
     public static function checkItemGroup($list, $level = 0)
     {
@@ -1791,6 +1818,13 @@ class ProductClass extends BaseClass
         if (isset($data['inTurn'])) {
             $upData['inTurn'] = $data['inTurn'];
         }
+        if (array_key_exists('itemRemark', $data)) {
+            $itemRemark = trim($data['itemRemark'] ?? '');
+            if (mb_strlen($itemRemark) > 200) {
+                util::fail('备注不能超过200字');
+            }
+            $upData['itemRemark'] = $itemRemark;
+        }
         if (isset($data['delStatus'])) {
             if (!in_array($data['delStatus'], [0, 1])) {
                 util::fail("删除错误");
@@ -2426,6 +2460,13 @@ class ProductClass extends BaseClass
         if ($weight === false) {
             util::fail("请输入花材重量");
         }
+        if (isset($data['itemRemark'])) {
+            $itemRemark = trim($data['itemRemark'] ?? '');
+            if (mb_strlen($itemRemark) > 200) {
+                util::fail('备注不能超过200字');
+            }
+            $data['itemRemark'] = $itemRemark;
+        }
         $cat = ItemClassClass::getById($classId, true);
         if (empty($cat)) {
             util::fail("花材分类不存在");

+ 7 - 0
biz-hd/item/classes/ItemClass.php

@@ -99,6 +99,13 @@ class ItemClass extends BaseClass
         if (isset($data['inTurn'])) {
             $upData['inTurn'] = $data['inTurn'];
         }
+        if (array_key_exists('itemRemark', $data)) {
+            $itemRemark = trim($data['itemRemark'] ?? '');
+            if (mb_strlen($itemRemark) > 200) {
+                util::fail('备注不能超过200字');
+            }
+            $upData['itemRemark'] = $itemRemark;
+        }
 
         //路上库存不能直接修改
         unset($data['onStock']);

+ 2 - 2
biz-hd/order/classes/OrderClass.php

@@ -262,7 +262,7 @@ class OrderClass extends BaseClass
                 $data['reachDate'] = date("Y-m-d");
             }
             if (empty($data['reachPeriod'])) {
-                $data['reachPeriod'] = '22:00';
+                $data['reachPeriod'] = date("H:i", time() + 3600);
             }
         } else {
             //如果不是到店自取的,则必须要有配送时间,默认当天
@@ -1345,4 +1345,4 @@ class OrderClass extends BaseClass
         }
         return $debtAmount;
     }
-}
+}

+ 1 - 1
common/components/util.php

@@ -145,7 +145,7 @@ class util
     public static function stop($string = '')
     {
         echo $string;
-        Yii::$app->end();
+        exit();
     }
 
     public static function end()

+ 392 - 36
console/controllers/ClearController.php

@@ -37,12 +37,18 @@ class ClearController extends Controller
     /** @var int 每处理多少条输出一次进度,0=仅每批汇总 */
     public $logEvery = 500;
 
+    /** @var int 每种 clearStyle 抽样条数(check-order-cg-clear-sample 默认 50000) */
+    public $samplePerStyle = 50000;
+
+    /** @var int 抽样检查最多打印的问题明细条数 */
+    public $showIssues = 30;
+
     /** @var string 回填进度文件后缀(内部使用) */
     private $backfillProgressKey = '';
 
     public function options($actionID)
     {
-        return array_merge(parent::options($actionID), [
+        $options = array_merge(parent::options($actionID), [
             'dryRun',
             'batchSize',
             'sleepMs',
@@ -51,6 +57,11 @@ class ClearController extends Controller
             'limit',
             'logEvery',
         ]);
+        if ($actionID === 'check-order-cg-clear-sample') {
+            $options[] = 'samplePerStyle';
+            $options[] = 'showIssues';
+        }
+        return $options;
     }
 
     //更新 ssh 2023503
@@ -81,20 +92,173 @@ class ClearController extends Controller
     }
 
     /**
-     * 回填 xhOrderCgClear:clearStyle 1(hd2Gys) + 2(gys2Hd)
+     * 抽样检查 xhOrderCgClear:clearStyle 1/2/3 各抽 N 条(默认 50000)
+     *
+     * 检查漏补、旧格式等问题;不检 amount=0(由 patch-order-cg-clear-amount 处理)
+     *
+     * 用法:
+     *   php yii clear/check-order-cg-clear-sample
+     *   php yii clear/check-order-cg-clear-sample --samplePerStyle=50000
+     *   php yii clear/check-order-cg-clear-sample --dryRun=0   # 发现问题顺带漏补
+     */
+    public function actionCheckOrderCgClearSample()
+    {
+        if (!$this->isCliOptionPassed('dryRun')) {
+            $this->dryRun = 1;
+        }
+        return $this->runSampleCheckOrderCgClear();
+    }
+
+    /**
+     * 补写 xhOrderCgClear.amount
+     *
+     * 1. 从 xhOrderCgClear 查 amount=0
+     * 2. 按 orderId/cgId + clearStyle 反查源单 actPrice
+     * 3. 按关系行 id 更新(且仍满足 amount=0 才写,避免覆盖已有值)
+     *
+     * 用法:
+     *   php yii clear/patch-order-cg-clear-amount --dryRun=1 --limit=1000
+     *   php yii clear/patch-order-cg-clear-amount --reset=1 --batchSize=100 --sleepMs=200
+     */
+    public function actionPatchOrderCgClearAmount()
+    {
+        ini_set('memory_limit', '512M');
+        set_time_limit(0);
+
+        if ($this->batchSize < 1) {
+            $this->stderr("batchSize 必须大于 0\n");
+            return ExitCode::UNSPECIFIED_ERROR;
+        }
+
+        $this->backfillProgressKey = 'amount';
+
+        if ($this->reset) {
+            $this->clearBackfillProgress();
+            $this->stdout("已重置补 amount 进度\n");
+        }
+
+        $cursorId = $this->loadBackfillProgress();
+        $total = 0;
+        $updated = 0;
+        $skipped = 0;
+        $batchNo = 0;
+
+        $this->stdout(sprintf(
+            "补 amount | 1查xhOrderCgClear.amount=0 → 2反查actPrice → 3更新 | dryRun=%d batchSize=%d sleepMs=%d limit=%d | id<%s\n",
+            (int)$this->dryRun,
+            $this->batchSize,
+            $this->sleepMs,
+            (int)$this->limit,
+            $cursorId === null ? 'max' : (string)$cursorId
+        ));
+
+        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;
+                }
+            }
+
+            $rows = OrderCgClearClass::fetchZeroAmountRelationBatch($cursorId, $fetchLimit);
+            if (empty($rows)) {
+                break;
+            }
+            $batchNo++;
+            $batchMinId = null;
+
+            $result = OrderCgClearClass::patchZeroAmountRows($rows, (bool)$this->dryRun, (bool)$this->dryRun);
+            $batchUpdated = intval($result['updated'] ?? 0);
+            $batchSkipped = intval($result['skipped'] ?? 0);
+            if ($this->dryRun && !empty($result['details'])) {
+                foreach ($result['details'] as $detail) {
+                    if (($detail['status'] ?? '') === 'would_update') {
+                        $this->stdout(sprintf(
+                            "  预览 id=%d clearId=%d orderId=%d cgId=%d => amount=%s\n",
+                            (int)$detail['id'],
+                            (int)$detail['clearId'],
+                            (int)$detail['orderId'],
+                            (int)$detail['cgId'],
+                            $detail['amount']
+                        ));
+                    }
+                }
+            }
+            $updated += $batchUpdated;
+            $skipped += $batchSkipped;
+            $total += count($rows);
+
+            foreach ($rows as $row) {
+                $batchMinId = intval($row['id']);
+                $cursorId = $batchMinId;
+            }
+
+            if (!$this->dryRun && $batchMinId !== null) {
+                $this->saveBackfillProgress($batchMinId);
+            }
+
+            $this->stdout(sprintf(
+                "第 %d 批:amount=0 行 %d 条,补写 %d,跳过 %d,checkpoint id=%d\n",
+                $batchNo,
+                count($rows),
+                $batchUpdated,
+                $batchSkipped,
+                (int)$batchMinId
+            ));
+
+            if (count($rows) < $fetchLimit) {
+                break;
+            }
+            $this->sleepBetweenBatch();
+        }
+
+        if (!$this->dryRun && $cursorId !== null) {
+            $this->stdout("进度已保存,下次从 id < {$cursorId} 继续\n");
+        }
+        $this->stdout(sprintf(
+            "完成:处理 amount=0 行 %d 条,补写 %d,跳过 %d\n",
+            $total,
+            $updated,
+            $skipped
+        ));
+        return ExitCode::OK;
+    }
+
+    /**
+     * 漏补 xhOrderCgClear:clearStyle 1(hd2Gys) + 2(gys2Hd) + 3(gys2KmGys) 统一扫描
+     *
+     * 仅处理:无关系行 / 关系行不足 / amount 为空 / gys2KmGys 旧格式;已完整则跳过。
      *
      * 用法:
-     *   php yii clear/backfill-order-cg-clear-hd-gys --dryRun=1 --limit=100
-     *   php yii clear/backfill-order-cg-clear-hd-gys --reset=1 --batchSize=50 --sleepMs=200
+     *   php yii clear/backfill-order-cg-clear --dryRun=1 --limit=1000
+     *   php yii clear/backfill-order-cg-clear --reset=1 --batchSize=50 --sleepMs=200
+     *   php yii clear/backfill-order-cg-clear --clearId=123
+     */
+    public function actionBackfillOrderCgClear()
+    {
+        return $this->runBackfillOrderCgClear(
+            OrderCgClearClass::getRelationClearStyles(),
+            'all',
+            'hd2Gys(1)+gys2Hd(2)+gys2KmGys(3)',
+            true
+        );
+    }
+
+    /**
+     * @deprecated 请改用 backfill-order-cg-clear(已合并 1+2+3)
      */
     public function actionBackfillOrderCgClearHdGys()
     {
-        $clearStyles = [
-            intval(dict::getDict('clearStyle', 'hd2Gys')),
-            intval(dict::getDict('clearStyle', 'gys2Hd')),
-        ];
+        $this->stderr("已合并至: php yii clear/backfill-order-cg-clear\n");
         return $this->runBackfillOrderCgClear(
-            $clearStyles,
+            [
+                intval(dict::getDict('clearStyle', 'hd2Gys')),
+                intval(dict::getDict('clearStyle', 'gys2Hd')),
+            ],
             'hd_gys',
             'hd2Gys(1)+gys2Hd(2)',
             false
@@ -102,37 +266,19 @@ class ClearController extends Controller
     }
 
     /**
-     * 回填 xhOrderCgClear:clearStyle 3(gys2KmGys) 供货商向昆明供货商结账
-     *
-     * 用法:
-     *   php yii clear/rebuild-order-cg-clear-index
-     *   php yii clear/backfill-order-cg-clear-km-gys --dryRun=1 --limit=100
-     *   php yii clear/backfill-order-cg-clear-km-gys --reset=1 --batchSize=50 --sleepMs=200
+     * @deprecated 请改用 backfill-order-cg-clear(已合并 1+2+3)
      */
     public function actionBackfillOrderCgClearKmGys()
     {
-        $clearStyles = [
-            intval(dict::getDict('clearStyle', 'gys2KmGys')),
-        ];
+        $this->stderr("已合并至: php yii clear/backfill-order-cg-clear\n");
         return $this->runBackfillOrderCgClear(
-            $clearStyles,
+            [intval(dict::getDict('clearStyle', 'gys2KmGys'))],
             'km_gys',
             'gys2KmGys(3)',
             true
         );
     }
 
-    /**
-     * @deprecated 请改用 backfill-order-cg-clear-hd-gys 与 backfill-order-cg-clear-km-gys
-     */
-    public function actionBackfillOrderCgClear()
-    {
-        $this->stderr("请使用独立脚本:\n");
-        $this->stderr("  php yii clear/backfill-order-cg-clear-hd-gys --reset=1\n");
-        $this->stderr("  php yii clear/backfill-order-cg-clear-km-gys --reset=1\n");
-        return ExitCode::UNSPECIFIED_ERROR;
-    }
-
     /**
      * 迁移 gys2KmGys 关系行:orderId/cgId 旧格式 -> cgId 存 xhGhsCgOrder、orderId=0
      *
@@ -182,6 +328,162 @@ class ClearController extends Controller
         return ExitCode::OK;
     }
 
+    /**
+     * clearStyle 1/2/3 各抽样 samplePerStyle 条,检查关系表漏补等问题(不含 amount=0)
+     */
+    private function runSampleCheckOrderCgClear()
+    {
+        ini_set('memory_limit', '512M');
+        set_time_limit(0);
+
+        $sampleSize = intval($this->samplePerStyle);
+        if ($sampleSize < 1) {
+            $this->stderr("samplePerStyle 必须大于 0\n");
+            return ExitCode::UNSPECIFIED_ERROR;
+        }
+
+        $styleLabels = [
+            intval(dict::getDict('clearStyle', 'hd2Gys')) => 'hd2Gys(1)',
+            intval(dict::getDict('clearStyle', 'gys2Hd')) => 'gys2Hd(2)',
+            intval(dict::getDict('clearStyle', 'gys2KmGys')) => 'gys2KmGys(3)',
+        ];
+
+        $this->stdout(sprintf(
+            "抽样检查 xhOrderCgClear | 各 clearStyle 抽 %d 条(id DESC) | 不检 amount=0 | dryRun=%d | 最多展示 %d 条问题明细\n",
+            $sampleSize,
+            (int)$this->dryRun,
+            (int)$this->showIssues
+        ));
+
+        $totalSampled = 0;
+        $totalIssues = 0;
+        $totalFixed = 0;
+        $allIssues = [];
+        $reasonStats = [];
+
+        foreach ($styleLabels as $clearStyle => $label) {
+            $list = $this->fetchClearSampleByStyle($clearStyle, $sampleSize);
+            $sampled = count($list);
+            $totalSampled += $sampled;
+
+            if ($sampled === 0) {
+                $this->stdout("{$label}:无数据\n");
+                continue;
+            }
+
+            $clearIds = array_map('intval', array_column($list, 'id'));
+            $relationCountMap = [];
+            $legacyGys2KmGysClearIds = [];
+            foreach (array_chunk($clearIds, 2000) as $clearIdChunk) {
+                $relationCountMap += OrderCgClearClass::getRelationCountMap($clearIdChunk);
+                $legacyGys2KmGysClearIds = array_merge(
+                    $legacyGys2KmGysClearIds,
+                    OrderCgClearClass::getClearIdsWithLegacyGys2KmGysRows($clearIdChunk)
+                );
+            }
+            $legacyGys2KmGysClearIds = array_values(array_unique(array_map('intval', $legacyGys2KmGysClearIds)));
+
+            $styleIssues = 0;
+            $styleFixed = 0;
+
+            foreach ($list as $clear) {
+                $currentClearId = intval($clear['id'] ?? 0);
+                $relationCount = $relationCountMap[$currentClearId] ?? 0;
+                $issue = OrderCgClearClass::detectBackfillIssue(
+                    $clear,
+                    $relationCount,
+                    [],
+                    $legacyGys2KmGysClearIds,
+                    false
+                );
+                if ($issue === null) {
+                    continue;
+                }
+
+                $styleIssues++;
+                $totalIssues++;
+                $reason = $issue['reason'];
+                $reasonStats[$reason] = ($reasonStats[$reason] ?? 0) + 1;
+
+                $allIssues[] = [
+                    'clearId' => $currentClearId,
+                    'clearStyle' => $clearStyle,
+                    'label' => $label,
+                    'reason' => $reason,
+                    'relationCount' => $issue['relationCount'],
+                    'expected' => $issue['expected'],
+                ];
+
+                if (!$this->dryRun) {
+                    $count = OrderCgClearClass::backfillFromClear($clear, true);
+                    if ($count > 0) {
+                        $styleFixed++;
+                        $totalFixed += $count;
+                    }
+                }
+            }
+
+            $this->stdout(sprintf(
+                "%s:抽样 %d 条,问题 %d 条%s\n",
+                $label,
+                $sampled,
+                $styleIssues,
+                !$this->dryRun ? ",已写入关系 {$styleFixed} 行" : ''
+            ));
+        }
+
+        if (!empty($reasonStats)) {
+            $this->stdout("问题类型汇总:\n");
+            foreach ($reasonStats as $reason => $cnt) {
+                $this->stdout("  {$reason}:{$cnt}\n");
+            }
+        }
+
+        $showLimit = max(0, intval($this->showIssues));
+        if ($showLimit > 0 && !empty($allIssues)) {
+            $this->stdout("问题明细(前 {$showLimit} 条):\n");
+            foreach (array_slice($allIssues, 0, $showLimit) as $item) {
+                $this->stdout(sprintf(
+                    "  clearId=%d %s %s 已有=%d 应有≈%d\n",
+                    $item['clearId'],
+                    $item['label'],
+                    $item['reason'],
+                    $item['relationCount'],
+                    $item['expected']
+                ));
+            }
+            if (count($allIssues) > $showLimit) {
+                $this->stdout('  ... 还有 ' . (count($allIssues) - $showLimit) . " 条未展示\n");
+            }
+        }
+
+        $this->stdout(sprintf(
+            "检查完成:共抽样 %d 条,发现问题结账单 %d 个%s\n",
+            $totalSampled,
+            $totalIssues,
+            $totalIssues === 0 ? ',未发现漏补' : ''
+        ));
+        if (!$this->dryRun && $totalFixed > 0) {
+            $this->stdout("已漏补写入关系 {$totalFixed} 行\n");
+        }
+        if ($this->dryRun && $totalIssues > 0) {
+            $this->stdout("请加 --dryRun=0 执行漏补,或运行 backfill-order-cg-clear --reset=1 全量漏补\n");
+        }
+        if ($totalIssues === 0) {
+            $this->stdout("amount=0 请单独跑: php yii clear/patch-order-cg-clear-amount\n");
+        }
+
+        return $totalIssues > 0 ? ExitCode::UNSPECIFIED_ERROR : ExitCode::OK;
+    }
+
+    private function fetchClearSampleByStyle($clearStyle, $limit)
+    {
+        return Yii::$app->db->createCommand(
+            'SELECT id, orderSn, saleIds, purchaseIds, status, clearStyle FROM ' . Clear::tableName()
+            . ' WHERE clearStyle = :style ORDER BY id DESC LIMIT ' . intval($limit)
+        )->bindValue(':style', intval($clearStyle))->queryAll();
+    }
+
     /**
      * @param int[] $clearStyles
      * @param string $progressKey 进度文件后缀
@@ -213,10 +515,12 @@ class ClearController extends Controller
         $total = 0;
         $inserted = 0;
         $skipped = 0;
+        $needBackfill = 0;
         $batchNo = 0;
+        $styleStats = [];
 
         $this->stdout(sprintf(
-            "开始回填 xhOrderCgClear [%s] | id DESC 分批 | dryRun=%d batchSize=%d sleepMs=%d limit=%d | 续跑 id<%s | 进度=%s\n",
+            "开始漏补 xhOrderCgClear [%s] | id DESC 分批 | dryRun=%d batchSize=%d sleepMs=%d limit=%d | 续跑 id<%s | 进度=%s\n",
             $label,
             (int)$this->dryRun,
             $this->batchSize,
@@ -254,13 +558,20 @@ class ClearController extends Controller
             foreach ($list as $clear) {
                 $total++;
                 $currentClearId = intval($clear['id'] ?? 0);
+                $clearStyle = intval($clear['clearStyle'] ?? 0);
                 $batchMinId = $currentClearId;
                 $cursorId = $currentClearId;
 
+                if (!isset($styleStats[$clearStyle])) {
+                    $styleStats[$clearStyle] = ['scan' => 0, 'need' => 0, 'skip' => 0];
+                }
+                $styleStats[$clearStyle]['scan']++;
+
                 $saleIds = trim($clear['saleIds'] ?? '');
                 $purchaseIds = trim($clear['purchaseIds'] ?? '');
                 if ($saleIds === '' && $purchaseIds === '') {
                     $skipped++;
+                    $styleStats[$clearStyle]['skip']++;
                     continue;
                 }
 
@@ -272,12 +583,24 @@ class ClearController extends Controller
                     $legacyGys2KmGysClearIds
                 )) {
                     $skipped++;
+                    $styleStats[$clearStyle]['skip']++;
                     continue;
                 }
 
+                $needBackfill++;
+                $styleStats[$clearStyle]['need']++;
+
                 if ($this->dryRun) {
-                    if ($this->logEvery > 0 && $total % $this->logEvery === 0) {
-                        $this->stdout("dryRun 已扫描 {$total} 条,当前 clearId={$currentClearId}\n");
+                    $expected = OrderCgClearClass::expectedRelationCountFromClear($clear);
+                    $this->stdout(sprintf(
+                        "需漏补 clearId=%d clearStyle=%d 已有关系=%d 应有≈%d\n",
+                        $currentClearId,
+                        $clearStyle,
+                        $relationCount,
+                        $expected
+                    ));
+                    if ($this->logEvery > 0 && $needBackfill % $this->logEvery === 0) {
+                        $this->stdout("dryRun 需漏补累计 {$needBackfill} 条\n");
                     }
                     continue;
                 }
@@ -287,9 +610,12 @@ class ClearController extends Controller
                     $inserted += $count;
                 } else {
                     $skipped++;
+                    $styleStats[$clearStyle]['skip']++;
+                    $styleStats[$clearStyle]['need']--;
+                    $needBackfill--;
                 }
                 if ($this->logEvery > 0 && $total % $this->logEvery === 0) {
-                    $this->stdout("进度:扫描 {$total},写入关系 {$inserted} 行,跳过 {$skipped},当前 clearId={$currentClearId}\n");
+                    $this->stdout("进度:扫描 {$total},需漏补 {$needBackfill},写入关系 {$inserted} 行,跳过 {$skipped},当前 clearId={$currentClearId}\n");
                 }
             }
 
@@ -298,10 +624,11 @@ class ClearController extends Controller
             }
 
             $this->stdout(sprintf(
-                "第 %d 批完成:本批 %d 条,累计扫描 %d,写入关系 %d 行,跳过 %d,checkpoint id=%d\n",
+                "第 %d 批完成:本批 %d 条,累计扫描 %d,需漏补 %d,写入关系 %d 行,跳过 %d,checkpoint id=%d\n",
                 $batchNo,
                 count($list),
                 $total,
+                $needBackfill,
                 $inserted,
                 $skipped,
                 (int)$batchMinId
@@ -316,7 +643,26 @@ class ClearController extends Controller
         if (!$this->dryRun && $this->clearId <= 0 && $cursorId !== null) {
             $this->stdout("进度已保存,下次从 id < {$cursorId} 继续\n");
         }
-        $this->stdout("全部完成 [{$label}]:扫描 {$total} 条 xhClear,写入 {$inserted} 行关系,跳过 {$skipped} 条\n");
+        $this->stdout(sprintf(
+            "全部完成 [%s]:扫描 %d 条 xhClear,需漏补 %d 条,写入 %d 行关系,跳过 %d 条\n",
+            $label,
+            $total,
+            $needBackfill,
+            $inserted,
+            $skipped
+        ));
+        if (!empty($styleStats)) {
+            ksort($styleStats);
+            foreach ($styleStats as $style => $stat) {
+                $this->stdout(sprintf(
+                    "  clearStyle=%d:扫描 %d,需漏补 %d,跳过 %d\n",
+                    $style,
+                    (int)$stat['scan'],
+                    (int)$stat['need'],
+                    (int)$stat['skip']
+                ));
+            }
+        }
         return ExitCode::OK;
     }
 
@@ -405,4 +751,14 @@ class ClearController extends Controller
         }
     }
 
+    private function isCliOptionPassed($name)
+    {
+        foreach ($_SERVER['argv'] ?? [] as $arg) {
+            if ($arg === '--' . $name || strpos($arg, '--' . $name . '=') === 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
 }