Browse Source

认证 与 迁移

shish 2 months ago
parent
commit
7e5ea17de2

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

@@ -496,7 +496,8 @@ class TestController extends BaseController
             //['name' => '广州花粉工坊', 'merchantNo' => '8225810599201RY', 'wx' => [892145716, 892145723], 'zfb' => '2088580818465548'],
             //['name' => '张家口小武鲜花批发', 'merchantNo' => '82213805992007R', 'wx' => [894997028, 894996929], 'zfb' => '2088680439799041'],
             //['name' => '淮北花述语鲜花批发', 'merchantNo' => '82236605992001E', 'wx' => [895627520, 895627914], 'zfb' => '2088680541422679'],
-            ['name' => '泉州馨喜鲜花批发', 'merchantNo' => '8223970599200KD', 'wx' => [896099124, 896098673], 'zfb' => '2088680670320387'],
+            //['name' => '泉州馨喜鲜花批发', 'merchantNo' => '8223970599200KD', 'wx' => [896099124, 896098673], 'zfb' => '2088680670320387'],
+            ['name' => '东莞石龙我要花', 'merchantNo' => '8226020599200RC', 'wx' => [896365688, 896365294], 'zfb' => '2088680722085035'],
             //['name' => '厦门纯彩花艺', 'merchantNo' => '8223930599200LD', 'wx' => [894984230, 894983290], 'zfb' => '2088680439152343'],
             //['name' => '东莞我要花鲜花', 'merchantNo' => '8226020599200R8', 'wx' => [894794554, 894794917], 'zfb' => '2088680405463378'],
             //['name' => '镇江花珩鲜花批发', 'merchantNo' => '82231405992003N', 'wx' => [894925711, 894925626], 'zfb' => '2088680426877183'],

+ 4 - 1
biz-ghs/clear/classes/ClearClass.php

@@ -77,7 +77,7 @@ class ClearClass extends BaseClass
         if (empty($arr)) {
             util::fail('请选择采购单');
         }
-        $purchaseList = PurchaseOrderClass::getAllByCondition(['id' => ['in', $arr]], null, ['id', 'actPrice', 'realPrice', 'debt', 'ghsId'], null, true);
+        $purchaseList = PurchaseOrderClass::getAllByCondition(['id' => ['in', $arr]], null, ['id', 'orderSn', 'actPrice', 'realPrice', 'debt', 'ghsId'], null, true);
         if (empty($purchaseList)) {
             util::fail('请选择采购单');
         }
@@ -137,8 +137,11 @@ class ClearClass extends BaseClass
         $data['deadline'] = date("Y-m-d H:i:s", $deadTime);
         $data['status'] = self::STATUS_AWAIT_PAY;
         $order = self::add($data, true);
+        $pairs = OrderCgClearClass::buildPairsFromGhsCgOrders($purchaseList);
+        OrderCgClearClass::bindRelations($order, $pairs);
         $payWay = $data['payWay'] ?? dict::getDict('payWay', 'wxPay');
         self::complete($order, $payWay);
+        OrderCgClearClass::markDoneByClearId($order->id);
         return $order;
     }
 

+ 347 - 19
biz-ghs/clear/classes/OrderCgClearClass.php

@@ -3,7 +3,9 @@
 namespace bizGhs\clear\classes;
 
 use bizGhs\base\classes\BaseClass;
+use bizGhs\clear\models\Clear;
 use bizGhs\order\classes\OrderClass;
+use bizGhs\order\classes\PurchaseOrderClass;
 use bizHd\purchase\classes\PurchaseClass;
 use bizHd\purchase\classes\PurchaseClearClass;
 use common\components\dict;
@@ -98,6 +100,81 @@ class OrderCgClearClass extends BaseClass
         return $pairs;
     }
 
