logFile = ROOT_PATH . 'runtime' . DS . 'expert_field_ai.log'; } /** * 启动链式关联(从 expert_id=0 之后找下一位待处理专家)。 */ public function startLinkChain($force = false, $delay = 1, $queue = '') { return $this->enqueueNextLink($delay, $queue, 0, $force); } /** * 链式:处理 expert_id > $afterExpertId 的下一位。 */ public function enqueueNextLink($delay = 1, $queue = '', $afterExpertId = 0, $force = false) { if ($queue === '') { $queue = self::QUEUE_NAME; } $afterExpertId = intval($afterExpertId); $expertId = $this->findNextLinkExpertId($afterExpertId, $force); if ($expertId <= 0) { $this->log('[ExpertFieldAi] link chain finished after expert_id=' . $afterExpertId); return false; } $data = [ 'expert_id' => $expertId, 'queue' => $queue, 'force' => $force ? 1 : 0, 'mode' => 'link', ]; $jobClass = 'app\\api\\job\\ExpertFieldAiFill@fire'; if ($delay > 0) { Queue::later($delay, $jobClass, $data, $queue); } else { Queue::push($jobClass, $data, $queue); } $this->log('[ExpertFieldAi] enqueued expert_id=' . $expertId . ' queue=' . $queue); return true; } /** * 单个 expert:尝试从 user 邮箱关联 field_ai。 * * @return array{ok:bool, linked?:bool, skipped?:bool, field_ai?:string, user_id?:int, error?:string} */ public function linkFromUser($expertId, $force = false) { $expertId = intval($expertId); if ($expertId <= 0) { return ['ok' => false, 'error' => 'invalid expert_id']; } $expert = Db::name('expert')->where('expert_id', $expertId)->find(); if (!$expert) { return ['ok' => false, 'error' => 'expert not found']; } if (!$force && intval($expert['field_ai_status']) === self::STATUS_DONE && trim((string)$expert['field_ai']) !== '') { return [ 'ok' => true, 'skipped' => true, 'field_ai' => (string)$expert['field_ai'], 'source' => (string)($expert['field_ai_source'] ?? ''), ]; } $email = strtolower(trim((string)($expert['email'] ?? ''))); if ($email === '') { $this->updateFieldAi($expertId, '', self::STATUS_NO_USER_LINK, '', 'no email'); return ['ok' => true, 'linked' => false, 'reason' => 'empty email']; } $user = Db::name('user') ->where('email', $email) ->where('state', 0) ->field('user_id,email,realname') ->find(); if (!$user) { $this->updateFieldAi($expertId, '', self::STATUS_NO_USER_LINK, '', 'no matching user'); return ['ok' => true, 'linked' => false, 'reason' => 'user not found']; } $uri = Db::name('user_reviewer_info') ->where('reviewer_id', intval($user['user_id'])) ->where('state', 0) ->find(); $fieldAi = $uri ? trim((string)($uri['field_ai'] ?? '')) : ''; $userStatus = $uri ? intval($uri['field_ai_status']) : 0; if ($fieldAi === '' || $userStatus !== UserFieldAiService::STATUS_DONE) { $this->updateFieldAi($expertId, '', self::STATUS_NO_USER_LINK, '', 'user field_ai not ready'); return [ 'ok' => true, 'linked' => false, 'user_id' => intval($user['user_id']), 'reason' => 'user has no field_ai', ]; } $this->updateFieldAi($expertId, $fieldAi, self::STATUS_DONE, self::SOURCE_USER_LINK, 'linked from user_id=' . $user['user_id']); return [ 'ok' => true, 'linked' => true, 'field_ai' => $fieldAi, 'user_id' => intval($user['user_id']), 'source' => self::SOURCE_USER_LINK, ]; } /** * 批量同步(同步执行,适合小批量调试)。 */ public function batchLinkFromUser(array $expertIds, $force = false) { $linked = 0; $skipped = 0; $noLink = 0; $failed = 0; $details = []; foreach ($expertIds as $expertId) { $expertId = intval($expertId); if ($expertId <= 0) { continue; } $result = $this->linkFromUser($expertId, $force); if (empty($result['ok'])) { $failed++; } elseif (!empty($result['skipped'])) { $skipped++; } elseif (!empty($result['linked'])) { $linked++; } else { $noLink++; } $details[] = array_merge(['expert_id' => $expertId], $result); } return [ 'total' => count($details), 'linked' => $linked, 'skipped' => $skipped, 'no_link' => $noLink, 'failed' => $failed, 'details' => $details, ]; } /** * 预览:expert 是否可关联到 user.field_ai。 */ public function previewLink($expertId) { $expertId = intval($expertId); $expert = Db::name('expert')->where('expert_id', $expertId)->find(); if (!$expert) { return ['ok' => false, 'error' => 'expert not found']; } $email = strtolower(trim((string)($expert['email'] ?? ''))); $user = null; $uri = null; if ($email !== '') { $user = Db::name('user')->where('email', $email)->where('state', 0)->field('user_id,email,realname')->find(); if ($user) { $uri = Db::name('user_reviewer_info') ->where('reviewer_id', intval($user['user_id'])) ->where('state', 0) ->find(); } } $canLink = $user && $uri && trim((string)($uri['field_ai'] ?? '')) !== '' && intval($uri['field_ai_status']) === UserFieldAiService::STATUS_DONE; return [ 'ok' => true, 'expert_id' => $expertId, 'expert_email' => $email, 'expert_field_ai' => (string)($expert['field_ai'] ?? ''), 'expert_field_ai_status'=> intval($expert['field_ai_status'] ?? 0), 'matched_user_id' => $user ? intval($user['user_id']) : 0, 'matched_user_name' => $user ? (string)$user['realname'] : '', 'user_field_ai' => $uri ? (string)($uri['field_ai'] ?? '') : '', 'user_field_ai_status' => $uri ? intval($uri['field_ai_status']) : 0, 'can_link' => $canLink, ]; } /** * user 生成 field_ai 后,反向同步到同邮箱 expert(可选调用)。 */ public function syncExpertsByUserId($userId, $force = false) { $userId = intval($userId); $user = Db::name('user')->where('user_id', $userId)->where('state', 0)->field('user_id,email')->find(); if (!$user || trim((string)$user['email']) === '') { return ['ok' => false, 'error' => 'user not found']; } $email = strtolower(trim((string)$user['email'])); $expertIds = Db::name('expert') ->where('email', $email) ->where('state', '<>', 5) ->column('expert_id'); if (empty($expertIds)) { return ['ok' => true, 'synced' => 0, 'msg' => 'no expert with same email']; } return array_merge(['ok' => true], $this->batchLinkFromUser($expertIds, $force)); } private function findNextLinkExpertId($afterExpertId, $force) { $batch = 50; $cursor = intval($afterExpertId); while (true) { $query = Db::name('expert') ->where('expert_id', '>', $cursor) ->where('state', '<>', 5); if (!$force) { $query->where(function ($q) { $q->where('field_ai_status', self::STATUS_PENDING) ->whereOr('field_ai_status', self::STATUS_FAILED); }); } $ids = $query->order('expert_id asc')->limit($batch)->column('expert_id'); if (empty($ids)) { return 0; } foreach ($ids as $expertId) { $expertId = intval($expertId); $cursor = $expertId; if (!$force) { $row = Db::name('expert')->where('expert_id', $expertId)->field('field_ai,field_ai_status')->find(); if ($row && intval($row['field_ai_status']) === self::STATUS_DONE && trim((string)$row['field_ai']) !== '') { continue; } } return $expertId; } } } private function updateFieldAi($expertId, $fieldAi, $status, $source, $note) { $data = [ 'field_ai' => mb_substr(trim((string)$fieldAi), 0, 512), 'field_ai_status' => intval($status), 'field_ai_utime' => time(), 'field_ai_source' => mb_substr(trim((string)$source), 0, 32), ]; Db::name('expert')->where('expert_id', intval($expertId))->update($data); if ($note !== '') { $this->log('[ExpertFieldAi] expert_id=' . $expertId . ' status=' . $status . ' note=' . $note); } } public function statusLabel($status) { $map = [ self::STATUS_PENDING => 'pending', self::STATUS_DONE => 'done', self::STATUS_INSUFFICIENT => 'insufficient', self::STATUS_FAILED => 'failed', self::STATUS_NO_USER_LINK => 'no_user_link', ]; return isset($map[$status]) ? $map[$status] : 'unknown'; } public function log($msg) { $line = date('Y-m-d H:i:s') . ' ' . $msg . PHP_EOL; @file_put_contents($this->logFile, $line, FILE_APPEND); } }