error('工单编号错误'); } if (empty($jtbh)) { $this->error('机台编号错误'); } $orderProductName = $this->getOrderProductName(trim($gdbh)); if ($orderProductName === '') { $this->error('未找到对应工单编号或产品名称为空'); } $finalPrice = $this->resolvePiecePriceByAi(trim($gdbh), trim($jtbh)); if ($finalPrice === null) { $this->error('AI估算单价失败'); } $this->success('计算成功', [ 'product_name' => $orderProductName, 'piece_price' => $finalPrice, ]); } /** * 按机台编号获取车间单价库数据 * @param string $jtbh * @return array */ public function fetchWorkshopRatesByMachine($jtbh) { $jtbh = trim((string)$jtbh); if ($jtbh === '') { return []; } return Db::name('workshop_box_piece_rate_final') ->field('id,product_name,piece_price,machine_code') ->where('product_name', '<>', '') ->where('piece_price', '>', 0) ->where('machine_code', $jtbh) ->order('id desc') ->select() ?: []; } /** * 按产品名称匹配车间单价 * @param array $rates * @param string $productName * @return array|null */ public function matchWorkshopRateByProductName(array $rates, $productName) { $productName = trim((string)$productName); if ($productName === '' || empty($rates)) { return null; } $orderMatchName = preg_replace('/[()()\s]/u', '', $productName); foreach ($rates as $row) { $keywords = $this->extractRateKeywords($row['product_name']); usort($keywords, function ($a, $b) { return mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8'); }); foreach ($keywords as $kw) { if ($kw === '') { continue; } if (mb_strpos($productName, $kw) !== false || mb_strpos($orderMatchName, $kw) !== false) { return $row; } } } return null; } /** * AI对照估算计件单价 * @param string $gdbh * @param string $jtbh * @return float|null */ public function resolvePiecePriceByAi($gdbh, $jtbh) { $gdbh = trim((string)$gdbh); $jtbh = trim((string)$jtbh); if ($gdbh === '' || $jtbh === '') { return null; } $orderProductName = $this->getOrderProductName($gdbh); if ($orderProductName === '') { return null; } $allRates = $this->fetchWorkshopRatesByMachine($jtbh); if (empty($allRates)) { return null; } $orderMatchName = preg_replace('/[()()\s]/u', '', $orderProductName); $candidateList = $this->buildAiCandidateRates($allRates, $orderProductName, $orderMatchName); $rateJson = json_encode($candidateList, JSON_UNESCAPED_UNICODE); $prompt = "角色:计件单价分析助手。 背景:单价表由人工维护,覆盖不全。请参考给定单价数据,评估当前工单产品最合理的计件单价(不是简单照搬某一条记录)。 【工单】 工单号:{$gdbh} 产品名称:{$orderProductName} 【参考单价数据】 {$rateJson} 【规则】 1. 综合产品名称、系列、款式、工艺等信息,判断该工单产品应对应哪类单价最合理。 2. 如果产品名称中包含国家名称,则全部属于外烟产品 3. 参考相近产品的单价水平进行评估,给出最合理的计件单价。 4. 必须返回数字单价,不得为 null 或空。 【输出】 仅输出 JSON,无其他文字: {\"product_name\":\"{$orderProductName}\",\"piece_price\":数字}"; $apiKey = 'sk-fxlawqVtbbQbNW0wInR3E4wsLo5JHozDC2XOHzMa711su6ss'; $apiUrl = 'https://chatapi.onechats.top/v1/chat/completions'; $postData = [ 'model' => 'gpt-4.1', 'messages' => [ [ 'role' => 'user', 'content' => $prompt, ], ], 'max_tokens' => 1024, 'temperature' => 0.2, ]; $ch = curl_init($apiUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData, JSON_UNESCAPED_UNICODE)); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Authorization: Bearer ' . $apiKey, ]); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $response = curl_exec($ch); if (curl_errno($ch)) { curl_close($ch); return null; } curl_close($ch); $responseData = json_decode($response, true); if (!isset($responseData['choices'][0]['message']['content'])) { return null; } $gptReply = trim($responseData['choices'][0]['message']['content']); $gptReply = preg_replace('/^```(?:json)?\s*|\s*```$/u', '', $gptReply); $result = json_decode($gptReply, true); if (!is_array($result) || !isset($result['piece_price']) || !is_numeric($result['piece_price'])) { return null; } return (float)$result['piece_price']; } /** * 获取工单产品名称 * @param string $gdbh * @return string */ protected function getOrderProductName($gdbh) { $order = Db::name('工单_基本资料') ->field('Gd_cpmc') ->where('Gd_gdbh', $gdbh) ->find(); if (!$order || empty(trim($order['Gd_cpmc']))) { return ''; } return trim($order['Gd_cpmc']); } /** * 构建AI候选单价数据 * @param array $allRates * @param string $orderProductName * @param string $orderMatchName * @return array */ protected function buildAiCandidateRates(array $allRates, $orderProductName, $orderMatchName) { $list = []; foreach ($allRates as $row) { $keywords = $this->extractRateKeywords($row['product_name']); usort($keywords, function ($a, $b) { return mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8'); }); foreach ($keywords as $kw) { if ($kw === '') { continue; } if (mb_strpos($orderProductName, $kw) !== false || mb_strpos($orderMatchName, $kw) !== false) { $list[] = $row; break; } } } return !empty($list) ? array_slice($list, 0, 20) : array_slice($allRates, 0, 20); } /** * 从单价表产品名提取匹配关键字 * @param string $productName 产品名称 * @return array */ protected function extractRateKeywords(string $productName): array { $ignoreWords = ['小盒', '条盒', '系列', '彩样', '打样', '裱上盖', '拆片', '做底']; $keywords = []; // 斜杠分割多产品名称 $parts = preg_split('/[\/]/u', $productName); foreach ($parts as $part) { // 清除括号内备注内容 $cleanPart = preg_replace('/[((][^))]*[))]/u', '', $part); $cleanPart = trim($cleanPart); if ($cleanPart === '') continue; // 提取品牌前缀(系列/小盒之前文字) if (preg_match('/^(.+?)(?:系列|小盒|条盒|裱|做底|\+)/u', $cleanPart, $match)) { $brand = trim($match[1]); if ($brand !== '' && !in_array($brand, $ignoreWords, true)) { $keywords[] = $brand; } } // 提取2~4位中文核心关键词 if (preg_match('/^([\x{4e00}-\x{9fa5}]{2,})/u', $cleanPart, $match)) { $maxLen = min(4, mb_strlen($match[1], 'UTF-8')); for ($i = 2; $i <= $maxLen; $i++) { $kw = mb_substr($match[1], 0, $i, 'UTF-8'); if (!in_array($kw, $ignoreWords, true)) { $keywords[] = $kw; } } } } // 去重返回 return array_values(array_unique($keywords)); } }