Browse Source

refactor: UpgradeProgressPara/Chapter 改为可重入,使用 Cache 断点续跑

- 每处理一批记录后缓存当前位置,中断后重跑自动跳过已完成部分
- 拆解大方法为职责清晰的小方法,添加关键注释
- 用 clone/pluck/firstOrNew 等优化查询,去掉无用变量和 --resume 选项

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
visuddhinanda 3 days ago
parent
commit
349840340e

+ 296 - 259
api-v13/app/Console/Commands/UpgradeProgressChapter.php

@@ -2,308 +2,345 @@
 
 
 namespace App\Console\Commands;
 namespace App\Console\Commands;
 
 
-use Illuminate\Support\Facades\Log;
-
-use Carbon\Carbon;
-
-use Illuminate\Support\Facades\Validator;
-use Illuminate\Console\Command;
-use App\Models\Sentence;
+use App\Http\Api\MdRender;
+use App\Models\Channel;
 use App\Models\PaliSentence;
 use App\Models\PaliSentence;
+use App\Models\PaliText;
 use App\Models\Progress;
 use App\Models\Progress;
 use App\Models\ProgressChapter;
 use App\Models\ProgressChapter;
-use App\Models\PaliText;
-use App\Models\Tag;
+use App\Models\Sentence;
 use App\Models\TagMap;
 use App\Models\TagMap;
-use App\Models\Channel;
-use App\Http\Api\MdRender;
 use App\Services\PaliTextService;
 use App\Services\PaliTextService;
