SaltController.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  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. * 职责:查询全部 xhClear 记录,循环重新生成并设置 salt
  88. */
  89. public function actionFillClear()
  90. {
  91. $db = Yii::$app->db;
  92. // 查询全部结账单 ID
  93. $ids = $db->createCommand("SELECT id FROM xhClear ORDER BY id DESC")->queryColumn();
  94. $count = 0;
  95. foreach ($ids as $id) {
  96. // 支持 limit 参数限制更新数量
  97. if ($this->limit > 0 && $count >= $this->limit) {
  98. break;
  99. }
  100. if ($this->dryRun) {
  101. $this->stdout("[dry-run] xhClear id={$id}\n");
  102. $count++;
  103. continue;
  104. }
  105. // 重新生成 10 位随机 salt 并更新
  106. $salt = stringUtil::charsShuffleLowerCase(10);
  107. $db->createCommand("UPDATE xhClear SET salt = :salt WHERE id = :id")
  108. ->bindValues([':salt' => $salt, ':id' => $id])
  109. ->execute();
  110. $count++;
  111. // 支持 sleepMs 降低数据库压力
  112. if ($this->sleepMs > 0) {
  113. usleep($this->sleepMs * 1000);
  114. }
  115. }
  116. $this->stdout("xhClear salt 全部重新设置完成,共处理 {$count} 条记录\n");
  117. return ExitCode::OK;
  118. }
  119. /**
  120. * 补充 xhSettle 结算单历史数据的 salt 字段
  121. * 职责:查询全部 xhSettle 记录,循环重新生成并设置 salt
  122. */
  123. public function actionFillSettle()
  124. {
  125. $db = Yii::$app->db;
  126. // 查询全部结算单 ID
  127. $ids = $db->createCommand("SELECT id FROM xhSettle ORDER BY id DESC")->queryColumn();
  128. $count = 0;
  129. foreach ($ids as $id) {
  130. // 支持 limit 参数限制更新数量
  131. if ($this->limit > 0 && $count >= $this->limit) {
  132. break;
  133. }
  134. if ($this->dryRun) {
  135. $this->stdout("[dry-run] xhSettle id={$id}\n");
  136. $count++;
  137. continue;
  138. }
  139. // 重新生成 10 位随机 salt 并更新
  140. $salt = stringUtil::charsShuffleLowerCase(10);
  141. $db->createCommand("UPDATE xhSettle SET salt = :salt WHERE id = :id")
  142. ->bindValues([':salt' => $salt, ':id' => $id])
  143. ->execute();
  144. $count++;
  145. // 支持 sleepMs 降低数据库压力
  146. if ($this->sleepMs > 0) {
  147. usleep($this->sleepMs * 1000);
  148. }
  149. }
  150. $this->stdout("xhSettle salt 全部重新设置完成,共处理 {$count} 条记录\n");
  151. return ExitCode::OK;
  152. }
  153. private function runFill(array $tables)
  154. {
  155. ini_set('memory_limit', '512M');
  156. set_time_limit(0);
  157. if ($this->batchSize < 1) {
  158. $this->stderr("batchSize 必须大于 0\n");
  159. return ExitCode::UNSPECIFIED_ERROR;
  160. }
  161. if ($this->scanSize < 1) {
  162. $this->stderr("scanSize 必须大于 0\n");
  163. return ExitCode::UNSPECIFIED_ERROR;
  164. }
  165. if (!in_array($this->mode, ['empty', 'pk'], true)) {
  166. $this->stderr("mode 仅支持 empty 或 pk\n");
  167. return ExitCode::UNSPECIFIED_ERROR;
  168. }
  169. $exitCode = ExitCode::OK;
  170. foreach ($tables as $label => $tableName) {
  171. $code = $this->fillTable($label, $tableName);
  172. if ($code !== ExitCode::OK) {
  173. $exitCode = $code;
  174. }
  175. }
  176. return $exitCode;
  177. }
  178. private function fillTable($label, $tableName)
  179. {
  180. $db = Yii::$app->db;
  181. $db->createCommand('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED')->execute();
  182. $this->stdout("{$label}({$tableName}) 开始补充 salt,模式: {$this->mode}\n");
  183. if ($this->mode === 'empty') {
  184. $processed = $this->fillByEmptySalt($label, $tableName);
  185. } else {
  186. $processed = $this->fillByPrimaryKeyScan($label, $tableName);
  187. }
  188. $this->stdout("{$label} 完成,本次处理 {$processed} 条\n");
  189. return ExitCode::OK;
  190. }
  191. /**
  192. * 只查 salt='' 的记录,从最大 id 向小 id 分批处理
  193. */
  194. private function fillByEmptySalt($label, $tableName)
  195. {
  196. $db = Yii::$app->db;
  197. $processed = 0;
  198. $lastId = null;
  199. while (true) {
  200. if ($this->limit > 0 && $processed >= $this->limit) {
  201. break;
  202. }
  203. $batchLimit = $this->limit > 0 ? min($this->batchSize, $this->limit - $processed) : $this->batchSize;
  204. if ($lastId === null) {
  205. $rows = $db->createCommand(
  206. "SELECT id FROM {$tableName} WHERE salt = '' ORDER BY id DESC LIMIT {$batchLimit}"
  207. )->queryColumn();
  208. } else {
  209. $rows = $db->createCommand(
  210. "SELECT id FROM {$tableName} WHERE salt = '' AND id < :lastId ORDER BY id DESC LIMIT {$batchLimit}"
  211. )->bindValue(':lastId', $lastId)->queryColumn();
  212. }
  213. if (empty($rows)) {
  214. break;
  215. }
  216. $processed += $this->updateIds($label, $tableName, $rows);
  217. $lastId = (int)min($rows);
  218. $this->stdout("{$label} 已处理 {$processed} 条,当前 id >= {$lastId}\n");
  219. $this->sleepBetweenBatch();
  220. }
  221. return $processed;
  222. }
  223. /**
  224. * 按主键从最大 id 向小 id 分段扫描;适合 empty 模式执行计划不佳时使用
  225. */
  226. private function fillByPrimaryKeyScan($label, $tableName)
  227. {
  228. $db = Yii::$app->db;
  229. $processed = 0;
  230. $lastId = null;
  231. while (true) {
  232. if ($this->limit > 0 && $processed >= $this->limit) {
  233. break;
  234. }
  235. if ($lastId === null) {
  236. $ids = $db->createCommand(
  237. "SELECT id FROM {$tableName} ORDER BY id DESC LIMIT {$this->scanSize}"
  238. )->queryColumn();
  239. } else {
  240. $ids = $db->createCommand(
  241. "SELECT id FROM {$tableName} WHERE id < :lastId ORDER BY id DESC LIMIT {$this->scanSize}"
  242. )->bindValue(':lastId', $lastId)->queryColumn();
  243. }
  244. if (empty($ids)) {
  245. break;
  246. }
  247. $lastId = (int)min($ids);
  248. $minId = (int)min($ids);
  249. $maxId = (int)max($ids);
  250. $emptyIds = $db->createCommand(
  251. "SELECT id FROM {$tableName} WHERE id >= :minId AND id <= :maxId AND salt = '' ORDER BY id DESC"
  252. )->bindValues([
  253. ':minId' => $minId,
  254. ':maxId' => $maxId,
  255. ])->queryColumn();
  256. if (!empty($emptyIds)) {
  257. if ($this->limit > 0) {
  258. $emptyIds = array_slice($emptyIds, 0, $this->limit - $processed);
  259. }
  260. $processed += $this->updateIds($label, $tableName, $emptyIds);
  261. }
  262. $this->stdout("{$label} 已处理 {$processed} 条,已扫描到 id >= {$lastId}\n");
  263. $this->sleepBetweenBatch();
  264. }
  265. return $processed;
  266. }
  267. private function updateIds($label, $tableName, array $ids)
  268. {
  269. $db = Yii::$app->db;
  270. $updated = 0;
  271. foreach ($ids as $id) {
  272. $id = (int)$id;
  273. if ($id <= 0) {
  274. continue;
  275. }
  276. if ($this->dryRun) {
  277. $this->stdout("[dry-run] {$label} id={$id}\n");
  278. $updated++;
  279. continue;
  280. }
  281. $salt = stringUtil::charsShuffleLowerCase(10);
  282. $affected = $db->createCommand(
  283. "UPDATE {$tableName} SET salt = :salt WHERE id = :id AND salt = ''"
  284. )->bindValues([
  285. ':salt' => $salt,
  286. ':id' => $id,
  287. ])->execute();
  288. if ($affected > 0) {
  289. $updated++;
  290. }
  291. }
  292. return $updated;
  293. }
  294. private function sleepBetweenBatch()
  295. {
  296. if ($this->sleepMs > 0) {
  297. usleep($this->sleepMs * 1000);
  298. }
  299. }
  300. }