+    /**
+     * gys2KmGys:purchaseIds 为 xhGhsCgOrder,cgId 存基地采购单 id,orderId=0
+     */
+    public static function buildPairsFromGhsCgOrders($cgList)
+    {
+        if (empty($cgList)) {
+            return [];
+        }
+        $pairs = [];
+        foreach ($cgList as $cg) {
+            $cgId = intval(is_array($cg) ? ($cg['id'] ?? 0) : ($cg->id ?? 0));
+            if ($cgId <= 0) {
+                continue;
+            }
+            $cgSn = is_array($cg) ? ($cg['orderSn'] ?? '') : ($cg->orderSn ?? '');
+            $pairs[] = [
+                'orderId' => 0,
+                'orderSn' => '',
+                'cgId' => $cgId,
+                'cgSn' => $cgSn,
+                'amount' => self::resolveDebtAmount($cg),
+            ];
+        }
+        return $pairs;
+    }
+
+    public static function getRelationClearStyles()
+    {
+        return [
+            intval(dict::getDict('clearStyle', 'hd2Gys')),
+            intval(dict::getDict('clearStyle', 'gys2Hd')),
+            intval(dict::getDict('clearStyle', 'gys2KmGys')),
+        ];
+    }
+
+    public static function isRelationClearStyle($clearStyle)
+    {
+        return in_array(intval($clearStyle), self::getRelationClearStyles(), true);
+    }
+
+    public static function buildPairsFromClear($clear)
+    {
+        $clearStyle = intval(is_object($clear) ? ($clear->clearStyle ?? 0) : ($clear['clearStyle'] ?? 0));
+        if (!self::isRelationClearStyle($clearStyle)) {
+            return [];
+        }
+        $saleIds = is_object($clear) ? ($clear->saleIds ?? '') : ($clear['saleIds'] ?? '');
+        $purchaseIds = is_object($clear) ? ($clear->purchaseIds ?? '') : ($clear['purchaseIds'] ?? '');
+        $gys2KmGys = intval(dict::getDict('clearStyle', 'gys2KmGys'));
+        if ($clearStyle === $gys2KmGys) {
+            $cgArr = self::parseIds($purchaseIds);
+            if (empty($cgArr)) {
+                return [];
+            }
+            $cgList = PurchaseOrderClass::getAllByCondition(['id' => ['in', $cgArr]], null, '*', null, true);
+            return self::buildPairsFromGhsCgOrders($cgList);
+        }
+        $saleArr = self::parseIds($saleIds);
+        if (!empty($saleArr)) {
+            $orderList = OrderClass::getByIds($saleArr);
+            if (!empty($orderList)) {
+                return self::buildPairsFromOrders($orderList);
+            }
+        }
+        $cgArr = self::parseIds($purchaseIds);
+        if (empty($cgArr)) {
+            return [];
+        }
+        $purchaseList = PurchaseClass::getAllByCondition(['id' => ['in', $cgArr]], null, '*', null, true);
+        if (empty($purchaseList)) {
+            return [];
+        }
+        return self::buildPairsFromPurchases($purchaseList);
+    }
+
     public static function bindRelations($clear, $pairs, $status = self::STATUS_AWAIT_PAY)
     {
         $clearId = is_object($clear) ? ($clear->id ?? 0) : ($clear['id'] ?? 0);
@@ -237,28 +314,14 @@ class OrderCgClearClass extends BaseClass
                 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)])) {
+        if (!self::isRelationClearStyle(is_object($clear) ? ($clear->clearStyle ?? 0) : ($clear['clearStyle'] ?? 0))) {
             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);
-            }
+        $clearStyle = intval(is_object($clear) ? ($clear->clearStyle ?? 0) : ($clear['clearStyle'] ?? 0));
+        if ($clearStyle === intval(dict::getDict('clearStyle', 'gys2KmGys'))) {
+            self::migrateLegacyGys2KmGysRowsByClearId($clearId, false);
         }
+        $pairs = self::buildPairsFromClear($clear);
         if (empty($pairs)) {
             return 0;
         }
@@ -270,6 +333,65 @@ class OrderCgClearClass extends BaseClass
         return count($pairs);
     }
 
