123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. <?php
  2. namespace com;
  3. use think\Cache;
  4. /**
  5. * 定时任务
  6. *
  7. * @link https://github.com/yunwuxin/think-cron
  8. * @author yunwuxin
  9. */
  10. abstract class Cron
  11. {
  12. /**
  13. * 任务周期
  14. * 分 时 日 月 周
  15. */
  16. public $expression = '* * * * *';
  17. /**
  18. * 执行时间
  19. */
  20. public $expiresAt = 1200;
  21. /**
  22. * 过滤方法
  23. */
  24. protected $filters = [];
  25. /**
  26. * 跳过方法
  27. */
  28. protected $rejects = [];
  29. /**
  30. * 任务是否可以重叠执行
  31. */
  32. public $withoutOverlapping = false;
  33. /**
  34. * 日志目录
  35. */
  36. const LOG_PATH = RUNTIME_PATH . 'cron_log';
  37. public function __construct()
  38. {
  39. $this->configure();
  40. }
  41. /**
  42. * 是否到期执行
  43. * @return bool
  44. */
  45. public function isDue()
  46. {
  47. $e = explode(' ', $this->expression);
  48. $e = array_values(array_filter($e));
  49. if (count($e) == 5) {
  50. $ex = ['i', 'G', 'j', 'n', 'w'];
  51. foreach ($ex as $k => $v) {
  52. if ($e[$k] !== '*' && !in_array((int) date($v), $this->tt($e[$k]))) {
  53. $this->msg = "{$v} 不合格";
  54. return false;
  55. }
  56. }
  57. return true;
  58. } else {
  59. $this->msg = '任务表达式不合法';
  60. return false;
  61. }
  62. }
  63. /**
  64. * 简单的cron表达式处理
  65. *
  66. * @param string $e
  67. * @param array $res
  68. * @return void
  69. * @author Ymob
  70. * @datetime 2019-12-19 10:10:21
  71. */
  72. protected function tt($e = '', $res = [])
  73. {
  74. if (false !== strpos($e, ',')) {
  75. foreach (array_map('trim', explode(',', $e)) as $temp) {
  76. $res = $this->tt($temp, $res);
  77. }
  78. } else {
  79. if (\is_numeric($e)) {
  80. $res[] = (int) $e;
  81. } elseif (1 == substr_count($e, '-')) {
  82. list($start, $end) = array_map('trim', explode('-', $e));
  83. if (is_numeric($start) && is_numeric($end)) {
  84. for ($start; $start <= $end; $start++) {
  85. $res[] = (int) $start;
  86. }
  87. }
  88. } elseif (1 == substr_count($e, '/')) {
  89. list($start, $step) = array_map('trim', explode('/', $e));
  90. if ((is_numeric($start) || $start == '*') && is_numeric($step)) {
  91. if ($start == '*') {
  92. $start = 0;
  93. }
  94. for ($start; $start < 60; $start += $step) {
  95. $res[] = (int) $start;
  96. }
  97. }
  98. }
  99. }
  100. return $res;
  101. }
  102. /**
  103. * 配置任务
  104. */
  105. protected function configure()
  106. {
  107. }
  108. /**
  109. * 执行任务
  110. * @return mixed
  111. */
  112. abstract protected function execute();
  113. final public function run()
  114. {
  115. if (!$this->withoutOverlapping && !$this->createMutex()) {
  116. $this->log('repeated execution, continue.');
  117. return;
  118. }
  119. register_shutdown_function(function () {
  120. $this->removeMutex();
  121. });
  122. try {
  123. $this->execute();
  124. } finally {
  125. $this->removeMutex();
  126. }
  127. }
  128. /**
  129. * 过滤
  130. * @return bool
  131. */
  132. public function filtersPass()
  133. {
  134. foreach ($this->filters as $callback) {
  135. if (!call_user_func($callback)) {
  136. return false;
  137. }
  138. }
  139. foreach ($this->rejects as $callback) {
  140. if (call_user_func($callback)) {
  141. return false;
  142. }
  143. }
  144. return true;
  145. }
  146. /**
  147. * 任务标识
  148. */
  149. public function mutexName()
  150. {
  151. return 'task-' . sha1(static::class);
  152. }
  153. /**
  154. * 删除互斥
  155. */
  156. protected function removeMutex()
  157. {
  158. return Cache::rm($this->mutexName());
  159. }
  160. /**
  161. * 创建互斥
  162. */
  163. protected function createMutex()
  164. {
  165. $name = $this->mutexName();
  166. if (!Cache::has($name)) {
  167. Cache::set($name, true, $this->expiresAt);
  168. return true;
  169. }
  170. return false;
  171. }
  172. /**
  173. * 是否存在互斥
  174. */
  175. protected function existsMutex()
  176. {
  177. return Cache::has($this->mutexName());
  178. }
  179. /**
  180. * 添加过滤筛选
  181. */
  182. public function when(Closure $callback)
  183. {
  184. $this->filters[] = $callback;
  185. return $this;
  186. }
  187. /**
  188. * 添加跳过筛选
  189. */
  190. public function skip(Closure $callback)
  191. {
  192. $this->rejects[] = $callback;
  193. return $this;
  194. }
  195. /**
  196. * 允许重叠执行程序
  197. */
  198. public function withoutOverlapping($expiresAt = 1440)
  199. {
  200. $this->withoutOverlapping = true;
  201. $this->expiresAt = $expiresAt;
  202. return $this->skip(function () {
  203. return $this->existsMutex();
  204. });
  205. }
  206. /**
  207. * 任务日志
  208. *
  209. * @param string $content
  210. * @datetime 2019-12-19 11:27:01
  211. */
  212. public function log($content = 'success')
  213. {
  214. if (!file_exists(self::LOG_PATH)) {
  215. mkdir(self::LOG_PATH);
  216. }
  217. $log_file = self::LOG_PATH . DS . str_replace('\\', '-', static::class) . '.log';
  218. if (!is_string($content)) {
  219. $content = json_encode($content);
  220. }
  221. $time = "[" . date('Y-m-d H:i:s') . "]" . PHP_EOL;
  222. $content = $time . $content . PHP_EOL . PHP_EOL;
  223. file_put_contents($log_file, $content, FILE_APPEND);
  224. }
  225. }