'系统审核中', 11 => '系统审核通过', 110 => '订单药房流转制作中', 20 => '物流中', 30 => '完成', 90 => '拦截', 91 => '主动撤单', 92 => '驳回', ]; /** * 物流商名称 → express_company 编码映射 */ private const EXPRESS_MAP = [ '顺丰' => 'sf', '京东' => 'jd', '极兔' => 'jt', '圆通' => 'yt', '中通' => 'zt', '韵达' => 'yd', '申通' => 'st', '邮政' => 'yz', 'EMS' => 'ems', ]; /** * 甘草订单状态回调入口 */ public function orderStatus(): Response { try { $rawBody = file_get_contents('php://input'); $headers = $this->request->header(); $accessAppkey = (string) ($headers['access-appkey'] ?? ''); $accessNonce = (string) ($headers['access-nonce'] ?? ''); $accessTimestamp = (string) ($headers['access-timestamp'] ?? ''); $accessSign = (string) ($headers['access-sign'] ?? ''); Log::info('Gancao callback received', [ 'appkey' => $accessAppkey, 'nonce' => $accessNonce, 'ts' => $accessTimestamp, 'sign' => $accessSign, 'body' => $rawBody, ]); if (!$this->verifySign($accessAppkey, $accessNonce, $accessTimestamp, $accessSign, $rawBody)) { Log::warning('Gancao callback sign verification failed'); return $this->ok(); } $data = json_decode($rawBody, true); if (!is_array($data)) { Log::error('Gancao callback invalid json', ['body' => $rawBody]); return $this->ok(); } $this->handleCallback($data); } catch (\Throwable $e) { Log::error('Gancao callback exception', [ 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), ]); } return $this->ok(); } /* ------------------------------------------------------------------ */ /* 签名验证 */ /* ------------------------------------------------------------------ */ /** * md5(access-appkey + secret-key + access-nonce + access-timestamp + $sBody) */ private function verifySign(string $appkey, string $nonce, string $timestamp, string $sign, string $body): bool { if ($sign === '' || $appkey === '') { return false; } $config = Config::get('gancao_scm', []); $cfgAppkey = (string) ($config['biz_ak'] ?? ''); $secretKey = (string) ($config['biz_sk'] ?? ''); if ($appkey !== $cfgAppkey) { Log::warning('Gancao callback appkey mismatch', compact('appkey', 'cfgAppkey')); return false; } $expected = md5($appkey . $secretKey . $nonce . $timestamp . $body); if (!hash_equals($expected, $sign)) { Log::warning('Gancao callback sign mismatch', compact('sign', 'expected')); return false; } return true; } /* ------------------------------------------------------------------ */ /* 回调数据处理 */ /* ------------------------------------------------------------------ */ private function handleCallback(array $data): void { $recipelOrderNo = (string) ($data['recipel_order_no'] ?? ''); $appOrderNo = (string) ($data['app_order_no'] ?? ''); $state = (int) ($data['state'] ?? 0); $ext = is_array($data['ext'] ?? null) ? $data['ext'] : []; if ($recipelOrderNo === '' && $appOrderNo === '') { Log::warning('Gancao callback missing order no', ['data' => $data]); return; } $order = $this->findOrder($recipelOrderNo, $appOrderNo); if (!$order) { Log::warning('Gancao callback order not found', compact('recipelOrderNo', 'appOrderNo')); return; } $this->updateOrderStatus($order, $state, $ext); $this->writeCallbackLog($order, $state, $ext); Log::info('Gancao callback processed', [ 'order_id' => $order->id, 'recipel_order_no' => $recipelOrderNo, 'state' => $state, 'ext' => $ext, ]); } /** * 通过甘草处方单号或应用商订单号查找本地订单 */ private function findOrder(string $recipelOrderNo, string $appOrderNo): ?PrescriptionOrder { if ($recipelOrderNo !== '') { $order = PrescriptionOrder::where('gancao_reciperl_order_no', $recipelOrderNo) ->whereNull('delete_time') ->find(); if ($order) { return $order; } } if ($appOrderNo !== '') { return PrescriptionOrder::where('order_no', $appOrderNo) ->whereNull('delete_time') ->find() ?: null; } return null; } /* ------------------------------------------------------------------ */ /* 订单状态更新 */ /* ------------------------------------------------------------------ */ /** * state 说明: * 10 系统审核中 * 11 系统审核通过 * 110 订单药房流转制作中(ext: flow_name, supplier) * 20 物流中(ext: shipping_name, nu, supplier) * 30 完成 - 终态(ext: shipping_name, nu, supplier) * 90 拦截 - 可恢复 * 91 主动撤单 - 终态(退费) * 92 驳回 - 终态(无法制作并退费) */ private function updateOrderStatus(PrescriptionOrder $order, int $state, array $ext): void { $order->gancao_order_state = $state; switch ($state) { case 10: case 11: break; case 110: $this->handleProduction($order, $ext); break; case 20: $this->handleShipping($order, $ext); break; case 30: $this->handleShipping($order, $ext); if ((int) $order->fulfillment_status !== 4) { $order->fulfillment_status = 3; // 已完成 } break; case 90: $order->gancao_remark = '甘草订单被拦截(可恢复)'; break; case 91: if ((int) $order->fulfillment_status !== 3) { $order->fulfillment_status = 4; // 已取消 } $order->gancao_remark = '甘草主动撤单(已退费)'; break; case 92: if ((int) $order->fulfillment_status !== 3) { $order->fulfillment_status = 4; // 已取消 } $order->gancao_remark = '甘草驳回(无法制作并退费)'; break; } try { $order->save(); } catch (\Throwable $e) { Log::error('Gancao callback save failed', [ 'order_id' => $order->id, 'error' => $e->getMessage(), ]); } } /** * state=110:药房流转制作中 */ private function handleProduction(PrescriptionOrder $order, array $ext): void { $flowName = (string) ($ext['flow_name'] ?? ''); $supplier = (string) ($ext['supplier'] ?? ''); if ($flowName !== '') { $order->gancao_flow_name = mb_substr($flowName, 0, 100); } if ($supplier !== '') { $order->gancao_supplier = mb_substr($supplier, 0, 100); } $fs = (int) $order->fulfillment_status; if ($fs === 2 && (str_contains($flowName, '发货') || str_contains($flowName, '寄出'))) { $order->fulfillment_status = 5; // 已发货 } } /** * state=20/30:物流中 / 已完成 — 回写快递单号与快递公司 */ private function handleShipping(PrescriptionOrder $order, array $ext): void { $shippingName = (string) ($ext['shipping_name'] ?? ''); $nu = (string) ($ext['nu'] ?? ''); $supplier = (string) ($ext['supplier'] ?? ''); if ($nu !== '') { $order->tracking_number = mb_substr($nu, 0, 80); } if ($shippingName !== '') { $order->gancao_shipping_name = mb_substr($shippingName, 0, 50); $order->express_company = $this->resolveExpressCode($shippingName); } if ($supplier !== '') { $order->gancao_supplier = mb_substr($supplier, 0, 100); } $fs = (int) $order->fulfillment_status; if (in_array($fs, [1, 2], true)) { $order->fulfillment_status = 5; // 已发货 } } /** * 将甘草返回的物流商名称解析为系统内 express_company 短码 */ private function resolveExpressCode(string $shippingName): string { foreach (self::EXPRESS_MAP as $keyword => $code) { if (str_contains($shippingName, $keyword)) { return $code; } } return 'auto'; } /* ------------------------------------------------------------------ */ /* 操作日志 */ /* ------------------------------------------------------------------ */ private function writeCallbackLog(PrescriptionOrder $order, int $state, array $ext): void { $stateName = self::STATE_MAP[$state] ?? "未知状态({$state})"; $summary = "甘草回调:{$stateName}"; if (isset($ext['flow_name'])) { $summary .= " | 流程:{$ext['flow_name']}"; } if (isset($ext['supplier'])) { $summary .= " | 药房:{$ext['supplier']}"; } if (isset($ext['shipping_name'])) { $summary .= " | 物流:{$ext['shipping_name']}"; } if (isset($ext['nu'])) { $summary .= " | 单号:{$ext['nu']}"; } try { $log = new PrescriptionOrderLog(); $log->prescription_order_id = (int) $order->id; $log->admin_id = 0; $log->admin_name = '甘草系统'; $log->action = 'gancao_callback'; $log->summary = mb_substr($summary, 0, 500); $log->create_time = time(); $log->save(); } catch (\Throwable $e) { Log::warning('Gancao callback log write failed', ['error' => $e->getMessage()]); } } /* ------------------------------------------------------------------ */ /* 响应 */ /* ------------------------------------------------------------------ */ private function ok(): Response { return response('ok', 200, [], 'html'); } }