完善expert领域
This commit is contained in:
161
application/api/controller/ExpertFieldAi.php
Normal file
161
application/api/controller/ExpertFieldAi.php
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use think\Db;
|
||||||
|
use app\common\ExpertFieldAiService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expert 领域总结(方案 C - 阶段1:邮箱关联 user.field_ai)
|
||||||
|
*
|
||||||
|
* POST startLinkChain 启动链式队列,批量关联
|
||||||
|
* POST linkOne 同步关联单个 expert_id
|
||||||
|
* POST linkBatch 同步批量关联 expert_ids
|
||||||
|
* POST syncByUser user 有 field_ai 后,同步到同邮箱 expert
|
||||||
|
* GET preview 预览是否可关联
|
||||||
|
* GET statistics 统计 field_ai 覆盖情况
|
||||||
|
*/
|
||||||
|
class ExpertFieldAi extends Base
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 启动链式关联队列
|
||||||
|
* Worker: php think queue:work --queue ExpertFieldAi
|
||||||
|
*/
|
||||||
|
public function startLinkChain()
|
||||||
|
{
|
||||||
|
$force = intval($this->request->param('force', 0)) === 1;
|
||||||
|
$delay = max(0, intval($this->request->param('delay', 1)));
|
||||||
|
|
||||||
|
$svc = new ExpertFieldAiService();
|
||||||
|
$started = $svc->startLinkChain($force, $delay);
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'started' => $started,
|
||||||
|
'queue' => ExpertFieldAiService::QUEUE_NAME,
|
||||||
|
'force' => $force,
|
||||||
|
'msg' => $started ? 'link chain enqueued' : 'no pending experts',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步关联单个 expert
|
||||||
|
*/
|
||||||
|
public function linkOne()
|
||||||
|
{
|
||||||
|
$expertId = intval($this->request->param('expert_id', 0));
|
||||||
|
$force = intval($this->request->param('force', 0)) === 1;
|
||||||
|
if ($expertId <= 0) {
|
||||||
|
return jsonError('expert_id required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$svc = new ExpertFieldAiService();
|
||||||
|
$result = $svc->linkFromUser($expertId, $force);
|
||||||
|
if (empty($result['ok'])) {
|
||||||
|
return jsonError(isset($result['error']) ? $result['error'] : 'failed');
|
||||||
|
}
|
||||||
|
return jsonSuccess($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步批量关联
|
||||||
|
* expert_ids: 逗号分隔,或传 limit 扫描待处理前 N 条
|
||||||
|
*/
|
||||||
|
public function linkBatch()
|
||||||
|
{
|
||||||
|
$force = intval($this->request->param('force', 0)) === 1;
|
||||||
|
$idsRaw = trim((string)$this->request->param('expert_ids', ''));
|
||||||
|
$limit = min(max(intval($this->request->param('limit', 0)), 0), 200);
|
||||||
|
|
||||||
|
$ids = [];
|
||||||
|
if ($idsRaw !== '') {
|
||||||
|
$ids = array_filter(array_map('intval', explode(',', $idsRaw)));
|
||||||
|
} elseif ($limit > 0) {
|
||||||
|
$ids = Db::name('expert')
|
||||||
|
->where('state', '<>', 5)
|
||||||
|
->where(function ($q) {
|
||||||
|
$q->where('field_ai_status', ExpertFieldAiService::STATUS_PENDING)
|
||||||
|
->whereOr('field_ai_status', ExpertFieldAiService::STATUS_FAILED);
|
||||||
|
})
|
||||||
|
->order('expert_id asc')
|
||||||
|
->limit($limit)
|
||||||
|
->column('expert_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($ids)) {
|
||||||
|
return jsonError('expert_ids 或 limit 必填');
|
||||||
|
}
|
||||||
|
|
||||||
|
$svc = new ExpertFieldAiService();
|
||||||
|
$result = $svc->batchLinkFromUser($ids, $force);
|
||||||
|
return jsonSuccess($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* user 更新 field_ai 后,同步到同邮箱 expert
|
||||||
|
*/
|
||||||
|
public function syncByUser()
|
||||||
|
{
|
||||||
|
$userId = intval($this->request->param('user_id', 0));
|
||||||
|
$force = intval($this->request->param('force', 0)) === 1;
|
||||||
|
if ($userId <= 0) {
|
||||||
|
return jsonError('user_id required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$svc = new ExpertFieldAiService();
|
||||||
|
$result = $svc->syncExpertsByUserId($userId, $force);
|
||||||
|
if (empty($result['ok'])) {
|
||||||
|
return jsonError(isset($result['error']) ? $result['error'] : 'failed');
|
||||||
|
}
|
||||||
|
return jsonSuccess($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览是否可关联
|
||||||
|
*/
|
||||||
|
public function preview()
|
||||||
|
{
|
||||||
|
$expertId = intval($this->request->param('expert_id', 0));
|
||||||
|
if ($expertId <= 0) {
|
||||||
|
return jsonError('expert_id required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$svc = new ExpertFieldAiService();
|
||||||
|
$result = $svc->previewLink($expertId);
|
||||||
|
if (empty($result['ok'])) {
|
||||||
|
return jsonError(isset($result['error']) ? $result['error'] : 'failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['field_ai_status_text'] = $svc->statusLabel(intval($result['expert_field_ai_status']));
|
||||||
|
return jsonSuccess($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计 field_ai 覆盖
|
||||||
|
*/
|
||||||
|
public function statistics()
|
||||||
|
{
|
||||||
|
$total = Db::name('expert')->where('state', '<>', 5)->count();
|
||||||
|
$done = Db::name('expert')->where('state', '<>', 5)->where('field_ai_status', ExpertFieldAiService::STATUS_DONE)->count();
|
||||||
|
$userLink = Db::name('expert')
|
||||||
|
->where('state', '<>', 5)
|
||||||
|
->where('field_ai_source', ExpertFieldAiService::SOURCE_USER_LINK)
|
||||||
|
->count();
|
||||||
|
$noUserLink = Db::name('expert')
|
||||||
|
->where('state', '<>', 5)
|
||||||
|
->where('field_ai_status', ExpertFieldAiService::STATUS_NO_USER_LINK)
|
||||||
|
->count();
|
||||||
|
$pending = Db::name('expert')
|
||||||
|
->where('state', '<>', 5)
|
||||||
|
->where('field_ai_status', ExpertFieldAiService::STATUS_PENDING)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'total' => $total,
|
||||||
|
'done' => $done,
|
||||||
|
'user_link' => $userLink,
|
||||||
|
'no_user_link' => $noUserLink,
|
||||||
|
'pending' => $pending,
|
||||||
|
'coverage_rate' => $total > 0 ? round($done / $total * 100, 2) . '%' : '0%',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,17 +44,22 @@ class ExpertManage extends Base
|
|||||||
|
|
||||||
$query = Db::name('expert')->alias('e');
|
$query = Db::name('expert')->alias('e');
|
||||||
$countQuery = Db::name('expert')->alias('e');
|
$countQuery = Db::name('expert')->alias('e');
|
||||||
$needJoin = ($field !== '');
|
|
||||||
|
|
||||||
if ($needJoin) {
|
if ($field !== '') {
|
||||||
$query->join('t_expert_field ef', 'ef.expert_id = e.expert_id AND ef.state = 0', 'inner');
|
$fieldExpertIds = Db::name('expert_field')
|
||||||
$countQuery->join('t_expert_field ef', 'ef.expert_id = e.expert_id AND ef.state = 0', 'inner');
|
->where('state', 0)
|
||||||
if ($field !== '') {
|
->where('field', 'like', '%' . $field . '%')
|
||||||
$query->where('ef.field', 'like', '%' . $field . '%');
|
->column('expert_id');
|
||||||
$countQuery->where('ef.field', 'like', '%' . $field . '%');
|
$fieldExpertIds = array_values(array_unique(array_filter(array_map('intval', $fieldExpertIds))));
|
||||||
}
|
|
||||||
$query->group('e.expert_id');
|
$fieldWhere = function ($q) use ($field, $fieldExpertIds) {
|
||||||
$countQuery->group('e.expert_id');
|
$q->where('e.field_ai', 'like', '%' . $field . '%');
|
||||||
|
if (!empty($fieldExpertIds)) {
|
||||||
|
$q->whereOr('e.expert_id', 'in', $fieldExpertIds);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$query->where($fieldWhere);
|
||||||
|
$countQuery->where($fieldWhere);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($state !== '-1' && $state !== '') {
|
if ($state !== '-1' && $state !== '') {
|
||||||
@@ -62,8 +67,8 @@ class ExpertManage extends Base
|
|||||||
$countQuery->where('e.state', intval($state));
|
$countQuery->where('e.state', intval($state));
|
||||||
}
|
}
|
||||||
if ($keyword !== '') {
|
if ($keyword !== '') {
|
||||||
$query->where('e.name|e.email|e.affiliation', 'like', '%' . $keyword . '%');
|
$query->where('e.name|e.email|e.affiliation|e.field_ai', 'like', '%' . $keyword . '%');
|
||||||
$countQuery->where('e.name|e.email|e.affiliation', 'like', '%' . $keyword . '%');
|
$countQuery->where('e.name|e.email|e.affiliation|e.field_ai', 'like', '%' . $keyword . '%');
|
||||||
}
|
}
|
||||||
if ($source !== '') {
|
if ($source !== '') {
|
||||||
$query->where('e.source', $source);
|
$query->where('e.source', $source);
|
||||||
@@ -72,7 +77,7 @@ class ExpertManage extends Base
|
|||||||
|
|
||||||
// $countQuery = clone $query;
|
// $countQuery = clone $query;
|
||||||
// $total = $countQuery->distinct('e.expert_id')->count();
|
// $total = $countQuery->distinct('e.expert_id')->count();
|
||||||
$total = $needJoin ? count($countQuery->group('e.expert_id')->column('e.expert_id')) : $countQuery->count();
|
$total = $countQuery->count();
|
||||||
|
|
||||||
$list = $query
|
$list = $query
|
||||||
->field('e.*')
|
->field('e.*')
|
||||||
|
|||||||
38
application/api/job/ExpertFieldAiFill.php
Normal file
38
application/api/job/ExpertFieldAiFill.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\job;
|
||||||
|
|
||||||
|
use think\queue\Job;
|
||||||
|
use app\common\ExpertFieldAiService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expert field_ai 链式任务(阶段1:邮箱关联 user.field_ai)
|
||||||
|
*
|
||||||
|
* data:
|
||||||
|
* - expert_id
|
||||||
|
* - queue 队列名,默认 ExpertFieldAi
|
||||||
|
* - force 1=强制重算
|
||||||
|
* - mode link(默认)
|
||||||
|
*
|
||||||
|
* Worker: php think queue:work --queue ExpertFieldAi
|
||||||
|
*/
|
||||||
|
class ExpertFieldAiFill
|
||||||
|
{
|
||||||
|
public function fire(Job $job, $data)
|
||||||
|
{
|
||||||
|
$expertId = isset($data['expert_id']) ? intval($data['expert_id']) : 0;
|
||||||
|
$queue = isset($data['queue']) ? (string)$data['queue'] : ExpertFieldAiService::QUEUE_NAME;
|
||||||
|
$force = !empty($data['force']);
|
||||||
|
$mode = isset($data['mode']) ? (string)$data['mode'] : 'link';
|
||||||
|
|
||||||
|
$svc = new ExpertFieldAiService();
|
||||||
|
if ($expertId > 0 && $mode === 'link') {
|
||||||
|
$svc->linkFromUser($expertId, $force);
|
||||||
|
}
|
||||||
|
|
||||||
|
$job->delete();
|
||||||
|
|
||||||
|
$delay = max(0, (int)(isset($data['delay']) ? $data['delay'] : 1));
|
||||||
|
$svc->enqueueNextLink($delay, $queue, $expertId, $force);
|
||||||
|
}
|
||||||
|
}
|
||||||
321
application/common/ExpertFieldAiService.php
Normal file
321
application/common/ExpertFieldAiService.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,6 +102,7 @@ class UserFieldAiService
|
|||||||
throw new Exception('LLM returned empty field');
|
throw new Exception('LLM returned empty field');
|
||||||
}
|
}
|
||||||
$this->updateFieldAi($userId, $fieldAi, self::STATUS_DONE, '');
|
$this->updateFieldAi($userId, $fieldAi, self::STATUS_DONE, '');
|
||||||
|
$this->syncLinkedExperts($userId);
|
||||||
return ['ok' => true, 'field_ai' => $fieldAi];
|
return ['ok' => true, 'field_ai' => $fieldAi];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->updateFieldAi($userId, '', self::STATUS_FAILED, mb_substr($e->getMessage(), 0, 500));
|
$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;
|
$line = date('Y-m-d H:i:s') . ' ' . $msg . PHP_EOL;
|
||||||
@file_put_contents($this->logFile, $line, FILE_APPEND);
|
@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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
sql/add_field_ai_to_expert.sql
Normal file
6
sql/add_field_ai_to_expert.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- Expert 主领域 AI 总结(方案 C:先邮箱关联 user.field_ai,后续可 AI 补全)
|
||||||
|
ALTER TABLE `t_expert`
|
||||||
|
ADD COLUMN `field_ai` VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'AI/关联总结的主要研究领域(中文)' AFTER `affiliation`,
|
||||||
|
ADD COLUMN `field_ai_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理 1已生成 2资料不足 3失败 4无user关联待AI' AFTER `field_ai`,
|
||||||
|
ADD COLUMN `field_ai_utime` INT NOT NULL DEFAULT 0 COMMENT 'field_ai 更新时间' AFTER `field_ai_status`,
|
||||||
|
ADD COLUMN `field_ai_source` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '来源: user_link / ai' AFTER `field_ai_utime`;
|
||||||
Reference in New Issue
Block a user