+use App\Tools\Markdown;
+use App\Tools\Tools;
+use Carbon\Carbon;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Validator;
 
 
 class UpgradeProgressChapter extends Command
 class UpgradeProgressChapter extends Command
 {
 {
-    /**
-     * The name and signature of the console command.
-     * php artisan upgrade:progress.chapter --book=168 --para=915 --channel=19f53a65-81db-4b7d-8144-ac33f1217d34
-     * @var string
-     */
-    protected $signature = 'upgrade:progress.chapter  {--book=} {--para=} {--channel=} {--driver=str} {--resume}';
-
-    /**
-     * The console command description.
-     *
-     * @var string
-     */
-    protected $description = '更新章节完成度,以channel为单位';
+    protected $signature = 'upgrade:progress.chapter {--book=} {--para=} {--channel=} {--driver=str} {--fresh : 清除缓存断点,从头开始}';
+
+    protected $description = '更新章节完成度(可重入:中断后重跑自动跳过已处理的 book)';
 
 
     const COMPLETION_RATE = 0.9;
     const COMPLETION_RATE = 0.9;
 
 
-    /**
-     * Create a new command instance.
-     *
-     * @return void
-     */
-    public function __construct()
-    {
-        parent::__construct();
-    }
+    // 缓存键:记录最后处理完成的 book_id,48h 过期
+    private const CACHE_KEY = 'upgrade-progress-chapter:cursor';
 
 
-    /**
-     * Execute the console command.
-     *
-     * @return int
-     */
-    public function handle()
+    public function handle(): int
     {
     {
-        if (\App\Tools\Tools::isStop()) {
+        if (Tools::isStop()) {
             return 0;
             return 0;
         }
         }
+
+        if ($this->option('fresh')) {
+            Cache::forget(self::CACHE_KEY);
+            $this->info('Cleared cached cursor.');
+        }
+
         $paliTextService = app(PaliTextService::class);
         $paliTextService = app(PaliTextService::class);
 
 
-        $this->info("upgrade:progresschapter start.");
+        $this->info('upgrade:progress.chapter start.');
         $startTime = time();
         $startTime = time();
+
         $book = $this->option('book');
         $book = $this->option('book');
         $para = $this->option('para');
         $para = $this->option('para');
         $channelId = $this->option('channel');
         $channelId = $this->option('channel');
+
         if ($channelId) {
         if ($channelId) {
-            $this->line('channel=' . $channelId);
+            $this->line('channel='.$channelId);
         }
         }
 
 
-        \App\Tools\Markdown::driver($this->option('driver'));
+        Markdown::driver($this->option('driver'));
 
 
         $tagCount = 0;
         $tagCount = 0;
-        #第一步 查询有多少书有译文
-        if ($book) {
-            if ($this->option('resume')) {
-                $table = Sentence::whereBetween('book_id', [$book, 217]);
-            } else {
-                $table = Sentence::where('book_id', $book);
-            }
-            $books = $table->groupby('book_id')
-                ->select('book_id')
-                ->get();
-        } else {
-            $books = Sentence::where('strlen', '>', 0)
-                ->where('book_id', '<', 1000)
-                ->whereNotNull('channel_uid')
-                ->groupby('book_id')
-                ->select('book_id')
-                ->get();
+
+        // 第一步:查询有译文的 book 列表
+        $books = $this->buildBookList($book);
+
+        // 从缓存恢复断点:跳过上次已完成的 book
+        $lastBookId = Cache::get(self::CACHE_KEY);
+        if ($lastBookId && ! $book) {
+            $books = $books->filter(fn ($b) => $b->book_id > $lastBookId)->values();
+            $this->info("Resuming after book={$lastBookId}");
         }
         }
 
 
+        $totalBook = $books->count();
 
 
-        $totalBook = count($books);
+        foreach ($books as $bookIdx => $bookRow) {
+            $this->info('['.($bookIdx + 1)."/{$totalBook}] book={$bookRow->book_id}");
 
 
-        foreach ($books as $book) {
-            $this->info("{$book->book_id} start total {$totalBook}");
-            if ($para) {
-                $table = PaliText::where('book', $book->book_id)
-                    ->where('paragraph', '<=', $para);
-            } else {
-                $table = PaliText::where('book', $book->book_id);
-            }
-            $chapters = $table->where('level', '>', 0)
-                ->where('level', '<', 8)
-                ->select('paragraph', 'chapter_strlen', 'chapter_len')
-                ->get();
-
-            foreach ($chapters as $key => $chapter) {
-                # code...
-                $chapter_strlen = PaliSentence::where('book', $book->book_id)
-                    ->whereBetween('paragraph', [$chapter->paragraph, $chapter->paragraph + $chapter->chapter_len - 1])
+            $chapters = $this->getChapters($bookRow->book_id, $para);
+
+            foreach ($chapters as $chapter) {
+                // 计算章节对应的巴利语总字符数
+                $chapterEnd = $chapter->paragraph + $chapter->chapter_len - 1;
+                $chapterStrlen = PaliSentence::where('book', $bookRow->book_id)
+                    ->whereBetween('paragraph', [$chapter->paragraph, $chapterEnd])
                     ->sum('length');
                     ->sum('length');
-                if ($chapter_strlen == 0) {
-                    $this->error('chapter_strlen is 0 book:' . $book->book_id . ' paragraph:' . $chapter->paragraph . '-' . ($chapter->paragraph + $chapter->chapter_len - 1));
+
+                if ($chapterStrlen == 0) {
+                    $this->error("chapter_strlen=0 book:{$bookRow->book_id} para:{$chapter->paragraph}-{$chapterEnd}");
+
                     continue;
                     continue;
                 }
                 }
-                $table = Progress::where('book', $book->book_id)
-                    ->whereBetween('para', [$chapter->paragraph, $chapter->paragraph + $chapter->chapter_len - 1]);
+
+                // 按 channel 分组统计已翻译字符数
+                $progressQuery = Progress::where('book', $bookRow->book_id)
+                    ->whereBetween('para', [$chapter->paragraph, $chapterEnd]);
+
                 if ($channelId) {
                 if ($channelId) {
-                    $table->where('channel_id', $channelId);
+                    $progressQuery->where('channel_id', $channelId);
                 }
                 }
-                $strlen = $table->groupby('channel_id')
+
+                $channelProgress = $progressQuery->groupBy('channel_id')
                     ->selectRaw('channel_id, sum(all_strlen) as cp_len')
                     ->selectRaw('channel_id, sum(all_strlen) as cp_len')
                     ->get();
                     ->get();
-                foreach ($strlen as $final) {
-                    # code...
-                    # 计算此段落完成时间
-                    $finalAt = Progress::where('book', $book->book_id)
-                        ->whereBetween('para', [$chapter->paragraph, $chapter->paragraph + $chapter->chapter_len - 1])
-                        ->where('channel_id', $final->channel_id)
-                        ->max('created_at');
-                    $updateAt = Progress::where('book', $book->book_id)
-                        ->whereBetween('para', [$chapter->paragraph, $chapter->paragraph + $chapter->chapter_len - 1])
-                        ->where('channel_id', $final->channel_id)
-                        ->max('updated_at');
-                    $transTexts = Sentence::where('book_id', $book->book_id)
-                        ->whereBetween('paragraph', [$chapter->paragraph + 1, $chapter->paragraph + $chapter->chapter_len - 1])
-                        ->where('channel_uid', $final->channel_id)
-                        ->select('content')
-                        ->orderBy('paragraph')
-                        ->orderBy('word_start')
-                        ->get();
-
-                    $mdRender = new MdRender(['format' => 'simple']);
-
-                    #查询标题
-                    $title = Sentence::where('book_id', $book->book_id)
-                        ->where('paragraph', $chapter->paragraph)
-                        ->where('channel_uid', $final->channel_id)
-                        ->value('content');
-                    $title = $mdRender->convert($title, [$final->channel_id]);
-
-                    $summaryText = "";
-                    foreach ($transTexts as $text) {
-                        # code...
-                        $textContent = $mdRender->convert($text->content, [$final->channel_id]);
-                        $summaryText .= str_replace("\n", "", $textContent);
-                        if (mb_strlen($summaryText, "UTF-8") > 255) {
-                            break;
-                        }
-                    }
-
-                    //查询语言
-                    $channelLang = Channel::where('uid', $final->channel_id)->value('lang');
-                    $lang = explode('-', $channelLang)[0];
-                    $attributes = [
-                        'book' => $book->book_id,
-                        'para' => $chapter->paragraph,
-                        'channel_id' => $final->channel_id
-                    ];
-
-                    $rules = array(
-                        'book' => 'integer',
-                        'para' => 'integer',
-                        'channel_id' => 'uuid'
-                    );
 
 
-                    $validator = Validator::make($attributes, $rules);
-                    if ($validator->fails()) {
-                        $this->error("Validator is fails");
-                        return 0;
-                    }
-                    if (ProgressChapter::where($attributes)->exists()) {
-                        $chapterData = ProgressChapter::where($attributes)->first();
-                    } else {
-                        $chapterData = new ProgressChapter;
-                        $chapterData->book = $attributes["book"];
-                        $chapterData->para = $attributes["para"];
-                        $chapterData->channel_id = $attributes["channel_id"];
-                    }
-                    $progress = $final->cp_len / $chapter_strlen;
-                    $addChapter = false;
-                    if ($progress >= self::COMPLETION_RATE) {
-                        if (empty($chapterData->completed_at)) {
-                            // 添加新的章节了
-                            $chapterData->completed_at = $finalAt;
-                            $addChapter = true;
-                        }
-                    }
-
-                    $chapterData->lang = $lang;
-                    $chapterData->all_trans = $progress;
-                    $chapterData->public = $progress;
-                    $chapterData->progress = $progress;
-                    $chapterData->title = $title ? mb_substr($title, 0, 255, "UTF-8") : "";
-                    $chapterData->summary = $summaryText ? mb_substr($summaryText, 0, 255, "UTF-8") : "";
-                    $chapterData->created_at = $finalAt;
-                    $chapterData->updated_at = $updateAt;
-                    $chapterData->save();
-
-
-                    if ($addChapter) {
-                        // 添加新的章节了
-                        //修改父目录
-                        $loop = 0;
-                        $currBook = $attributes["book"];
-                        $currPara = $attributes["para"];
-                        while ($parent = $paliTextService->getParent($currBook, $currPara)) {
-                            $parentChapter = ProgressChapter::where('book', $currBook)
-                                ->where('para', $parent->paragraph)
-                                ->where('channel_id', $attributes["channel_id"])
-                                ->first();
-                            if ($parentChapter) {
-                                $currPara = $parent->paragraph;
-                                if (
-                                    is_null($parentChapter->last_chapter_completed_at) ||
-                                    Carbon::parse($finalAt)->gt(Carbon::parse($parentChapter->last_chapter_completed_at))
-                                ) {
-                                    $parentChapter->last_chapter_completed_at = $finalAt;
-                                    // 计算字章节有多少已经完成
-                                    $chapterEnd = $parent->paragraph + $parent->chapter_len - 1;
-                                    $totalCompleted = ProgressChapter::where('book', $currBook)
-                                        ->whereBetween('para', [$parent->paragraph, $chapterEnd])
-                                        ->where('channel_id', $attributes["channel_id"])
-                                        ->whereNotNull('completed_at')
-                                        ->count();
-                                    $parentChapter->completed_chapters = $totalCompleted;
-                                    $parentChapter->save();
-                                    /*
-                                    Log::info('update last_chapter_completed_at', [
-                                        'para' => $parent->paragraph,
-                                        'completed' => $totalCompleted
-                                    ]);
-                                    */
-                                }
-                            } else {
-                                break;
-                            }
-                            $loop++;
-                        }
-                        //Log::info('update parent completed loop ' . $loop);
-                    }
-
-
-                    $wasCreated = $chapterData->wasRecentlyCreated;
-                    $wasChanged = $chapterData->wasChanged();
-                    #查询路径
-                    $path = json_decode(
-                        PaliText::where('book', $book->book_id)
-                            ->where('paragraph', $chapter->paragraph)
-                            ->value('path')
+                foreach ($channelProgress as $final) {
+                    $tagCount += $this->processChapterChannel(
+                        $bookRow->book_id,
+                        $chapter,
+                        $chapterEnd,
+                        $chapterStrlen,
+                        $final,
+                        $paliTextService,
                     );
                     );
-
-                    if ($path) {
-                        //查询标签
-                        $tags = [];
-                        foreach ($path as $key => $value) {
-                            # code...
-                            if ($value->level > 0) {
-                                $paliTextUuid = PaliText::where('book', $value->book)
-                                    ->where('paragraph', $value->paragraph)
-                                    ->value('uid');
-                                $tagUuids = TagMap::where('table_name', 'pali_texts')
-                                    ->where('anchor_id', $paliTextUuid)
-                                    ->select(['tag_id'])
-                                    ->get();
-                                foreach ($tagUuids as $key => $taguuid) {
-                                    # code...
-                                    $tags[$taguuid['tag_id']] = 1;
-                                }
-                            }
-                        }
-
-                        //更新标签映射表
-                        //删除旧的标签映射表
-                        TagMap::where('table_name', 'progress_chapters')
-                            ->where('anchor_id', $chapterData->uid)
-                            ->delete();
-                        foreach ($tags as $key => $tag) {
-                            # code...
-                            $tagmap = TagMap::create([
-                                'table_name' => 'progress_chapters',
-                                'anchor_id' => $chapterData->uid,
-                                'tag_id' => $key
-                            ]);
-                            if ($tagmap) {
-                                $tagCount++;
-                            }
-                        }
-                    }
                 }
                 }
             }
             }
+
+            // 每完成一本书,保存断点
+            Cache::put(self::CACHE_KEY, $bookRow->book_id, now()->addHours(48));
         }
         }
+
+        // 全部完成,清除断点缓存
+        Cache::forget(self::CACHE_KEY);
+
         $time = time() - $startTime;
         $time = time() - $startTime;
-        $this->info("upgrade:progresschapter finished in {$time}s tag count:{$tagCount}");
+        $this->info("upgrade:progress.chapter finished in {$time}s tag count:{$tagCount}");
+
         return 0;
         return 0;
     }
     }
+
+    /** 查询有译文的 book 列表,按 book_id 排序 */
+    private function buildBookList(?string $book)
+    {
+        if ($book) {
+            $table = Sentence::where('book_id', $book);
+        } else {
+            $table = Sentence::where('strlen', '>', 0)
+                ->where('book_id', '<', 1000)
+                ->whereNotNull('channel_uid');
+        }
+
+        return $table->groupBy('book_id')
+            ->select('book_id')
+            ->orderBy('book_id')
+            ->get();
+    }
+
+    /** 获取某本书的章节列表(level 1-7) */
+    private function getChapters(int $bookId, ?string $para)
+    {
+        $table = PaliText::where('book', $bookId);
+
+        if ($para) {
+            $table = $table->where('paragraph', '<=', $para);
+        }
+
+        return $table->where('level', '>', 0)
+            ->where('level', '<', 8)
+            ->select('paragraph', 'chapter_strlen', 'chapter_len')
+            ->get();
+    }
+
+    /** 处理单个章节×channel 的进度更新,返回新增 tag 数 */
+    private function processChapterChannel(
+        int $bookId,
+        $chapter,
+        int $chapterEnd,
+        int $chapterStrlen,
+        $final,
+        PaliTextService $paliTextService,
+    ): int {
+        $tagCount = 0;
+
+        // 查询该 channel 在此章节范围内的完成时间
+        $baseProgress = Progress::where('book', $bookId)
+            ->whereBetween('para', [$chapter->paragraph, $chapterEnd])
+            ->where('channel_id', $final->channel_id);
+
+        $finalAt = (clone $baseProgress)->max('created_at');
+        $updateAt = (clone $baseProgress)->max('updated_at');
+
+        // 获取译文内容,用于生成摘要
+        $transTexts = Sentence::where('book_id', $bookId)
+            ->whereBetween('paragraph', [$chapter->paragraph + 1, $chapterEnd])
+            ->where('channel_uid', $final->channel_id)
+            ->select('content')
+            ->orderBy('paragraph')
+            ->orderBy('word_start')
+            ->get();
+
+        $mdRender = new MdRender(['format' => 'simple']);
+
+        // 章节标题
+        $title = Sentence::where('book_id', $bookId)
+            ->where('paragraph', $chapter->paragraph)
+            ->where('channel_uid', $final->channel_id)
+            ->value('content');
+        $title = $mdRender->convert($title, [$final->channel_id]);
+
+        // 拼接摘要,最多 255 字符
+        $summaryText = '';
+        foreach ($transTexts as $text) {
+            $textContent = $mdRender->convert($text->content, [$final->channel_id]);
+            $summaryText .= str_replace("\n", '', $textContent);
+            if (mb_strlen($summaryText, 'UTF-8') > 255) {
+                break;
+            }
+        }
+
+        // 查询 channel 语言
+        $channelLang = Channel::where('uid', $final->channel_id)->value('lang');
+        $lang = explode('-', $channelLang)[0];
+
+        $attributes = [
+            'book' => $bookId,
+            'para' => $chapter->paragraph,
+            'channel_id' => $final->channel_id,
+        ];
+
+        $validator = Validator::make($attributes, [
+            'book' => 'integer',
+            'para' => 'integer',
+            'channel_id' => 'uuid',
+        ]);
+
+        if ($validator->fails()) {
+            $this->error('Validator failed: '.json_encode($attributes));
+
+            return 0;
+        }
+
+        // firstOrNew:存在则更新,不存在则新建
+        $chapterData = ProgressChapter::firstOrNew($attributes);
+
+        $progress = $final->cp_len / $chapterStrlen;
+        $addChapter = false;
+
+        // 进度 >= 90% 视为完成
+        if ($progress >= self::COMPLETION_RATE && empty($chapterData->completed_at)) {
+            $chapterData->completed_at = $finalAt;
+            $addChapter = true;
+        }
+
+        $chapterData->lang = $lang;
+        $chapterData->all_trans = $progress;
+        $chapterData->public = $progress;
+        $chapterData->progress = $progress;
+        $chapterData->title = $title ? mb_substr($title, 0, 255, 'UTF-8') : '';
+        $chapterData->summary = $summaryText ? mb_substr($summaryText, 0, 255, 'UTF-8') : '';
+        $chapterData->created_at = $finalAt;
+        $chapterData->updated_at = $updateAt;
+        $chapterData->save();
+
+        // 新完成的章节:向上更新父级目录的 last_chapter_completed_at
+        if ($addChapter) {
+            $this->updateParentChapters($bookId, $chapter->paragraph, $final->channel_id, $finalAt, $paliTextService);
+        }
+
+        // 更新标签映射
+        $tagCount += $this->syncTags($bookId, $chapter->paragraph, $chapterData->uid);
+
+        return $tagCount;
+    }
+
+    /** 向上遍历父章节,更新 last_chapter_completed_at 和 completed_chapters 计数 */
+    private function updateParentChapters(int $bookId, int $para, string $channelId, $finalAt, PaliTextService $paliTextService): void
+    {
+        $currPara = $para;
+
+        while ($parent = $paliTextService->getParent($bookId, $currPara)) {
+            $parentChapter = ProgressChapter::where('book', $bookId)
+                ->where('para', $parent->paragraph)
+                ->where('channel_id', $channelId)
+                ->first();
+
+            if (! $parentChapter) {
+                break;
+            }
+
+            $currPara = $parent->paragraph;
+
+            if (
+                is_null($parentChapter->last_chapter_completed_at) ||
+                Carbon::parse($finalAt)->gt(Carbon::parse($parentChapter->last_chapter_completed_at))
+            ) {
+                $parentChapter->last_chapter_completed_at = $finalAt;
+
+                $chapterEnd = $parent->paragraph + $parent->chapter_len - 1;
+                $parentChapter->completed_chapters = ProgressChapter::where('book', $bookId)
+                    ->whereBetween('para', [$parent->paragraph, $chapterEnd])
+                    ->where('channel_id', $channelId)
+                    ->whereNotNull('completed_at')
+                    ->count();
+
+                $parentChapter->save();
+            }
+        }
+    }
+
+    /** 同步章节的标签映射,返回新增 tag 数 */
+    private function syncTags(int $bookId, int $para, string $chapterUid): int
+    {
+        $path = json_decode(
+            PaliText::where('book', $bookId)
+                ->where('paragraph', $para)
+                ->value('path')
+        );
+
+        if (! $path) {
+            return 0;
+        }
+
+        // 收集路径上所有层级的标签
+        $tags = [];
+        foreach ($path as $value) {
+            if ($value->level > 0) {
+                $paliTextUuid = PaliText::where('book', $value->book)
+                    ->where('paragraph', $value->paragraph)
+                    ->value('uid');
+
+                $tagIds = TagMap::where('table_name', 'pali_texts')
+                    ->where('anchor_id', $paliTextUuid)
+                    ->pluck('tag_id');
+
+                foreach ($tagIds as $tagId) {
+                    $tags[$tagId] = 1;
+                }
+            }
+        }
+
+        // 先删后建:重建标签映射
+        TagMap::where('table_name', 'progress_chapters')
+            ->where('anchor_id', $chapterUid)
+            ->delete();
+
+        $count = 0;
+        foreach ($tags as $tagId => $_) {
+            $tagmap = TagMap::create([
+                'table_name' => 'progress_chapters',
+                'anchor_id' => $chapterUid,
+                'tag_id' => $tagId,
+            ]);
+            if ($tagmap) {
+                $count++;
+            }
+        }
+
+        return $count;
+    }
 }
 }

