conf = (array) config('qyt'); $this->crypto = new QytCrypto($this->conf); } // ================================================================ // 7.7 运单油费下发(一张订单 = 1 batch / 1 waybill / 1 employee) // ================================================================ /** * @param array $data ['batch_no','waybill_no','waybill_no_code','mobile','name','amount'(元)] * @return array 归一化 ['code'=>'00'|其它, 'data'=>[], 'message'=>'', 'dup_batch'=>bool] */ public function createAndSendQuota(array $data): array { $params = [ 'batch_no' => $data['batch_no'], 'business_license' => $this->conf['business_license'], 'passageway' => (string) self::PASSAGEWAY_ALIPAY, 'institution_id' => $this->conf['institution_id'], 'waybill_list' => [[ 'waybill_no' => $data['waybill_no'], 'employee_list' => [[ 'mobile' => $data['mobile'], 'name' => $data['name'], 'quota_amount' => self::yuanToFen($data['amount']), // 分 'voucher_name' => $this->conf['voucher_name'], 'effective_start' => '', // 空=当日生效 'effective_end' => '', // 空=长期有效 'waybill_no_code' => $data['waybill_no_code'], ]], ]], ]; return $this->post('/enterprise/api/zhyk/createAndSendQuota', $params); } // ================================================================ // 7.15 H5 免登加油地址(本地签名生成 URL,不发起接口请求) // ================================================================ public function h5Url(string $mobile): string { $creditcode = !empty($this->conf['h5_with_creditcode']) ? (string) $this->conf['business_license'] : ''; return $this->crypto->h5Url($mobile, $creditcode); } // ================================================================ // 内部:统一请求 // ================================================================ /** * @return array ['code' => '00'成功 | 其它, 'data' => array, 'message' => string, 'dup_batch' => bool, 'raw' => array] */ protected function post(string $path, array $bizParams): array { $url = rtrim((string) $this->conf['base_url'], '/') . $path; $body = $this->crypto->envelope($bizParams); $this->saveLog("[请求] {$path} " . json_encode(['url' => $url, 'params' => $bizParams], JSON_UNESCAPED_UNICODE)); $resp = $this->curlPost($url, json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->saveLog("[响应] {$path} " . $resp); $json = json_decode($resp, true); if (!is_array($json)) { return ['code' => 'ERR', 'data' => [], 'message' => '响应解析失败', 'dup_batch' => false, 'raw' => ['body' => $resp]]; } // 通用响应结构:{ status: ok|error, data, message, serialNo, sign } $ok = ($json['status'] ?? '') === 'ok'; // 【TODO 待对接人确认】是否所有响应都带 sign、验签串如何构造 // if ($ok && !empty($json['sign'])) { $ok = $this->crypto->verify($json['data'] ?? [], $json['sign']); } $message = (string) ($json['message'] ?? ''); // 幂等:batch_no 已存在 —— 视为「已受理」,供 7.7 重试判定 $dupBatch = false; if (!$ok && $message !== '') { foreach ((array) (config('qyt.dup_batch_hints') ?: ['已存在', '重复', 'duplicate', 'exist']) as $hint) { if (mb_stripos($message, $hint) !== false) { $dupBatch = true; break; } } } return [ 'code' => $ok ? '00' : 'ERR', 'data' => $json['data'] ?? [], 'message' => $message, 'dup_batch' => $dupBatch, 'raw' => $json, ]; } protected function curlPost(string $url, string $jsonBody): string { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $jsonBody, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => (int) ($this->conf['timeout'] ?? 15), CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Charset: UTF-8'], ]); $resp = curl_exec($ch); if ($resp === false) { $err = curl_error($ch); curl_close($ch); return json_encode(['status' => 'error', 'message' => 'cURL: ' . $err], JSON_UNESCAPED_UNICODE); } curl_close($ch); return (string) $resp; } // ================================================================ // 工具 // ================================================================ /** 元 → 分 */ public static function yuanToFen($yuan): int { return (int) bcmul((string) $yuan, '100', 0); } /** 分 → 元(字符串,保留 2 位) */ public static function fenToYuan($fen): string { return bcdiv((string) (int) $fen, '100', 2); } /** * 生成企油通全平台唯一编号(加企业前缀),确定性派生 -> 重试保持一致 * @param string $type B=batch / W=waybill / C=waybill_no_code * @param string $seed 订单号 / order_id 等业务种子 */ public static function genNo(string $type, string $seed): string { $prefix = (string) (config('qyt.no_prefix') ?: 'HM'); return $prefix . $type . $seed; } /** 记录日志到 runtime/storelog */ public function saveLog($data): void { if (empty($this->conf['log'])) { return; } $dir = runtime_path('storelog/') . date('Ym') . '/' . date('Ymd'); is_dir($dir) || @mkdir($dir, 0777, true); @file_put_contents( $dir . '/qyt_' . date('YmdH') . '.txt', date('Y-m-d H:i:s ') . (is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_UNICODE)) . "\r\n", FILE_APPEND ); } }