AutoSubmeter.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. <?php
  2. declare (strict_types = 1);
  3. namespace app\api\command;
  4. use app\common\model\SubmeterModel;
  5. use think\console\Command;
  6. use think\console\Input;
  7. use think\console\Output;
  8. use think\facade\Db;
  9. use utils\RedisCache;
  10. /**
  11. * 自动分表备份 by wes 每天凌晨 0点后运行,运行两次间隔10分钟
  12. * Class AutoSubmeter
  13. * @package app\api\command
  14. */
  15. class AutoSubmeter extends Command
  16. {
  17. protected $cacheTime = 86400; // 一天
  18. protected $tables = [
  19. // time 备份分表周期,save_time 数据保存时间天,time_field 时间字段
  20. 'money_log'=>['time'=>2, 'save_time'=> 14,'time_field'=>'create_at'],
  21. 'box_record'=>['time'=>2, 'save_time'=> 10,'time_field'=>'create_time'],
  22. 'box_mid_handle'=>['time'=>2, 'save_time'=> 10,'time_field'=>'create_time'],
  23. 'score_log'=>['time'=>2, 'save_time'=> 10,'time_field'=>'create_at'],
  24. 'box_handle'=>['time'=>1, 'save_time'=> 0,'time_field'=>'create_time'],
  25. ];
  26. protected function configure()
  27. {
  28. $this->setName('auto_backup_submeter')
  29. ->setDescription('the auto_backup_submeter command');
  30. }
  31. /**
  32. * 处理分表
  33. * @param Input $input
  34. * @param Output $output
  35. * @return int
  36. */
  37. protected function execute(Input $input, Output $output)
  38. {
  39. if(date('H:i') > '08:00'){
  40. echo json_encode(['code'=>500,'msg'=>'不在运行时间段内,请于早上0点~8点执行','date'=>date('Y-m-d H:i:s')], 256)."\n";
  41. return false;
  42. }
  43. if(RedisCache::get("caches:submeter:lock")){
  44. echo json_encode(['code'=>500,'msg'=>'请不要频繁执行,稍后再试~','date'=>date('Y-m-d H:i:s')], 256)."\n";
  45. return false;
  46. }
  47. RedisCache::setnx("caches:submeter:lock", date('Y-m-d H:i:s'), rand(10, 20));
  48. Db::startTrans();
  49. try {
  50. $count = 0;
  51. $hasCount = 0;
  52. $cacheKey = "caches:submeter:backup:";
  53. foreach ($this->tables as $tableName => $item) {
  54. $time = isset($item['time']) ? $item['time'] : 0; // 备份周期
  55. $saveTime = isset($item['save_time']) ? $item['save_time'] : 0; // 保存时间
  56. $timeField = isset($item['time_field']) ? $item['time_field'] : 0; // 时间字段
  57. $time = $time ? $time : 2;
  58. // 验证是否备份
  59. $dateTime = strtotime(date('Y-m-d')) - 86400;
  60. if($tableName == 'box_handle'){
  61. $index = 'all';
  62. $newTableName = $tableName . '_' . $index;
  63. $date = date('Y-m-d H:i:s', $dateTime);
  64. if ($this->checkCatchByTime($tableName, $index, $time)) {
  65. $hasCount++;
  66. RedisCache::set($cacheKey . "{$newTableName}_error", ['data' => $item,'name'=>$newTableName, 'error' => '未到备份时间间隔'], $this->cacheTime);
  67. continue;
  68. }
  69. // 复制数据
  70. $prefix = env('database.prefix','db_');
  71. $count1 = Db::name($tableName)->where('is_delete', 2)->whereNotIn('id',Db::name($newTableName)->column('id'))->count();
  72. if(!$count1){
  73. $hasCount++;
  74. RedisCache::set($cacheKey . "{$newTableName}_error", ['sql'=>Db::name($tableName)->getLastSql(),'count'=>$count1,'data' => $item,'name'=>$newTableName, 'error' => '没有数据处理'], $this->cacheTime);
  75. continue;
  76. }
  77. $count2 = Db::name($newTableName)->count();
  78. $sql = "insert into `{$prefix}{$newTableName}` (select * from `{$prefix}{$tableName}` where `id` not in (select `id` from `{$prefix}{$newTableName}`) and `is_delete` = 2 and `create_time` < '".date('Y-m-d')."')";
  79. $res = Db::query($sql);
  80. $count3 = Db::name($newTableName)->count();
  81. if (($count3 - $count2) < $count1) {
  82. RedisCache::set($cacheKey."{$newTableName}_error", ['count'=>$count,'sql1'=>$sql,'msg'=> '复制分表数据错误','data'=> $item,'date'=>date('Y-m-d H:i:s')], $this->cacheTime);
  83. sr_throw("复制[{$newTableName}]分表数据错误");
  84. }
  85. // 清理原表多余数据
  86. Db::name($tableName)->where(['is_delete'=>1])->where('update_time','<',$date)->delete();
  87. $res = Db::name($tableName)->whereIn('id',Db::name($newTableName)->column('id'))->update(['is_delete'=>1,'update_time'=>date('Y-m-d H:i:s')]);
  88. if(!$res){
  89. RedisCache::set($cacheKey."{$newTableName}_error", ['count'=>$count,'sql'=>$sql,'sql1'=>Db::name($tableName)->getLastSql(),'msg'=> '清理备份分表数据错误','data'=> $item,'date'=>date('Y-m-d H:i:s')], $this->cacheTime);
  90. sr_throw("清理[{$tableName}]表旧数据错误");
  91. }
  92. }else{
  93. $index = date('Ymd', $dateTime);
  94. $newTableName = $tableName . '_' . $index;
  95. if ($this->checkCatchByTime($tableName, $index, $time)) {
  96. $hasCount++;
  97. RedisCache::set($cacheKey . "{$newTableName}_error", ['data' => $item,'name'=>$newTableName, 'error' => '未到备份时间间隔'], $this->cacheTime);
  98. continue;
  99. }
  100. if ($this->checkCatch($tableName, $index)) {
  101. $hasCount++;
  102. RedisCache::set($cacheKey . "{$newTableName}_error", ['data' => $item,'name'=>$newTableName, 'error' => '已经处理过'], $this->cacheTime);
  103. continue;
  104. }
  105. // 备份处理(1:复制表,2-清除日期间隔数据)
  106. $prefix = env('database.prefix','db_');
  107. $date = date('Y-m-d H:i:s', $dateTime);
  108. // 复制表结构
  109. $sql = "create table IF not exists `{$prefix}{$newTableName}` like `{$prefix}{$tableName}`";
  110. Db::query($sql);
  111. // 复制数据
  112. $count1 = Db::name($tableName)->count();
  113. $sql1 = "insert ignore `{$prefix}{$newTableName}` select * from `{$prefix}{$tableName}`";
  114. Db::query($sql1);
  115. if ($count1 != Db::name($newTableName)->count()) {
  116. RedisCache::set($cacheKey."{$newTableName}_error", ['sql'=>$sql,'sql1'=>$sql1,'msg'=> '复制分表数据错误','data'=> $item,'date'=>date('Y-m-d H:i:s')], $this->cacheTime);
  117. sr_throw("复制备份[{$newTableName}]分表数据错误");
  118. }
  119. // 清理原表多余数据
  120. $expiredRow = Db::name($tableName)
  121. ->where($timeField,'<', $date)
  122. ->where(function ($query) use($tableName){
  123. if($tableName == 'money_log'){
  124. $query->whereNotIn('type', [8]);
  125. }
  126. })
  127. ->count();
  128. $isDelete = Db::name($tableName)
  129. ->where($timeField,'<', $date)
  130. ->where(function ($query) use($tableName){
  131. if($tableName == 'money_log'){
  132. $query->whereNotIn('type', [8]);
  133. }
  134. })
  135. ->delete();
  136. if($expiredRow && !$isDelete){
  137. RedisCache::set($cacheKey."{$newTableName}_error", ['sql'=>$sql,'msg'=> '清理备份分表数据错误','data'=> $item,'date'=>date('Y-m-d H:i:s')], $this->cacheTime);
  138. sr_throw("清理[{$tableName}]表旧数据错误");
  139. }
  140. }
  141. $data = [
  142. 'table_name'=> $tableName,
  143. 'table_index'=> $index,
  144. 'update_time'=> date('Y-m-d H:i:s'),
  145. 'expired_at'=> $saveTime?date('Y-m-d H:i:s', time() + $saveTime * 24 * 3600):'',
  146. 'status'=>1
  147. ];
  148. if(!SubmeterModel::insertGetId($data)){
  149. RedisCache::set($cacheKey."{$newTableName}_error", ['sql'=>$sql,'msg'=> '处理分表记录错误','log'=>$data,'data'=> $item,'date'=>date('Y-m-d H:i:s')], $this->cacheTime);
  150. sr_throw("处理[{$newTableName}]分表记录错误");
  151. }
  152. RedisCache::set($cacheKey."{$newTableName}_success", ['sql'=>$sql,'msg'=> '处理分表备份成功','log'=>$data,'data'=> $item,'date'=>date('Y-m-d H:i:s')], $this->cacheTime);
  153. $count++;
  154. }
  155. Db::commit();
  156. echo json_encode(['code'=>200,'msg'=>"运行成功:共处理{$count}个表备份,{$hasCount}个已备份过",'date'=>date('Y-m-d H:i:s')],256)."\n";
  157. }catch (\Exception $exception){
  158. Db::rollback();
  159. RedisCache::clear("caches:submeter:lock");
  160. RedisCache::set("caches:submeter:error", ['msg'=> '运行错误:'.$exception->getMessage(),'trace'=>$exception->getTrace(),'date'=>date('Y-m-d H:i:s')], $this->cacheTime);
  161. echo json_encode(['code'=>500,'msg'=>'运行错误:'.$exception->getMessage(),'date'=>date('Y-m-d H:i:s')], 256)."\n";
  162. }
  163. return true;
  164. }
  165. /**
  166. * 验证是否备份分表过
  167. * @param $tableName 表名
  168. * @return bool|mixed
  169. */
  170. protected function checkCatch($tableName, $index)
  171. {
  172. $cacheKey = "caches:submeter:check:{$tableName}_{$index}";
  173. if(RedisCache::get($cacheKey)){
  174. return true;
  175. }
  176. $data = SubmeterModel::where(['table_name'=> $tableName,'table_index'=>$index,'status'=>1])->value('id');
  177. if($data){
  178. RedisCache::set($cacheKey, $data, rand(10, 20));
  179. }
  180. return $data;
  181. }
  182. /**
  183. * 验证是否备份分表过
  184. * @param $tableName 表名
  185. * @return bool|mixed
  186. */
  187. protected function checkCatchByTime($tableName, $index, $day)
  188. {
  189. $cacheKey = "caches:submeter:check:time_{$tableName}_{$index}_{$day}";
  190. if(RedisCache::get($cacheKey)){
  191. // return true;
  192. }
  193. $date = $day == 1? date('Y-m-d') : date('Y-m-d H:i:s', time() - $day * 24 * 3600);
  194. $data = SubmeterModel::where(['table_name'=> $tableName,'status'=>1])
  195. ->where('update_time','>=', $date)
  196. ->value('id');
  197. var_dump(SubmeterModel::getLastSql());
  198. if($data){
  199. RedisCache::set($cacheKey, $data, rand(10, 20));
  200. }
  201. return $data;
  202. }
  203. }