/qytNotify/consume 企油通 7.10「运单加油明细推送」 // | public/qytwaybill.php -> /qytNotify/waybill 企油通 3.10「发放结果通知」 // | // | 两个回调都是 multipart/form-data,业务参数在表单字段里(JSON 字符串): // | 7.10 -> params 我方须返回 {"code":"100","msg":"Success"} // | 3.10 -> data + sign 我方只要返回 HTTP 2xx 即算成功,响应体不做要求 // +---------------------------------------------------------------------- declare (strict_types=1); namespace app\api\controller; use app\common\crypto\Qyt as QytCrypto; use app\common\service\QueueService; use app\common\service\QytOil; use think\facade\Request; class QytNotify extends Controller { /** * 7.10 运单加油明细推送 * @return \think\response\Json */ public function consume() { $request = Request::instance(); // ⚠️ 企油通用 multipart/form-data 推送,业务参数在表单字段 params 里(JSON 字符串)。 // 这种 Content-Type 下 PHP 不会填充 php://input,读请求体恒为空 —— 必须取表单字段。 // 回退读请求体只是为了兼容 JSON 推送与本地测试脚本。 // ⚠️ 这里必须直接读 $_POST:cores\Request 给所有入参挂了全局过滤器 // ['my_trim','my_htmlspecialchars','filter_emoji'],走 $request->post() 取到的 JSON // 引号会被转义成 ",json_decode 必然失败(回调会被静默当成空明细忽略) $formParams = isset($_POST['params']) ? trim((string) $_POST['params']) : ''; $raw = $request->getContent(); (new QytOil())->saveLog('7.10 消费回调原文[' . $request->contentType() . ']:' . ($formParams !== '' ? 'params=' . $formParams : $raw)); if ($formParams !== '') { $params = json_decode($formParams, true); // 兜底:若上游或中间层做过 HTML 转义,反转义后再解析一次 if (!is_array($params) && strpos($formParams, '"') !== false) { $params = json_decode(htmlspecialchars_decode($formParams, ENT_QUOTES), true); } } else { // 兼容:外层信封 {appId,timestamp,sign,params(JSON字符串)} 或 直接推 body $body = json_decode($raw, true); $params = $body; if (is_array($body) && isset($body['params'])) { $params = is_string($body['params']) ? json_decode($body['params'], true) : $body['params']; } } // TODO 验签(规则待对接人确认) // if (is_array($body) && !empty($body['sign'])) { // $crypto = new QytCrypto(); // if (!$crypto->verify($params, $body['sign'])) { // return json(['code' => '99', 'msg' => '验签失败']); // } // } // 无明细也回 100,避免企油通重复推送 if (is_array($params) && !empty($params['order_list'])) { (new QueueService())->push('callback_queue', json_encode([ 'header' => ['method' => 'qyt_consume'], 'res' => $params, ], 320)); } else { (new QytOil())->saveLog('7.10 消费回调无 order_list,已忽略'); } return json(['code' => '100', 'msg' => 'Success']); } /** * 3.10 发放结果通知(运单油费 / 企业福利) * * 企油通把 7.7 提交的批次处理完后(全部成功 / 部分成功 / 处理失败)主动推过来, * 一个批次一条,内容与 7.8 查询结果一致。接上之后就不必再靠轮询等结果。 * * 约定(见文档 3.10): * · form-data,字段 data(JSON 字符串)+ sign * · scene 区分场景:oil=运单油费下发(7.7),welfare=员工福利下发(9.3) * · 接收端返回 HTTP 2xx 即视为成功;非 2xx 或 5 秒内无响应会重试,最多 5 次 * · 同一批次可能收到多次(重试 / 批次重新提交),必须幂等 * · 明细最多约 1 万条,报文可能很大 * * 所以这里只做「尽快收下、丢进队列、立刻回 200」,真正的状态跃迁交给 * callback:queue 异步做,避免大报文处理超过上游 5 秒超时导致无谓重试。 * * @return \think\response\Json */ public function waybill() { $request = Request::instance(); // ⚠️ 必须直接读 $_POST:cores\Request 给所有入参挂了全局过滤器 // ['my_trim','my_htmlspecialchars','filter_emoji'],走 $request->post() 取到的 // JSON 引号会被转义成 ",json_decode 必然失败(通知会被静默丢掉) $formData = isset($_POST['data']) ? trim((string) $_POST['data']) : ''; $sign = isset($_POST['sign']) ? trim((string) $_POST['sign']) : ''; $raw = $request->getContent(); $payload = $formData !== '' ? $formData : $raw; $qyt = new QytOil(); // 明细可能上万条,日志只留头部,完整报文靠 batch_no 去企油通后台对 $qyt->saveLog('3.10 发放结果通知原文[' . $request->contentType() . '](截断):' . mb_substr($payload, 0, 4000) . (mb_strlen($payload) > 4000 ? ' …(已截断)' : '')); $data = json_decode($payload, true); // 兜底:若上游或中间层做过 HTML 转义,反转义后再解析一次 if (!is_array($data) && strpos($payload, '"') !== false) { $data = json_decode(htmlspecialchars_decode($payload, ENT_QUOTES), true); } // 兼容:直接推 JSON body 且外层包了一层 {data: ..., sign: ...} if (is_array($data) && isset($data['data'])) { $sign === '' && $sign = (string) ($data['sign'] ?? ''); $data = is_string($data['data']) ? json_decode($data['data'], true) : $data['data']; } if (!is_array($data) || empty($data['batch_no'])) { $qyt->saveLog('3.10 发放结果通知解析失败或缺少 batch_no,已忽略'); // 仍然回 2xx:报文我方解析不了,让上游重试 5 次也没有意义, // 这类单由 7.8 轮询兜底(轮询并没有下线) return json(['code' => '100', 'msg' => 'Success']); } // TODO 验签(对 data 原串签名,规则见开发指南 2.5,待对接人给到平台公钥与算法确认) // if ($sign !== '' && !(new QytCrypto())->verifyNotify($payload, $sign)) { // $qyt->saveLog('3.10 发放结果通知验签失败 batch_no=' . $data['batch_no']); // return json(['code' => '99', 'msg' => 'sign error']); // } // 只处理运单油费(7.7)。员工福利(9.3)我方没有对接,收下回 2xx 即可, // 否则上游会按失败重试 5 次。 $scene = (string) ($data['scene'] ?? 'oil'); if ($scene !== 'oil') { $qyt->saveLog('3.10 发放结果通知 scene=' . $scene . '(非运单油费),已忽略 batch_no=' . $data['batch_no']); return json(['code' => '100', 'msg' => 'Success']); } (new QueueService())->push('callback_queue', json_encode([ 'header' => ['method' => 'qyt_waybill_result'], 'res' => $data, ], 320)); return json(['code' => '100', 'msg' => 'Success']); } }