$_post为空是正常现象,因微信回调发raw xml、支付宝发form-encoded body,均不进入$_post;须改用getrawinput()或php://input读取,且立即记日志并验签。

$_POST 为空不是 bug,是微信/支付宝根本没走它
TP6.0 默认用 $this->request->post() 拿参数,但微信回调发的是 raw XML,支付宝发的是 form-encoded body —— 这俩都不进 $_POST。直接读 $_POST 必然为空,验签前就挂了。
- 必须改用
$this->request->getRawInput()(TP6.0+)或file_get_contents('php://input'),且只读一次 - 读完立刻记日志:
file_put_contents(LOG_PATH . 'notify_raw.log', date('c') . "\n" . $raw . "\n---\n", FILE_APPEND) - XML 要先
simplexml_load_string()再转数组;支付宝表单要用parse_str($raw, $data),别用json_decode - 字段名不能被框架自动转换:微信的
out_trade_no若变成outTradeNo,验签必失败
回调入口不写日志,等于没上线
很多 TP6 项目在控制器里直接调验签、更新订单,中间没留痕。一旦失败,连“请求是否到达”都确认不了。
ThinkPHP 8.1.0 正式发布,深度优化路由与验证机制,完美兼容 PHP 8.4。本版本修复了数组路由配置异常,新增枚举值校验与高级数组验证功能,支持路由分类默认处理。作为高性能 PHP 框架的最新迭代,它延续了简洁实用的设计原则,提供更稳定的底层架构与更流畅的开发体验,助力开发者快速构建现代化 Web 应用与企业级系统。
- 日志必须包含:
$_SERVER['HTTP_USER_AGENT']、$_SERVER['REMOTE_ADDR']、原始 body、验签原文字符串(不是最终签名值)、DB 执行影响行数 - 不要用
Log::info()包裹整个流程——它可能异步写入,而进程崩溃时日志丢失;用file_put_contents同步追加更可靠 - 日志路径建议绝对路径,避开 TP 的 runtime 目录(可能被清理),例如
/var/log/tp6_notify/ - 每条日志开头加时间戳和唯一 trace_id,方便关联 Nginx access.log 和数据库记录
持久化必须在验签前完成,且用 INSERT IGNORE 或 SELECT FOR UPDATE
验签失败也要存原始数据。否则你永远不知道是密钥错了,还是参数被截断了。
- 建表
payment_callback_log,字段含callback_data TEXT(存 raw body)、status TINYINT(0=待处理,1=成功,2=验签失败,3=DB失败) - 入口第一行就执行插入:
Db::table('payment_callback_log')->insert(['order_no' => $order_no, 'callback_data' => $raw, 'status' => 0]) - 更新订单状态时,务必用原子 SQL:
UPDATE orders SET status = 'paid', paid_at = NOW() WHERE order_no = ? AND status = 'unpaid',影响行数为 0 就说明已处理过 - 高并发下避免用
SELECT + UPDATE,改用INSERT IGNORE INTO payment_callback_log (order_no, ...) VALUES (?, ...)防重
补偿任务不能靠 sleep 或 while retry,得进队列+定时扫描
TP6 自带的 think-queue 不适合支付补偿:它依赖 PHP-FPM 进程,失败后无法自动恢复;且没有“下次执行时间”字段,没法做指数退避。
- 建补偿表
compensation_task,关键字段:next_execute_at DATETIME、retry_count INT、task_status ENUM('pending','running','failed','done') - 回调失败时,插入一条
task_status = 'pending'记录,并设next_execute_at = DATE_ADD(NOW(), INTERVAL 1 MINUTE) - 用 Linux cron 每分钟跑一次命令:
php think compensation:scan,只查WHERE task_status = 'pending' AND next_execute_at - 扫描脚本里限制单次最多处理 50 条,避免锁表;每处理一条,更新
last_execute_at和retry_count,失败则设next_execute_at为DATE_ADD(NOW(), INTERVAL POW(2, retry_count) MINUTE)
callback_data 快照,而不是只存 order_no。因为重试时平台可能已变更字段(如微信升级 v3 接口),旧逻辑拿不到新字段就会崩。