+    /**
+     * 从 xhClear 估算应有关系行数(saleIds 优先,否则 purchaseIds 个数)
+     */
+    public static function expectedRelationCountFromClear($clear)
+    {
+        $saleIds = is_object($clear) ? ($clear->saleIds ?? '') : ($clear['saleIds'] ?? '');
+        $purchaseIds = is_object($clear) ? ($clear->purchaseIds ?? '') : ($clear['purchaseIds'] ?? '');
+        $saleArr = self::parseIds($saleIds);
+        if (!empty($saleArr)) {
+            return count($saleArr);
+        }
+        return count(self::parseIds($purchaseIds));
+    }
+
+    /**
+     * @param int[] $clearIds
+     * @return array<int,int> clearId => 关系行数
+     */
+    public static function getRelationCountMap(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 clearId, COUNT(*) AS cnt FROM ' . $table
+            . ' WHERE clearId IN (' . implode(',', $clearIds) . ')'
+            . ' GROUP BY clearId'
+        )->queryAll();
+        $map = [];
+        foreach ($rows as $row) {
+            $map[intval($row['clearId'])] = intval($row['cnt']);
+        }
+        return $map;
+    }
+
+    /**
+     * 是否可跳过回填:关系行数已满、金额非空、无 gys2KmGys 旧格式
+     */
+    public static function shouldSkipBackfill($clear, $relationCount, array $emptyAmountClearIds, array $legacyGys2KmGysClearIds)
+    {
+        $clearId = intval(is_object($clear) ? ($clear->id ?? 0) : ($clear['id'] ?? 0));
+        if ($clearId <= 0) {
+            return true;
+        }
+        if (in_array($clearId, $legacyGys2KmGysClearIds, true)) {
+            return false;
+        }
+        if (in_array($clearId, $emptyAmountClearIds, true)) {
+            return false;
+        }
+        $expected = self::expectedRelationCountFromClear($clear);
+        if ($expected <= 0) {
+            return true;
+        }
+        return $relationCount >= $expected;
+    }
+
     /**
      * 批量查询关系表已存在的 clearId(避免逐条 SELECT)
      * @param int[] $clearIds
@@ -311,6 +433,212 @@ class OrderCgClearClass extends BaseClass
         return array_values(array_unique(array_map('intval', $rows)));
     }
 
+    const LEGACY_UNIQUE_INDEX = 'order_id_clear_id';
+    const RELATION_UNIQUE_INDEX = 'uk_clear_cg_order';
+
+    /**
+     * gys2KmGys 旧数据:orderId 误存基地采购单 id、cgId=0 的 clearId
+     * @param int[] $clearIds
+     * @return int[]
+     */
+    public static function getClearIdsWithLegacyGys2KmGysRows(array $clearIds)
+    {
+        $clearIds = array_values(array_unique(array_filter(array_map('intval', $clearIds))));
+        if (empty($clearIds)) {
+            return [];
+        }
+        $gys2KmGys = intval(dict::getDict('clearStyle', 'gys2KmGys'));
+        $relTable = self::$baseFile::tableName();
+        $clearTable = Clear::tableName();
+        $rows = Yii::$app->db->createCommand(
+            'SELECT DISTINCT r.clearId FROM ' . $relTable . ' r'
+            . ' INNER JOIN ' . $clearTable . ' c ON c.id = r.clearId AND c.clearStyle = :style'
+            . ' WHERE r.clearId IN (' . implode(',', $clearIds) . ')'
+            . ' AND r.cgId = 0 AND r.orderId > 0'
+        )->bindValue(':style', $gys2KmGys)->queryColumn();
+        if (empty($rows)) {
+            return [];
+        }
+        return array_values(array_unique(array_map('intval', $rows)));
+    }
+
+    /**
+     * 将 gys2KmGys 关系行从旧格式(orderId=基地采购单id,cgId=0)迁到新格式(cgId=基地采购单id,orderId=0)
+     * 需在删除旧唯一索引 (clearId,orderId) 之后执行,或首次迁移时由 rebuildRelationUniqueIndex 串联
+     */
+    public static function migrateLegacyGys2KmGysRows($dryRun = false, $batchSize = 500, $sleepMs = 50)
+    {
+        $gys2KmGys = intval(dict::getDict('clearStyle', 'gys2KmGys'));
+        $relTable = self::$baseFile::tableName();
+        $clearTable = Clear::tableName();
+        $migrated = 0;
+        $lastId = 0;
+
+        while (true) {
+            $rows = Yii::$app->db->createCommand(
+                'SELECT r.id, r.clearId, r.orderId, r.orderSn, r.cgSn FROM ' . $relTable . ' r'
+                . ' INNER JOIN ' . $clearTable . ' c ON c.id = r.clearId AND c.clearStyle = :style'
+                . ' WHERE r.id > :lastId AND r.cgId = 0 AND r.orderId > 0'
+                . ' ORDER BY r.id ASC LIMIT ' . intval($batchSize)
+            )->bindValues([
+                ':style' => $gys2KmGys,
+                ':lastId' => $lastId,
+            ])->queryAll();
+
+            if (empty($rows)) {
+                break;
+            }
+
+            foreach ($rows as $row) {
+                $lastId = intval($row['id']);
+                $relId = $lastId;
+                $clearId = intval($row['clearId']);
+                $cgId = intval($row['orderId']);
+                $cgSn = trim($row['cgSn'] ?? '');
+                if ($cgSn === '') {
+                    $cgSn = trim($row['orderSn'] ?? '');
+                }
+
+                if (!$dryRun) {
+                    $target = self::getByCondition([
+                        'clearId' => $clearId,
+                        'orderId' => 0,
+                        'cgId' => $cgId,
+                    ], true);
+                    if (!empty($target)) {
+                        Yii::$app->db->createCommand()->delete($relTable, ['id' => $relId])->execute();
+                    } else {
+                        Yii::$app->db->createCommand()->update($relTable, [
+                            'cgId' => $cgId,
+                            'cgSn' => $cgSn,
+                            'orderId' => 0,
+                            'orderSn' => '',
+                        ], ['id' => $relId])->execute();
+                    }
+                }
+                $migrated++;
+            }
+
+            if ($sleepMs > 0 && !$dryRun) {
+                usleep($sleepMs * 1000);
+            }
+        }
+
+        return $migrated;
+    }
+
+    /**
+     * 迁移单个 gys2KmGys 结账单下的旧格式关系行
+     */
+    public static function migrateLegacyGys2KmGysRowsByClearId($clearId, $dryRun = false)
+    {
+        $clearId = intval($clearId);
+        if ($clearId <= 0) {
+            return 0;
+        }
+        $gys2KmGys = intval(dict::getDict('clearStyle', 'gys2KmGys'));
+        $clear = Clear::find()->where(['id' => $clearId, 'clearStyle' => $gys2KmGys])->one();
+        if (empty($clear)) {
+            return 0;
+        }
+        $relTable = self::$baseFile::tableName();
+        $rows = Yii::$app->db->createCommand(
+            'SELECT id, clearId, orderId, orderSn, cgSn FROM ' . $relTable
+            . ' WHERE clearId = :clearId AND cgId = 0 AND orderId > 0'
+        )->bindValue(':clearId', $clearId)->queryAll();
+        if (empty($rows)) {
+            return 0;
+        }
+        $migrated = 0;
+        foreach ($rows as $row) {
+            $relId = intval($row['id']);
+            $cgId = intval($row['orderId']);
+            $cgSn = trim($row['cgSn'] ?? '');
+            if ($cgSn === '') {
+                $cgSn = trim($row['orderSn'] ?? '');
+            }
+            if (!$dryRun) {
+                $target = self::getByCondition([
+                    'clearId' => $clearId,
+                    'orderId' => 0,
+                    'cgId' => $cgId,
+                ], true);
+                if (!empty($target)) {
+                    Yii::$app->db->createCommand()->delete($relTable, ['id' => $relId])->execute();
+                } else {
+                    Yii::$app->db->createCommand()->update($relTable, [
+                        'cgId' => $cgId,
+                        'cgSn' => $cgSn,
+                        'orderId' => 0,
+                        'orderSn' => '',
+                    ], ['id' => $relId])->execute();
+                }
+            }
+            $migrated++;
+        }
+        return $migrated;
+    }
+
+    /**
+     * 重建关系表唯一索引:(clearId,cgId,orderId)
+     * 顺序:删旧索引 -> 迁 gys2KmGys 旧行 -> 建新索引
+     */
+    public static function rebuildRelationUniqueIndex($dryRun = false)
+    {
+        $table = self::$baseFile::tableName();
+        $indexMap = self::getTableIndexMap($table);
+        $hasLegacy = isset($indexMap[self::LEGACY_UNIQUE_INDEX]);
+        $hasNew = isset($indexMap[self::RELATION_UNIQUE_INDEX]);
+        $legacyRows = intval(Yii::$app->db->createCommand(
+            'SELECT COUNT(*) FROM ' . $table . ' r'
+            . ' INNER JOIN ' . Clear::tableName() . ' c ON c.id = r.clearId AND c.clearStyle = :style'
+            . ' WHERE r.cgId = 0 AND r.orderId > 0'
+        )->bindValue(':style', intval(dict::getDict('clearStyle', 'gys2KmGys')))->queryScalar());
+
+        if ($dryRun) {
+            return [
+                'hasLegacyIndex' => $hasLegacy,
+                'hasNewIndex' => $hasNew,
+                'legacyRows' => $legacyRows,
+            ];
+        }
+
+        if ($hasLegacy) {
+            Yii::$app->db->createCommand(
+                'ALTER TABLE ' . $table . ' DROP INDEX ' . self::LEGACY_UNIQUE_INDEX
+            )->execute();
+        }
+
+        if ($legacyRows > 0) {
+            self::migrateLegacyGys2KmGysRows(false, 500, 50);
+        }
+
+        if (!$hasNew) {
+            Yii::$app->db->createCommand(
+                'ALTER TABLE ' . $table . ' ADD UNIQUE KEY ' . self::RELATION_UNIQUE_INDEX . ' (clearId, cgId, orderId)'
+            )->execute();
+        }
+
+        return [
+            'droppedLegacyIndex' => $hasLegacy,
+            'addedNewIndex' => !$hasNew,
+            'migratedLegacyRows' => $legacyRows,
+        ];
+    }
+
+    protected static function getTableIndexMap($table)
+    {
+        $rows = Yii::$app->db->createCommand('SHOW INDEX FROM ' . $table)->queryAll();
+        $map = [];
+        foreach ($rows as $row) {
+            $name = $row['Key_name'] ?? '';
+            if ($name !== '') {
+                $map[$name] = true;
+            }
+        }
+        return $map;
+    }
+
     /**
      * 关系行 amount 取订单/采购单 actPrice
      */

