Browse Source

refactor: 术语表加载下沉到 TermService::getGlossary

- TermService 新增 getGlossary / getChannelGlossary / toNfc 及 USER_DICT_IDS
- ExtractPaliTerm 改用 TermService::getGlossary,移除本地重复实现

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
visuddhinanda 3 weeks ago
parent
commit
d907e5ee2f
2 changed files with 159 additions and 97 deletions
  1. 4 52
      api-v13/app/Console/Commands/ExtractPaliTerm.php
  2. 155 45
      api-v13/app/Services/TermService.php

+ 4 - 52
api-v13/app/Console/Commands/ExtractPaliTerm.php

@@ -5,12 +5,11 @@ namespace App\Console\Commands;
 use App\Helpers\LlmResponseParser;
 use App\Http\Api\ChannelApi;
 use App\Http\Resources\AiModelResource;
-use App\Models\DhammaTerm;
 use App\Models\PaliText;
-use App\Models\UserDict;
 use App\Services\AIModelService;
 use App\Services\OpenAIService;
 use App\Services\SentenceService;
+use App\Services\TermService;
 use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Log;
@@ -21,14 +20,6 @@ class ExtractPaliTerm extends Command
     // 使命令可重入——中断后重跑自动跳过已完成的 chapter
     private const CURSOR_KEY = 'extract:pali.term:cursor';
 
-    // 术语表额外纳入的 user_dicts 词典 id(其 word 去除连字符与空格后加入)
-    private const USER_DICT_IDS = [
-        'b089de57-f146-4095-b886-057863728c43',
-        '0bfd87ec-f3ac-49a2-985e-28388779078d',
-        '7c7ee287-35ba-4cf3-b87b-30f1fa6e57c9',
-        '4ac8a0d5-9c6f-4b9f-983d-84288d47f993',
-    ];
-
     /**
      * 用 LLM 从巴利文经文中提取术语。
      *
@@ -80,7 +71,8 @@ class ExtractPaliTerm extends Command
     public function __construct(
         protected AIModelService $modelService,
         protected OpenAIService $openAIService,
-        protected SentenceService $sentenceService
+        protected SentenceService $sentenceService,
+        protected TermService $termService
     ) {
         parent::__construct();
     }
@@ -177,27 +169,7 @@ class ExtractPaliTerm extends Command
      */
     private function loadGlossaryJson(): ?string
     {
-        // 1) dhamma_terms 全表 distinct 词条原形(不限 channel),排除 word 含空格的短语条目
-        $dhammaWords = DhammaTerm::query()
-            ->whereNotNull('word')
-            ->where('word', '!=', '')
-            ->where('word', 'not like', '% %')
-            ->distinct()
-            ->pluck('word')
-            ->all();
-
-        // 2) user_dicts 指定词典的词,去除连字符与空格后加入
-        $userWords = UserDict::query()
-            ->whereIn('dict_id', self::USER_DICT_IDS)
-            ->whereNotNull('word')
-            ->where('word', '!=', '')
-            ->pluck('word')
-            ->map(fn ($w) => str_replace(['-', ' '], '', (string) $w))
-            ->all();
-
-        // 合并 + NFC 归一(避免巴利文变音符 NFC/NFD 不一致)+ 去空 + 去重
-        $terms = array_map(fn ($w) => $this->toNfc(trim((string) $w)), array_merge($dhammaWords, $userWords));
-        $terms = array_values(array_unique(array_filter($terms, fn ($w) => $w !== '')));
+        $terms = $this->termService->getGlossary();
 
         if (empty($terms)) {
             $this->error('glossary is empty');
@@ -209,11 +181,6 @@ class ExtractPaliTerm extends Command
         $this->glossarySet = array_fill_keys($terms, true);
 
         $this->info('glossary loaded: '.count($terms).' terms (dhamma_terms + user_dicts)');
-        Log::info('extract:pali.term glossary loaded', [
-            'total' => count($terms),
-            'dhamma_terms' => count($dhammaWords),
-            'user_dicts' => count($userWords),
-        ]);
 
         return json_encode($terms, JSON_UNESCAPED_UNICODE);
     }
@@ -297,21 +264,6 @@ class ExtractPaliTerm extends Command
         return $next ? (int) $next - 1 : $maxParagraph;
     }
 
