visuddhinanda 6 дней назад
Родитель
Сommit
d3836ffb63

+ 9 - 9
api-v13/app/Http/Controllers/UserStatisticController.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers;
 
+use App\Facades\RedisCache;
 use App\Http\Api\UserApi;
 use App\Models\DhammaTerm;
 use App\Models\Sentence;
@@ -11,7 +12,6 @@ use App\Models\UserOperationLog;
 use App\Models\Wbw;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
-use Illuminate\Support\Facades\Cache;
 
 class UserStatisticController extends Controller
 {
@@ -68,7 +68,7 @@ class UserStatisticController extends Controller
         $myDictCount = 0;
         // 总经验值
         if (! $request->has('view') || $request->input('view') === 'exp-sum') {
-            $expSum = Cache::remember(
+            $expSum = RedisCache::remember(
                 "user/{$userName}/exp/sum",
                 $cacheExpiry,
                 function () use ($queryUserId) {
@@ -80,7 +80,7 @@ class UserStatisticController extends Controller
 
         // 逐词解析
         if (! $request->has('view') || $request->input('view') === 'wbw-count') {
-            $wbwCount = Cache::remember(
+            $wbwCount = RedisCache::remember(
                 "user/{$userName}/wbw/count",
                 $cacheExpiry,
                 function () use ($queryUserId) {
@@ -92,7 +92,7 @@ class UserStatisticController extends Controller
 
         // 查字典次数
         if (! $request->has('view') || $request->input('view') === 'lookup-count') {
-            $lookupCount = Cache::remember(
+            $lookupCount = RedisCache::remember(
                 "user/{$userName}/lookup/count",
                 $cacheExpiry,
                 function () use ($queryUserId) {
@@ -105,7 +105,7 @@ class UserStatisticController extends Controller
         // 译文
         // TODO 判断是否是译文channel
         if (! $request->has('view') || $request->input('view') === 'translation-count') {
-            $translationCount = Cache::remember(
+            $translationCount = RedisCache::remember(
                 "user/{$userName}/translation/count",
                 $cacheExpiry,
                 function () use ($queryUserUuid) {
@@ -113,7 +113,7 @@ class UserStatisticController extends Controller
                         ->count();
                 }
             );
-            $translationCountPub = Cache::remember(
+            $translationCountPub = RedisCache::remember(
                 "user/{$userName}/translation/count-pub",
                 $cacheExpiry,
                 function () use ($queryUserUuid) {
@@ -125,7 +125,7 @@ class UserStatisticController extends Controller
         }
         // 术语
         if (! $request->has('view') || $request->input('view') === 'term-count') {
-            $termCount = Cache::remember(
+            $termCount = RedisCache::remember(
                 "user/{$userName}/term/count",
                 $cacheExpiry,
                 function () use ($queryUserId) {
@@ -133,7 +133,7 @@ class UserStatisticController extends Controller
                         ->count();
                 }
             );
-            $termCountWithNote = Cache::remember(
+            $termCountWithNote = RedisCache::remember(
                 "user/{$userName}/term/count-note",
                 $cacheExpiry,
                 function () use ($queryUserId) {
@@ -145,7 +145,7 @@ class UserStatisticController extends Controller
         }
         // 单词本
         if (! $request->has('view') || $request->input('view') === 'my-dict-count') {
-            $myDictCount = Cache::remember(
+            $myDictCount = RedisCache::remember(
                 "user/{$userName}/dict/count",
                 $cacheExpiry,
                 function () use ($queryUserId) {

+ 2 - 2
api-v13/app/Http/Controllers/VocabularyController.php

@@ -2,11 +2,11 @@
 
 namespace App\Http\Controllers;
 
+use App\Facades\RedisCache;
 use App\Http\Resources\VocabularyResource;
 use App\Models\Vocabulary;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
-use Illuminate\Support\Facades\Cache;
 
 class VocabularyController extends Controller
 {
@@ -25,7 +25,7 @@ class VocabularyController extends Controller
                     return $this->ok(['rows' => [], 'count' => 0]);
                 }
 
-                $payload = Cache::remember(
+                $payload = RedisCache::remember(
                     'v13:dict_vocabulary:'.md5($key),
                     config('mint.cache.expire'),
                     function () use ($key) {

+ 3 - 3
api-v13/app/Http/Controllers/WbwLookupController.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers;
 
+use App\Facades\RedisCache;
 use App\Http\Api\DictApi;
 use App\Models\Channel;
 use App\Models\DictInfo;
@@ -11,7 +12,6 @@ use App\Services\AuthService;
 use App\Tools\CaseMan;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
-use Illuminate\Support\Facades\Cache;
 
 class WbwLookupController extends Controller
 {
@@ -167,11 +167,11 @@ class WbwLookupController extends Controller
             'case' => 8,
         ];
         $fieldId = $fieldMap[$field];
-        $myPreference = Cache::get("{$prefix}/{$word}/{$fieldId}/{$userId}");
+        $myPreference = RedisCache::get("{$prefix}/{$word}/{$fieldId}/{$userId}");
         if (! empty($myPreference)) {
             return ['value' => $myPreference, 'status' => 5];
         } else {
-            $myPreference = Cache::get("{$prefix}/{$word}/3/0");
+            $myPreference = RedisCache::get("{$prefix}/{$word}/3/0");
             if (! empty($myPreference)) {
                 return ['value' => $myPreference, 'status' => 5];
             }

+ 2 - 1
api-v13/app/Http/Controllers/WordIndexController.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers;
 
+use App\Facades\RedisCache;
 use App\Models\WordIndex;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
@@ -21,7 +22,7 @@ class WordIndexController extends Controller
             case 'key':
                 $key = $request->input('key');
                 /*
-                $result = Cache::remember("/word_index/{$key}",10,function() use($key){
+                $result = RedisCache::remember("/word_index/{$key}",10,function() use($key){
                     return WordIndex::where('word','like',$key."%")
                                     ->whereOr('word_en','like',$key."%")
                                     ->orderBy('word_en')

+ 5 - 5
api-v13/app/Http/Middleware/ApiLog.php

@@ -2,9 +2,9 @@
 
 namespace App\Http\Middleware;
 
+use App\Facades\RedisCache;
 use Closure;
 use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Log;
 use Symfony\Component\HttpFoundation\Response;
 
@@ -68,11 +68,11 @@ class ApiLog
         $delayKey = "{$key}/delay";
 
         // 原子操作(Redis 驱动下安全)
-        Cache::increment($countKey);
-        Cache::increment($delayKey, $delay);
+        RedisCache::increment($countKey);
+        RedisCache::increment($delayKey, $delay);
 
         // increment 不会刷新 TTL,需手动补
-        Cache::put($countKey, Cache::get($countKey, 0), $ttl);
-        Cache::put($delayKey, Cache::get($delayKey, 0), $ttl);
+        RedisCache::put($countKey, RedisCache::get($countKey, 0), $ttl);
+        RedisCache::put($delayKey, RedisCache::get($delayKey, 0), $ttl);
     }
 }

+ 3 - 3
api-v13/app/Http/Resources/RelationResource.php

@@ -15,9 +15,9 @@ class RelationResource extends JsonResource
      * Transform the resource into an array.
      *
      * The payload must contain only scalars and arrays: it is cached by
-     * RelationController, and Laravel 13 refuses to unserialize objects from
-     * cache unless they are listed in cache.serializable_classes. Hence the
-     * associative json_decode() and the ISO-8601 date strings.
+     * RelationController, and RedisCache stores JSON, so objects would come
+     * back as associative arrays anyway. Hence the associative json_decode()
+     * and the ISO-8601 date strings.
      *
      * @param  Request  $request
      * @return array|Arrayable|\JsonSerializable

+ 2 - 2
api-v13/app/Http/Resources/SearchResource.php

@@ -2,13 +2,13 @@
 
 namespace App\Http\Resources;
 
+use App\Facades\RedisCache;
 use App\Http\Api\ChannelApi;
 use App\Models\PaliText;
 use App\Models\Sentence;
 use Illuminate\Contracts\Support\Arrayable;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
-use Illuminate\Support\Facades\Cache;
 
 class SearchResource extends JsonResource
 {
@@ -35,7 +35,7 @@ class SearchResource extends JsonResource
         } elseif (isset($this->content)) {
             $data['content'] = $this->content;
         } else {
-            $channelId = Cache::remember('_System_Pali_VRI_', config('mint.cache.expire'), function () {
+            $channelId = RedisCache::remember('_System_Pali_VRI_', config('mint.cache.expire'), function () {
                 return ChannelApi::getSysChannel('_System_Pali_VRI_');
             });
 

+ 2 - 2
api-v13/app/Jobs/ProcessAITranslateJob.php

@@ -3,9 +3,9 @@
 namespace App\Jobs;
 
 use App\Exceptions\TaskFailException;
+use App\Facades\RedisCache;
 use App\Services\AiTranslateService;
 use App\Services\RabbitMQService;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Log;
 
 class ProcessAITranslateJob extends BaseRabbitMQJob
@@ -52,7 +52,7 @@ class ProcessAITranslateJob extends BaseRabbitMQJob
         $mq = app(RabbitMQService::class);
         $queue = 'ai_translate_v2';
         $msgId = $mq->publishMessage($queue, []);
-        Cache::put("/mq/message/{$msgId}/data", $data);
+        RedisCache::put("/mq/message/{$msgId}/data", $data);
 
         return $msgId;
     }

+ 3 - 2
api-v13/app/Mail/EmailCertif.php

@@ -2,10 +2,10 @@
 
 namespace App\Mail;
 
+use App\Facades\RedisCache;
 use Illuminate\Bus\Queueable;
 use Illuminate\Mail\Mailable;
 use Illuminate\Queue\SerializesModels;
-use Illuminate\Support\Facades\Cache;
 
 class EmailCertif extends Mailable
 {
@@ -38,7 +38,8 @@ class EmailCertif extends Mailable
         // 生成一个介于 1000 到 9999 之间的随机整数
         $randomNumber = random_int(1000, 9999);
         $key = '/email/certification/'.$this->uuid;
-        Cache::put($key, $randomNumber, 30 * 60);
+        // 存成字符串:GET /v2/email-certification/{id} 一直返回 "1234",v4 注册页用 === 比对表单里的字符串
+        RedisCache::put($key, (string) $randomNumber, 30 * 60);
 
         return $this->view('emails.certification.'.$this->lang)
             ->with([

+ 10 - 0
api-v13/app/Providers/AppServiceProvider.php

@@ -3,6 +3,7 @@
 namespace App\Providers;
 
 use App\Services\PaliSeriesesService;
+use App\Services\RedisCache;
 use App\Services\RomanizeService;
 use App\Services\UserService;
 use App\Tools\QueryBuilderMacro;
@@ -54,6 +55,15 @@ class AppServiceProvider extends ServiceProvider
         */
         $this->app->singleton(RomanizeService::class);
 
+        /*
+        |--------------------------------------------------------------------------
+        | Redis Cache(业务代码专用,门面 App\Facades\RedisCache,连 Redis Cluster)
+        |--------------------------------------------------------------------------
+        | 只登记这个类,不替换框架的 cache 绑定:RateLimiter、schedule 互斥锁、
+        | snowflake 序列等框架内部仍走 Laravel Cache(config/cache.php)。
+        */
+        $this->app->singleton(RedisCache::class);
+
         /*
         |--------------------------------------------------------------------------
         | Pali Serieses Service

+ 2 - 2
api-v13/app/Services/AIModelService.php

@@ -2,9 +2,9 @@
 
 namespace App\Services;
 
+use App\Facades\RedisCache;
 use App\Http\Resources\AiModelResource;
 use App\Models\AiModel;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Schema;
 
 class AIModelService
@@ -48,7 +48,7 @@ class AIModelService
 
         $sysModels = [];
         foreach ($types as $key => $type) {
-            $sysModels[$type] = $this->getModelsById(Cache::get('/ai/model/system/'.$type) ?? []);
+            $sysModels[$type] = $this->getModelsById(RedisCache::get('/ai/model/system/'.$type) ?? []);
         }
         if (! empty($type)) {
             return $sysModels[$type];

+ 9 - 9
api-v13/app/Services/AiTranslateService.php

@@ -4,6 +4,7 @@ namespace App\Services;
 
 use App\Exceptions\SectionTimeoutException;
 use App\Exceptions\TaskFailException;
+use App\Facades\RedisCache;
 use App\Http\Api\ChannelApi;
 use App\Http\Api\MdRender;
 use App\Models\AiModel;
@@ -13,7 +14,6 @@ use App\Models\Sentence;
 use App\Models\Task;
 use App\Tools\Tools;
 use Illuminate\Http\Client\RequestException;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Log;
 
@@ -68,12 +68,12 @@ class AiTranslateService
         $first = $messages[0];
         $this->task = $first->task->info;
         $taskId = $this->task->id;
-        Cache::put("/task/{$taskId}/message_id", $messageId);
+        RedisCache::put("/task/{$taskId}/message_id", $messageId);
         $pointerKey = "/task/{$taskId}/pointer";
         $pointer = 0;
-        if (Cache::has($pointerKey)) {
+        if (RedisCache::has($pointerKey)) {
             // 回到上次中断的点
-            $pointer = Cache::get($pointerKey);
+            $pointer = RedisCache::get($pointerKey);
             Log::info("last break point {$pointer}");
         }
 
@@ -108,7 +108,7 @@ class AiTranslateService
                 return false;
             }
 
-            Cache::put($pointerKey, $i);
+            RedisCache::put($pointerKey, $i);
             $message = $messages[$i];
             $taskDiscussionContent = [];
 
@@ -206,7 +206,7 @@ class AiTranslateService
         // 任务完成 修改任务状态为 done
         if ($i === count($messages)) {
             $this->setTaskStatus($this->task->id, 'done');
-            Cache::forget($pointerKey);
+            RedisCache::forget($pointerKey);
             Log::info('ai translate task complete');
         }
 
@@ -619,9 +619,9 @@ class AiTranslateService
         $aiModel['token'] = $modelToken;
 
         /**
-         * 返回值会被 ProcessAITranslateJob::publish() 写入缓存。Laravel 13 默认
-         * 拒绝从缓存反序列化对象(cache.serializable_classes = false),所以这里
-         * 先把模型摊平成数组。JSON 输出与直接编码模型完全一致。
+         * 返回值会被 ProcessAITranslateJob::publish() 写入缓存。RedisCache 存 JSON,
+         * 对象读回来会变成关联数组,所以这里先把模型摊平成数组,写入前后形状一致。
+         * JSON 输出与直接编码模型完全一致。
          */
         $aiModelData = $aiModel->toArray();
         $taskData = $task->toArray();

+ 50 - 45
api-v13/app/Services/OpenSearchService.php

@@ -4,17 +4,17 @@
 
 namespace App\Services;
 
+use App\Facades\RedisCache;
 use Exception;
 use GuzzleHttp\Client;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
-
 use OpenSearch\GuzzleClientFactory;
 
-
 class OpenSearchService
 {
+    private const EMBEDDING_CACHE_VERSION_KEY = 'embedding/version';
+
     protected $client;
 
     protected $http;
@@ -395,12 +395,12 @@ class OpenSearchService
     {
         try {
             $info = $this->client->info();
-            $message = 'OpenSearch 连接成功: ' . json_encode($info['version']['number']);
+            $message = 'OpenSearch 连接成功: '.json_encode($info['version']['number']);
             Log::info($message);
 
             return [true, $message];
         } catch (Exception $e) {
-            $message = 'OpenSearch 连接失败: ' . $e->getMessage();
+            $message = 'OpenSearch 连接失败: '.$e->getMessage();
             Log::error($message);
 
             return [false, $message];
@@ -497,14 +497,13 @@ class OpenSearchService
     public function updatePaliSynonymsPath(string $filename): array
     {
 
-
         $index = config('mint.opensearch.index');
 
         if (! $this->client->indices()->exists(['index' => $index])) {
             throw new Exception("Index [$index] does not exist.");
         }
 
-        $path = Str::finish(config('mint.opensearch.config.synonyms_path'), '/') . $filename;
+        $path = Str::finish(config('mint.opensearch.config.synonyms_path'), '/').$filename;
 
         Log::debug('OpenSearchService::updatePaliSynonymsPath', [
             'index' => $index,
@@ -879,7 +878,7 @@ class OpenSearchService
     {
         $fields = [];
         foreach ($this->weights['fuzzy'] as $field => $weight) {
-            $fields[] = $field . '^' . $weight;
+            $fields[] = $field.'^'.$weight;
         }
 
         return [
@@ -910,7 +909,7 @@ class OpenSearchService
             if (in_array($field, ['fuzzy_ratio', 'semantic_ratio'])) {
                 continue;
             }
-            $fuzzyFields[] = $field . '^' . $weight;
+            $fuzzyFields[] = $field.'^'.$weight;
         }
 
         $fuzzyPart = [
@@ -970,7 +969,7 @@ class OpenSearchService
      * 调用 OpenAI Embedding API 将文本转为向量
      *
      * 使用 Redis 缓存(TTL 7 天),相同文本不会重复请求 API,
-     * 缓存 key 格式为 "embedding:{md5(text)}"。
+     * 缓存 key 格式见 embeddingCacheKey()。
      *
      * @param  string  $text  输入文本
      * @return array 1536 维 float 向量
@@ -983,29 +982,40 @@ class OpenSearchService
             throw new Exception('请在 .env 设置 OPENAI_API_KEY');
         }
 
-        $cacheKey = 'embedding:' . md5($text);
+        return RedisCache::remember($this->embeddingCacheKey($text), now()->addDays(7), function () use ($text) {
+            $response = $this->http->post('embeddings', [
+                'headers' => [
+                    'Authorization' => 'Bearer '.$this->openaiApiKey,
+                    'Content-Type' => 'application/json',
+                ],
+                'json' => [
+                    'model' => 'text-embedding-3-small',
+                    'input' => $text,
+                ],
+            ]);
 
-        return Cache::tags(['embedding'])
-            ->remember($cacheKey, now()->addDays(7), function () use ($text) {
-                $response = $this->http->post('embeddings', [
-                    'headers' => [
-                        'Authorization' => 'Bearer ' . $this->openaiApiKey,
-                        'Content-Type' => 'application/json',
-                    ],
-                    'json' => [
-                        'model' => 'text-embedding-3-small',
-                        'input' => $text,
-                    ],
-                ]);
+            $json = json_decode((string) $response->getBody(), true);
 
-                $json = json_decode((string) $response->getBody(), true);
+            if (empty($json['data'][0]['embedding'])) {
+                throw new Exception('OpenAI embedding 返回异常: '.json_encode($json));
+            }
 
-                if (empty($json['data'][0]['embedding'])) {
-                    throw new Exception('OpenAI embedding 返回异常: ' . json_encode($json));
-                }
+            return $json['data'][0]['embedding'];
+        });
+    }
 
-                return $json['data'][0]['embedding'];
-            });
+    /**
+     * embedding 缓存键:"embedding/v{版本}/{md5(text)}"。
+     *
+     * 版本号存在 EMBEDDING_CACHE_VERSION_KEY,clearAllEmbeddingCache() 把它加一,
+     * 旧版本的键就再也不会被读到,7 天后自然过期。原来用 Cache::tags(),
+     * tag 索引是另外的 key,在 Redis Cluster 上跨 slot。
+     */
+    private function embeddingCacheKey(string $text): string
+    {
+        $version = RedisCache::get(self::EMBEDDING_CACHE_VERSION_KEY, 0);
+
+        return "embedding/v{$version}/".md5($text);
     }
 
     /**
@@ -1019,25 +1029,20 @@ class OpenSearchService
      */
     public function clearEmbeddingCache(string $text): bool
     {
-        $cacheKey = 'embedding:' . md5($text);
-
-        return Cache::forget($cacheKey);
+        return RedisCache::forget($this->embeddingCacheKey($text));
     }
 
     /**
-     * 清除 Redis 中所有 embedding 缓存
-     *
-     * 匹配 "embedding:*" 模式的全部键,生产环境请谨慎调用。
+     * 让所有 embedding 缓存失效
      *
-     * @return int 已删除的缓存条数
+     * 不逐条删除,而是把版本号加一;旧键 7 天内自然过期。
      *
      * @example
-     *   $count = $service->clearAllEmbeddingCache();
-     *   echo "已清理缓存 {$count} 条";
+     *   $service->clearAllEmbeddingCache();
      */
     public function clearAllEmbeddingCache(): void
     {
-        Cache::tags(['embedding'])->flush();
+        RedisCache::increment(self::EMBEDDING_CACHE_VERSION_KEY);
     }
 
     /**
@@ -1097,7 +1102,7 @@ class OpenSearchService
         // 过滤无效字段
         $searchFields = array_values(array_filter(
             $searchFields,
-            fn($field) => isset($fieldMap[$field])
+            fn ($field) => isset($fieldMap[$field])
         ));
 
         if (empty($searchFields)) {
@@ -1107,7 +1112,7 @@ class OpenSearchService
         // 构建 suggest DSL
         $suggests = [];
         foreach ($searchFields as $field) {
-            $suggests[$field . '_suggest'] = [
+            $suggests[$field.'_suggest'] = [
                 'prefix' => $query,
                 'completion' => [
                     'field' => $fieldMap[$field],
@@ -1131,7 +1136,7 @@ class OpenSearchService
         // 整理结果,附加来源字段
         $results = [];
         foreach ($searchFields as $field) {
-            $options = $response['suggest'][$field . '_suggest'][0]['options'] ?? [];
+            $options = $response['suggest'][$field.'_suggest'][0]['options'] ?? [];
 
             foreach ($options as $opt) {
                 $results[] = [
@@ -1145,7 +1150,7 @@ class OpenSearchService
         }
 
         // 按分数降序排序
-        usort($results, fn($a, $b) => $b['score'] <=> $a['score']);
+        usort($results, fn ($a, $b) => $b['score'] <=> $a['score']);
 
         return $results;
     }
@@ -1198,10 +1203,10 @@ class OpenSearchService
         ]);
 
         $tokens = array_map(
-            fn($token) => $token['token'] ?? '',
+            fn ($token) => $token['token'] ?? '',
             $response['tokens'] ?? []
         );
 
-        return array_values(array_unique(array_filter($tokens, fn($t) => $t !== '')));
+        return array_values(array_unique(array_filter($tokens, fn ($t) => $t !== '')));
     }
 }

+ 3 - 3
api-v13/app/Services/PacketService.php

@@ -2,10 +2,10 @@
 
 namespace App\Services;
 
+use App\Facades\RedisCache;
 use App\Http\Api\ChannelApi;
 use App\Models\Channel;
 use Illuminate\Support\Facades\App;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
@@ -304,10 +304,10 @@ class PacketService
     {
         $key = '/offline/index';
 
-        if (! Cache::has($key)) {
+        if (! RedisCache::has($key)) {
             return [];
         }
-        $fileInfo = Cache::get($key);
+        $fileInfo = RedisCache::get($key);
         $output = [];
         foreach ($fileInfo as $key => $file) {
             if ($id) {

+ 5 - 5
api-v13/app/Services/PaliContentService.php

@@ -4,6 +4,7 @@
 
 namespace App\Services;
 
+use App\Facades\RedisCache;
 use App\Http\Api\ChannelApi;
 use App\Http\Api\MdRender;
 use App\Http\Api\StudioApi;
@@ -18,7 +19,6 @@ use App\Models\SentSimIndex;
 use App\Models\Wbw;
 use App\Models\WbwBlock;
 use Illuminate\Support\Arr;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
 
@@ -73,7 +73,7 @@ class PaliContentService
         } else {
             $keyCanRead .= 'guest';
         }
-        $channelCanRead = Cache::remember(
+        $channelCanRead = RedisCache::remember(
             $keyCanRead,
             config('mint.cache.expire'),
             function () use ($userUuid) {
@@ -338,7 +338,7 @@ class PaliContentService
 
                                 break;
                             case 'nissaya':
-                                $newSent['html'] = Cache::remember(
+                                $newSent['html'] = RedisCache::remember(
                                     "/sent/{$channelId}/{$ids}/{$format}",
                                     config('mint.cache.expire'),
                                     function () use ($row, $mode, $format) {
@@ -663,7 +663,7 @@ class PaliContentService
         $level = $this->paragraphLevel($book, $para);
         // 缓存句子,段落外壳与标题级别有关,不进缓存
         $key = self::paragraphCacheKey($book, $para, $channelUid, $format);
-        $cached = Cache::remember(
+        $cached = RedisCache::remember(
             $key,
             config('mint.cache.expire'),
             function () use ($book, $para, $channelUid, $format) {
@@ -734,7 +734,7 @@ class PaliContentService
     public static function forgetParagraph(int $book, int $para, string $channelUid): void
     {
         foreach (self::FORMATS as $format) {
-            Cache::forget(self::paragraphCacheKey($book, $para, $channelUid, $format));
+            RedisCache::forget(self::paragraphCacheKey($book, $para, $channelUid, $format));
         }
     }
 

+ 4 - 4
api-v13/app/Services/SummaryService.php

@@ -2,7 +2,7 @@
 
 namespace App\Services;
 
-use Illuminate\Support\Facades\Cache;
+use App\Facades\RedisCache;
 use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Log;
 
@@ -57,10 +57,10 @@ class SummaryService
         $cacheKey = 'summary_'.md5($text);
 
         // 2️⃣ 检查缓存命中
-        if (! $forceRefresh && Cache::has($cacheKey)) {
+        if (! $forceRefresh && RedisCache::has($cacheKey)) {
             Log::debug('SummaryService cache hit', ['key' => $cacheKey]);
 
-            return Cache::get($cacheKey);
+            return RedisCache::get($cacheKey);
         }
 
         Log::debug('SummaryService generating new summary', [
@@ -94,7 +94,7 @@ class SummaryService
         }
 
         // 4️⃣ 写入缓存(默认缓存 1 周)
-        Cache::put($cacheKey, $finalSummary, now()->addWeek());
+        RedisCache::put($cacheKey, $finalSummary, now()->addWeek());
 
         Log::debug('SummaryService cached new summary', [
             'key' => $cacheKey,

+ 18 - 9
api-v13/app/Services/Template/TemplateService.php

@@ -2,12 +2,14 @@
 
 namespace App\Services\Template;
 
+use App\Facades\RedisCache;
 use App\Services\Template\Renderers\RendererFactory;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Log;
 
 class TemplateService
 {
+    private const CACHE_VERSION_KEY = 'template/version';
+
     private TemplateParser $parser;
 
     private bool $cacheEnabled;
@@ -30,8 +32,8 @@ class TemplateService
             // 生成缓存键
             $cacheKey = $this->generateCacheKey($content, $format);
 
-            if ($this->cacheEnabled && Cache::has($cacheKey)) {
-                return Cache::get($cacheKey);
+            if ($this->cacheEnabled && RedisCache::has($cacheKey)) {
+                return RedisCache::get($cacheKey);
             }
 
             // 解析内容
@@ -48,7 +50,7 @@ class TemplateService
 
             // 缓存结果
             if ($this->cacheEnabled) {
-                Cache::put($cacheKey, $result, $this->cacheTtl);
+                RedisCache::put($cacheKey, $result, $this->cacheTtl);
             }
 
             return $result;
@@ -137,20 +139,27 @@ class TemplateService
     }
 
     /**
-     * 生成缓存键
+     * 生成缓存键。
+     *
+     * 键里带版本号:模板定义变了就把版本号加一,旧版本的缓存不再被读到、到期自然消失。
      */
     private function generateCacheKey(string $content, string $format): string
     {
-        return 'template_'.$format.'_'.md5($content);
+        $version = RedisCache::get(self::CACHE_VERSION_KEY, 0);
+
+        return "template/v{$version}/{$format}/".md5($content);
     }
 
     /**
-     * 清除模板相关缓存
+     * 让所有模板渲染缓存失效。
+     *
+     * 缓存键是内容的 md5,按模板名查不出受影响的键,所以整体换版本号。
+     * 原来用的是 Cache::flush(),在 Redis Cluster 上会清空所有 master,
+     * 连队列和其他业务缓存一起清掉。
      */
     private function clearTemplateCache(string $templateName): void
     {
-        // 这里可以实现更精确的缓存清除逻辑
-        Cache::flush(); // 简单起见,清除所有缓存
+        RedisCache::increment(self::CACHE_VERSION_KEY);
     }
 
     /**

+ 6 - 6
api-v13/app/Services/V3/SignUpService.php

@@ -3,11 +3,11 @@
 namespace App\Services\V3;
 
 use App\Exceptions\BusinessException;
+use App\Facades\RedisCache;
 use App\Mail\V3\EmailCertificationMail;
 use App\Models\Channel;
 use App\Models\Invite;
 use App\Models\UserInfo;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Mail;
@@ -63,7 +63,7 @@ class SignUpService
     public function sendCode(string $email, ?string $lang): void
     {
         $code = (string) random_int(100000, 999999);
-        Cache::put($this->codeKey($email), [
+        RedisCache::put($this->codeKey($email), [
             'digest' => hash('sha256', $code),
             'attempts' => 0,
         ], now()->addMinutes(self::CODE_TTL_MINUTES));
@@ -89,20 +89,20 @@ class SignUpService
     public function verifyCode(string $email, string $code): Invite
     {
         $key = $this->codeKey($email);
-        $entry = Cache::get($key);
+        $entry = RedisCache::get($key);
 
         if (! is_array($entry) || ! hash_equals($entry['digest'], hash('sha256', $code))) {
             if (is_array($entry)) {
                 $attempts = $entry['attempts'] + 1;
                 $attempts >= self::CODE_MAX_ATTEMPTS
-                    ? Cache::forget($key)
-                    : Cache::put($key, [...$entry, 'attempts' => $attempts], now()->addMinutes(self::CODE_TTL_MINUTES));
+                    ? RedisCache::forget($key)
+                    : RedisCache::put($key, [...$entry, 'attempts' => $attempts], now()->addMinutes(self::CODE_TTL_MINUTES));
             }
 
             throw ValidationException::withMessages(['code' => __('messages.sign_up_code_invalid')]);
         }
 
-        Cache::forget($key);
+        RedisCache::forget($key);
 
         $invite = Invite::whereRaw('lower(email) = ?', [mb_strtolower($email)])->first()
             ?? (new Invite)->forceFill([

+ 3 - 3
api-v13/app/Tools/ExportDownload.php

@@ -2,8 +2,8 @@
 
 namespace App\Tools;
 
+use App\Facades\RedisCache;
 use Illuminate\Support\Facades\App;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Str;
@@ -70,7 +70,7 @@ class ExportDownload
         if ($this->downloadUrl) {
             $data['url'] = $this->downloadUrl;
         }
-        Cache::put(
+        RedisCache::put(
             $this->currStatusKey,
             $data,
             $this->statusExpiry
@@ -82,7 +82,7 @@ class ExportDownload
 
     public function getStatus()
     {
-        return Cache::get($this->currStatusKey);
+        return RedisCache::get($this->currStatusKey);
     }
 
     public function upload(string $type, $sections, $bookMeta)

+ 17 - 15
api-v13/app/Tools/TurboSplit.php

@@ -4,9 +4,9 @@ namespace App\Tools;
 
 require_once __DIR__.'/../../public/app/public/casesuf.inc';
 
+use App\Facades\RedisCache;
 use App\Models\UserDict;
 use App\Models\WordPart;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Log;
 
 class TurboSplit
@@ -196,7 +196,7 @@ class TurboSplit
         $search = $word;
 
         // 获取单词权重
-        $weight = Cache::remember(
+        $weight = RedisCache::remember(
             'palicanon/wordpart/weight/'.$search,
             config('mint.cache.expire'),
             function () use ($search) {
@@ -229,7 +229,7 @@ class TurboSplit
             $base_weight = 0;
             $len = 0;
             foreach ($newWord as $x => $x_len) {
-                $weight = Cache::remember(
+                $weight = RedisCache::remember(
                     'palicanon/wordpart/weight/'.$x,
                     config('mint.cache.expire'),
                     function () use ($x) {
@@ -260,7 +260,7 @@ class TurboSplit
 
         $isFound = false;
         $count = 0;
-        $wordPart = Cache::remember(
+        $wordPart = RedisCache::remember(
             "turbosplit/part/{$word}",
             config('mint.cache.expire'),
             function () use ($word) {
@@ -284,18 +284,20 @@ class TurboSplit
 
         // fomular of confidence value 信心值计算公式
         if ($isFound) {
-            $cf = Cache::remember(
-                'turbosplit/confidence/'.$word,
-                config('mint.cache.expire'),
-                function () use ($word, $count, $case_len) {
-                    $len = mb_strlen($word, 'UTF-8') - $case_len;
-                    $len_correct = 1.2;
-                    $count2 = 1.1 + pow($count, 1.18);
-                    $conf_num = pow(1 / $count2, pow(($len - 0.5), $len_correct));
-
-                    return round(1 / (1 + 640 * $conf_num), 9);
+            // 词本身就是语尾时 $len = 0,pow(-0.5, 1.2) 得 NAN;JSON 存不了 NAN,这种值不进缓存
+            $cacheKey = 'turbosplit/confidence/'.$word;
+            $cf = RedisCache::get($cacheKey);
+            if ($cf === null) {
+                $len = mb_strlen($word, 'UTF-8') - $case_len;
+                $len_correct = 1.2;
+                $count2 = 1.1 + pow($count, 1.18);
+                $conf_num = pow(1 / $count2, pow(($len - 0.5), $len_correct));
+                $cf = round(1 / (1 + 640 * $conf_num), 9);
+
+                if (is_finite($cf)) {
+                    RedisCache::put($cacheKey, $cf, config('mint.cache.expire'));
                 }
-            );
+            }
 
             return $cf;
         } else {

+ 3 - 3
api-v13/tests/Feature/ReadingAnnotationsTest.php

@@ -1,9 +1,9 @@
 <?php
 
+use App\Facades\RedisCache;
 use App\Models\Discussion;
 use App\Services\PaliContentService;
 use Illuminate\Foundation\Testing\RefreshDatabase;
-use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Str;
 
 uses(RefreshDatabase::class);
@@ -89,9 +89,9 @@ it('drops the paragraph cache when either kind of annotation changes', function
     $this->getJson($url)->assertOk();
 
     $key = PaliContentService::paragraphCacheKey(9101, 1, $channel, 'html');
-    expect(Cache::has($key))->toBeTrue();
+    expect(RedisCache::has($key))->toBeTrue();
 
     makeAnnotation($sentence->uid, 'note', '译者按。', 5);
-    expect(Cache::has($key))->toBeFalse();
+    expect(RedisCache::has($key))->toBeFalse();
     expect($this->getJson($url)->json('data.0.display'))->toContain('译者按。');
 });