+ 146 - 30
console/controllers/ClearController.php

@@ -37,6 +37,9 @@ class ClearController extends Controller
     /** @var int 每处理多少条输出一次进度,0=仅每批汇总 */
     public $logEvery = 500;
 
+    /** @var string 回填进度文件后缀(内部使用) */
+    private $backfillProgressKey = '';
+
     public function options($actionID)
     {
         return array_merge(parent::options($actionID), [
@@ -78,18 +81,114 @@ class ClearController extends Controller
     }
 
     /**
-     * 历史 xhClear 回填 xhOrderCgClear
+     * 回填 xhOrderCgClear:clearStyle 1(hd2Gys) + 2(gys2Hd)
      *
-     * 大数据量友好:按主键 id 分批倒序扫描,无长事务;批间休眠;关系表 clearId 批量去重。
+     * 用法:
+     *   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
+     */
+    public function actionBackfillOrderCgClearHdGys()
+    {
+        $clearStyles = [
+            intval(dict::getDict('clearStyle', 'hd2Gys')),
+            intval(dict::getDict('clearStyle', 'gys2Hd')),
+        ];
+        return $this->runBackfillOrderCgClear(
+            $clearStyles,
+            'hd_gys',
+            'hd2Gys(1)+gys2Hd(2)',
+            false
+        );
+    }
+
+    /**
+     * 回填 xhOrderCgClear:clearStyle 3(gys2KmGys) 供货商向昆明供货商结账
      *
      * 用法:
-     *   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
+     *   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
+     */
+    public function actionBackfillOrderCgClearKmGys()
+    {
+        $clearStyles = [
+            intval(dict::getDict('clearStyle', 'gys2KmGys')),
+        ];
+        return $this->runBackfillOrderCgClear(
+            $clearStyles,
+            '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
+     *
+     * 用法:
+     *   php yii clear/migrate-gys2-km-gys-order-cg-clear --dryRun=1
+     *   php yii clear/migrate-gys2-km-gys-order-cg-clear
+     *
+     * 注意:若仍存在唯一索引 order_id_clear_id(clearId,orderId),请先执行 rebuild-order-cg-clear-index
+     */
+    public function actionMigrateGys2KmGysOrderCgClear()
+    {
+        set_time_limit(0);
+        $this->stdout("迁移 gys2KmGys 关系行 | dryRun={$this->dryRun} batchSize={$this->batchSize}\n");
+        $count = OrderCgClearClass::migrateLegacyGys2KmGysRows((bool)$this->dryRun, $this->batchSize, $this->sleepMs);
+        $this->stdout(($this->dryRun ? '预览' : '完成') . ":处理 {$count} 行\n");
+        return ExitCode::OK;
+    }
+
+    /**
+     * 重建 xhOrderCgClear 唯一索引为 (clearId, cgId, orderId)
+     *
+     * 用法:
+     *   php yii clear/rebuild-order-cg-clear-index --dryRun=1
+     *   php yii clear/rebuild-order-cg-clear-index
+     */
+    public function actionRebuildOrderCgClearIndex()
+    {
+        set_time_limit(0);
+        if ($this->dryRun) {
+            $info = OrderCgClearClass::rebuildRelationUniqueIndex(true);
+            $this->stdout(sprintf(
+                "预览:旧索引=%s 新索引=%s 待迁移旧格式行=%d\n",
+                !empty($info['hasLegacyIndex']) ? '有' : '无',
+                !empty($info['hasNewIndex']) ? '有' : '无',
+                (int)($info['legacyRows'] ?? 0)
+            ));
+            return ExitCode::OK;
+        }
+        $this->stdout("开始重建唯一索引 uk_clear_cg_order(clearId,cgId,orderId)\n");
+        $result = OrderCgClearClass::rebuildRelationUniqueIndex(false);
+        $this->stdout(sprintf(
+            "完成:删旧索引=%s 建新索引=%s 迁移旧格式行=%d\n",
+            !empty($result['droppedLegacyIndex']) ? '是' : '否',
+            !empty($result['addedNewIndex']) ? '是' : '否',
+            (int)($result['migratedLegacyRows'] ?? 0)
+        ));
+        return ExitCode::OK;
+    }
+
+    /**
+     * @param int[] $clearStyles
+     * @param string $progressKey 进度文件后缀
+     * @param string $label 日志标签
+     * @param bool $checkLegacyGys2KmGys 是否检测 gys2KmGys 旧格式行
+     */
+    private function runBackfillOrderCgClear(array $clearStyles, $progressKey, $label, $checkLegacyGys2KmGys)
     {
         ini_set('memory_limit', '512M');
         set_time_limit(0);
@@ -103,12 +202,11 @@ class ClearController extends Controller
             return ExitCode::UNSPECIFIED_ERROR;
         }
 
-        $hd2Gys = intval(dict::getDict('clearStyle', 'hd2Gys'));
-        $gys2Hd = intval(dict::getDict('clearStyle', 'gys2Hd'));
+        $this->backfillProgressKey = $progressKey;
 
         if ($this->reset && $this->clearId <= 0) {
             $this->clearBackfillProgress();
-            $this->stdout("已重置回填进度\n");
+            $this->stdout("已重置回填进度 [{$progressKey}]\n");
         }
 
         $cursorId = $this->clearId > 0 ? null : $this->loadBackfillProgress();
@@ -118,7 +216,8 @@ class ClearController extends Controller
         $batchNo = 0;
 
         $this->stdout(sprintf(
-            "开始回填 xhOrderCgClear | 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,
             $this->sleepMs,
@@ -139,17 +238,18 @@ class ClearController extends Controller
                 }
             }
 
-            $list = $this->fetchClearBatch($hd2Gys, $gys2Hd, $cursorId, $fetchLimit);
+            $list = $this->fetchClearBatch($clearStyles, $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);
+            $legacyGys2KmGysClearIds = $checkLegacyGys2KmGys
+                ? OrderCgClearClass::getClearIdsWithLegacyGys2KmGysRows($clearIds)
+                : [];
+            $relationCountMap = OrderCgClearClass::getRelationCountMap($clearIds);
 
             foreach ($list as $clear) {
                 $total++;
@@ -163,16 +263,25 @@ class ClearController extends Controller
                     $skipped++;
                     continue;
                 }
-                if (isset($existingMap[$currentClearId]) && !isset($emptyAmountMap[$currentClearId])) {
+
+                $relationCount = $relationCountMap[$currentClearId] ?? 0;
+                if (OrderCgClearClass::shouldSkipBackfill(
+                    $clear,
+                    $relationCount,
+                    $emptyAmountClearIds,
+                    $legacyGys2KmGysClearIds
+                )) {
                     $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;
@@ -207,33 +316,39 @@ class ClearController extends Controller
         if (!$this->dryRun && $this->clearId <= 0 && $cursorId !== null) {
             $this->stdout("进度已保存,下次从 id < {$cursorId} 继续\n");
         }
-        $this->stdout("全部完成:扫描 {$total} 条 xhClear,写入 {$inserted} 行关系,跳过 {$skipped} 条\n");
+        $this->stdout("全部完成 [{$label}]:扫描 {$total} 条 xhClear,写入 {$inserted} 行关系,跳过 {$skipped} 条\n");
         return ExitCode::OK;
     }
 
     /**
      * 按主键倒序轻量拉取一批 xhClear(只查回填所需字段,LIMIT 走主键索引)
      */
-    private function fetchClearBatch($hd2Gys, $gys2Hd, $cursorId, $limit)
+    private function fetchClearBatch(array $clearStyles, $cursorId, $limit)
     {
+        $clearStyles = array_values(array_unique(array_filter(array_map('intval', $clearStyles))));
+        if (empty($clearStyles)) {
+            return [];
+        }
+        $inParts = [];
+        $params = [];
+        foreach ($clearStyles as $idx => $style) {
+            $key = ':s' . $idx;
+            $inParts[] = $key;
+            $params[$key] = $style;
+        }
+        $inSql = implode(',', $inParts);
+
         if ($this->clearId > 0) {
+            $params[':id'] = intval($this->clearId);
             $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();
+                . ' WHERE id = :id AND clearStyle IN (' . $inSql . ') LIMIT 1'
+            )->bindValues($params)->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,
-        ];
+            . ' WHERE clearStyle IN (' . $inSql . ')';
         if ($cursorId !== null) {
             $sql .= ' AND id < :cursorId';
             $params[':cursorId'] = intval($cursorId);
@@ -251,7 +366,8 @@ class ClearController extends Controller
 
     private function getBackfillProgressFile()
     {
-        return Yii::getAlias('@console/runtime/order_cg_clear_backfill.last_id');
+        $key = $this->backfillProgressKey !== '' ? $this->backfillProgressKey : 'all';
+        return Yii::getAlias('@console/runtime/order_cg_clear_backfill_' . $key . '.last_id');
     }
 
     private function loadBackfillProgress()