InvoicePlane 1.7.2 常用 CLI 命令与任务调度详解
InvoicePlane 1.7.2 命令侧与任务调度实测详解:官方内置 CLI 路由(migrate/create_default_user)php index.php 实测回显、循环发票调度端点 CRON_KEY 鉴权与防爆破限流(正确/错误/429 全回显)、crontab 定时落地与端到端自动生成发票验证、guest 公开链接与支付网关端点能力,全部命令带实测回显。
InvoicePlane 1.7.2 常用 CLI 命令与任务调度详解
目标:讲清 InvoicePlane 1.7.2 在服务器上的全部「命令侧」能力——官方内置 CLI 路由(
php index.php)能做什么、循环发票调度端点如何用 CRON_KEY 鉴权并用 curl 实测、定时任务如何在服务器上落地并真实触发自动开票、除调度端点外还有哪些可用的 Web 端点。全部命令与回显均在 Ubuntu 22.04 + InvoicePlane 1.7.2 + MariaDB 11 实测环境逐条执行并照抄(2026-10-24)。部署过程见 《InvoicePlane 1.7.2 Docker Compose 部署指南》,参数体系见 《InvoicePlane 1.7.2 配置参数详解》,软件定位与选型见 《InvoicePlane 介绍与选型指南》。
一、环境信息
本篇所有命令在以下实测环境中执行,回显均照抄自真实执行结果:
| 项目 | 值 |
|---|---|
| 操作系统 | Ubuntu 22.04(x86_64,内核 6.8.0-138-generic) |
| 应用版本 | InvoicePlane 1.7.2(官方 git tag v1.7.2,提交 aaeea1e) |
| PHP | 8.2.35(cli,应用内置运行时实测 php -v 回显) |
| 数据库 | MariaDB 11(实测服务端 11.8.9-MariaDB-ubu2404) |
应用访问地址(IP_URL) | http://192.168.146.128:8128 |
| 应用根目录 | /var/www/html(Web 根目录,php index.php 在此执行) |
| 后台演示账号 | admin@example.com / q1w2e3r4(仅演示环境,生产必须更换) |
| 演示环境 cron_key | LH4UcspYjfyhn2da(实测库 ip_settings 表当前值;⚠️ 演示值,生产环境必须重生成,见 3.2) |
| 日志目录 | /var/www/html/application/logs/(log-YYYY-MM-DD.php 按天滚动) |
二、工具概览
先说结论:InvoicePlane 1.7.2 没有独立 CLI 工具——应用目录里不存在 bin/ 可执行文件,也没有 Laravel 式的 artisan(官方源码 AGENTS.md 明确写有 "InvoicePlane does not have php artisan commands")。它的「命令侧」能力由四部分构成,本篇逐一详解:
| 能力 | 形态 | 用途 | 鉴权 | 实测结论 |
|---|---|---|---|---|
| 内置 CLI 路由 | php index.php <模块>/<控制器>/<方法> | 数据库迁移、创建初始管理员、命令行触发调度 | 仅限命令行(HTTP 访问返回 403) | 3 条路由全部实测通过 |
| 循环发票调度端点 | GET /invoices/cron/recur/<cron_key> | 按 recur_next_date 自动生成循环发票、可选自动发邮件 | cron_key(URL 段) | 正确 key 返回 200 空体;错误 key 返回 500;连续错误触发 429 限流 |
| 定时任务落地 | crontab / systemd timer | 让调度端点被周期性调用 | 系统层 | crontab 写入 + 守护触发 + 自动生成发票端到端实测通过 |
| Web 端点能力 | guest 公开链接、支付网关回调、setup | 客户免登录查看/下载发票、在线支付、安装向导 | URL Key / 会话 | guest 查看页与 PDF 实测 200;已安装后 setup 返回 403 |
为什么调度端点这么重要:循环发票(Recurring Invoices)是 InvoicePlane 唯一的自动化开票通道——对月费、订阅费这类周期账单设置频率后,必须靠外部周期性调用该端点才会生成新发票,应用自身不含任何后台常驻调度进程。与之配套的 cron_key 与限流参数在 《InvoicePlane 1.7.2 配置参数详解》的设置层已作参数级说明,本篇聚焦「怎么用、怎么验证、出问题怎么排」。
三、前置条件
3.1 部署与安装就绪
本篇所有操作要求目标服务器已满足:
- InvoicePlane 1.7.2 已部署且已完成安装向导(未安装时访问应用会强制进入 setup,见 7.3);
- 服务器上 PHP 命令行可用,且能在应用根目录执行
php index.php(部署形态不同路径不同:官方发布包解压目录、或 Web 根目录 /var/www/html,本篇实测环境为 /var/www/html); - 已知当前实例的 cron_key(获取方式见 3.2)。
验证应用已就绪的最快方式是访问首页——已安装实例会把未登录访问 307 重定向到登录页:
curl -s -L -o /dev/null -w 'root-final: %{url_effective} [%{http_code}]\n' http://192.168.146.128:8128/
实测回显:
root-final: http://192.168.146.128:8128/sessions/login [200]
3.2 cron_key 的查看、生成与验证
cron_key 是调度端点的唯一凭据,安装完成时自动生成,有三种查看/生成途径:
途径一:后台界面查看与一键重生成。登录后台(演示账号 admin@example.com / q1w2e3r4)→「系统设置」→「常规(General)」标签页底部有「Cron Key」只读输入框与「Generate」按钮,按钮数据来自后台 AJAX 端点 settings/ajax/get_cron_key(该端点仅登录会话可调)。生成逻辑为 generate_secure_token(8)——8 字节随机数展开成 16 位十六进制字符串。
途径二:数据库直查(与后台界面显示同源,均为 ip_settings 表;在数据库服务器上用 mariadb 客户端执行,本实测环境应用与数据库同机部署):
mariadb -uinvoiceplane -pinvoiceplane invoiceplane -e "SELECT setting_value FROM ip_settings WHERE setting_key='cron_key';"
实测回显:
setting_value
LH4UcspYjfyhn2da
途径三:命令行重新生成后写回。调度密钥属于安全凭据,泄露(例如出现在日志、监控告警、代码仓库里)后必须立即重生成——后台点 Generate,或等价地在命令行生成同规格(16 位十六进制)随机值后经数据库层覆盖:
openssl rand -hex 8
实测回显(随机值,每次执行不同;仅演示命令本身,与当前实例的 key 无关):
dee966b0220a6700
⚠️ 注意:本篇演示环境的
LH4UcspYjfyhn2da是随安装自动生成的演示值,正文所有 curl 示例都使用它保证可复现。生产环境上线后必须重生成一次(后台 Generate 按钮),重生成后旧 key 立即失效,需同步更新 crontab 中的 URL。
四、官方内置 CLI 路由详解(php index.php)
4.1 路由形态与命令行隔离
InvoicePlane 基于 CodeIgniter 3 框架,CLI 调用形态为 php index.php <模块>/<控制器>/<方法>(工作目录为应用根目录)。1.7.2 官方提供两个 CLI 专用控制器方法(源码 application/modules/setup/controllers/Cli.php):
| CLI 命令 | 用途 | 何时用 |
|---|---|---|
php index.php setup/cli/migrate | 建表 / 增量迁移数据库结构 | 部署初始化、版本升级后 |
php index.php setup/cli/create_default_user | 无用户时创建初始管理员 | 自动化部署的引导阶段 |
这两个方法在构造器里做了 is_cli() 强制校验,只允许命令行调用。用 curl 从 HTTP 侧访问同一路由实测会被拒绝:
curl -s -o /tmp/cli_http.html -w 'setup/cli/migrate via HTTP: %{http_code}\n' 'http://192.168.146.128:8128/index.php/setup/cli/migrate'
grep -oE 'This controller can only be accessed via the command line\.' /tmp/cli_http.html
实测回显:
setup/cli/migrate via HTTP: 403
This controller can only be accessed via the command line.
💡 说明:403 错误页文案即源码
show_error('This controller can only be accessed via the command line.', 403)的原文。这意味着 CLI 路由不会暴露成 Web 攻击面,迁移与建号只能在服务器上由有系统权限的人执行。
4.2 setup/cli/migrate:建表与增量迁移
这条命令用于两阶段:全新安装时建立全部基础表(检测到 ip_versions 表不存在即执行全量建表),已安装时执行待迁移项。在已安装的实测环境执行:
cd /var/www/html && php index.php setup/cli/migrate
实测回显:
Running pending migrations...
Migrations completed successfully.
行为说明(源码 Cli::migrate()):
- 无
ip_versions表 → 视为全新环境,先执行全量建表(输出No existing installation detected. Running fresh install...与Base tables installed successfully.),建表失败逐条打印错误并以退出码 1 结束; - 已安装 → 执行增量迁移,成功即上面两行回显;失败同样逐条打印
Migration failed with errors:与错误明细、退出码 1。
验证方式:退出码 0 且无错误输出。若需确认结构变更结果,可复查库中 ip_versions 表的最新版本记录。
4.3 setup/cli/create_default_user:创建初始管理员
用于自动化部署引导:库中没有任何用户时,按环境变量 DEFAULT_ADMIN_EMAIL、DEFAULT_ADMIN_NAME、DEFAULT_ADMIN_PASSWORD 创建管理员(type=1);密码未提供时随机生成并打印到 stdout。实测环境已有用户,执行验证其幂等跳过逻辑:
cd /var/www/html && php index.php setup/cli/create_default_user
实测回显:
Users already exist — skipping default user creation.
行为说明(源码 Cli::create_default_user()):
- 已有任意用户 → 直接跳过(即上面回显),不改动现有账号;
- 无用户 → 创建后打印
Default admin user created.与邮箱;若密码未通过DEFAULT_ADMIN_PASSWORD提供,会额外打印Password: <随机密码> (generated — please change this); - 环境变量未设时的默认值:邮箱 admin@localhost、名称 admin、密码随机。实测环境的演示账号 admin@example.com / q1w2e3r4 即由部署时注入的
DEFAULT_ADMIN_EMAIL/DEFAULT_ADMIN_PASSWORD创建。
4.4 CLI 直接触发循环发票调度
调度端点本质是普通控制器方法(invoices 模块 Cron 控制器 recur 方法),没有 CLI 限制,因此也能在命令行以同一 URL 路径触发,效果与 HTTP 调用完全一致(走同一套鉴权、限流与生成逻辑):
cd /var/www/html && php index.php invoices/cron/recur/LH4UcspYjfyhn2da; echo "exit=$?"
实测回显(当前无到期循环规则,静默成功):
exit=0
💡 说明:命令行触发适合「手动补跑一次」场景(例如服务器时钟修正后补生成漏掉的账单);周期性自动触发仍以 crontab 调用 HTTP 端点为标准做法(见第六章),因为 crontab 与 Web 服务解耦、失败可用
-f退出码对接监控。CLI 触发时客户端 IP 为 0.0.0.0,错误 key 的限流计数同样落在ip_login_log表(计数键为cron_key:+ 该 IP 的 SHA-256)。
五、循环发票调度端点详解
5.1 端点 URL 与两种形态
调度端点由 invoices 模块的 Cron 控制器提供,路由为 invoices/cron/recur/<cron_key>。因为应用自带 .htaccess 把请求重写到 index.php,同一端点存在两种 URL 形态,实测均可用:
| 形态 | URL | 实测结果 |
|---|---|---|
| 显式入口 | http://192.168.146.128:8128/index.php/invoices/cron/recur/LH4UcspYjfyhn2da | HTTP 200 |
| 重写后干净 URL | http://192.168.146.128:8128/invoices/cron/recur/LH4UcspYjfyhn2da | HTTP 200 |
干净形态实测(返回码 200 即端点可达且鉴权通过):
curl -s -o /dev/null -w 'cron-noindex: %{http_code}\n' 'http://192.168.146.128:8128/invoices/cron/recur/LH4UcspYjfyhn2da'
实测回显:
cron-noindex: 200
两种形态任选其一写入 crontab 即可;若部署在子目录(如 https://example.com/billing),URL 前缀相应为 https://example.com/billing/invoices/cron/recur/<key>。
5.2 鉴权与 curl 实测(正确 / 错误 key)
鉴权逻辑:控制器把 URL 中的 key 与设置值做常数时间比较(hash_equals(get_setting('cron_key'), $cron_key)),通过才进入生成流程。三种调用的实测结果:
正确 key——返回 200,响应体为空(0 字节,静默成功是它的正常形态):
curl -s -o /tmp/cron_ok_body.txt -w 'cron-correctkey: %{http_code}\n' 'http://192.168.146.128:8128/index.php/invoices/cron/recur/LH4UcspYjfyhn2da'
wc -c /tmp/cron_ok_body.txt
实测回显:
cron-correctkey: 200
0 /tmp/cron_ok_body.txt
错误 key——返回 500 与错误页(页面红底白字,文案 Wrong cron key provided!):
curl -s -o /dev/null -w 'cron-wrongkey: %{http_code}\n' 'http://192.168.146.128:8128/index.php/invoices/cron/recur/WRONGKEYTEST'
实测回显:
cron-wrongkey: 500
错误页响应体实测结构(节选,全文 822 字节):
<!DOCTYPE html>
<html style="display:table;width:100%;">
<head>
<meta charset="utf-8">
<title>InvoicePlane - An Error Was Encountered</title>
<h4>An Error Was Encountered</h4>
<p><p>Wrong cron key provided!</p></p>
同时错误 key 会写一条应用日志(/var/www/html/application/logs/log-2026-10-24.php 实测行):
ERROR - 2026-10-24 00:36:39 --> [Cron Recurring Invoices] Wrong cron key provided! WRONGKEYTEST
💡 说明:日志行末尾原样记录了调用方传入的错误 key(源码经
sanitize_for_logging()消毒后输出),这既是排障线索也提示——别把真实 cron_key 放进 URL 交给日志系统长期留存,泄露即重生成。
5.3 防爆破限流:10 次 / 15 分钟与 429 实测
端点无会话、仅靠 URL 中的 key 鉴权,官方为此内置了按 IP 的错误尝试限流(源码 Cron::_is_cron_rate_limited()):
| 参数 | 环境变量 | 默认值 | 含义 |
|---|---|---|---|
| 错误尝试阈值 | CRON_IP_MAX_ATTEMPTS | 10 | 同一 IP 累计错误次数达到该值即拒绝 |
| 计数窗口 | CRON_IP_WINDOW_MINUTES | 15 | 计数刷新窗口(分钟),超窗自动清零 |
两个参数经 env() 读取,ipconfig.php.example 中没有对应常量项,需要调整时直接注入同名环境变量。计数落在数据库 ip_login_log 表,键名为 cron_key: + 客户端 IP 的 SHA-256 哈希。达到阈值后,后续请求在检查 key 之前就被 429 拒绝(连正确 key 也会被拒,这是防止阈值探测的设计)。
实测过程(计数器已清零后,用错误 key 连续调用 11 次,完整序列一次跑出——前 10 次各计 1 次错误并返回 500,第 11 次命中阈值返回 429):
seq 1 11 | xargs -I__ curl -s -o /dev/null -w '%{http_code} ' 'http://192.168.146.128:8128/index.php/invoices/cron/recur/WRONGKEYTEST'; echo
实测回显:
500 500 500 500 500 500 500 500 500 500 429
查计数表实态(错误尝试按客户端 IP 记一笔计数,11 次调用后恰好 10 次):
mariadb -uinvoiceplane -pinvoiceplane invoiceplane -e "SELECT login_name, log_count, log_create_timestamp FROM ip_login_log;"
实测回显:
login_name log_count log_create_timestamp
cron_key:ccdd5fee032e658c4f7229368a4a7e51fb9f8ac05b39b423ca99e95e0ae67bb1 10 2026-10-24 00:40:06
阈值命中后持续生效:窗口内同 IP 再打(无论 key 对错)一律 429,实测复测 11 次全部返回 429。429 响应体抓取(页面文案与 500 相同):
curl -s 'http://192.168.146.128:8128/index.php/invoices/cron/recur/WRONGKEYTEST' -o /tmp/r429.html
grep -oE 'Wrong cron key provided!|Too many attempts' /tmp/r429.html
实测回显:
Wrong cron key provided!
💡 说明:源码中 429 分支写作
show_error(..., 429); exit('Too many attempts.');,但show_error()内部即终止脚本,Too many attempts.永远不会输出——所以 429 与 500 的响应体文案一致、只能靠 HTTP 状态码区分,监控告警要按状态码而不是文案判断限流。
限流解除有两种方式:等计数窗口(15 分钟)自然过期,或清除计数行后立即恢复。实测验证「正确 key 通过后计数自动清零」——鉴权成功会 _reset_cron_rate_limit() 删除本 IP 计数行:
curl -s -o /dev/null -w '%{http_code}\n' 'http://192.168.146.128:8128/index.php/invoices/cron/recur/LH4UcspYjfyhn2da'
mariadb -uinvoiceplane -pinvoiceplane invoiceplane -e "DELETE FROM ip_login_log; SELECT COUNT(*) AS remaining FROM ip_login_log;"
实测回显:
200
remaining
0
⚠️ 注意:如果生产调度机的公网出口 IP 曾被限流(例如换 key 时拼错过几次 URL),15 分钟内连正确 key 也会 429。此时先修对 URL,再等窗口过期;急用可在库中删除对应
login_name计数行恢复。
5.4 调度逻辑与发票生成闭环实测
端点被正确调用后做什么(源码 Cron::recur() 主流程):
- 取出全部「到期」的循环规则——过滤条件为
recur_next_date <= 当天且(recur_end_date > 当天或未设结束日期); - 逐条在事务内处理:先原子认领(把
recur_next_date按频率推进,条件更新保证并发调用只有一个赢者,防止同一账单重复生成),再基于源发票复制生成新发票(编号走发票组规则,invoice_date_created= 认领前的recur_next_date); - 若后台开启了「循环后自动发邮件」(
automatic_email_on_recur)且邮件服务已配置,按邮件模板给客户发 PDF;未开启则记一条 error 日志。
循环频率支持档位(源码 Mdl_Invoices_Recurring::$recur_frequencies 全表):按天 1D/2D/3D/4D/5D/6D/15D/30D、按周 7D/14D/21D/28D、按月 1M~11M、按年 1Y~5Y。
端到端闭环实测(在演示库执行,生产等价操作为后台「发票详情页 → 循环设置」创建规则;此处用 SQL 直插保证可复现)。给编号 1 的发票挂一条每日循环规则,recur_next_date 设为当天使其立即到期,并回读全表确认插入结果:
mariadb -uinvoiceplane -pinvoiceplane invoiceplane -e "INSERT INTO ip_invoices_recurring (invoice_id, recur_start_date, recur_end_date, recur_frequency, recur_next_date) VALUES (2, CURDATE(), NULL, '1D', CURDATE());"
mariadb -uinvoiceplane -pinvoiceplane invoiceplane -e "SELECT invoice_recurring_id, invoice_id, recur_start_date, recur_next_date, recur_frequency, recur_end_date FROM ip_invoices_recurring;"
实测回显(规则 3 即新插入的 1D 规则,recur_next_date = 当天 2026-10-24,已到期;规则 1/2 为演示库原有数据,尚未到期):
invoice_recurring_id invoice_id recur_start_date recur_next_date recur_frequency recur_end_date
1 1 2026-10-24 2026-11-22 30D NULL
2 2 2026-10-20 2026-11-23 1M NULL
3 2 2026-10-24 2026-10-24 1D NULL
调用调度端点(正确 key):
curl -s -o /dev/null -w 'cron-run: %{http_code}\n' 'http://192.168.146.128:8128/index.php/invoices/cron/recur/LH4UcspYjfyhn2da'
实测回显:
cron-run: 200
验证两张证据:规则日期被推进、新发票已生成:
mariadb -uinvoiceplane -pinvoiceplane invoiceplane -e "SELECT invoice_recurring_id, recur_next_date FROM ip_invoices_recurring WHERE invoice_recurring_id=3; SELECT invoice_id, invoice_number, invoice_date_created, invoice_date_due, client_id, invoice_status_id FROM ip_invoices ORDER BY invoice_id;"
实测回显:
invoice_recurring_id recur_next_date
3 2026-10-25
invoice_id invoice_number invoice_date_created invoice_date_due client_id invoice_status_id
1 NULL 2026-10-20 2026-11-03 1 1
2 1 2026-10-23 2026-11-22 1 3
3 2 2026-10-23 2026-11-22 1 3
4 3 2026-10-24 2026-11-23 1 1
解读:新发票 id=4(编号 3,编号沿用发票组自增序列)由调度端点自动复制生成,开票日 2026-10-24 即规则原 recur_next_date,到期日 +30 天(设置层「到期天数 invoices_due_after=30」决定);规则 recur_next_date 由 10-24 推进到 10-25(1D 频率),次日同一调用会再次生成下一期。新发票状态为 1(草稿),自动邮件未开启时不会自行发出。
本次调用同时留下自动邮件未配置的日志行(/var/www/html/application/logs/log-2026-10-24.php 实测):
ERROR - 2026-10-24 00:39:06 --> [Cron Recurring Invoices] Automatic_email_on_recur was not set or mailer was not configured
💡 说明:要让调度端点「开票 + 发信」全自动闭环,需同时满足三件事——后台「系统设置 → 发票」页开启 automatic_email_on_recur;「邮件模板」中选定 invoice 邮件模板(email_invoice_template);「邮件」页 SMTP 配置有效。任一缺失都只开票不发信,并在日志中留 error 行(上面实测即未配置时的原文)。
5.5 日志观测与巡检
调度端点本身静默(200 空响应体),运行观测靠应用日志。日志按天滚动于 /var/www/html/application/logs/log-YYYY-MM-DD.php,与本端点相关的行均以 [Cron Recurring Invoices] 前缀出现。巡检建议直接 grep 当天日志:
grep "Cron Recurring Invoices" /var/www/html/application/logs/log-2026-10-24.php | tail -3
实测回显(节选自当日真实日志):
ERROR - 2026-10-24 00:36:52 --> [Cron Recurring Invoices] Wrong cron key provided! WRONGKEYTEST
ERROR - 2026-10-24 00:39:06 --> [Cron Recurring Invoices] Automatic_email_on_recur was not set or mailer was not configured
健康巡检判据:正常日子应只有 Automatic_email_on_recur ...(未开自动邮件时)或无输出;出现 Wrong cron key provided! 说明有错误 key 的调用(自己排查 crontab 拼写或有人在探测);开启 IP_DEBUG 后还会额外输出 debug 级明细(处理条数、每条生成结果)。
六、定时任务落地方式
6.1 定时调度方式选型
调度端点必须被外部周期性调用才会开票。落地方式有三种:
💡 技术拓展:周期性调用一个 HTTP 端点的调度方式对比——
方式 原理/特点 适用场景/取舍 crontab 系统自带 cron 守护按 crontab 表达式执行命令;最轻、零依赖 单机自托管首选;多机部署时各机独立、无统一视图 systemd timer systemd 单元定时触发,日志进 journal,可配依赖与精度 已全面 systemd 化的服务器;需要 journalctl统一观测时外部调度平台 分布式调度器/监控平台回调 URL(带重试、告警、执行历史) 多实例集中管理、要求触发留痕与失败告警 本方案采用 crontab:InvoicePlane 典型形态是单机自托管,cron 零额外依赖且任何 Linux 发行版自带;触发结果用 5.5 节的应用日志巡检兜底。
6.2 crontab 落地实测
以 root(或运行 Web 服务的用户)身份编辑 crontab,每 5 分钟调用一次调度端点:
crontab -l
实测回显(全新环境):
no crontab for root
写入调度条目(URL 使用 5.1 干净形态,-fsS 让失败时 curl 以非零码退出并输出错误,>/dev/null 吞掉成功时的空响应体):
echo '*/5 * * * * curl -fsS "http://192.168.146.128:8128/invoices/cron/recur/LH4UcspYjfyhn2da" > /dev/null' | crontab -
crontab -l
实测回显:
*/5 * * * * curl -fsS "http://192.168.146.128:8128/invoices/cron/recur/LH4UcspYjfyhn2da" > /dev/null
确认 cron 守护在运行(Debian/Ubuntu 服务名 cron;RHEL 系为 crond):
service cron start
实测回显:
Starting periodic command scheduler: cron.
条目逐项说明:
*/5 * * * *:每 5 分钟触发一次。循环发票的生成精度由该表达式决定——最短频率 1D 的账单,每天第一次到期后的调用即生成,不必调得更密;调密只影响「错过点的恢复速度」;curl -fsS:-f使 HTTP 4xx/5xx 以非零码退出、-s静默、-S出错时仍输出错误(失败信息进系统邮件或重定向文件);- URL 中的 key 换行无空格,与 3.2 查到的值完全一致——错误 key 会吃 5.3 的限流,连续拼错会把调度机 IP 封进 15 分钟窗口。
6.3 定时触发端到端验证(真实等一个周期)
crontab 写好后,真实等一个调度周期验证「无人值守自动开票」。给编号 1 的发票再挂一条 30D 到期规则(recur_next_date = 当天 2026-10-24),回读确认规则 4 已就绪,并把 crontab 临时改为每分钟触发、输出落文件:
mariadb -uinvoiceplane -pinvoiceplane invoiceplane -e "INSERT INTO ip_invoices_recurring (invoice_id, recur_start_date, recur_end_date, recur_frequency, recur_next_date) VALUES (2, CURDATE(), NULL, '30D', CURDATE()); SELECT invoice_recurring_id, invoice_id, recur_frequency, recur_next_date FROM ip_invoices_recurring ORDER BY invoice_recurring_id;"
实测回显(规则 4 即新插入的 30D 规则,recur_next_date = 当天,已到期):
invoice_recurring_id invoice_id recur_frequency recur_next_date
1 1 30D 2026-11-22
2 2 1M 2026-11-23
3 2 1D 2026-10-25
4 2 30D 2026-10-24
echo '* * * * * curl -fsS "http://192.168.146.128:8128/invoices/cron/recur/LH4UcspYjfyhn2da" > /tmp/cron-run.log 2>&1' | crontab -
crontab -l
实测回显:
* * * * * curl -fsS "http://192.168.146.128:8128/invoices/cron/recur/LH4UcspYjfyhn2da" > /tmp/cron-run.log 2>&1
等待下一个整分钟周期(约 75 秒)后核验三处:curl 输出文件(成功即 0 字节空体)、规则日期推进、新发票生成:
ls -la /tmp/cron-run.log
mariadb -uinvoiceplane -pinvoiceplane invoiceplane -e "SELECT invoice_recurring_id, recur_next_date FROM ip_invoices_recurring WHERE invoice_recurring_id=4; SELECT invoice_id, invoice_number, invoice_date_created FROM ip_invoices ORDER BY invoice_id DESC LIMIT 2;"
实测回显:
-rw-r--r-- 1 root root 0 Oct 24 00:48 /tmp/cron-run.log
invoice_recurring_id recur_next_date
4 2026-11-23
invoice_id invoice_number invoice_date_created
5 4 2026-10-24
4 3 2026-10-24
结论:cron 守护在 00:48 按表触发 curl(响应空体故落盘 0 字节),30D 规则的 recur_next_date 自动推进 2026-10-24 → 2026-11-23,新发票编号 4 无人工干预生成——「定时任务 → 调度端点 → 自动开票」全链路实测通过。验证完成后把 crontab 恢复为正式的 */5 节奏即可。
6.4 systemd timer 形态(替代方案)
偏好 systemd 的服务器可用 timer 替代 crontab,触发同一 URL。服务与定时器单元配置示例(/etc/systemd/system/invoiceplane-cron.service 与 /etc/systemd/system/invoiceplane-cron.timer):
[Unit]
Description=InvoicePlane recurring invoices cron call
[Service]
Type=oneshot
ExecStart=/usr/bin/curl -fsS "http://192.168.146.128:8128/invoices/cron/recur/LH4UcspYjfyhn2da"
[Unit]
Description=Call InvoicePlane cron endpoint every 5 minutes
[Timer]
OnCalendar=*:0/5
Persistent=true
[Install]
WantedBy=timers.target
配置项说明:OnCalendar=*:0/5 等价于 crontab 的 */5 * * * *;Persistent=true 让错过的周期在服务恢复后补跑一次;Type=oneshot 的 service 由 timer 拉起、执行完即退。⚠️ 此方式要求宿主机启用 systemd(Ubuntu 22.04 等主流发行版默认满足)。启用命令(重载单元定义、注册并启动定时器、确认在列):
systemctl daemon-reload
systemctl enable invoiceplane-cron.timer
systemctl start invoiceplane-cron.timer
systemctl list-timers invoiceplane-cron.timer
触发结果结合 5.5 节应用日志双重确认。无论 timer 还是 crontab,调用的 URL 与鉴权逻辑完全一致。
七、可用 Web 端点能力一览
除调度端点外,1.7.2 对外还提供三类 Web 端点。这一章把它们盘清,便于规划监控、反代与防火墙规则。
7.1 guest 公开发票/报价链接
每张发票/报价创建时自动生成 32 位十六进制的 invoice_url_key,客户凭该链接免登录查看。实测先取一张已发送发票的 key:
mariadb -uinvoiceplane -pinvoiceplane invoiceplane -e "SELECT invoice_id, invoice_status_id, invoice_url_key FROM ip_invoices WHERE invoice_id=2\G"
实测回显:
*************************** 1. row ***************************
invoice_id: 2
invoice_status_id: 3
invoice_url_key: 6301ff971166b201bfc8c4b427be9e3f
公开查看页实测(HTTP 200,页面渲染 Invoice 1 明细):
curl -s -o /tmp/guest_view.html -w '\nHTTP:%{http_code}\n' 'http://192.168.146.128:8128/index.php/guest/view/invoice/6301ff971166b201bfc8c4b427be9e3f'
grep -oE '<h2>[^<]*</h2>' /tmp/guest_view.html | head -1
实测回显:
HTTP:200
<h2>Invoice 1</h2>
同页自带 PDF 下载端点,实测返回真实 PDF(application/pdf、34105 字节):
curl -s -o /tmp/guest.pdf -w 'guest-pdf: %{http_code} type:%{content_type} size:%{size_download}\n' 'http://192.168.146.128:8128/index.php/guest/view/generate_invoice_pdf/6301ff971166b201bfc8c4b427be9e3f'
实测回显:
guest-pdf: 200 type:application/pdf size:34105
可见性门槛(实测):guest 视图按发票状态过滤(源码 guest_visible() 限定状态 IN (2,3,4),即已发送/已查看/已支付)。草稿态发票同链接直接 404:
curl -s -o /tmp/guest_draft.html -w 'guest/view/invoice(draft): %{http_code}\n' 'http://192.168.146.128:8128/index.php/guest/view/invoice/30c5c2492850422ee4ac8f3df37370a1'
grep -oE '<title>[^<]*</title>' /tmp/guest_draft.html
实测回显(30c5c2… 为草稿态发票的 url_key):
guest/view/invoice(draft): 404
<title>InvoicePlane - 404 Page Not Found</title>
同族其余端点(形态源码核实,与发票同构):报价公开页 guest/view/quote/<quote_url_key>、报价 PDF guest/view/generate_quote_pdf/<key>、报价客户确认/拒绝 guest/view/approve_quote/<key> 与 guest/view/reject_quote/<key>。
7.2 在线支付端点
公开发票页内嵌「在线支付」入口,走 guest 模块的支付信息与网关控制器(源码核实,形态如下,触发条件为后台已启用对应支付网关):
| 端点 | 方法 | 用途 |
|---|---|---|
guest/payment_information/form/<invoice_url_key>[/<provider>] | GET | 支付方式选择页 |
guest/payment_information/stripe/<invoice_url_key> | GET | 发起 Stripe 支付 |
guest/payment_information/paypal/<invoice_url_key> | GET | 发起 PayPal 支付 |
guest/gateways/stripe/create_checkout_session/<invoice_url_key> | GET | 创建 Stripe Checkout 会话 |
guest/gateways/stripe/callback/<checkout_session_id> | GET | Stripe 回调确认 |
guest/gateways/paypal/paypal_create_order/<invoice_url_key> | GET | 创建 PayPal 订单 |
guest/gateways/paypal/paypal_capture_payment/<order_id> | GET | PayPal 扣款确认 |
⚠️ 注意:未启用任何网关时这些端点不可达,公开页不渲染支付按钮。网关凭据与开关在后台「系统设置 → 在线支付」页配置(参数细节见 《InvoicePlane 1.7.2 配置参数详解》设置层)。
7.3 setup 安装向导与后台辅助端点
setup 模块承载安装向导,已安装实例必须确认其处于关闭态(防止重装攻击)。实测已安装环境访问:
curl -s -o /tmp/setup403.html -w 'setup: %{http_code}\n' 'http://192.168.146.128:8128/index.php/setup'
grep -oE '<p>[^<]*</p>' /tmp/setup403.html | head -1
实测回显:
setup: 403
<p>The setup is disabled.</p>
后台 AJAX 辅助端点(需登录会话):settings/ajax/get_cron_key 为 3.2 所述 Generate 按钮的数据源。生产巡检建议把 setup → 403 纳入拨测断言——一旦返回其他状态码说明实例状态异常。
八、注意事项
- cron_key 是唯一凭据:调度端点无用户名密码,URL 即凭据。泄露后立即后台重生成并同步改 crontab;不要把带 key 的 URL 提交进代码仓库、监控平台明文字段或群聊。
- 限流会误伤调度机:同一出口 IP 错误 key 累计 10 次(15 分钟窗口)即 429,期间正确 key 也被拒。换 key、改 URL 时先本地 curl 验证再上 crontab;中招后等窗口过期或清
ip_login_log对应行(见 5.3)。 - 静默成功要配巡检:端点成功时 0 字节空响应,失败才说话。crontab 加
-fsS+ 系统邮件(或落盘重定向),配合每天 grep[Cron Recurring Invoices]日志行,才能及时发现「没人调它」或「调了没生成」。 - 重复生成有防护但别并发轰炸:官方用条件更新原子认领防止同一账单被并发调用重复生成(5.4),事务失败会回滚到下轮重试。正常一分钟一次的频率毫无压力,无需为「怕漏」而叠加多个调度源。
- 时区影响账单日期:
recur_next_date与当天比较、invoice_date_created取值都依赖 PHP/数据库时区。部署时把系统时区、PHP 时区与业务时区对齐,否则跨日时段的调用会提前/推迟一天开票。 - CLI 与 HTTP 等效但有边界:
php index.php触发调度适合手动补跑,但要求在应用根目录执行且以能读写 application/ 目录的用户身份运行;周期任务仍走 HTTP 端点,保持与 Web 服务同一套鉴权与日志。 - guest 链接的暴露范围:任何人持有 url_key 即可查看发票与 PDF(无需登录),草稿态不可见。对外分享用已发送之后的链接;安全要求高的场景在反代层对该路径加访问控制。
九、常见问题
Q1:调度端点返回 429 怎么办?
本 IP 错误 key 已达 10 次/15 分钟限流。先确认 crontab 里 key 与 3.2 实查值一致(常见坑:复制 URL 带了空格或引号);修对后等窗口过期自动恢复,或删除 ip_login_log 中 login_name 为 cron_key:<sha256> 的计数行立即恢复(见 5.3 实测)。429 与 500 的页面文案相同,务必按状态码判断。
Q2:返回 500 Wrong cron key provided! 但我确认 key 没错?
逐一排查:URL 中 key 是否完整 16 位十六进制;是否把后台「Generate」重生成后的旧 key 留在了 crontab(重生成后旧 key 立即失效);是否环境变量 CRON_IP_MAX_ATTEMPTS 被调小导致还没改对就被限流。用 5.2 的 curl 命令手工验证 key,通过后再更新 crontab。
Q3:端点返回 200 但没有生成新发票?
200 只代表鉴权通过,无到期规则时就是静默空跑。按 5.4 的过滤条件自查:规则 recur_next_date 是否 <= 当天;recur_end_date 是否已过期(到期当天即失活,源码 stop 逻辑把结束日期设为当天并清空 next date);源发票是否还存在。数据库直查 ip_invoices_recurring 核对这三项。
Q4:crontab 明明写了却不执行?
按层排查:service cron status 确认守护在跑;crontab -l 确认条目在当前用户的表里(/etc/cron.d 与用户 crontab 是两套);命令里 curl 用绝对路径 /usr/bin/curl(cron 环境 PATH 精简,这是最常见翻车点);重定向输出到文件观察退出码。全链路验证法见 6.3——临时改成每分钟,两分钟内看结果。
Q5:HTTP 访问 /index.php/setup/cli/migrate 返回 403?
这是预期行为。CLI 路由做了 is_cli() 强制校验,只能 php index.php 命令行执行(见 4.1 实测)。它不是故障,也意味着这些管理动作不会暴露给 Web。
Q6:调度端点干了什么、从哪里看痕迹?
应用日志 /var/www/html/application/logs/log-YYYY-MM-DD.php 中 grep [Cron Recurring Invoices](见 5.5 实测行式);数据库侧看 ip_invoices_recurring.recur_next_date 是否推进、ip_invoices 是否新增编号(见 5.4/6.3 实测)。开启 ENABLE_DEBUG 后日志会更详细,但排障完记得关(参数细节见配置篇)。
Q7:能否不暴露 HTTP,直接在服务器内部触发调度?
可以,cd /var/www/html && php index.php invoices/cron/recur/<key>(见 4.4 实测,exit=0 静默成功)。适合禁止对外暴露该 URL 的内网部署;crontab 里用这条命令替代 curl 即可,鉴权与限流逻辑完全相同。
十、参考链接
- 调度控制器源码:主仓库
application/modules/invoices/controllers/Cron.php(限流、鉴权、生成事务;v1.7.2 tag,2026-10-24 核对) - CLI 控制器源码:主仓库
application/modules/setup/controllers/Cli.php(migrate / create_default_user,is_cli()隔离;2026-10-24 核对) - 循环规则模型:主仓库
application/modules/invoices/models/Mdl_invoices_recurring.php(active 过滤、频率表、原子认领;2026-10-24 核对) - guest 模块控制器:主仓库
application/modules/guest/controllers/View.php、Payment_information.php、gateways/Stripe.php、gateways/Paypal.php(公开端点形态;2026-10-24 核对) - cron_key 生成端点:主仓库
application/modules/settings/controllers/Ajax.php(generate_secure_token(8);2026-10-24 核对) - 官方主仓库:https://github.com/InvoicePlane/InvoicePlane (v1.7.2 tag;2026-10-24 查询)
- 官方 Wiki(使用与设置):https://wiki.invoiceplane.com/ (2026-10-24 查询)
- 部署过程:《InvoicePlane 1.7.2 Docker Compose 部署指南》
- 参数体系(cron_key / 限流 / 自动邮件开关逐项说明):《InvoicePlane 1.7.2 配置参数详解》
- 软件定位与选型:《InvoicePlane 介绍与选型指南》