SaltController.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. <?php
  2. namespace console\controllers;
  3. use bizGhs\order\models\Order as GhsOrder;
  4. use bizHd\purchase\models\Purchase;
  5. use common\components\stringUtil;
  6. use Yii;
  7. use yii\console\Controller;
  8. use yii\console\ExitCode;
  9. /**
  10. * 补充历史订单 salt 字段
  11. *
  12. * 大表安全说明:
  13. * - 不做 COUNT(*),避免全表统计
  14. * - 默认 empty 模式只查 salt='' 的记录,补完后即停止,不会扫全表
  15. * - 查询均为普通 SELECT(MVCC 快照读),不会锁表;UPDATE 仅锁单行
  16. * - 大表建议先加索引: ALTER TABLE xhGhsOrder ADD INDEX idx_salt_id (salt, id);
  17. *
  18. * 用法:
  19. * php yii salt/fill
  20. * php yii salt/fill --dryRun=1
  21. * php yii salt/fill --batchSize=1000 --sleepMs=200
  22. * php yii salt/fill --mode=pk --scanSize=1000
  23. * php yii salt/fill-ghs-order
  24. * php yii salt/fill-cg
  25. * php yii salt/fill-clear
  26. * php yii salt/fill-settle
  27. */
  28. class SaltController extends Controller
  29. {
  30. /** @var int 每批处理条数 */
  31. public $batchSize = 1000;
  32. /** @var int pk 模式下每次主键游标扫描行数 */
  33. public $scanSize = 1000;
  34. /**
  35. * 扫描模式:
  36. * - empty: 只查 salt='',适合待补数据量较少(默认)
  37. * - pk: 按主键分段扫描,适合 salt 列无索引且 empty 模式执行计划不佳时
  38. */
  39. public $mode = 'empty';
  40. /** @var int 每批之间的休眠毫秒数,降低白天执行时的数据库压力 */
  41. public $sleepMs = 100;
  42. /** @var int 最多处理条数,0 表示全部 */
  43. public $limit = 0;
  44. /** @var int 是否仅预览,1=是 */
  45. public $dryRun = 0;
  46. public function options($actionID)
  47. {
  48. return array_merge(parent::options($actionID), [
  49. 'batchSize',
  50. 'scanSize',
  51. 'mode',
  52. 'sleepMs',
  53. 'limit',
  54. 'dryRun',
  55. ]);
  56. }
  57. /**
  58. * 补充 xhGhsOrder、xhCg 历史数据的 salt 字段
  59. */
  60. public function actionFill()
  61. {
  62. return $this->runFill([
  63. 'xhGhsOrder' => GhsOrder::tableName(),
  64. 'xhCg' => Purchase::tableName(),
  65. ]);
  66. }
  67. /**
  68. * 仅补充 xhGhsOrder
  69. */
  70. public function actionFillGhsOrder()
  71. {
  72. return $this->runFill([
  73. 'xhGhsOrder' => GhsOrder::tableName(),
  74. ]);
  75. }
  76. /**
  77. * 仅补充 xhCg
  78. */
  79. public function actionFillCg()
  80. {
  81. return $this->runFill([
  82. 'xhCg' => Purchase::tableName(),
  83. ]);
  84. }
  85. /**
  86. * 补充 xhClear 结账单历史数据的 salt 字段
  87. * 职责:利用主键游标进行分页查询,分批重新生成并设置 salt,确保线上大表安全
  88. */
  89. public function actionFillClear()
  90. {
  91. $db = Yii::$app->db;
  92. $this->stdout("开始分页重新设置 xhClear 表的 salt 字段...\n");
  93. $processed = 0;
  94. $lastId = null;
  95. $batchSize = $this->batchSize > 0 ? $this->batchSize : 1000;
  96. while (true) {
  97. // 如果设置了 limit 且已达到上限,则退出
  98. if ($this->limit > 0 && $processed >= $this->limit) {
  99. break;
  100. }
  101. // 计算当前批次需要查询的数量
  102. $currentBatchSize = $batchSize;
  103. if ($this->limit > 0) {
  104. $currentBatchSize = min($batchSize, $this->limit - $processed);
  105. }
  106. // 使用主键游标分页查询 ID(利用主键索引,性能极高,避免大表全表扫描)
  107. if ($lastId === null) {
  108. $ids = $db->createCommand("SELECT id FROM xhClear ORDER BY id DESC LIMIT :limit")
  109. ->bindValue(':limit', $currentBatchSize)
  110. ->queryColumn();
  111. } else {
  112. $ids = $db->createCommand("SELECT id FROM xhClear WHERE id < :lastId ORDER BY id DESC LIMIT :limit")
  113. ->bindValues([':lastId' => $lastId, ':limit' => $currentBatchSize])
  114. ->queryColumn();
  115. }
  116. if (empty($ids)) {
  117. break;
  118. }
  119. // 循环更新当前批次的记录
  120. foreach ($ids as $id) {
  121. if ($this->dryRun) {
  122. $this->stdout("[dry-run] xhClear id={$id}\n");
  123. $processed++;
  124. continue;
  125. }
  126. // 重新生成 10 位随机 salt 并更新
  127. $salt = stringUtil::charsShuffleLowerCase(10);
  128. $db->createCommand("UPDATE xhClear SET salt = :salt WHERE id = :id")
  129. ->bindValues([':salt' => $salt, ':id' => $id])
  130. ->execute();
  131. $processed++;
  132. // 每次更新后休眠,降低数据库写入压力
  133. if ($this->sleepMs > 0) {
  134. usleep($this->sleepMs * 1000);
  135. }
  136. }
  137. // 更新游标为当前批次中最小的 ID
  138. $lastId = min($ids);
  139. $this->stdout("xhClear 已处理 {$processed} 条记录,当前 ID 游标: {$lastId}\n");
  140. }
  141. $this->stdout("xhClear salt 全部重新设置完成,共处理 {$processed} 条记录\n");
  142. return ExitCode::OK;
  143. }
  144. /**
  145. * 补充 xhSettle 结算单历史数据的 salt 字段
  146. * 职责:利用主键游标进行分页查询,分批重新生成并设置 salt,确保线上大表安全
  147. */
  148. public function actionFillSettle()
  149. {
  150. $db = Yii::$app->db;
  151. $this->stdout("开始分页重新设置 xhSettle 表的 salt 字段...\n");
  152. $processed = 0;
  153. $lastId = null;
  154. $batchSize = $this->batchSize > 0 ? $this->batchSize : 1000;
  155. while (true) {
  156. // 如果设置了 limit 且已达到上限,则退出
  157. if ($this->limit > 0 && $processed >= $this->limit) {
  158. break;
  159. }
  160. // 计算当前批次需要查询的数量
  161. $currentBatchSize = $batchSize;
  162. if ($this->limit > 0) {
  163. $currentBatchSize = min($batchSize, $this->limit - $processed);
  164. }
  165. // 使用主键游标分页查询 ID(利用主键索引,性能极高,避免大表全表扫描)
  166. if ($lastId === null) {
  167. $ids = $db->createCommand("SELECT id FROM xhSettle ORDER BY id DESC LIMIT :limit")
  168. ->bindValue(':limit', $currentBatchSize)
  169. ->queryColumn();
  170. } else {
  171. $ids = $db->createCommand("SELECT id FROM xhSettle WHERE id < :lastId ORDER BY id DESC LIMIT :limit")
  172. ->bindValues([':lastId' => $lastId, ':limit' => $currentBatchSize])
  173. ->queryColumn();
  174. }
  175. if (empty($ids)) {
  176. break;
  177. }
  178. // 循环更新当前批次的记录
  179. foreach ($ids as $id) {
  180. if ($this->dryRun) {
  181. $this->stdout("[dry-run] xhSettle id={$id}\n");
  182. $processed++;
  183. continue;
  184. }
  185. // 重新生成 10 位随机 salt 并更新
  186. $salt = stringUtil::charsShuffleLowerCase(10);
  187. $db->createCommand("UPDATE xhSettle SET salt = :salt WHERE id = :id")
  188. ->bindValues([':salt' => $salt, ':id' => $id])
  189. ->execute();
  190. $processed++;
  191. // 每次更新后休眠,降低数据库写入压力
  192. if ($this->sleepMs > 0) {
  193. usleep($this->sleepMs * 1000);
  194. }
  195. }
  196. // 更新游标为当前批次中最小的 ID
  197. $lastId = min($ids);
  198. $this->stdout("xhSettle 已处理 {$processed} 条记录,当前 ID 游标: {$lastId}\n");
  199. }
  200. $this->stdout("xhSettle salt 全部重新设置完成,共处理 {$processed} 条记录\n");
  201. return ExitCode::OK;
  202. }
  203. private function runFill(array $tables)
  204. {
  205. ini_set('memory_limit', '512M');
  206. set_time_limit(0);
  207. if ($this->batchSize < 1) {
  208. $this->stderr("batchSize 必须大于 0\n");
  209. return ExitCode::UNSPECIFIED_ERROR;
  210. }
  211. if ($this->scanSize < 1) {
  212. $this->stderr("scanSize 必须大于 0\n");
  213. return ExitCode::UNSPECIFIED_ERROR;
  214. }
  215. if (!in_array($this->mode, ['empty', 'pk'], true)) {
  216. $this->stderr("mode 仅支持 empty 或 pk\n");
  217. return ExitCode::UNSPECIFIED_ERROR;
  218. }
  219. $exitCode = ExitCode::OK;
  220. foreach ($tables as $label => $tableName) {
  221. $code = $this->fillTable($label, $tableName);
  222. if ($code !== ExitCode::OK) {
  223. $exitCode = $code;
  224. }
  225. }
  226. return $exitCode;
  227. }
  228. private function fillTable($label, $tableName)
  229. {
  230. $db = Yii::$app->db;
  231. $db->createCommand('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED')->execute();
  232. $this->stdout("{$label}({$tableName}) 开始补充 salt,模式: {$this->mode}\n");
  233. if ($this->mode === 'empty') {
  234. $processed = $this->fillByEmptySalt($label, $tableName);
  235. } else {
  236. $processed = $this->fillByPrimaryKeyScan($label, $tableName);
  237. }
  238. $this->stdout("{$label} 完成,本次处理 {$processed} 条\n");
  239. return ExitCode::OK;
  240. }
  241. /**
  242. * 只查 salt='' 的记录,从最大 id 向小 id 分批处理
  243. */
  244. private function fillByEmptySalt($label, $tableName)
  245. {
  246. $db = Yii::$app->db;
  247. $processed = 0;
  248. $lastId = null;
  249. while (true) {
  250. if ($this->limit > 0 && $processed >= $this->limit) {
  251. break;
  252. }
  253. $batchLimit = $this->limit > 0 ? min($this->batchSize, $this->limit - $processed) : $this->batchSize;
  254. if ($lastId === null) {
  255. $rows = $db->createCommand(
  256. "SELECT id FROM {$tableName} WHERE salt = '' ORDER BY id DESC LIMIT {$batchLimit}"
  257. )->queryColumn();
  258. } else {
  259. $rows = $db->createCommand(
  260. "SELECT id FROM {$tableName} WHERE salt = '' AND id < :lastId ORDER BY id DESC LIMIT {$batchLimit}"
  261. )->bindValue(':lastId', $lastId)->queryColumn();
  262. }
  263. if (empty($rows)) {
  264. break;
  265. }
  266. $processed += $this->updateIds($label, $tableName, $rows);
  267. $lastId = (int)min($rows);
  268. $this->stdout("{$label} 已处理 {$processed} 条,当前 id >= {$lastId}\n");
  269. $this->sleepBetweenBatch();
  270. }
  271. return $processed;
  272. }
  273. /**
  274. * 按主键从最大 id 向小 id 分段扫描;适合 empty 模式执行计划不佳时使用
  275. */
  276. private function fillByPrimaryKeyScan($label, $tableName)
  277. {
  278. $db = Yii::$app->db;
  279. $processed = 0;
  280. $lastId = null;
  281. while (true) {
  282. if ($this->limit > 0 && $processed >= $this->limit) {
  283. break;
  284. }
  285. if ($lastId === null) {
  286. $ids = $db->createCommand(
  287. "SELECT id FROM {$tableName} ORDER BY id DESC LIMIT {$this->scanSize}"
  288. )->queryColumn();
  289. } else {
  290. $ids = $db->createCommand(
  291. "SELECT id FROM {$tableName} WHERE id < :lastId ORDER BY id DESC LIMIT {$this->scanSize}"
  292. )->bindValue(':lastId', $lastId)->queryColumn();
  293. }
  294. if (empty($ids)) {
  295. break;
  296. }
  297. $lastId = (int)min($ids);
  298. $minId = (int)min($ids);
  299. $maxId = (int)max($ids);
  300. $emptyIds = $db->createCommand(
  301. "SELECT id FROM {$tableName} WHERE id >= :minId AND id <= :maxId AND salt = '' ORDER BY id DESC"
  302. )->bindValues([
  303. ':minId' => $minId,
  304. ':maxId' => $maxId,
  305. ])->queryColumn();
  306. if (!empty($emptyIds)) {
  307. if ($this->limit > 0) {
  308. $emptyIds = array_slice($emptyIds, 0, $this->limit - $processed);
  309. }
  310. $processed += $this->updateIds($label, $tableName, $emptyIds);
  311. }
  312. $this->stdout("{$label} 已处理 {$processed} 条,已扫描到 id >= {$lastId}\n");
  313. $this->sleepBetweenBatch();
  314. }
  315. return $processed;
  316. }
  317. private function updateIds($label, $tableName, array $ids)
  318. {
  319. $db = Yii::$app->db;
  320. $updated = 0;
  321. foreach ($ids as $id) {
  322. $id = (int)$id;
  323. if ($id <= 0) {
  324. continue;
  325. }
  326. if ($this->dryRun) {
  327. $this->stdout("[dry-run] {$label} id={$id}\n");
  328. $updated++;
  329. continue;
  330. }
  331. $salt = stringUtil::charsShuffleLowerCase(10);
  332. $affected = $db->createCommand(
  333. "UPDATE {$tableName} SET salt = :salt WHERE id = :id AND salt = ''"
  334. )->bindValues([
  335. ':salt' => $salt,
  336. ':id' => $id,
  337. ])->execute();
  338. if ($affected > 0) {
  339. $updated++;
  340. }
  341. }
  342. return $updated;
  343. }
  344. private function sleepBetweenBatch()
  345. {
  346. if ($this->sleepMs > 0) {
  347. usleep($this->sleepMs * 1000);
  348. }
  349. }
  350. }