runFill([ 'xhGhsOrder' => GhsOrder::tableName(), 'xhCg' => Purchase::tableName(), ]); } /** * 仅补充 xhGhsOrder */ public function actionFillGhsOrder() { return $this->runFill([ 'xhGhsOrder' => GhsOrder::tableName(), ]); } /** * 仅补充 xhCg */ public function actionFillCg() { return $this->runFill([ 'xhCg' => Purchase::tableName(), ]); } private function runFill(array $tables) { ini_set('memory_limit', '512M'); set_time_limit(0); if ($this->batchSize < 1) { $this->stderr("batchSize 必须大于 0\n"); return ExitCode::UNSPECIFIED_ERROR; } if ($this->scanSize < 1) { $this->stderr("scanSize 必须大于 0\n"); return ExitCode::UNSPECIFIED_ERROR; } if (!in_array($this->mode, ['empty', 'pk'], true)) { $this->stderr("mode 仅支持 empty 或 pk\n"); return ExitCode::UNSPECIFIED_ERROR; } $exitCode = ExitCode::OK; foreach ($tables as $label => $tableName) { $code = $this->fillTable($label, $tableName); if ($code !== ExitCode::OK) { $exitCode = $code; } } return $exitCode; } private function fillTable($label, $tableName) { $db = Yii::$app->db; $db->createCommand('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED')->execute(); $this->stdout("{$label}({$tableName}) 开始补充 salt,模式: {$this->mode}\n"); if ($this->mode === 'empty') { $processed = $this->fillByEmptySalt($label, $tableName); } else { $processed = $this->fillByPrimaryKeyScan($label, $tableName); } $this->stdout("{$label} 完成,本次处理 {$processed} 条\n"); return ExitCode::OK; } /** * 只查 salt='' 的记录,从最大 id 向小 id 分批处理 */ private function fillByEmptySalt($label, $tableName) { $db = Yii::$app->db; $processed = 0; $lastId = null; while (true) { if ($this->limit > 0 && $processed >= $this->limit) { break; } $batchLimit = $this->limit > 0 ? min($this->batchSize, $this->limit - $processed) : $this->batchSize; if ($lastId === null) { $rows = $db->createCommand( "SELECT id FROM {$tableName} WHERE salt = '' ORDER BY id DESC LIMIT {$batchLimit}" )->queryColumn(); } else { $rows = $db->createCommand( "SELECT id FROM {$tableName} WHERE salt = '' AND id < :lastId ORDER BY id DESC LIMIT {$batchLimit}" )->bindValue(':lastId', $lastId)->queryColumn(); } if (empty($rows)) { break; } $processed += $this->updateIds($label, $tableName, $rows); $lastId = (int)min($rows); $this->stdout("{$label} 已处理 {$processed} 条,当前 id >= {$lastId}\n"); $this->sleepBetweenBatch(); } return $processed; } /** * 按主键从最大 id 向小 id 分段扫描;适合 empty 模式执行计划不佳时使用 */ private function fillByPrimaryKeyScan($label, $tableName) { $db = Yii::$app->db; $processed = 0; $lastId = null; while (true) { if ($this->limit > 0 && $processed >= $this->limit) { break; } if ($lastId === null) { $ids = $db->createCommand( "SELECT id FROM {$tableName} ORDER BY id DESC LIMIT {$this->scanSize}" )->queryColumn(); } else { $ids = $db->createCommand( "SELECT id FROM {$tableName} WHERE id < :lastId ORDER BY id DESC LIMIT {$this->scanSize}" )->bindValue(':lastId', $lastId)->queryColumn(); } if (empty($ids)) { break; } $lastId = (int)min($ids); $minId = (int)min($ids); $maxId = (int)max($ids); $emptyIds = $db->createCommand( "SELECT id FROM {$tableName} WHERE id >= :minId AND id <= :maxId AND salt = '' ORDER BY id DESC" )->bindValues([ ':minId' => $minId, ':maxId' => $maxId, ])->queryColumn(); if (!empty($emptyIds)) { if ($this->limit > 0) { $emptyIds = array_slice($emptyIds, 0, $this->limit - $processed); } $processed += $this->updateIds($label, $tableName, $emptyIds); } $this->stdout("{$label} 已处理 {$processed} 条,已扫描到 id >= {$lastId}\n"); $this->sleepBetweenBatch(); } return $processed; } private function updateIds($label, $tableName, array $ids) { $db = Yii::$app->db; $updated = 0; foreach ($ids as $id) { $id = (int)$id; if ($id <= 0) { continue; } if ($this->dryRun) { $this->stdout("[dry-run] {$label} id={$id}\n"); $updated++; continue; } $salt = stringUtil::charsShuffleLowerCase(10); $affected = $db->createCommand( "UPDATE {$tableName} SET salt = :salt WHERE id = :id AND salt = ''" )->bindValues([ ':salt' => $salt, ':id' => $id, ])->execute(); if ($affected > 0) { $updated++; } } return $updated; } private function sleepBetweenBatch() { if ($this->sleepMs > 0) { usleep($this->sleepMs * 1000); } } }