Files
easygoadmin-wiki/EasyGoAdmin_Beego_Layui/zh/modules/job.md
T
2026-10-20 21:19:44 +08:00

788 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 6.11 定时任务
:::tip 概述
定时任务系统由三部分组成:JobLogic(调度逻辑)+ BaseTask(任务处理器)+ JobRunCommand(命令行入口)。支持三种触发方式:Linux Crontab、Windows 计划任务、Daemon 守护进程。
:::
## 系统架构
```text
┌─────────────────────────────────────────────────────────────────┐
│ 触发层(三选一) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Linux Crontab│ │Windows 计划 │ │ Daemon 守护 │ │
│ │ │ │任务 │ │ 进程 │ │
│ │ * * * * * │ │ schtasks │ │ php think │ │
│ │ php think │ │ │ │ job:daemon │ │
│ │ job:run │ │ │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ JobRunCommand │ │
│ │ php think job:run │ │
│ │ 或 php think job:daemon│ │
│ └────────────┬────────────┘ │
└───────────────────────────┼─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 调度层(JobLogic) │
│ │
│ runPending() shouldRun() │
│ ├─ 查询 status=1 的任务 ├─ 解析 cron 表达式 │
│ ├─ 逐个判断是否到执行时间 ├─ 分 时 日 月 周 逐段匹配 │
│ └─ 到期则执行 └─ 全部匹配返回 true │
│ │
│ executeJob($job) │
│ ├─ 检查执行策略(立即/排队/放弃) │
│ ├─ 调用 doExecute() 实际执行 │
│ ├─ 记录 JobLog 执行日志 │
│ └─ 返回执行结果 │
└───────────────────────────┼─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 执行层 │
│ │
│ doExecute($job) │
│ ├─ 判断 URL 类型 │
│ │ │
│ ├─ http:// / https:// → executeHttp() │
│ │ └─ curl 发起 HTTP GET/POST 请求 │
│ │ │
│ └─ 其它 → executeInternal() │
│ └─ 映射到 app\task\{Name}Task::run() │
│ │
│ 示例: │
│ url = "SendSms" → app\task\SendSmsTask::run() │
│ url = "Order/Close" → app\task\Order\CloseTask::run() │
│ url = "https://..." → HTTP GET/POST │
└─────────────────────────────────────────────────────────────────┘
```
## 状态说明
| 状态值 | 说明 | 可执行 | 可暂停 | 可启动 |
|--------|------|--------|--------|--------|
| 0 | 未发布 | ✗ | ✗ | ✓ |
| 1 | 运行中 | ✓ | ✓ | ✗ |
| 2 | 暂停 | ✗ | ✗ | ✓ |
| 3 | 已删除 | ✗ | ✗ | ✗ |
## 执行策略
| 策略值 | 说明 | 行为 |
|--------|------|------|
| 1 | 立即执行 | 丢弃之前的执行,立即运行 |
| 2 | 执行一次 | 等待上一次完成后执行 |
| 3 | 放弃执行 | 如果上一次还在运行则跳过 |
:::tip 枚举文本自动翻译
JobLogic 配置了 `serializeMaps`,API 响应中会自动包含翻译后的文本字段:
| 原始字段 | 文本字段 | 字典编码 | 说明 |
|----------|----------|----------|------|
| `status` | `statusText` | `job_status` | 任务状态(未发布/运行中/暂停/删除) |
| `execute_policy` | `executePolicyText` | `job_execute_policy` | 执行策略(立即执行/执行一次/放弃执行) |
字典数据在代码生成器自动创建,也可在「系统管理 → 字典管理」中手动维护。
:::
## Cron 表达式语法
标准 5 段格式:`分 时 日 月 周`
```text
┌────────── 分钟(0-59)
│ ┌──────── 小时(0-23)
│ │ ┌────── 日(1-31)
│ │ │ ┌──── 月(1-12)
│ │ │ │ ┌── 周(0-6,0=周日)
│ │ │ │ │
* * * * *
```
| 表达式 | 说明 |
|--------|------|
| `* * * * *` | 每分钟 |
| `0 * * * *` | 每小时整点 |
| `0 0 * * *` | 每天 0 点 |
| `0 2 * * *` | 每天凌晨 2 点 |
| `0 0 * * 1` | 每周一 0 点 |
| `0 0 1 * *` | 每月 1 号 0 点 |
| `*/5 * * * *` | 每 5 分钟 |
| `0 */2 * * *` | 每 2 小时 |
| `0 9-18 * * 1-5` | 工作日 9-18 点每小时 |
| `0 2 1,15 * *` | 每月 1 号和 15 号凌晨 2 点 |
---
## 方式一:Linux Crontab(推荐)
:::tip 适用场景
Linux / macOS 服务器,最常用的定时任务触发方式。系统级 cron 每分钟触发一次 PHP 命令。
:::
### 配置步骤
#### 1. 编辑 crontab
```bash
crontab -e
```
#### 2. 添加定时任务
```bash
# 每分钟执行一次(推荐)
* * * * * cd /www/api && php think job:run >> /dev/null 2>&1
# 每分钟执行一次,日志写入文件(调试用)
* * * * * cd /www/api && php think job:run >> /var/log/job.log 2>&1
# 每分钟执行一次,带环境变量
* * * * * cd /www/api && /usr/bin/php think job:run >> /dev/null 2>&1
```
:::warning 路径注意
- `cd /www/api` 必须切换到项目根目录,否则 ThinkPHP 无法找到配置文件
- `php` 建议使用绝对路径(如 `/usr/bin/php`),避免 cron 环境变量不同导致找不到 PHP
- `>> /dev/null 2>&1` 表示丢弃输出,调试时可改为写入日志文件
:::
#### 3. 保存并验证
```bash
# 保存后 cron 自动生效
# 查看已配置的 cron
crontab -l
# 查看 cron 日志(确认是否正常触发)
tail -f /var/log/syslog | grep CRON
# 手动执行一次验证
cd /www/api && php think job:run
```
#### 4. 权限设置
```bash
# 确保 PHP 有权限执行
chmod +x /usr/bin/php
# 确保项目目录权限
chmod -R 755 /www/api/runtime/
```
### Crontab 常用命令
```bash
crontab -e # 编辑当前用户的 cron
crontab -l # 列出当前用户的 cron
crontab -r # 删除当前用户的所有 cron(慎用)
crontab -u username -l # 查看指定用户的 cron
```
### 系统 cron 目录
```text
/etc/crontab # 系统级 cron 配置
/etc/cron.d/ # 额外的 cron 配置文件目录
/etc/cron.daily/ # 每天执行的脚本
/etc/cron.hourly/ # 每小时执行的脚本
/etc/cron.weekly/ # 每周执行的脚本
/etc/cron.monthly/ # 每月执行的脚本
```
---
## 方式二:Windows 计划任务
:::tip 适用场景
Windows Server 或本地开发环境,通过任务计划程序定时触发。
:::
### 方式 A:图形界面配置
#### 1. 打开任务计划程序
```text
方法1:Win + R → 输入 taskschd.msc → 回车
方法2:控制面板 → 管理工具 → 任务计划程序
方法3:开始菜单搜索"任务计划程序"
```
#### 2. 创建基本任务
1. 右侧点击"创建基本任务"
2. 名称:`RXThinkCMF 定时任务`
3. 描述:`每分钟执行一次定时任务扫描`
4. 触发器:选择"每天"
5. 开始时间:设置当前时间
6. 重复任务间隔:`1 分钟`
7. 持续时间:`无限期`
8. 操作:选择"启动程序"
9. 程序或脚本:
```text
C:\xampp\php\php.exe
```
10. 添加参数:
```text
think job:run
```
11. 起始于:
```text
D:\xampp8.2\htdocs\RXThinkCMF_ThinkPhp8_EleVue
```
12. 勾选"不管用户是否登录都要运行"
13. 点击完成
#### 3. 验证
```powershell
# 手动运行测试
cd D:\xampp8.2\htdocs\RXThinkCMF_ThinkPhp8_EleVue
C:\xampp\php\php.exe think job:run
# 在任务计划程序中右键任务 → 运行
# 查看"上次运行结果"是否为 0x0
```
### 方式 B:命令行配置(schtasks)
```powershell
# 创建每分钟执行的任务
schtasks /create /tn "RXThinkCMF_Job" /tr "C:\xampp\php\php.exe think job:run" /sc minute /mo 1 /st 00:00 /f
# 参数说明:
# /tn 任务名称
# /tr 要执行的命令
# /sc 频率:minute(分钟)、hourly(小时)、daily(天)
# /mo 间隔:1 表示每 1 分钟
# /st 开始时间
# /f 强制覆盖同名任务
# 创建任务(指定工作目录)
schtasks /create /tn "RXThinkCMF_Job" /tr "cmd /c cd /d D:\xampp8.2\htdocs\RXThinkCMF_ThinkPhp8_EleVue && C:\xampp\php\php.exe think job:run" /sc minute /mo 1 /f
# 查询任务
schtasks /query /tn "RXThinkCMF_Job"
# 手动运行
schtasks /run /tn "RXThinkCMF_Job"
# 删除任务
schtasks /delete /tn "RXThinkCMF_Job" /f
# 查看所有任务
schtasks /query /fo table
```
### Windows 注意事项
:::warning 常见问题
1. **路径空格**:如果路径含空格,需要用引号包裹
2. **PHP 路径**:使用 `php.exe` 的完整路径
3. **工作目录**:必须设置为项目根目录
4. **权限**:确保任务以有权限的用户身份运行
5. **日志**:可在参数中添加 `>> log.txt 2>&1` 重定向输出
:::
```powershell
# 带日志输出的完整命令
schtasks /create /tn "RXThinkCMF_Job" /tr "cmd /c cd /d D:\xampp8.2\htdocs\RXThinkCMF_ThinkPhp8_EleVue && C:\xampp\php\php.exe think job:run >> runtime\job.log 2>&1" /sc minute /mo 1 /f
```
---
## 方式三:Daemon 守护进程
:::tip 适用场景
需要更精确的执行时机(秒级),或不想依赖系统 cron 的场景。进程常驻内存,自行调度。
:::
### 原理
```text
Daemon 进程启动
│
▼
┌─────────────────────┐
│ while (true) │ 无限循环
│ │ │
│ ├─ runPending() │ 扫描并执行到期任务
│ ├─ sleep(60) │ 休眠 60 秒
│ └─ 继续循环 │
└─────────────────────┘
```
### 实现方式
#### 方式 A:使用 Supervisor 管理(推荐生产环境)
创建命令文件:
```php
// app/command/JobDaemonCommand.php
<?php
declare(strict_types=1);
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use app\logic\JobLogic;
/**
* 定时任务守护进程命令
*
* 常驻内存运行,每分钟扫描并执行到期任务。
* 建议通过 Supervisor 管理,确保进程异常退出后自动重启。
*
* 用法:php think job:daemon
*/
class JobDaemonCommand extends Command
{
protected function configure(): void
{
$this->setName('job:daemon')
->setDescription('定时任务守护进程(常驻内存)');
}
protected function execute(Input $input, Output $output): void
{
$output->writeln('[' . date('Y-m-d H:i:s') . '] Daemon 启动...');
$logic = new JobLogic();
while (true) {
try {
$count = $logic->runPending();
if ($count > 0) {
$output->writeln('[' . date('Y-m-d H:i:s') . '] 执行了 ' . $count . ' 个任务');
}
} catch (\Exception $e) {
$output->writeln('[' . date('Y-m-d H:i:s') . '] 异常: ' . $e->getMessage());
}
// 休眠 60 秒
sleep(60);
}
}
}
```
Supervisor 配置:
```ini
; /etc/supervisor/conf.d/rxthinkcmf-job.conf
[program:rxthinkcmf-job]
command=php /www/api/think job:daemon
directory=/www/api
autostart=true
autorestart=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/log/rxthinkcmf-job.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
```
启动:
```bash
# 重新加载配置
sudo supervisorctl reread
sudo supervisorctl update
# 启动
sudo supervisorctl start rxthinkcmf-job
# 查看状态
sudo supervisorctl status rxthinkcmf-job
# 重启
sudo supervisorctl restart rxthinkcmf-job
# 查看日志
tail -f /var/log/rxthinkcmf-job.log
```
#### 方式 B:Windows CMD 运行
在项目根目录打开 CMD,执行以下命令:
```cmd
:: 启动 Daemon(前台运行,关闭窗口则停止)
php think job:daemon
:: 停止 Daemon(另开一个 CMD 窗口执行)
php think job:daemon --stop
```
**启动成功输出:**
```text
[2026-09-23 09:06:07] Daemon 已启动 (PID: 22720)
[2026-09-23 09:06:07] 每分钟检查一次到期任务,按 Ctrl+C 优雅退出
```
**运行过程中的日志输出:**
```text
[2026-09-23 11:25:00] [执行] 定时任务演示 (cron: */5 * * * *)
[2026-09-23 11:25:00] [执行] 定时任务案例 (cron: */5 * * * *)
[2026-09-23 11:25:00] 本轮执行完成,共执行 2 个任务
[2026-09-23 11:30:00] [执行] 定时任务演示 (cron: */5 * * * *)
[2026-09-23 11:30:00] [跳过] 定时任务案例 - 当前分钟已执行过
[2026-09-23 11:30:00] 本轮执行完成,共执行 1 个任务
```
日志类型说明:
| 标记 | 含义 |
|------|------|
| `[执行]` | 任务已触发执行,括号内显示 cron 表达式 |
| `[跳过]` | 同一分钟内该任务已执行过,自动跳过(防重复) |
| `[失败]` | 任务执行异常,后跟错误信息 |
| `本轮执行完成` | 当前分钟的所有任务扫描完毕,显示执行数量 |
:::tip 提示
- 没有到期任务时,Daemon 不输出任何日行(静默等待),属于正常行为
- `[跳过]` 是防重复机制,同一任务在同一分钟内只会执行一次
- 出现 `[失败]` 时检查对应的任务处理器代码,单个任务失败不影响其他任务
:::
**前端联动:** Daemon 启动后,后台管理系统的「定时任务」模块会自动检测到守护进程状态,页面顶部显示:
```text
┌──────────────────────────────────────────────────────┐
│ [新增] [删除] [🟢 守护进程运行中 (PID: 22720)] │
└──────────────────────────────────────────────────────┘
```
- 绿色「守护进程运行中」按钮 — 表示 Daemon 正常工作,鼠标悬停可查看 PID
- 灰色「守护进程未运行」— 表示 Daemon 未启动或已停止
:::tip 状态检测原理
前端通过 `GET /api/job/daemonStatus` 接口轮询状态,后端读取 `runtime/job_daemon.lock` 锁文件中的 PID 并检查进程是否存活。因此即使 Daemon 非正常退出(如强制杀进程),只要锁文件残留,前端仍可能显示「运行中」,此时需手动删除 `runtime/job_daemon.lock` 或执行 `php think job:daemon --stop` 清理。
:::
:::warning Windows 注意事项
1. **无信号扩展**:Windows 不支持 `pcntl_signal`,`Ctrl+C` 会强制终止进程而非优雅退出
2. **停止方式**:推荐使用 `php think job:daemon --stop`,或关闭 CMD 窗口
3. **防重复启动**:命令内部通过文件锁(`runtime/job_daemon.lock`)防止重复启动,启动前会自动检测并提示 `Daemon 已在运行中 (PID: xxx)`
4. **后台运行**:如需后台运行,可使用 `start /B php think job:daemon`,停止时用 `php think job:daemon --stop`
5. **窗口关闭即停止**:前台运行时关闭 CMD 窗口会终止进程,生产环境建议配合 Windows 服务或计划任务守护
:::
#### 方式 C:nohup 后台运行(简单场景)
```bash
# 后台启动
nohup php /www/api/think job:daemon >> /var/log/job.log 2>&1 &
# 查看进程
ps aux | grep job:daemon
# 停止
kill $(pgrep -f "job:daemon")
```
#### 方式 D:systemd 服务(推荐)
```ini
; /etc/systemd/system/rxthinkcmf-job.service
[Unit]
Description=RXThinkCMF Job Daemon
After=network.target mysql.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/www/api
ExecStart=/usr/bin/php /www/api/think job:daemon
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
```bash
# 启用并启动
sudo systemctl enable rxthinkcmf-job
sudo systemctl start rxthinkcmf-job
# 查看状态
sudo systemctl status rxthinkcmf-job
# 查看日志
sudo journalctl -u rxthinkcmf-job -f
# 重启
sudo systemctl restart rxthinkcmf-job
```
---
## 三种方式对比
| 特性 | Crontab | Windows 计划任务 | Daemon 守护进程 |
|------|---------|-----------------|----------------|
| 适用系统 | Linux / macOS | Windows | 全平台 |
| 最小间隔 | 1 分钟 | 1 分钟 | 1 秒 |
| 资源占用 | 低(按需启动) | 低(按需启动) | 常驻内存 |
| 可靠性 | 高 | 高 | 高(配合 Supervisor) |
| 配置复杂度 | 低 | 中 | 低 |
| 日志追踪 | 需重定向 | 任务计划程序 | 自带输出 |
| 推荐场景 | 生产环境 | Windows 生产 | 需要秒级精度 / 开发调试 |
:::tip 推荐选择
- **Linux 生产环境**:Crontab(简单可靠)
- **Windows 生产环境**:任务计划程序 或 Daemon 守护进程
- **需要秒级精度**:Daemon + Supervisor
- **Docker 环境**:Daemon(容器内运行)
- **本地开发调试**:Daemon 前台运行(直接观察日志输出)
:::
---
## 新建定时任务步骤
### 1. 创建任务处理器
```php
<?php
// app/task/CleanLogTask.php
declare(strict_types=1);
namespace app\task;
/**
* 清理过期日志任务
*
* 数据库 url 字段:CleanLog
* 参数格式:{"days": 30}
*/
class CleanLogTask extends BaseTask
{
/**
* 执行任务
*
* @param array $params 任务参数
* @return string 执行结果消息
* @throws \Exception 执行失败时抛出
*/
public function run(array $params = []): string
{
$days = $params['days'] ?? 30;
$cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));
// 清理登录日志
$loginCount = \app\model\LoginLog::where('create_time', '<', $cutoffDate)->delete();
// 清理操作日志
$operCount = \app\model\OperationLog::where('create_time', '<', $cutoffDate)->delete();
// 清理任务日志
$jobCount = \app\model\JobLog::where('start_time', '<', $cutoffDate)->delete();
return "已清理 {$days} 天前的日志:登录日志 {$loginCount} 条,操作日志 {$operCount} 条,任务日志 {$jobCount} 条";
}
}
```
### 2. 在数据库 job 表中配置
```sql
INSERT INTO think_job (
job_name, job_alias, job_group, job_trigger,
url, params, cron_expression,
execute_policy, status, is_sync,
is_delete, create_user, create_time
) VALUES (
'清理过期日志', 'CleanLog', '系统维护', 'cron',
'CleanLog', '{"days":30}', '0 2 * * *',
2, 1, 0,
0, 'admin', NOW()
);
```
### 3. 配置触发方式(三选一)
```bash
# 方式一:Linux Crontab
crontab -e
# 添加:* * * * * cd /www/api && php think job:run >> /dev/null 2>&1
# 方式二:Windows 计划任务
schtasks /create /tn "RXThinkCMF_Job" /tr "cmd /c cd /d D:\xampp8.2\htdocs\RXThinkCMF_ThinkPhp8_EleVue && C:\xampp\php\php.exe think job:run" /sc minute /mo 1 /f
# 方式三:Daemon 守护进程
php think job:daemon
# 停止:php think job:daemon --stop
```
---
## API 接口
| 方法 | 路径 | 权限码 | 说明 |
|------|------|--------|------|
| GET | `/api/job/page` | sys:job:page | 分页列表 |
| GET | `/api/job/list` | sys:job:list | 全量列表 |
| GET | `/api/job/detail/:id` | sys:job:detail | 详情 |
| GET | `/api/job/runOnce/:id` | sys:job:execute | 手动执行一次 |
| GET | `/api/job/pause/:id` | sys:job:pause | 暂停任务 |
| GET | `/api/job/resume/:id` | sys:job:resume | 恢复任务 |
| GET | `/api/job/daemonStatus` | - | 守护进程状态 |
| POST | `/api/job/add` | sys:job:add | 新增任务 |
| POST | `/api/job/status` | sys:job:update | 设置状态 |
| PUT | `/api/job/update` | sys:job:update | 修改任务 |
| DELETE | `/api/job/delete/:id` | sys:job:delete | 删除任务 |
| DELETE | `/api/job/batchDelete` | sys:job:batchDelete | 批量删除 |
### 任务日志
| 方法 | 路径 | 权限码 | 说明 |
|------|------|--------|------|
| GET | `/api/job/log/page` | sys:job:log:page | 日志分页 |
| GET | `/api/job/log/detail/:id` | sys:job:log:detail | 日志详情 |
| DELETE | `/api/job/log/delete/:id` | sys:job:log:delete | 删除日志 |
| DELETE | `/api/job/log/batchDelete` | sys:job:log:batchDelete | 批量删除 |
---
## 核心代码解析
### Cron 表达式匹配(JobLogic::matchCronField)
```php
// app/logic/JobLogic.php
/**
* 匹配 cron 字段
*
* 支持以下语法:
* - *:匹配所有
* - */n:每隔 n 个单位
* - n-m:范围匹配
* - n,m,...:逗号分隔的多值
* - n:单个值精确匹配
*/
protected function matchCronField(string $cronField, int $current): bool
{
if ($cronField === '*') {
return true;
}
// 处理 */n(每隔 n 分钟/小时等)
if (preg_match('/^\*\/(\d+)$/', $cronField, $m)) {
return $current % (int)$m[1] === 0;
}
// 处理 n-m(范围)
if (preg_match('/^(\d+)-(\d+)$/', $cronField, $m)) {
return $current >= (int)$m[1] && $current <= (int)$m[2];
}
// 处理逗号分隔的多个值
if (strpos($cronField, ',') !== false) {
$values = array_map('intval', explode(',', $cronField));
return in_array($current, $values);
}
// 单个值
return (int)$cronField === $current;
}
```
### 防重复执行(JobLogic::hasRunThisMinute)
```php
/**
* 检查任务在当前分钟是否已执行过
*
* Daemon 模式下,防止同一分钟内重复执行。
*/
public function hasRunThisMinute(int $jobId): bool
{
$minuteStart = date('Y-m-d H:i') . ':00';
$minuteEnd = date('Y-m-d H:i') . ':59';
return JobLog::where('job_id', $jobId)
->where('start_time', '>=', $minuteStart)
->where('start_time', '<=', $minuteEnd)
->count() > 0;
}
```
---
## 常见问题
### Q: 任务不执行?
A: 检查以下几点:
1. 任务状态是否为 `1`(运行中)
2. Cron 表达式格式是否正确(5 段)
3. Crontab 是否已配置且 cron 服务已启动
4. PHP 路径是否正确(建议使用绝对路径)
5. 手动执行 `php think job:run` 是否正常
### Q: 任务重复执行?
A: Daemon 模式下检查 `hasRunThisMinute` 是否生效。Crontab 模式下确保只有一个 cron 在触发。
### Q: 任务执行超时?
A: 检查 PHP 的 `max_execution_time` 配置,长时间任务建议使用 Daemon 模式或设置 `set_time_limit(0)`。
### Q: 如何查看任务执行日志?
A: 两种方式:
1. **控制台实时日志**:Daemon 前台运行时,`[执行]`、`[跳过]`、`[失败]` 等日志直接输出到终端
2. **数据库持久化日志**:通过 API `/api/job/log/page` 查询,或直接查 `think_job_log` 表
### Q: Daemon 控制台没有新日志输出,是卡住了吗?
A: 不是。Daemon 每分钟检查一次,没有到期任务时静默等待,不输出任何日志。这是正常行为,说明当前没有任务需要执行。
### Q: Windows 下 Daemon 如何启动和停止?
A: 在项目根目录打开 CMD:
```cmd
:: 启动
php think job:daemon
:: 停止(另开一个 CMD 窗口)
php think job:daemon --stop
```
启动成功输出:
```text
[2026-09-23 09:06:07] Daemon 已启动 (PID: 22720)
[2026-09-23 09:06:07] 每分钟检查一次到期任务,按 Ctrl+C 优雅退出
```
启动后,后台管理系统「定时任务」模块顶部会显示绿色「守护进程运行中」按钮(带 PID 提示),表示 Daemon 正常工作。
如提示 `Daemon 已在运行中 (PID: xxx)`,先执行 `php think job:daemon --stop` 停止旧进程,或手动删除 `runtime/job_daemon.lock` 文件。