normalizePayloadForHash($payload), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $existingRef = Db::name('enterprise_customer_order_refs') ->where('customer_id', (int)$customer['id']) ->where('external_order_no', $externalOrderNo) ->find(); if ($existingRef) { if (($existingRef['payload_hash'] ?? '') !== $payloadHash) { throw new \RuntimeException('external_order_no 已存在,但请求内容不一致'); } return [ 'idempotent' => true, 'order' => $this->buildOrderProgress((int)$customer['id'], $existingRef, (string)$customer['customer_code']), ]; } $serviceProvider = trim((string)($payload['service_provider'] ?? 'anxinyan')); if (!in_array($serviceProvider, ['anxinyan', 'zhongjian'], true)) { throw new \InvalidArgumentException('service_provider 无效'); } $pricePackageCode = trim((string)($payload['price_package_code'] ?? '')); try { $servicePackage = $this->pricePackageSnapshot($serviceProvider, $pricePackageCode); } catch (\RuntimeException $e) { throw new \InvalidArgumentException($e->getMessage()); } $product = $this->normalizeProduct((array)($payload['product_info'] ?? [])); $returnAddress = $this->normalizeReturnAddress((array)($payload['return_address'] ?? [])); $materials = $this->normalizeMaterials((array)($payload['materials'] ?? [])); $now = date('Y-m-d H:i:s'); $orderNo = 'AXY' . date('YmdHis') . mt_rand(100, 999); $appraisalNo = 'AXY-APP-' . date('Ymd') . '-' . mt_rand(1000, 9999); $estimated = date('Y-m-d H:i:s', strtotime(sprintf('+%d hours', (int)$servicePackage['sla_hours']))); $userId = (new EnterpriseCustomerService())->ensureVirtualUser($customer); $productName = $this->resolveProductName($product); Db::startTrans(); try { $orderId = (int)Db::name('orders')->insertGetId([ 'order_no' => $orderNo, 'appraisal_no' => $appraisalNo, 'user_id' => $userId, 'service_mode' => 'physical', 'service_provider' => $serviceProvider, 'payment_status' => 'paid', 'order_status' => 'pending_shipping', 'display_status' => '待寄送商品', 'estimated_finish_time' => $estimated, 'source_channel' => 'enterprise_push', 'source_customer_id' => $customer['customer_code'], 'price_package_id' => $servicePackage['price_package_id'], 'price_package_name' => $servicePackage['price_package_name'], 'price_package_code' => $servicePackage['price_package_code'], 'price_package_price' => $servicePackage['price_package_price'], 'pay_amount' => $servicePackage['pay_amount'], 'paid_at' => $now, 'created_at' => $now, 'updated_at' => $now, ]); Db::name('order_products')->insert(array_merge($product, [ 'order_id' => $orderId, 'product_name' => $productName, 'product_cover' => $materials[0]['file_url'] ?? '', 'created_at' => $now, 'updated_at' => $now, ])); $extra = (array)($payload['extra_info'] ?? []); Db::name('order_extras')->insert([ 'order_id' => $orderId, 'purchase_channel' => trim((string)($extra['purchase_channel'] ?? '')), 'purchase_price' => (float)($extra['purchase_price'] ?? 0), 'purchase_date' => $extra['purchase_date'] ?? null, 'usage_status' => trim((string)($extra['usage_status'] ?? '')), 'condition_desc' => trim((string)($extra['condition_desc'] ?? '')), 'has_accessories' => !empty($extra['has_accessories']) ? 1 : 0, 'accessories_json' => json_encode(array_values((array)($extra['accessories'] ?? [])), JSON_UNESCAPED_UNICODE), 'remark' => trim((string)($extra['remark'] ?? '')), 'created_at' => $now, 'updated_at' => $now, ]); if ($returnAddress) { Db::name('order_return_addresses')->insert(array_merge($returnAddress, [ 'order_id' => $orderId, 'user_address_id' => null, 'created_at' => $now, 'updated_at' => $now, ])); } $shippingTarget = (new WarehouseService())->bindOrderTarget( $orderId, $serviceProvider, !empty($product['category_id']) ? (int)$product['category_id'] : null, null ); $this->insertMaterials($orderId, $materials, $now); $this->insertTimelines($orderId, $now, $shippingTarget); $this->insertTask($orderId, $serviceProvider, $estimated, $now); $this->insertInboundLogistics($orderId, $this->normalizeInboundLogistics($payload), $now); Db::name('enterprise_customer_order_refs')->insert([ 'customer_id' => (int)$customer['id'], 'external_order_no' => $externalOrderNo, 'order_id' => $orderId, 'order_no' => $orderNo, 'appraisal_no' => $appraisalNo, 'payload_hash' => $payloadHash, 'created_at' => $now, 'updated_at' => $now, ]); Db::commit(); } catch (\Throwable $e) { Db::rollback(); throw $e; } (new EnterpriseWebhookService())->recordOrderEvent($orderId, 'order_created', [ 'product_name' => $productName, 'price_package_name' => $servicePackage['price_package_name'], 'pay_amount' => (float)$servicePackage['pay_amount'], ]); $ref = Db::name('enterprise_customer_order_refs')->where('order_id', $orderId)->find(); return [ 'idempotent' => false, 'order' => $this->buildOrderProgress((int)$customer['id'], $ref, (string)$customer['customer_code']), ]; } public function findOrder(array $customer, string $externalOrderNo = '', string $orderNo = ''): array { $query = Db::name('enterprise_customer_order_refs')->where('customer_id', (int)$customer['id']); if ($externalOrderNo !== '') { $query->where('external_order_no', $externalOrderNo); } elseif ($orderNo !== '') { $query->where('order_no', $orderNo); } else { throw new \InvalidArgumentException('external_order_no 或 order_no 不能为空'); } $ref = $query->find(); if (!$ref) { throw new \RuntimeException('订单不存在'); } return $this->buildOrderProgress((int)$customer['id'], $ref, (string)$customer['customer_code']); } public function buildOrderProgress(int $customerId, array $ref, string $customerCode = ''): array { $order = Db::name('orders')->where('id', (int)$ref['order_id'])->find(); if (!$order) { throw new \RuntimeException('订单不存在'); } $timeline = Db::name('order_timelines')->where('order_id', (int)$order['id'])->order('occurred_at', 'asc')->select()->toArray(); $sendLogistics = Db::name('order_logistics')->where('order_id', (int)$order['id'])->where('logistics_type', 'send_to_center')->order('id', 'desc')->find(); $returnLogistics = Db::name('order_logistics')->where('order_id', (int)$order['id'])->where('logistics_type', 'return_to_user')->order('id', 'desc')->find(); $report = Db::name('reports') ->where('order_id', (int)$order['id']) ->where('report_status', 'published') ->order('id', 'desc') ->find(); $verify = $report ? (Db::name('report_verifies')->where('report_id', (int)$report['id'])->find() ?: null) : null; return [ 'customer_id' => $customerCode !== '' ? $customerCode : (string)$customerId, 'customer_code' => $customerCode !== '' ? $customerCode : (string)$customerId, 'external_order_no' => (string)$ref['external_order_no'], 'order_id' => (int)$order['id'], 'order_no' => (string)$order['order_no'], 'appraisal_no' => (string)$order['appraisal_no'], 'order_status' => (string)$order['order_status'], 'display_status' => (string)$order['display_status'], 'payment_status' => (string)$order['payment_status'], 'price_package_name' => (string)($order['price_package_name'] ?? ''), 'price_package_code' => (string)($order['price_package_code'] ?? ''), 'price_package_price' => (float)($order['price_package_price'] ?? 0), 'pay_amount' => (float)$order['pay_amount'], 'estimated_finish_time' => (string)($order['estimated_finish_time'] ?? ''), 'created_at' => (string)$order['created_at'], 'timeline' => array_map(fn(array $item) => [ 'node_code' => (string)$item['node_code'], 'node_text' => (string)$item['node_text'], 'node_desc' => (string)$item['node_desc'], 'occurred_at' => (string)$item['occurred_at'], ], $timeline), 'inbound_logistics' => $this->formatLogistics($sendLogistics), 'return_logistics' => $this->formatLogistics($returnLogistics), 'report_summary' => $report ? [ 'report_no' => (string)$report['report_no'], 'report_title' => (string)$report['report_title'], 'report_status' => (string)$report['report_status'], 'publish_time' => (string)($report['publish_time'] ?? ''), 'verify_url' => (string)($verify['verify_url'] ?? ''), 'report_page_url' => (string)($verify['verify_qrcode_url'] ?? ''), 'verify_status' => (string)($verify['verify_status'] ?? ''), ] : null, ]; } private function normalizePayloadForHash(array $payload): array { ksort($payload); foreach ($payload as &$value) { if (is_array($value)) { $value = $this->normalizePayloadForHash($value); } } unset($value); return $payload; } private function normalizeProduct(array $product): array { $productName = trim((string)($product['product_name'] ?? '')); return [ 'category_id' => !empty($product['category_id']) ? (int)$product['category_id'] : null, 'category_name' => trim((string)($product['category_name'] ?? '')), 'brand_id' => !empty($product['brand_id']) ? (int)$product['brand_id'] : null, 'brand_name' => trim((string)($product['brand_name'] ?? '')), 'color' => trim((string)($product['color'] ?? '')), 'size_spec' => trim((string)($product['size_spec'] ?? '')), 'serial_no' => trim((string)($product['serial_no'] ?? '')), 'product_name' => $productName, ]; } private function normalizeReturnAddress(array $address): ?array { $requiredKeys = ['consignee', 'mobile', 'province', 'city', 'district', 'detail_address']; $hasAnyValue = false; foreach ($requiredKeys as $key) { if (trim((string)($address[$key] ?? '')) !== '') { $hasAnyValue = true; break; } } if (!$hasAnyValue) { return null; } foreach ($requiredKeys as $key) { if (trim((string)($address[$key] ?? '')) === '') { throw new \InvalidArgumentException("return_address.{$key} 不能为空"); } } return [ 'consignee' => trim((string)$address['consignee']), 'mobile' => trim((string)$address['mobile']), 'province' => trim((string)$address['province']), 'city' => trim((string)$address['city']), 'district' => trim((string)$address['district']), 'detail_address' => trim((string)$address['detail_address']), ]; } private function normalizeMaterials(array $materials): array { $list = []; foreach ($materials as $index => $item) { if (is_string($item)) { $url = trim($item); $itemCode = 'material_' . ($index + 1); $itemName = '鉴定资料'; } elseif (is_array($item)) { $url = trim((string)($item['file_url'] ?? $item['url'] ?? '')); $itemCode = trim((string)($item['item_code'] ?? 'material_' . ($index + 1))); $itemName = trim((string)($item['item_name'] ?? '鉴定资料')); } else { $url = ''; $itemCode = 'material_' . ($index + 1); $itemName = '鉴定资料'; } if ($url === '' || !preg_match('/^https?:\/\//i', $url)) { throw new \InvalidArgumentException('materials 只支持 http/https 图片 URL'); } $list[] = [ 'item_code' => $itemCode, 'item_name' => $itemName, 'file_url' => $url, 'thumbnail_url' => is_array($item) ? trim((string)($item['thumbnail_url'] ?? $url)) : $url, 'is_required' => is_array($item) && !empty($item['is_required']) ? 1 : 0, ]; } return $list; } private function normalizeInboundLogistics(array $payload): array { $logistics = (array)($payload['inbound_logistics'] ?? []); if (!empty($payload['express_company']) || !empty($payload['tracking_no'])) { $logistics = array_merge($logistics, [ 'express_company' => $payload['express_company'] ?? ($logistics['express_company'] ?? ''), 'tracking_no' => $payload['tracking_no'] ?? ($logistics['tracking_no'] ?? ''), ]); } return $logistics; } private function resolveProductName(array $product): string { $productName = trim((string)($product['product_name'] ?? '')); if ($productName !== '') { return $productName; } return trim(($product['brand_name'] ?? '') . ' ' . ($product['category_name'] ?? '')); } private function pricePackageSnapshot(string $serviceProvider, string $packageCode = ''): array { return (new AppraisalServicePricePackageService())->snapshotForOrder($serviceProvider, 0, $packageCode); } private function insertMaterials(int $orderId, array $materials, string $now): void { foreach ($materials as $item) { $uploadItemId = (int)Db::name('order_upload_items')->insertGetId([ 'order_id' => $orderId, 'template_id' => null, 'item_code' => $item['item_code'], 'item_name' => $item['item_name'], 'is_required' => $item['is_required'], 'source_type' => 'initial', 'status' => 'uploaded', 'created_at' => $now, 'updated_at' => $now, ]); Db::name('order_upload_files')->insert([ 'order_upload_item_id' => $uploadItemId, 'file_id' => md5($item['file_url']), 'file_url' => $item['file_url'], 'thumbnail_url' => $item['thumbnail_url'], 'quality_status' => 'uploaded', 'quality_message' => '', 'uploaded_by_user_id' => null, 'created_at' => $now, 'updated_at' => $now, ]); } } private function insertTimelines(int $orderId, string $now, array $shippingTarget): void { Db::name('order_timelines')->insertAll([ [ 'order_id' => $orderId, 'node_code' => 'created', 'node_text' => '下单成功', 'node_desc' => '大客户订单已推送并创建成功', 'operator_type' => 'system', 'operator_id' => null, 'occurred_at' => $now, 'created_at' => $now, ], [ 'order_id' => $orderId, 'node_code' => 'pending_shipping', 'node_text' => '待寄送商品', 'node_desc' => sprintf('请将商品寄送至%s', $shippingTarget['warehouse_name'] ?: '鉴定中心'), 'operator_type' => 'system', 'operator_id' => null, 'occurred_at' => $now, 'created_at' => $now, ], ]); } private function insertTask(int $orderId, string $serviceProvider, string $estimated, string $now): void { Db::name('appraisal_tasks')->insert([ 'order_id' => $orderId, 'task_stage' => 'first_review', 'service_provider' => $serviceProvider, 'status' => 'pending', 'assignee_id' => null, 'assignee_name' => '未分配', 'started_at' => null, 'submitted_at' => null, 'sla_deadline' => $estimated, 'is_overtime' => 0, 'created_at' => $now, 'updated_at' => $now, ]); } private function insertInboundLogistics(int $orderId, array $logistics, string $now): void { $expressCompany = trim((string)($logistics['express_company'] ?? '')); $trackingNo = trim((string)($logistics['tracking_no'] ?? '')); if ($expressCompany === '' || $trackingNo === '') { return; } $latestDesc = sprintf('客户已提交寄送运单:%s %s,等待鉴定中心签收。', $expressCompany, $trackingNo); $logisticsId = (int)Db::name('order_logistics')->insertGetId([ 'order_id' => $orderId, 'logistics_type' => 'send_to_center', 'express_company' => $expressCompany, 'tracking_no' => $trackingNo, 'tracking_status' => 'submitted', 'latest_desc' => $latestDesc, 'latest_time' => $now, 'created_at' => $now, 'updated_at' => $now, ]); Db::name('order_logistics_nodes')->insert([ 'logistics_id' => $logisticsId, 'node_time' => $now, 'node_desc' => $latestDesc, 'node_location' => '', 'created_at' => $now, ]); } private function formatLogistics(?array $logistics): ?array { if (!$logistics) { return null; } return [ 'express_company' => (string)$logistics['express_company'], 'tracking_no' => (string)$logistics['tracking_no'], 'tracking_status' => (string)$logistics['tracking_status'], 'latest_desc' => (string)$logistics['latest_desc'], 'latest_time' => (string)($logistics['latest_time'] ?? ''), ]; } }