完善expert领域

This commit is contained in:
wangjinlei
2026-06-05 11:01:16 +08:00
parent 28023be44a
commit 93d25de094
6 changed files with 558 additions and 13 deletions

View File

@@ -0,0 +1,321 @@
<?php
namespace app\common;
use think\Db;
use think\Queue;
/**
* Expert 领域总结(方案 C
* 阶段1通过 email 关联 t_user / t_user_reviewer_info复用 user.field_ai
* 阶段2后续对 field_ai_status=4 的记录走 LLM 总结
*/
class ExpertFieldAiService
{
const QUEUE_NAME = 'ExpertFieldAi';
const STATUS_PENDING = 0;
const STATUS_DONE = 1;
const STATUS_INSUFFICIENT = 2;
const STATUS_FAILED = 3;
const STATUS_NO_USER_LINK = 4;
const SOURCE_USER_LINK = 'user_link';
const SOURCE_AI = 'ai';
private $logFile;
public function __construct()
{
$this->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);
}
}

View File

@@ -102,6 +102,7 @@ class UserFieldAiService
throw new Exception('LLM returned empty field');
}
$this->updateFieldAi($userId, $fieldAi, self::STATUS_DONE, '');
$this->syncLinkedExperts($userId);
return ['ok' => true, 'field_ai' => $fieldAi];
} catch (\Throwable $e) {
$this->updateFieldAi($userId, '', self::STATUS_FAILED, mb_substr($e->getMessage(), 0, 500));
@@ -460,4 +461,17 @@ class UserFieldAiService
$line = date('Y-m-d H:i:s') . ' ' . $msg . PHP_EOL;
@file_put_contents($this->logFile, $line, FILE_APPEND);
}
/**
* user.field_ai 更新后,同步到同邮箱 expert方案 C 关联)。
*/
private function syncLinkedExperts($userId)
{
try {
$svc = new ExpertFieldAiService();
$svc->syncExpertsByUserId(intval($userId), true);
} catch (\Throwable $e) {
$this->log('[FieldAi] sync expert fail user_id=' . $userId . ' ' . $e->getMessage());
}
}
}