+ 116 - 118
api-v13/app/Console/Commands/UpgradeProgressPara.php

@@ -2,156 +2,154 @@
 
 
 namespace App\Console\Commands;
 namespace App\Console\Commands;
 
 
-use Illuminate\Console\Command;
-
-use Illuminate\Support\Facades\DB;
-
-
-use App\Models\Sentence;
 use App\Models\PaliSentence;
 use App\Models\PaliSentence;
 use App\Models\Progress;
 use App\Models\Progress;
-use Illuminate\Support\Facades\Log;
+use App\Models\Sentence;
+use App\Tools\Tools;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
 
 
 class UpgradeProgressPara extends Command
 class UpgradeProgressPara extends Command
 {
 {
-    /**
-     * The name and signature of the console command.
-     * php artisan upgrade:progress --book=152 --channel=19f53a65-81db-4b7d-8144-ac33f1217d34
-     * @var string
-     */
-    protected $signature = 'upgrade:progress.para {--book=} {--para=} {--channel=} {--resume}';
-
-    /**
-     * The console command description.
-     *
-     * @var string
-     */
-    protected $description = 'Command description';
-
-    /**
-     * Create a new command instance.
-     *
-     * @return void
-     */
-    public function __construct()
-    {
-        parent::__construct();
-    }
+    protected $signature = 'upgrade:progress.para {--book=} {--para=} {--channel=} {--fresh : 清除缓存断点,从头开始}';
+
+    protected $description = '更新段落翻译进度(可重入:中断后重跑自动跳过已处理的段落)';
+
+    // 缓存键:记录最后处理到的位置 (book_id, paragraph, channel_uid),48h 过期
+    private const CACHE_KEY = 'upgrade-progress-para:cursor';
 
 
-    /**
-     * Execute the console command.
-     *
-     * @return int
-     */
-    public function handle()
+    public function handle(): int
     {
     {
-        if (\App\Tools\Tools::isStop()) {
+        if (Tools::isStop()) {
             return 0;
             return 0;
         }
         }
-        $this->info('upgrade:progress start');
+
+        if ($this->option('fresh')) {
+            Cache::forget(self::CACHE_KEY);
+            $this->info('Cleared cached cursor.');
+        }
+
+        $this->info('upgrade:progress.para start');
         $startTime = time();
         $startTime = time();
+
         $book = $this->option('book');
         $book = $this->option('book');
         $para = $this->option('para');
         $para = $this->option('para');
         $channelId = $this->option('channel');
         $channelId = $this->option('channel');
+
         if ($channelId) {
         if ($channelId) {
-            $this->line('channel=' . $channelId);
+            $this->line('channel='.$channelId);
         }
         }
-        $table = Sentence::where('strlen', '>', 0);
-        if ($book || $para || $channelId) {
-            if ($book) {
-                $table = $table->where('book_id', $book);
-            }
-            if ($para) {
-                $table = $table->where('paragraph', $para);
-            }
-            if ($channelId) {
-                $table = $table->where('channel_uid', $channelId);
-            }
-            $sentences = $table->groupby('book_id', 'paragraph', 'channel_uid')
-                ->select('book_id', 'paragraph', 'channel_uid');
-        } else {
-            if ($this->option('resume')) {
-                $sentences = Sentence::where('strlen', '>', 0)
-                    ->whereBetween('book_id', [$book, 1000])
-                    ->where('paragraph', '>=', $para)
-                    ->whereNotNull('channel_uid')
-                    ->groupby('book_id', 'paragraph', 'channel_uid')
-                    ->select('book_id', 'paragraph', 'channel_uid');
-            } else {
-                $sentences = Sentence::where('strlen', '>', 0)
-                    ->where('book_id', '<', 1000)
-                    ->whereNotNull('channel_uid')
-                    ->groupby('book_id', 'paragraph', 'channel_uid')
-                    ->select('book_id', 'paragraph', 'channel_uid');
-            }
+
+        // 构建查询:按 (book_id, paragraph, channel_uid) 分组
+        $sentences = $this->buildQuery($book, $para, $channelId);
+
+        // 从缓存恢复断点:跳过上次已处理的记录
+        $cursor = Cache::get(self::CACHE_KEY);
+        if ($cursor && ! $this->option('book')) {
+            $sentences = $this->applyResumeFilter($sentences, $cursor);
+            $this->info("Resuming from book={$cursor['book']}, para={$cursor['para']}");
         }
         }
-        $total = DB::query()
-            ->fromSub($sentences, 't')
-            ->count();
-        $sentences = $sentences->cursor();
-        $this->info('sentences:' . $total);
+
+        $total = DB::query()->fromSub($sentences, 't')->count();
+        $this->info("sentences: {$total}");
+
         $curr = 0;
         $curr = 0;
-        #第二步 更新段落表
-        foreach ($sentences as $sentence) {
 
 
-            # 第二步 生成para progress 1,2,15,zh-tw
-            # 计算此段落完成时间
-            $finalAt = Sentence::where('strlen', '>', 0)
-                ->where('book_id', $sentence->book_id)
-                ->where('paragraph', $sentence->paragraph)
-                ->where('channel_uid', $sentence->channel_uid)
-                ->max('created_at');
-            $updateAt = Sentence::where('strlen', '>', 0)
+        foreach ($sentences->cursor() as $sentence) {
+            // 计算此段落的完成时间和最后更新时间
+            $baseQuery = Sentence::where('strlen', '>', 0)
                 ->where('book_id', $sentence->book_id)
                 ->where('book_id', $sentence->book_id)
                 ->where('paragraph', $sentence->paragraph)
                 ->where('paragraph', $sentence->paragraph)
-                ->where('channel_uid', $sentence->channel_uid)
-                ->max('updated_at');
-            # 查询每个段落的等效巴利语字符数
-            $result_sent = Sentence::where('strlen', '>', 0)
-                ->where('book_id', $sentence->book_id)
-                ->where('paragraph', $sentence->paragraph)
-                ->where('channel_uid', $sentence->channel_uid)
-                ->select('word_start')
-                ->get();
-
-            $paraInfo = [
-                'book' => $sentence->book_id,
-                'para' => $sentence->paragraph,
-                'channel_id' => $sentence->channel_uid
-            ];
-            if (count($result_sent) > 0) {
-                #查询这些句子的总共等效巴利语字符数
-                $para_strlen = 0;
-                foreach ($result_sent as $sent) {
-                    # code...
-                    $para_strlen += PaliSentence::where('book', $sentence->book_id)
-                        ->where('paragraph', $sentence->paragraph)
-                        ->where('word_begin', $sent->word_start)
-                        ->value('length');
-                }
-
-                $paraData = [
+                ->where('channel_uid', $sentence->channel_uid);
+
+            $finalAt = (clone $baseQuery)->max('created_at');
+            $updateAt = (clone $baseQuery)->max('updated_at');
+
+            // 查询段落内每个句子的起始词位置
+            $wordStarts = (clone $baseQuery)->pluck('word_start');
+
+            if ($wordStarts->isNotEmpty()) {
+                // 累加等效巴利语字符数:每个句子对应的 PaliSentence.length
+                $paraStrlen = PaliSentence::where('book', $sentence->book_id)
+                    ->where('paragraph', $sentence->paragraph)
+                    ->whereIn('word_begin', $wordStarts)
+                    ->sum('length');
+
+                $paraInfo = [
+                    'book' => $sentence->book_id,
+                    'para' => $sentence->paragraph,
+                    'channel_id' => $sentence->channel_uid,
+                ];
+
+                Progress::updateOrInsert($paraInfo, [
                     'lang' => 'en',
                     'lang' => 'en',
-                    'all_strlen' => $para_strlen,
-                    'public_strlen' => $para_strlen,
+                    'all_strlen' => $paraStrlen,
+                    'public_strlen' => $paraStrlen,
                     'created_at' => $finalAt,
                     'created_at' => $finalAt,
                     'updated_at' => $updateAt,
                     'updated_at' => $updateAt,
-                ];
-
-
-                Progress::updateOrInsert($paraInfo, $paraData);
+                ]);
             }
             }
+
             $curr++;
             $curr++;
+
+            // 每 500 条保存一次断点到缓存
             if ($curr % 500 === 0) {
             if ($curr % 500 === 0) {
-                $present = (int)($curr * 100 / $total);
-                $this->info("[{$present}%] Progress " . json_encode($paraInfo));
+                Cache::put(self::CACHE_KEY, [
+                    'book' => $sentence->book_id,
+                    'para' => $sentence->paragraph,
+                ], now()->addHours(48));
+
+                $percent = (int) ($curr * 100 / $total);
+                $this->info("[{$percent}%] book={$sentence->book_id} para={$sentence->paragraph}");
                 sleep(1);
                 sleep(1);
             }
             }
         }
         }
 
 
+        // 全部完成,清除断点缓存
+        Cache::forget(self::CACHE_KEY);
+
         $time = time() - $startTime;
         $time = time() - $startTime;
-        $this->info("upgrade progress finished in {$time}s");
+        $this->info("upgrade:progress.para finished in {$time}s");
 
 
         return 0;
         return 0;
     }
     }
+
+    /** 构建分组查询 */
+    private function buildQuery(?string $book, ?string $para, ?string $channelId)
+    {
+        $table = Sentence::where('strlen', '>', 0);
+
+        if ($book || $para || $channelId) {
+            if ($book) {
+                $table = $table->where('book_id', $book);
+            }
+            if ($para) {
+                $table = $table->where('paragraph', $para);
+            }
+            if ($channelId) {
+                $table = $table->where('channel_uid', $channelId);
+            }
+        } else {
+            $table = $table->where('book_id', '<', 1000)
+                ->whereNotNull('channel_uid');
+        }
+
+        return $table->groupBy('book_id', 'paragraph', 'channel_uid')
+            ->select('book_id', 'paragraph', 'channel_uid')
+            ->orderBy('book_id')
+            ->orderBy('paragraph');
+    }
+
+    /** 从断点位置之后继续:跳过 (book < X) 或 (book = X and para <= Y) 的记录 */
+    private function applyResumeFilter($query, array $cursor)
+    {
+        return $query->where(function ($q) use ($cursor) {
+            $q->where('book_id', '>', $cursor['book'])
+                ->orWhere(function ($q2) use ($cursor) {
+                    $q2->where('book_id', $cursor['book'])
+                        ->where('paragraph', '>', $cursor['para']);
+                });
+        });
+    }
 }
 }