Просмотр исходного кода

:sparkles: para-info 独立成 ParaInfoController,关联 CS6 锚点

把 PaliTextController 的 paragraphs-info 分支提取成独立的 ParaInfoController,
路由 GET /api/v2/para-info?book=&para=(apiResource,随 v2 组)。

业务逻辑增加:关联 related_paragraphs(pali_texts.book = related_paragraphs.book,
pali_texts.paragraph = related_paragraphs.para),每条输出加 book_name 与 cs_para,
没有 CS6 锚点的段落这两个字段为 null。

两处数据上的取舍:

- (book, para) 在 related_paragraphs 里不唯一。519818 个组合中 511615 个
  (98.4%)只有一行,其余 8203 个有 2 到 844 行;同组内 book_name 全局唯一,
  差异只在 cs_para——一个段落对应一串连续的 CS6 段号。取 min(cs_para) 作为
  起始锚点,与 RelatedParagraphController::index() 的 first() + cs_para > 0
  约定一致。
- chapter_len 最大到 15943,逐段查关联会打出上万次 SQL,所以走一次
  GROUP BY para 的聚合查询再 keyBy。实测最大章节 15943 行只用 3 次 SQL。

输出字段 lenght 改回正确拼写 length,pali_texts 的列名不动。

PaliTextController 里的 paragraphs-info 一并删除:case 本体、view 校验白名单
里的取值、以及末尾补 progress_line 时的排除条件。已确认整个 monorepo(含
dashboard-v4/v6、magnolia、coconut 等前端)没有任何调用方,故不保留转发。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
visuddhinanda 3 дней назад
Родитель
Сommit
e306f67351

+ 2 - 24
api-v13/app/Http/Controllers/PaliTextController.php

