Files
tougao/application/common/ExpertFieldAiService.php
2026-06-05 11:01:16 +08:00

322 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
}