-    /**
-     * Unicode 规范化为 NFC(巴利文变音符匹配需统一 NFC/NFD)。intl 不可用时原样返回。
-     */
-    private function toNfc(string $s): string
-    {
-        if (class_exists(\Normalizer::class)) {
-            $normalized = \Normalizer::normalize($s, \Normalizer::FORM_C);
-            if ($normalized !== false) {
-                return $normalized;
-            }
-        }
-
-        return $s;
-    }
-
     /**
      * 判断某 chapter 是否在断点游标(含)之前——即已完成、应跳过。
      *

+ 155 - 45
api-v13/app/Services/TermService.php

@@ -2,17 +2,24 @@
 
 namespace App\Services;
 
-use App\Models\DhammaTerm;
 use App\Http\Api\ChannelApi;
 use App\Http\Resources\TermResource;
-use Illuminate\Http\Request;
-
+use App\Models\DhammaTerm;
+use App\Models\UserDict;
 use App\Tools\Tools;
-
-
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
 
 class TermService
 {
+    // 术语表额外纳入的 user_dicts 词典 id(其 word 去除连字符与空格后加入)
+    private const USER_DICT_IDS = [
+        'b089de57-f146-4095-b886-057863728c43',
+        '0bfd87ec-f3ac-49a2-985e-28388779078d',
+        '7c7ee287-35ba-4cf3-b87b-30f1fa6e57c9',
+        '4ac8a0d5-9c6f-4b9f-983d-84288d47f993',
+    ];
+
     public function attachLocalName(array $categoryData, string $lang): array
     {
         $allNames = [];
@@ -20,12 +27,12 @@ class TermService
         // 收集所有 name
         foreach ($categoryData as $item) {
 
-            if (!empty($item['category']['name'])) {
+            if (! empty($item['category']['name'])) {
                 $allNames[] = $item['category']['name'];
             }
 
             foreach ($item['children'] as $child) {
-                if (!empty($child['name'])) {
+                if (! empty($child['name'])) {
                     $allNames[] = $child['name'];
                 }
             }
@@ -61,53 +68,119 @@ class TermService
 
         return $categoryData;
     }
+
     public function glossaryByLemma(array $words, string $lang)
     {
         $localTermChannel = ChannelApi::getSysChannel(
-            "_community_term_" . strtolower($lang) . "_"
+            '_community_term_' . strtolower($lang) . '_'
         );
-        if (!$localTermChannel) {
+        if (! $localTermChannel) {
             return null;
         }
         $result = DhammaTerm::select(['guid', 'word', 'tag', 'meaning', 'other_meaning'])
             ->whereIn('word', $words)
             ->where('channal', $localTermChannel)
             ->get();
+
         return $result;
     }
-    public function getCommunityGlossary($lang)
+
+    public function getCommunityGlossary(string $lang)
     {
         $localTermChannel = ChannelApi::getSysChannel(
-            "_community_term_" . strtolower($lang) . "_",
-            "_community_term_en_"
+            '_community_term_' . strtolower($lang) . '_',
+            '_community_term_en_'
         );
-        $result = DhammaTerm::select(['guid', 'word', 'tag', 'meaning', 'other_meaning'])
+        $result = DhammaTerm::select([
+            'guid',
+            'word',
+            'tag',
+            'meaning',
+            'other_meaning'
+        ])
             ->where('channal', $localTermChannel)
             ->get();
+
         return ['items' => $result, 'total' => count($result)];
     }
-    public function getGrammarGlossary($lang)
+
+    public function getChannelGlossary(string $channelId)
+    {
+        $terms = DhammaTerm::whereNotNull('word')
+            ->where('word', '!=', '')
+            ->whereNotNull('word')
+            ->where('channal', $channelId)
+            ->select(['word', 'meaning', 'tag'])
+            ->get();
+        return $terms->toArray();
+    }
+
+    public function getGlossary()
+    {
+        // 1) dhamma_terms 全表 distinct 词条原形(不限 channel),排除 word 含空格的短语条目
+        $dhammaWords = DhammaTerm::query()
+            ->whereNotNull('word')
+            ->where('word', '!=', '')
+            ->where('word', 'not like', '% %')
+            ->distinct()
+            ->pluck('word')
+            ->all();
+
+        // 2) user_dicts 指定词典的词,去除连字符与空格后加入
+        $userWords = UserDict::query()
+            ->whereIn('dict_id', self::USER_DICT_IDS)
+            ->whereNotNull('word')
+            ->where('word', '!=', '')
+            ->pluck('word')
+            ->map(fn($w) => str_replace(['-', ' '], '', (string) $w))
+            ->all();
+
+        // 合并 + NFC 归一(避免巴利文变音符 NFC/NFD 不一致)+ 去空 + 去重
+        $terms = array_map(fn($w) => $this->toNfc(trim((string) $w)), array_merge($dhammaWords, $userWords));
+        $terms = array_values(array_unique(array_filter($terms, fn($w) => $w !== '')));
+
+        return $terms;
+    }
+
+    /**
+     * Unicode 规范化为 NFC(巴利文变音符匹配需统一 NFC/NFD)。intl 不可用时原样返回。
+     */
+    private function toNfc(string $s): string
+    {
+        if (class_exists(\Normalizer::class)) {
+            $normalized = \Normalizer::normalize($s, \Normalizer::FORM_C);
+            if ($normalized !== false) {
+                return $normalized;
+            }
+        }
+
+        return $s;
+    }
+
+    public function getGrammarGlossary(string $lang)
     {
         $localTermChannel = ChannelApi::getSysChannel(
-            "_System_Grammar_Term_" . strtolower($lang) . "_",
-            "_System_Grammar_Term_en_"
+            '_System_Grammar_Term_' . strtolower($lang) . '_',
+            '_System_Grammar_Term_en_'
         );
         $result = DhammaTerm::select(['guid', 'word', 'tag', 'meaning', 'other_meaning'])
             ->where('channal', $localTermChannel)
             ->get();
+
         return ['items' => $result, 'total' => count($result)];
     }
 
     public function getRaw(string $id)
     {
         $result = DhammaTerm::find($id);
+
         return $result;
     }
 
     public function isCommunity(?string $channelId)
     {
         $channel = ChannelApi::getById($channelId);
-        if (!$channel) {
+        if (! $channel) {
             return false;
         }
         if (strpos($channel['name'], '_community_term_') === false) {
@@ -116,10 +189,11 @@ class TermService
             return true;
         }
     }
+
     public function communityTerm(string $word, string $lang, string $format)
     {
         $localTermChannel = ChannelApi::getSysChannel(
-            "_community_term_" . strtolower($lang) . "_"
+            '_community_term_' . strtolower($lang) . '_'
         );
         $result = DhammaTerm::where('word', $word)
             ->where('channal', $localTermChannel)
@@ -128,7 +202,7 @@ class TermService
             $resource = new TermResource($result);
             $urlParam = ['format' => $format];
             $fakeRequest = Request::create('', 'GET', $urlParam);
-            $termArray    = $resource->toArray($fakeRequest);
+            $termArray = $resource->toArray($fakeRequest);
             if ($result) {
                 return $termArray;
             } else {
@@ -142,7 +216,7 @@ class TermService
     public function communityWiki(string $word, string $lang, string $format)
     {
         $localTermChannel = ChannelApi::getSysChannel(
-            "_community_translation_" . strtolower($lang) . "_"
+            '_community_translation_' . strtolower($lang) . '_'
         );
         $result = DhammaTerm::where('word', $word)
             ->where('channal', $localTermChannel)
@@ -151,7 +225,7 @@ class TermService
             $resource = new TermResource($result);
             $urlParam = ['format' => $format];
             $fakeRequest = Request::create('', 'GET', $urlParam);
-            $termArray    = $resource->toArray($fakeRequest);
+            $termArray = $resource->toArray($fakeRequest);
             if ($result) {
                 return $termArray;
             } else {
@@ -165,7 +239,7 @@ class TermService
     public function communityTerms(string $lang)
     {
         $localTermChannel = ChannelApi::getSysChannel(
-            "_community_term_" . strtolower($lang) . "_"
+            '_community_term_' . strtolower($lang) . '_'
         );
         $result = DhammaTerm::where('channal', $localTermChannel)
             ->whereNotNull('note')
@@ -173,9 +247,10 @@ class TermService
             ->take(10)
             ->orderBy('updated_at', 'desc')
             ->get();
+
         return [
-            "data" => TermResource::collection($result),
-            "count" => 10
+            'data' => TermResource::collection($result),
+            'count' => 10,
         ];
     }
 
@@ -185,7 +260,7 @@ class TermService
         $resource = new TermResource($result);
         $urlParam = ['format' => $format];
         $fakeRequest = Request::create('', 'GET', $urlParam);
-        $termArray    = $resource->toArray($fakeRequest);
+        $termArray = $resource->toArray($fakeRequest);
         if ($result) {
             return $termArray;
         } else {
@@ -225,34 +300,69 @@ class TermService
         if ($term) {
             // 已存在,直接更新
             $term->update([
-                'meaning'       => $data['meaning'],
+                'meaning' => $data['meaning'],
                 'other_meaning' => $data['other_meaning'] ?? null,
-                'note'          => $data['note'] ?? null,
-                'redirect'      => $data['redirect'] ?? null,
-                'editor_id'     => $data['editor_id'],
-                'modify_time'   => $now,
+                'note' => $data['note'] ?? null,
+                'redirect' => $data['redirect'] ?? null,
+                'editor_id' => $data['editor_id'],
+                'modify_time' => $now,
             ]);
         } else {
             // 不存在,新建(一次性写入所有字段)
-            $term = new DhammaTerm();
-            $term->id           = app('snowflake')->id();
-            $term->guid         = (string) \Illuminate\Support\Str::uuid();
-            $term->word         = $data['word'];
-            $term->tag          = $data['tag'] ?? null;
-            $term->channal      = $data['channel_id'];
-            $term->meaning      = $data['meaning'];
+            $term = new DhammaTerm;
+            $term->id = app('snowflake')->id();
+            $term->guid = (string) Str::uuid();
+            $term->word = $data['word'];
+            $term->tag = $data['tag'] ?? null;
+            $term->channal = $data['channel_id'];
+            $term->meaning = $data['meaning'];
             $term->other_meaning = $data['other_meaning'] ?? null;
-            $term->note         = $data['note'] ?? null;
-            $term->redirect     = $data['redirect'] ?? null;
-            $term->editor_id    = $data['editor_id'];  // 注意:需传入 int 类型的 editor id
-            $term->owner        = $channelInfo['studio_id'];
-            $term->word_en      = Tools::getWordEn($data['word']);
-            $term->language     = $channelInfo['lang'] ?? 'zh-Hans';
-            $term->create_time  = $now;
-            $term->modify_time  = $now;
+            $term->note = $data['note'] ?? null;
+            $term->redirect = $data['redirect'] ?? null;
+            $term->editor_id = $data['editor_id'];  // 注意:需传入 int 类型的 editor id
+            $term->owner = $channelInfo['studio_id'];
+            $term->word_en = Tools::getWordEn($data['word']);
+            $term->language = $channelInfo['lang'] ?? 'zh-Hans';
+            $term->create_time = $now;
+            $term->modify_time = $now;
             $term->save();
         }
 
         return $term->guid;
     }
+
+    /**
+     * 按 channel + word 查找术语;不存在则新建一条空白词条(释义留待后续填充)。
+     *
+     * @return DhammaTerm 既有或新建的术语记录
+     */
+    public function findOrCreate(string $channelId, string $word): DhammaTerm
+    {
+        $term = DhammaTerm::where('word', $word)
+            ->where('channal', $channelId)
+            ->first();
+
+        if ($term) {
+            return $term;
+        }
+
+        $channelInfo = ChannelApi::getById($channelId);
+        $now = time();
+
+        $term = new DhammaTerm;
+        $term->id = app('snowflake')->id();
+        $term->guid = (string) Str::uuid();
+        $term->word = $word;
+        $term->meaning = $word;
+        $term->channal = $channelId;
+        $term->word_en = Tools::getWordEn($word);
+        $term->owner = $channelInfo['studio_id'] ?? null;
+        $term->language = $channelInfo['lang'] ?? 'en';
+        $term->editor_id = 0;
+        $term->create_time = $now;
+        $term->modify_time = $now;
+        $term->save();
+
+        return $term;
+    }
 }