@@ -24,7 +24,7 @@ class PaliTextController extends Controller
     {
         //
         $request->validate([
-            'view' => 'required|in:chapter-tag,chapter,chapter_children,children,paragraph,paragraphs-info,book-toc',
+            'view' => 'required|in:chapter-tag,chapter,chapter_children,children,paragraph,book-toc',
         ]);
         $all_count = 0;
         switch ($request->input('view')) {
@@ -241,27 +241,6 @@ class PaliTextController extends Controller
                     return $this->error('no data');
                 }
                 break;
-            case 'paragraphs-info':
-                /**
-                 * 取章节的 pali_texts 全部段落 记录,按 (book, para) 精确匹配。
-                 *
-                 *
-                 * @param  int  $book
-                 * @param  int  $para
-                 */
-                $root = PaliText::where('book', $request->input('book'))
-                    ->where('paragraph', $request->input('para'))
-                    ->first();
-                if (! $root) {
-                    return $this->error('no paragraph');
-                }
-                $chapters = PaliText::where('book', $request->input('book'))
-                    ->whereBetween('paragraph', [$root->paragraph, $root->paragraph + $root->chapter_len - 1])
-                    ->select(['book', 'paragraph', 'toc', 'level', 'lenght', 'chapter_len'])
-                    ->orderBy('paragraph', 'asc')
-                    ->get();
-                $all_count = count($chapters);
-                break;
             case 'book-toc':
                 /**
                  * 获取全书目录
@@ -326,8 +305,7 @@ class PaliTextController extends Controller
                 return $this->error('unknown view', 400, 400);
         }
 
-        if ($request->input('view') !== 'book-toc' &&
-        $request->input('view') !== 'paragraphs-info') {
+        if ($request->input('view') !== 'book-toc') {
             foreach ($chapters as $key => $value) {
                 if (is_object($value)) {
                     // TODO $value->book 可能不存在

+ 131 - 0
api-v13/app/Http/Controllers/ParaInfoController.php

@@ -0,0 +1,131 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\PaliText;
+use App\Models\RelatedParagraph;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+use Illuminate\Support\Collection;
+
+class ParaInfoController extends Controller
+{
+    /**
+     * 取一个章节内 pali_texts 的全部段落记录。
+     *
+     * 以 (book, para) 定位章节起始段,再按 chapter_len 取出该章节覆盖的整段区间。
+     * 每条记录附带 related_paragraphs 里的 CS6 锚点(book_name + cs_para),
+     * 没有锚点的段落这两个字段为 null——约 2% 的段落没有锚点,属正常情况。
+     *
+     * @param  int  $book  书号
+     * @param  int  $para  章节起始段落号
+     * @return JsonResponse
+     */
+    public function index(Request $request)
+    {
+        $validated = $request->validate([
+            'book' => ['required', 'integer'],
+            'para' => ['required', 'integer'],
+        ]);
+
+        $root = PaliText::where('book', $validated['book'])
+            ->where('paragraph', $validated['para'])
+            ->first();
+        if (! $root) {
+            return $this->error('no paragraph');
+        }
+
+        $paraFrom = $root->paragraph;
+        $paraTo = $root->paragraph + $root->chapter_len - 1;
+
+        $chapters = PaliText::where('book', $root->book)
+            ->whereBetween('paragraph', [$paraFrom, $paraTo])
+            ->select(['book', 'paragraph', 'toc', 'level', 'lenght', 'chapter_len'])
+            ->orderBy('paragraph', 'asc')
+            ->get();
+
+        $anchors = $this->relatedAnchors($root->book, $paraFrom, $paraTo);
+
+        $rows = [];
+        foreach ($chapters as $chapter) {
+            $anchor = $anchors[$chapter->paragraph] ?? null;
+            $rows[] = [
+                'book' => $chapter->book,
+                'paragraph' => $chapter->paragraph,
+                'toc' => $chapter->toc,
+                'level' => $chapter->level,
+                // pali_texts 的列名拼错成 lenght,输出用正确拼写
+                'length' => $chapter->lenght,
+                'chapter_len' => $chapter->chapter_len,
+                'book_name' => $anchor->book_name ?? null,
+                'cs_para' => $anchor->cs_para ?? null,
+            ];
+        }
+
+        return $this->ok(['rows' => $rows, 'count' => count($rows)]);
+    }
+
+    /**
+     * 批量取段落区间的 CS6 锚点,按 paragraph 索引。
+     *
+     * 一次聚合查询取完,不能按段落逐条查——chapter_len 最大到 15943,
+     * 逐条查会打出上万次 SQL。
+     *
+     * 一个段落可能对应多个 cs_para(约 1.6% 的段落如此,通常是连续区间),
+     * 取最小值作为该段的起始锚点;book_name 在同一 (book, para) 内唯一,
+     * min() 只是为了配合 GROUP BY。cs_para = 0 表示没有锚点,直接排除。
+     *
+     * @return Collection<int, object{book_name: string, cs_para: int}>
+     */
+    private function relatedAnchors(int $book, int $paraFrom, int $paraTo)
+    {
+        return RelatedParagraph::where('book', $book)
+            ->whereBetween('para', [$paraFrom, $paraTo])
+            ->where('cs_para', '>', 0)
+            ->groupBy('para')
+            ->selectRaw('para, min(book_name) as book_name, min(cs_para) as cs_para')
+            ->get()
+            ->keyBy('para');
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @return Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @return Response
+     */
+    public function show(string $id)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @return Response
+     */
+    public function update(Request $request, string $id)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @return Response
+     */
+    public function destroy(string $id)
+    {
+        //
+    }
+}

+ 5 - 5
api-v13/routes/api.php

@@ -1,8 +1,5 @@
 <?php
 
-use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Route;
-
 use App\Http\Controllers\AccessTokenController;
 use App\Http\Controllers\AiAssistantController;
 use App\Http\Controllers\AiModelController;
@@ -69,6 +66,7 @@ use App\Http\Controllers\OfflineIndexController;
 use App\Http\Controllers\PaliBookCategoryController;
 use App\Http\Controllers\PaliTextController;
 use App\Http\Controllers\ParagraphContentController;
+use App\Http\Controllers\ParaInfoController;
 use App\Http\Controllers\PgPaliDictDownloadController;
 use App\Http\Controllers\ProgressChapterController;
 use App\Http\Controllers\ProgressController;
@@ -112,6 +110,7 @@ use App\Http\Controllers\TaskStatusController;
 use App\Http\Controllers\TermExportController;
 use App\Http\Controllers\TermSummaryController;
 use App\Http\Controllers\TermVocabularyController;
+use App\Http\Controllers\TipitakaContentController;
 use App\Http\Controllers\TransferController;
 use App\Http\Controllers\UpdatePaliSynonymsController;
 use App\Http\Controllers\UpgradeController;
@@ -129,8 +128,8 @@ use App\Http\Controllers\WbwSentenceController;
 use App\Http\Controllers\WbwTemplateController;
 use App\Http\Controllers\WebHookController;
 use App\Http\Controllers\WordIndexController;
-use App\Http\Controllers\TipitakaContentController;
-
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Route;
 
 /*
 |--------------------------------------------------------------------------
@@ -174,6 +173,7 @@ Route::group([
     Route::apiResource('sent_history', SentHistoryController::class);
     Route::get('sent_history_contribution', [SentHistoryController::class, 'contribution']);
     Route::apiResource('palitext', PaliTextController::class);
+    Route::apiResource('para-info', ParaInfoController::class);
     Route::apiResource('channel', ChannelController::class);
     Route::patch('channel', [ChannelController::class, 'patch']);
     Route::get('channel-name/{name}', [ChannelController::class, 'showByName']);