فهرست منبع

Merge branch 'iapt-platform:laravel' into laravel

Bhikkhu China Kosalla 1 سال پیش
والد
کامیت
45b4fbb05c
100فایلهای تغییر یافته به همراه5365 افزوده شده و 797 حذف شده
  1. 1 0
      .env.example
  2. 2 0
      .gitignore
  3. 34 25
      README.md
  4. 79 0
      app/Console/Commands/CacheDictPreference.php
  5. 116 0
      app/Console/Commands/CacheWbwPreference.php
  6. 180 0
      app/Console/Commands/CopyUserBook.php
  7. 158 0
      app/Console/Commands/ExportArticle.php
  8. 38 17
      app/Console/Commands/ExportChannel.php
  9. 331 0
      app/Console/Commands/ExportChapter.php
  10. 46 18
      app/Console/Commands/ExportChapterIndex.php
  11. 69 0
      app/Console/Commands/ExportCreateDb.php
  12. 109 0
      app/Console/Commands/ExportFtsPali.php
  13. 3 0
      app/Console/Commands/ExportNissaya.php
  14. 77 15
      app/Console/Commands/ExportOffline.php
  15. 28 12
      app/Console/Commands/ExportPalitext.php
  16. 59 22
      app/Console/Commands/ExportSentence.php
  17. 33 14
      app/Console/Commands/ExportTag.php
  18. 29 13
      app/Console/Commands/ExportTagmap.php
  19. 100 0
      app/Console/Commands/ExportTerm.php
  20. 149 0
      app/Console/Commands/ExportZip.php
  21. 147 0
      app/Console/Commands/ImportArticle.php
  22. 213 0
      app/Console/Commands/ImportArticleMap.php
  23. 18 6
      app/Console/Commands/InitCs6sentence.php
  24. 6 3
      app/Console/Commands/InitDependence.php
  25. 31 2
      app/Console/Commands/InitSystemChannel.php
  26. 4 1
      app/Console/Commands/InitSystemDict.php
  27. 14 33
      app/Console/Commands/Install.php
  28. 9 5
      app/Console/Commands/InstallPaliSeries.php
  29. 18 15
      app/Console/Commands/InstallPaliText.php
  30. 13 10
      app/Console/Commands/InstallWbwTemplate.php
  31. 9 6
      app/Console/Commands/InstallWordAll.php
  32. 8 5
      app/Console/Commands/InstallWordBook.php
  33. 8 5
      app/Console/Commands/InstallWordIndex.php
  34. 8 5
      app/Console/Commands/InstallWordStatistics.php
  35. 160 0
      app/Console/Commands/MqDiscussion.php
  36. 71 0
      app/Console/Commands/MqExport.php
  37. 73 0
      app/Console/Commands/MqExportArticle.php
  38. 74 0
      app/Console/Commands/MqExportPaliChapter.php
  39. 53 0
      app/Console/Commands/MqIssues.php
  40. 163 0
      app/Console/Commands/MqPr.php
  41. 70 0
      app/Console/Commands/MqProgress.php
  42. 57 0
      app/Console/Commands/MqTask.php
  43. 63 0
      app/Console/Commands/MqWbwAnalyses.php
  44. 44 0
      app/Console/Commands/PatchWbwPageNumber.php
  45. 61 0
      app/Console/Commands/RemoveTermCache.php
  46. 3 0
      app/Console/Commands/StatisticsDict.php
  47. 3 0
      app/Console/Commands/StatisticsExp.php
  48. 3 0
      app/Console/Commands/StatisticsNissaya.php
  49. 3 0
      app/Console/Commands/StatisticsWbw.php
  50. 11 4
      app/Console/Commands/TestCaseMan.php
  51. 3 0
      app/Console/Commands/TestJsonToXml.php
  52. 83 0
      app/Console/Commands/TestMarkdownToTpl.php
  53. 103 47
      app/Console/Commands/TestMdRender.php
  54. 21 21
      app/Console/Commands/TestMq.php
  55. 85 14
      app/Console/Commands/TestMqWorker.php
  56. 60 3
      app/Console/Commands/TestRedis.php
  57. 48 0
      app/Console/Commands/TestSchedule.php
  58. 77 0
      app/Console/Commands/TestSearchPali.php
  59. 54 0
      app/Console/Commands/TestSendMail.php
  60. 102 0
      app/Console/Commands/TestTex.php
  61. 62 0
      app/Console/Commands/UpdateRelationTo.php
  62. 89 0
      app/Console/Commands/UpdateSentenceUnique.php
  63. 58 0
      app/Console/Commands/UpdateSentenceVer.php
  64. 3 0
      app/Console/Commands/UpgradeAt20230227.php
  65. 8 5
      app/Console/Commands/UpgradeChapterDynamic.php
  66. 20 7
      app/Console/Commands/UpgradeChapterDynamicWeekly.php
  67. 20 12
      app/Console/Commands/UpgradeCommunityTerm.php
  68. 118 69
      app/Console/Commands/UpgradeCompound.php
  69. 27 14
      app/Console/Commands/UpgradeDaily.php
  70. 15 5
      app/Console/Commands/UpgradeDict.php
  71. 6 2
      app/Console/Commands/UpgradeDictDefaultMeaning.php
  72. 62 0
      app/Console/Commands/UpgradeDictId.php
  73. 156 0
      app/Console/Commands/UpgradeDictSysRegular.php
  74. 3 0
      app/Console/Commands/UpgradeDictSysWbwExtract.php
  75. 4 1
      app/Console/Commands/UpgradeDictVocabulary.php
  76. 71 24
      app/Console/Commands/UpgradeFts.php
  77. 141 0
      app/Console/Commands/UpgradeGrammarBook.php
  78. 75 0
      app/Console/Commands/UpgradePageNumber.php
  79. 29 8
      app/Console/Commands/UpgradePaliText.php
  80. 4 1
      app/Console/Commands/UpgradePaliTextId.php
  81. 7 4
      app/Console/Commands/UpgradePaliTextTag.php
  82. 6 3
      app/Console/Commands/UpgradePaliToc.php
  83. 18 3
      app/Console/Commands/UpgradePcdBookId.php
  84. 45 29
      app/Console/Commands/UpgradeProgress.php
  85. 62 31
      app/Console/Commands/UpgradeProgressChapter.php
  86. 141 0
      app/Console/Commands/UpgradeQuote.php
  87. 0 226
      app/Console/Commands/UpgradeRegular.php
  88. 5 2
      app/Console/Commands/UpgradeRelatedParagraph.php
  89. 59 0
      app/Console/Commands/UpgradeSimIndex.php
  90. 3 0
      app/Console/Commands/UpgradeTestData.php
  91. 3 0
      app/Console/Commands/UpgradeVocabulary.php
  92. 20 6
      app/Console/Commands/UpgradeWbwAnalyses.php
  93. 122 0
      app/Console/Commands/UpgradeWbwParaNum.php
  94. 6 3
      app/Console/Commands/UpgradeWbwTemplate.php
  95. 133 0
      app/Console/Commands/UpgradeWbwWeight.php
  96. 20 3
      app/Console/Commands/UpgradeWeekly.php
  97. 25 2
      app/Console/Commands/UpgradeWordPart.php
  98. 3 0
      app/Console/Commands/UuidViranyani.php
  99. 7 5
      app/Console/Commands/WebHook.php
  100. 14 11
      app/Console/Commands/WebHookArticleNew.php

+ 1 - 0
.env.example

@@ -8,6 +8,7 @@ APP_KEY=
 APP_DEBUG=true
 APP_URL=http://localhost
 API_URL=http://localhost/api
+DASHBOARD_URL=http://localhost/ds
 
 LOG_CHANNEL=stack
 LOG_DEPRECATIONS_CHANNEL=null

+ 2 - 0
.gitignore

@@ -14,8 +14,10 @@ npm-debug.log
 yarn-error.log
 /.idea
 /.vscode
+/.VSCodeCounter
 /package-lock.json
 /yarn.lock
 /composer.lock
 *.swp
 *.log
+/.stop

+ 34 - 25
README.md

@@ -28,15 +28,13 @@
 - v1 旧版数据迁移任务脚本
 - deploy 运维代码 
 
-## 安装
+## 开发环境
 
 >请注意。此安装方法**只针对开发人员**。生产线请参考[deploy/README.md](deploy/README.md)
 
-### 开发环境
+使用 **Linux** 的开发者请参阅 [<项目文件夹>/docker/readme.md](docker/readme.md) 容器中包含了全部开发环境。请忽略下面关于开发环境的安装。
 
-使用 Linux 的开发者请参阅 [<项目文件夹>/docker/readme.md](docker/readme.md) 容器中包含了全部开发环境。请忽略下面关于开发环境的安装。
-
-#### PostgreSQL
+### PostgreSQL
 
 最小版本v12。下载链接
 https://www.postgresql.org/download/
@@ -54,7 +52,7 @@ create database iapt
 
 `\q` 退出psql
 
-#### PHP 8
+### PHP 8
 编辑 php.ini 文件,打开pgsql和sqlite的PDO扩展,以及fileinfo
 ```
 extension=pdo_pgsql
@@ -62,26 +60,31 @@ extension=pdo_sqlite
 extension=fileinfo
 ```
 
-#### Redis
+### Redis
 最新版的Redis不支持Windows平台,可以安装第三方修改的[Windows版Redis5.0](https://github.com/tporadowski/redis)
 
-#### composer
+### composer
+
+### npm
+
+### Rabbitmq
 
-#### npm
+[Downloading and Installing RabbitMQ](https://www.rabbitmq.com/download.html)
 
 
-### Fork
+
+## Fork
 
 Fork https://github.com/iapt-platform/mint 到你自己的仓库
 
-### Clone
+## Clone
 
 ```
 git clone https://github.com/<your>/mint.git
 
 ```
 
-### 安装依赖
+## 安装依赖
 
 项目根目录下运行
 
@@ -98,9 +101,9 @@ npm install
 ```
 
 
-### 修改配置文件
+## 修改配置文件
 
-#### .env
+### .env
 
 复制 `<项目目录>/.env.example` 的一个副本。改文件名为 `.env`
 
@@ -126,7 +129,7 @@ ASSETS_SERVER :网站资源文件,非用户的图片,音频,视频
 - 或直接引用离您最近的assets server
 
 
-#### public/app/config.php
+### public/app/config.php
 
 复制 `<项目目录>/public/app/config.example.php` 改文件名为`config.php`
 
@@ -212,7 +215,7 @@ var HELP_SERVER = "https://help-hk.wikipali.org";
 var GRAMMAR_SERVER = "https://grammar-hk.wikipali.org";
 ```
 
-### 复制巴利语全文搜索单词表
+## 复制巴利语全文搜索单词表
 
 获取pg share dir
 在命令行窗口运行
@@ -237,14 +240,14 @@ sudo cp ./public/app/fts/pali.syn /usr/share/postgresql/14/tsearch_data/
 到你的 shardir/tsearch_data 目录下
 
 
-### application encryption key
+## application encryption key
 
 在<工程目录>下运行
 ```dash
 php artisan key:generate
 ```
 
-### 数据库迁移
+## 数据库迁移
 
 在根目录下运行
 
@@ -252,23 +255,23 @@ php artisan key:generate
 php artisan migrate
 ```
 
-### Redis数据库填充
+## Redis数据库填充
 
 在命令行运行<项目目录>下面的命令
 
 **Liunx**
 ```dash
 cd ./v1/scripts
-sh ./redis_upgrade.sh
+sh ./upgrade_redis.sh
 ```
 
 **Window**
 ```dash
 cd ./v1/scripts
-./redis_upgrade.bat
+./upgrade_redis.bat
 ```
 
-### 语料数据库填充
+## 语料数据库填充
 
 **Liunx**
 ```dash
@@ -283,12 +286,18 @@ cd ./v1/scripts
 ```
 运行时间较长。本地开发环境大约4小时。
 
+如果不想等待,可以导入其他人已经部署好的postgresql数据库
 
-运行时间较长。本地开发环境大约4小时。
+## 启动消息队列works
 
-如果不想等待,可以导入其他人已经部署好的postgresql数据库
+```dash
+php artisan mq:discussion
+php artisan mq:pr
+php artisan mq:progress
+php artisan mq:wbw.analyses
+```
 
-### 运行dev server
+## 运行dev server
 
 ```dash
 php artisan serve

+ 79 - 0
app/Console/Commands/CacheDictPreference.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Cache;
+use App\Models\UserDict;
+use Illuminate\Support\Facades\DB;
+use App\Tools\RedisClusters;
+
+class CacheDictPreference extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'cache:dict.preference';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '从第三方字典中提取首选项';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $prefix = 'dict-preference';
+        $words = UserDict::select(['word','language'])
+                       ->groupBy(['word','language'])
+                       ->cursor();
+        $wordCount = DB::select('SELECT count(*) from (
+                     SELECT word,language from user_dicts group by word,language) T');
+        $bar = $this->output->createProgressBar($wordCount[0]->count);
+        $count = 0;
+        foreach ($words as $key => $word) {
+            $meaning = UserDict::where('word',$word->word)
+                ->where('language',$word->language)
+                ->where('source','_PAPER_RICH_')
+                ->whereNotNull('mean')
+                ->value('mean');
+            $meaning = trim($meaning," $");
+            if(!empty($meaning)){
+                $m = explode('$',$meaning);
+                RedisClusters::put("{$prefix}/{$word->word}/{$word->language}",$m[0]);
+            }
+            $bar->advance();
+            $count++;
+            if($count%1000 === 0){
+                if(\App\Tools\Tools::isStop()){
+                    return 0;
+                }
+            }
+        }
+        $bar->finish();
+
+        return 0;
+    }
+}

+ 116 - 0
app/Console/Commands/CacheWbwPreference.php

@@ -0,0 +1,116 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Cache;
+use App\Models\WbwAnalysis;
+use Illuminate\Support\Facades\DB;
+use App\Tools\RedisClusters;
+
+class CacheWbwPreference extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'cache:wbw.preference {--editor=} {--view=all}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '逐词解析的首选项预热';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $prefix = 'wbw-preference';
+        if($this->option('view')==='all' ||
+           $this->option('view')==='my'){
+            $this->info('个人数据');
+            /**
+             * 个人数据算法
+             * 最新优先
+             */
+            $wbw = WbwAnalysis::select(['wbw_word','type','editor_id']);
+            $wbwCount = DB::select('SELECT count(*) from (
+                SELECT wbw_word,type,editor_id from wbw_analyses group by wbw_word,type,editor_id) T');
+            if($this->option('editor')){
+                $wbw = $wbw->where('editor_id',$this->option('editor'));
+                $wbwCount = DB::select('SELECT count(*) from (
+                    SELECT wbw_word,type,editor_id from wbw_analyses where editor_id=? group by wbw_word,type,editor_id) T',
+                    [$this->option('editor')]);
+            }
+            $wbw = $wbw->groupBy(['wbw_word','type','editor_id'])->cursor();
+            $bar = $this->output->createProgressBar($wbwCount[0]->count);
+            $count = 0;
+            foreach ($wbw as $key => $value) {
+                $data = WbwAnalysis::where('wbw_word',$value->wbw_word)
+                                    ->where('type',$value->type)
+                                    ->where('editor_id',$value->editor_id)
+                                    ->orderBy('updated_at','desc')
+                                    ->value('data');
+                RedisClusters::put("{$prefix}/{$value->wbw_word}/{$value->type}/{$value->editor_id}",$data);
+                $bar->advance();
+                $count++;
+                if($count%1000 === 0){
+                    if(\App\Tools\Tools::isStop()){
+                        return 0;
+                    }
+                }
+            }
+            $bar->finish();
+        }
+
+        if($this->option('view')==='all' ||
+           $this->option('view')==='community'
+           ){
+            $this->info('社区通用');
+            /**
+             * 社区数据算法
+             * 多的优先
+             */
+            $wbw = WbwAnalysis::select(['wbw_word','type']);
+            $count = DB::select('SELECT count(*) from (
+                SELECT wbw_word,type from wbw_analyses group by wbw_word,type) T');
+            $wbw = $wbw->groupBy(['wbw_word','type'])->cursor();
+
+            $bar = $this->output->createProgressBar($count[0]->count);
+            foreach ($wbw as $key => $value) {
+                $data = WbwAnalysis::where('wbw_word',$value->wbw_word)
+                                    ->where('type',$value->type)
+                                    ->selectRaw('data,count(*)')
+                                    ->groupBy("data")
+                                    ->orderBy("count", "desc")
+                                    ->first();
+
+                Cache::put("{$prefix}/{$value->wbw_word}/{$value->type}/0",$data->data);
+                $bar->advance();
+            }
+            $bar->finish();
+        }
+
+        return 0;
+    }
+}

+ 180 - 0
app/Console/Commands/CopyUserBook.php

@@ -0,0 +1,180 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\CustomBookSentence;
+use App\Models\CustomBook;
+
+use App\Models\Channel;
+use App\Models\Sentence;
+use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Log;
+
+class CopyUserBook extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan copy:user.book
+     * @var string
+     */
+    protected $signature = 'copy:user.book {--lang} {--book=} {--test}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '复制用户自定书到sentence表';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        //获取全部语言列表
+        $lang = CustomBookSentence::select('lang')->groupBy('lang')->get();
+        foreach ($lang as $key => $value) {
+            $this->info('language:'.$value->lang);
+        }
+        if($this->option('lang')){
+            return 0;
+        }
+
+        if($this->option('test')){
+            $this->info('run in test mode');
+        }
+
+        $this->info('给CustomBook 添加channel');
+        $newChannel = 0;
+
+        foreach (CustomBook::get() as $key => $customBook) {
+            $this->info('doing book='.$customBook->book_id);
+            if(empty($customBook->channel_id)){
+                $bookLang = $customBook->lang;
+                if(empty($bookLang) || $bookLang === 'false' || $bookLang === 'null'  || $bookLang === 'none'){
+                    $this->info('language can not be empty change to pa, book='.$customBook->book_id);
+                    Log::warning('copy:user.book language can not be empty ,change to pa, book='.$customBook->book_id);
+                    $bookLang = 'pa';
+                }
+                $customBook->lang = $bookLang;
+                $channelName = '_user_book_'.$bookLang;
+                $channel = Channel::where('owner_uid',$customBook->owner)
+                                ->where('name',$channelName)->first();
+                if($channel === null){
+                    $this->info('create new channel');
+                    $channelUuid = Str::uuid();
+                    $channel = new Channel;
+                    $channel->id = app('snowflake')->id();
+                    $channel->uid = $channelUuid;
+                    $channel->owner_uid = $customBook->owner;
+                    $channel->name = $channelName;
+                    $channel->type = 'original';
+                    $channel->lang = $bookLang;
+                    $channel->editor_id = 0;
+                    $channel->is_system = true;
+                    $channel->create_time = time()*1000;
+                    $channel->modify_time = time()*1000;
+                    $channel->status = $customBook->status;
+                    if(!$this->option('test')){
+                        $saveOk = $channel->save();
+                        if($saveOk){
+                            $newChannel++;
+                            Log::debug('copy user book : create channel success name='.$channelName);
+                        }else{
+                            Log::error('copy user book : create channel fail.',['channel'=>$channelName,'book'=>$customBook->book_id]);
+                            $this->error('copy user book : create channel fail.  name='.$channelName);
+                            continue;
+                        }
+                    }
+                }
+                if(!Str::isUuid($channel->uid)){
+                    Log::error('copy user book : channel id error.',['channel'=>$channelName,'book'=>$customBook->book_id]);
+                    $this->error('copy user book : channel id error.  name='.$channelName);
+                    continue;
+                }
+                $customBook->channel_id = $channel->uid;
+                if(!$this->option('test')){
+                    $ok = $customBook->save();
+                    if(!$ok){
+                        Log::error('copy user book : create channel fail.',['book'=>$customBook->book_id]);
+                        continue;
+                    }
+                }
+            }
+        }
+        $this->info('给CustomBook 添加channel 结束');
+
+        $userBooks = CustomBook::get();
+        $this->info('book '. count($userBooks));
+        $copySent = 0;
+        foreach ($userBooks as $key => $book) {
+
+            $queryBook = $this->option('book');
+            if(!empty($queryBook)){
+                if($book->book_id != $queryBook){
+                    continue;
+                }
+            }
+            if(empty($book->channel_id)){
+                $this->error('book channel is empty');
+                continue;
+            }
+            $this->info('doing book '. $book->book_id);
+
+            $bookSentence = CustomBookSentence::where('book',$book->book_id)->cursor();
+            foreach ($bookSentence as $key => $sentence) {
+                $newRow = Sentence::firstOrNew(
+                    [
+                        "book_id" => $sentence->book,
+                        "paragraph" => $sentence->paragraph,
+                        "word_start" => $sentence->word_start,
+                        "word_end" => $sentence->word_end,
+                        "channel_uid" => $book->channel_id,
+                    ],
+                    [
+                        'id' => app('snowflake')->id(),
+                        'uid' => Str::uuid(),
+                        'create_time' => $sentence->create_time,
+                        'modify_time' => $sentence->modify_time,
+                    ]
+                    );
+                $newRow->editor_uid = $sentence->owner;
+                $newRow->content = $sentence->content;
+                $newRow->strlen = mb_strlen($sentence->content,"UTF-8");
+                $newRow->status = $sentence->status;
+                $newRow->content_type = $sentence->content_type;
+                $newRow->language = $book->lang;
+                if(empty($newRow->channel_uid)){
+                    $this->error('channel uuid is null book='.$sentence->book .' para='.$sentence->paragraph);
+                    Log::error('channel uuid is null ',['sentence'=>$sentence->book]);
+                }else{
+                    if(!$this->option('test')){
+                        $ok = $newRow->save();
+                        if(!$ok){
+                            Log::error('copy fail ',['sentence'=>$sentence->id]);
+                        }
+                        $copySent++;
+                    }
+                }
+            }
+            $this->info("book {$book->book} finished");
+        }
+        $this->info('all done ');
+        $this->info('channel create '.$newChannel);
+        $this->info('sentence copy '.$copySent);
+        return 0;
+    }
+}

+ 158 - 0
app/Console/Commands/ExportArticle.php

@@ -0,0 +1,158 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Storage;
+
+use App\Tools\RedisClusters;
+use App\Tools\ExportDownload;
+use App\Http\Api\MdRender;
+
+
+class ExportArticle extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan export:article 78c22ad3-58e2-4cf0-b979-67783ca3a375 123 --channel=7fea264d-7a26-40f8-bef7-bc95102760fb --format=html
+     * php artisan export:article 4732bcae-fb9d-4db4-b6b7-e8d0aa882f30 1234 --channel=7fea264d-7a26-40f8-bef7-bc95102760fb --anthology=eb9e3f7f-b942-4ca4-bd6f-b7876b59a523 --format=html --token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJuYmYiOjE2OTc3Mjg2ODUsImV4cCI6MTcyOTI2NDY4NSwidWlkIjoiYmE1NDYzZjMtNzJkMS00NDEwLTg1OGUtZWFkZDEwODg0NzEzIiwiaWQiOjR9.fiXhnY2LczZ9kKVHV0FfD3AJPZt-uqM5wrDe4EhToVexdd007ebPFYssZefmchfL0mx9nF0rgHSqjNhx4P0yDA
+     * @var string
+     */
+    protected $signature = 'export:article {id} {query_id} {--token=} {--anthology=} {--channel=}  {--origin=false} {--translation=true} {--format=tex} {--debug}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $this->info('task export chapter start');
+        Log::debug('task export chapter start');
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $upload = new ExportDownload([
+            'queryId'=>$this->argument('query_id'),
+            'format'=>$this->option('format'),
+            'debug'=>$this->option('debug'),
+            'filename'=>'article',
+        ]);
+
+        MdRender::init();
+        $m = new \Mustache_Engine(array('entity_flags'=>ENT_QUOTES,
+                                        'delimiters' => '[[ ]]',
+                                        'escape'=>function ($value){
+                                            return $value;
+                                        }));
+
+
+        $sections = array();
+        $articles = array();
+
+
+        $article = $this->fetch($this->argument('id'));
+        if(!$article){
+            return 1;
+        }
+
+        $bookMeta = array();
+        $bookMeta['book_author'] = "";
+        $bookMeta['book_title'] = $article['title_text'];
+
+        $articles[] = [
+            'level'=>1,
+            'title'=>$article['title_text'],
+            'content'=>isset($article['html'])?$article['html']:'',
+        ];
+        $progress = 0.1;
+        $this->info($upload->setStatus($progress,'export article content title='.$article['title_text']));
+
+        if(isset($article['toc']) && count($article['toc'])>0){
+            $this->info('has sub article '. count($article['toc']));
+            $step = 0.8 / count($article['toc']);
+            $baseLevel = 0;
+            foreach ($article['toc'] as $key => $value) {
+                if($baseLevel === 0){
+                    $baseLevel = $value['level'] - 2;
+                }
+                $progress += $step;
+                $this->info($upload->setStatus($progress,'exporting article title='.$value['title']));
+                $article = $this->fetch($value['key']);
+                if(!$article){
+                    $this->info($upload->setStatus($progress,'exporting article fail title='.$value['title']));
+                    continue;
+                }
+                $this->info($upload->setStatus($progress,'exporting article success title='.$article['title_text']));
+                $articles[] = [
+                    'level'=>$value['level']-$baseLevel,
+                    'title'=>$article['title_text'],
+                    'content'=>isset($article['html'])?$article['html']:'',
+                ];
+            }
+        }
+
+        $sections[] = [
+            'name'=>'articles',
+            'body'=>['articles'=>$articles],
+        ];
+        $this->info($upload->setStatus(0.9,'export article content done'));
+        Log::debug('导出结束');
+
+
+        $upload->upload('article',$sections,$bookMeta);
+        $this->info($upload->setStatus(1,'export article done'));
+        return 0;
+    }
+
+    private function fetch($articleId){
+        $api = config('mint.server.api.bamboo');
+        $basicUrl = $api . '/v2/article/';
+        $url =  $basicUrl . $articleId;;
+        $this->info('http request url='.$url);
+        $urlParam = [
+                'mode' => 'read',
+                'format' => 'html',
+                'anthology'=> $this->option('anthology'),
+                'channel' => $this->option('channel'),
+        ];
+
+        if($this->option('token')){
+            $response = Http::withToken($this->option('token'))->get($url,$urlParam);
+        }else{
+            $response = Http::get($url,$urlParam);
+        }
+
+        if($response->failed()){
+            $this->error('http request error'.$response->json('message'));
+            Log::error('http request error'.$response->json('message'));
+            return false;
+        }
+        if(!$response->json('ok')){
+            $this->error('http request error'.$response->json('message'));
+            return false;
+        }
+        $article = $response->json('data');
+        return $article;
+    }
+}

+ 38 - 17
app/Console/Commands/ExportChannel.php

@@ -7,6 +7,7 @@ namespace App\Console\Commands;
 use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Storage;
 use App\Models\Channel;
+use Illuminate\Support\Facades\Log;
 
 class ExportChannel extends Command
 {
@@ -15,7 +16,7 @@ class ExportChannel extends Command
      *
      * @var string
      */
-    protected $signature = 'export:channel';
+    protected $signature = 'export:channel {db}';
 
     /**
      * The console command description.
@@ -41,26 +42,46 @@ class ExportChannel extends Command
      */
     public function handle()
     {
-        $filename = "public/export/offline/channel.csv";
-        Storage::disk('local')->put($filename, "");
-        $file = fopen(storage_path("app/{$filename}"),"w");
-        fputcsv($file,['id','name','type','language','summary','owner_id','setting','created_at']);
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        Log::debug('task export offline channel-table start');
+        $exportFile = storage_path('app/public/export/offline/'.$this->argument('db').'-'.date("Y-m-d").'.db3');
+        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
+        $dbh->beginTransaction();
+
+        $query = "INSERT INTO channel ( id , name , type , language ,
+                                    summary , owner_id , setting,created_at )
+                                    VALUES ( ? , ? , ? , ? , ? , ? , ? , ?  )";
+        try{
+            $stmt = $dbh->prepare($query);
+        }catch(PDOException $e){
+            Log::info($e);
+            return 1;
+        }
+
         $bar = $this->output->createProgressBar(Channel::where('status',30)->count());
-        foreach (Channel::where('status',30)->select(['uid','name','type','lang','summary','owner_uid','setting','created_at'])->cursor() as $chapter) {
-            fputcsv($file,[
-                            $chapter->uid,
-                            $chapter->name,
-                            $chapter->type,
-                            $chapter->lang,
-                            $chapter->summary,
-                            $chapter->owner_uid,
-                            $chapter->setting,
-                            $chapter->created_at,
-                            ]);
+        foreach (Channel::where('status',30)
+                ->select(['uid','name','type','lang',
+                          'summary','owner_uid','setting','created_at'])
+                          ->cursor() as $row) {
+                $currData = array(
+                            $row->uid,
+                            $row->name,
+                            $row->type,
+                            $row->lang,
+                            $row->summary,
+                            $row->owner_uid,
+                            $row->setting,
+                            $row->created_at,
+                            );
+            $stmt->execute($currData);
             $bar->advance();
         }
-        fclose($file);
+        $dbh->commit();
         $bar->finish();
+        Log::debug('task export offline channel-table finished');
         return 0;
     }
 }

+ 331 - 0
app/Console/Commands/ExportChapter.php

@@ -0,0 +1,331 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
+
+use App\Models\ProgressChapter;
+use App\Models\Channel;
+use App\Models\PaliText;
+use App\Models\Sentence;
+
+use App\Http\Api\ChannelApi;
+use App\Http\Api\MdRender;
+use App\Tools\Export;
+use App\Tools\RedisClusters;
+use App\Tools\ExportDownload;
+
+
+class ExportChapter extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan export:chapter 213 3 a19eaf75-c63f-4b84-8125-1bce18311e23 213-3.html --format=html --origin=true
+     * php artisan export:chapter 168 915 7fea264d-7a26-40f8-bef7-bc95102760fb 168-915.html --format=html --debug
+     * php artisan export:chapter 168 915 7fea264d-7a26-40f8-bef7-bc95102760fb 168-915.html --format=html --origin=true
+     * @var string
+     */
+    protected $signature = 'export:chapter {book} {para} {channel} {query_id} {--token=} {--origin=false} {--translation=true} {--debug} {--format=tex} ';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $this->info('task export chapter start');
+        Log::debug('task export chapter start');
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $book = $this->argument('book');
+        $para = $this->argument('para');
+
+        $upload = new ExportDownload([
+            'queryId'=>$this->argument('query_id'),
+            'format'=>$this->option('format'),
+            'debug'=>$this->option('debug'),
+            'filename'=>$book.'-'.$para,
+        ]);
+
+        $m = new \Mustache_Engine(array('entity_flags'=>ENT_QUOTES,
+                                        'delimiters' => '[[ ]]',
+                                        'escape'=>function ($value){
+                                            return $value;
+                                        }));
+        $tplFile = resource_path("mustache/chapter/".$this->option('format')."/paragraph.".$this->option('format'));
+        $tplParagraph = file_get_contents($tplFile);
+
+        MdRender::init();
+
+
+        switch ($this->option('format')) {
+            case 'md':
+                $renderFormat='markdown';
+                break;
+            case 'html':
+                $renderFormat='html';
+                break;
+            default:
+                $renderFormat=$this->option('format');
+                break;
+        }
+
+        //获取原文channel
+        $orgChannelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
+
+        $tranChannelsId = explode('_',$this->argument('channel'));
+
+        $channelsId = array_merge([$orgChannelId],$tranChannelsId);
+
+        $channels = array();
+        $channelsIndex = array();
+        foreach ($channelsId as $key => $id) {
+            $channels[] = ChannelApi::getById($id);
+            $channelsIndex[$id] = ChannelApi::getById($id);
+        }
+
+        $bookMeta = array();
+        $bookMeta['book_author'] = "";
+        foreach ($channels as $key => $channel) {
+            $bookMeta['book_author'] .= $channel['name'] . ' ';
+        }
+
+        $chapter = PaliText::where('book',$book)
+                           ->where('paragraph',$para)->first();
+        if(!$chapter){
+            return $this->error("no data");
+        }
+
+        $currProgress = 0;
+        $this->info($upload->setStatus($currProgress,'start'));
+
+
+        if(empty($chapter->toc)){
+            $bookMeta['title'] = "unknown";
+        }else{
+            $bookMeta['book_title'] = '';
+            foreach ($channelsId as $key => $id) {
+                $title = ProgressChapter::where('book',$book)->where('para',$para)
+                        ->where('channel_id',$id)
+                        ->value('title');
+                $bookMeta['book_title'] .= $title;
+            }
+            $bookMeta['sub_title'] = $chapter->toc;
+        }
+
+        $subChapter = PaliText::where('book',$book)->where('parent',$para)
+                              ->where('level','<',8)
+                              ->orderBy('paragraph')
+                              ->get();
+        if(count($subChapter) === 0){
+            //没有子章节
+            $subChapter = PaliText::where('book',$book)->where('paragraph',$para)
+                              ->where('level','<',8)
+                              ->orderBy('paragraph')
+                              ->get();
+        }
+
+        $chapterParagraph = PaliText::where('book',$book)->where('paragraph',$para)->value('chapter_len');
+        if($chapterParagraph >0 ){
+            $step = 0.9 / $chapterParagraph;
+        }else{
+            $step = 0.9;
+            Log::error('段落长度不能为0',['book'=>$book,'para'=>$para]);
+        }
+
+        $outputChannelsId = [];
+        if($this->option('origin') === 'true'){
+            $outputChannelsId[] = $orgChannelId;
+        }
+        if($this->option('translation') === 'true'){
+            $outputChannelsId = array_merge($outputChannelsId,$tranChannelsId);
+        }
+
+        $sections = array();
+        foreach ($subChapter as $key => $sub) {
+            # 看这个章节是否存在译文
+            $hasChapter = false;
+            if($this->option('origin') === 'true'){
+                $hasChapter = true;
+            }
+            if($this->option('translation') === 'true'){
+                foreach ($tranChannelsId as $id) {
+                    if(ProgressChapter::where('book',$book)->where('para',$sub->paragraph)
+                        ->where('channel_id',$id)
+                        ->exists()){
+                            $hasChapter = true;
+                    }
+                }
+            }
+            if(!$hasChapter){
+                //不存在需要导出的数据
+                continue;
+            }
+            $filename = "{$sub->paragraph}.".$this->option('format');
+            $bookMeta['sections'][] = ['filename'=>$filename];
+            $paliTitle = PaliText::where('book',$book)
+                                 ->where('paragraph',$sub->paragraph)
+                                 ->value('toc');
+            $sectionTitle = $paliTitle;
+            if($this->option('translation') === 'true'){
+                $chapter = ProgressChapter::where('book',$book)->where('para',$sub->paragraph)
+                                        ->where('channel_id',$tranChannelsId[0])
+                                        ->first();
+                if($chapter && !empty($chapter->title)){
+                    $sectionTitle = $chapter->title;
+                }
+            }
+
+
+            $content = array();
+
+            $chapterStart = $sub->paragraph+1;
+            $chapterEnd = $sub->paragraph + $sub->chapter_len;
+            $chapterBody = PaliText::where('book',$book)
+                                    ->whereBetween('paragraph',[$chapterStart,$chapterEnd])
+                                    ->orderBy('paragraph')->get();
+
+
+
+            foreach ($chapterBody as $body) {
+                $currProgress += $step;
+                $this->info($upload->setStatus($currProgress,'export chapter '.$body->paragraph));
+                $paraData = array();
+                $paraData['translations'] = array();
+                foreach ($outputChannelsId as $key => $channelId) {
+                    $translationData = Sentence::where('book_id',$book)
+                                        ->where('paragraph',$body->paragraph)
+                                        ->where('channel_uid',$channelId)
+                                        ->orderBy('word_start')->get();
+                    $sentContent = array();
+                    foreach ($translationData as $sent) {
+                        $texText = MdRender::render($sent->content,
+                                                    [$sent->channel_uid],
+                                                    null,
+                                                    'read',
+                                                    $channelsIndex[$channelId]['type'],
+                                                    $sent->content_type,
+                                                    $renderFormat
+                                                    );
+                        $sentContent[] = trim($texText);
+                    }
+                    $paraContent = implode(' ',$sentContent);
+                    if($channelsIndex[$channelId]['type'] === 'original'){
+                        $paraData['origin'] = $paraContent;
+                    }else{
+                        $paraData['translations'][] = ['content'=>$paraContent];
+                    }
+                }
+                if($body->level > 7){
+                    $content[] = $m->render($tplParagraph,$paraData);
+                }else{
+                    $currLevel = $body->level - $sub->level;
+                    if($currLevel<=0){
+                        $currLevel = 1;
+                    }
+
+                    if(count($paraData['translations'])===0){
+                        $subSessionTitle = PaliText::where('book',$book)
+                                            ->where('paragraph',$body->paragraph)
+                                            ->value('toc');
+                    }else{
+                        $subSessionTitle = $paraData['translations'][0]['content'];
+                    }
+                    switch ($this->option('format')) {
+                        case 'tex':
+                            $subStr = array_fill(0,$currLevel,'sub');
+                            $content[] = '\\'. implode('',$subStr) . "section{".$subSessionTitle.'}';
+                            break;
+                        case 'md':
+                            $subStr = array_fill(0,$currLevel,'#');
+                            $content[] = implode('',$subStr) . " ".$subSessionTitle;
+                            break;
+                        case 'html':
+                            $level = $currLevel+2;
+                            $content[] = "<h{$currLevel}>".$subSessionTitle."</h{$currLevel}>";
+                            break;
+                    }
+                }
+                $content[] = "\n\n";
+            }
+
+            $sections[] = [
+                    'name'=>$filename,
+                    'body'=>[
+                        'title'=>$sectionTitle,
+                        'content'=>implode('',$content)
+                    ]
+                ];
+        }
+
+        //导出术语表
+        $keyPali = array();
+        $keyMeaning = array();
+        if(isset($GLOBALS['glossary'])){
+            $glossary = $GLOBALS['glossary'];
+            foreach ($glossary as $word => $meaning) {
+                $keyMeaning[$meaning] = $word;
+                $keyPali[$word] = $meaning;
+            }
+        }
+
+        ksort($keyPali);
+        krsort($keyMeaning);
+        $glossaryData = [];
+        $glossaryData['pali'] = [];
+        $glossaryData['meaning'] = [];
+        foreach ($keyPali as $word => $meaning) {
+            $glossaryData['pali'][] = ['pali'=>$word,'meaning'=>$meaning];
+        }
+        foreach ($keyMeaning as $meaning => $word) {
+            $glossaryData['meaning'][] = ['pali' => $word,'meaning'=>$meaning];
+        }
+
+        Log::debug('glossary',['data' => $glossaryData]);
+
+        $tplFile = resource_path("mustache/chapter/".$this->option('format')."/glossary.".$this->option('format'));
+        $tplGlossary = file_get_contents($tplFile);
+
+        $glossaryContent = $m->render($tplGlossary,$glossaryData);
+
+        $sections[] = [
+            'name'=>'glossary.'.$this->option('format'),
+            'body'=>[
+                'title' => 'glossary',
+                'content' => $glossaryContent
+            ]
+        ];
+        $this->info($upload->setStatus($currProgress,'export glossary '. count($keyPali)));
+
+        $this->info($upload->setStatus(0.9,'export content done sections='.count($sections)));
+
+        Log::debug('导出结束',['sections'=>count($sections)]);
+
+        $upload->upload('chapter',$sections,$bookMeta);
+        $this->info($upload->setStatus(1,'export chapter done'));
+
+        return 0;
+    }
+}

+ 46 - 18
app/Console/Commands/ExportChapterIndex.php

@@ -5,6 +5,10 @@ namespace App\Console\Commands;
 use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Storage;
 use App\Models\ProgressChapter;
+use App\Models\Channel;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
+use App\Tools\RedisClusters;
 
 class ExportChapterIndex extends Command
 {
@@ -13,7 +17,7 @@ class ExportChapterIndex extends Command
      *
      * @var string
      */
-    protected $signature = 'export:chapter.index';
+    protected $signature = 'export:chapter.index {db : db file name wikipali-offline or wikipali-offline-index}';
 
     /**
      * The console command description.
@@ -39,26 +43,50 @@ class ExportChapterIndex extends Command
      */
     public function handle()
     {
-        $filename = "public/export/offline/chapter.csv";
-        Storage::disk('local')->put($filename, "");
-        $file = fopen(storage_path("app/{$filename}"),"w");
-        fputcsv($file,['id','book','paragraph','language','title','channel_id','progress','updated_at']);
-        $bar = $this->output->createProgressBar(ProgressChapter::count());
-        foreach (ProgressChapter::select(['uid','book','para','lang','title','channel_id','progress','updated_at'])->cursor() as $chapter) {
-            fputcsv($file,[
-                            $chapter->uid,
-                            $chapter->book,
-                            $chapter->para,
-                            $chapter->lang,
-                            $chapter->title,
-                            $chapter->channel_id,
-                            $chapter->progress,
-                            $chapter->updated_at,
-                            ]);
+        Log::debug('task export offline chapter-index-table start');
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+
+        $exportFile = storage_path('app/public/export/offline/'.$this->argument('db').'-'.date("Y-m-d").'.db3');
+        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
+        $dbh->beginTransaction();
+
+        $query = "INSERT INTO chapter ( id , book , paragraph,
+                                    language , title , channel_id , progress,updated_at  )
+                                    VALUES ( ? , ? , ? , ? , ? , ? , ? , ?  )";
+        try{
+            $stmt = $dbh->prepare($query);
+        }catch(PDOException $e){
+            Log::info($e);
+            return 1;
+        }
+
+        $publicChannels = Channel::where('status',30)->select('uid')->get();
+        $rows = ProgressChapter::whereIn('channel_id',$publicChannels)->count();
+        RedisClusters::put("/export/chapter/count",$rows,3600*10);
+        $bar = $this->output->createProgressBar($rows);
+        foreach (ProgressChapter::whereIn('channel_id',$publicChannels)
+                                ->select(['uid','book','para',
+                                'lang','title','channel_id',
+                                'progress','updated_at'])->cursor() as $row) {
+            $currData = array(
+                            $row->uid,
+                            $row->book,
+                            $row->para,
+                            $row->lang,
+                            $row->title,
+                            $row->channel_id,
+                            $row->progress,
+                            $row->updated_at,
+                            );
+            $stmt->execute($currData);
             $bar->advance();
         }
-        fclose($file);
+        $dbh->commit();
         $bar->finish();
+        Log::debug('task export offline chapter-index-table finished');
         return 0;
     }
 }

+ 69 - 0
app/Console/Commands/ExportCreateDb.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Log;
+
+class ExportCreateDb extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:create.db';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        Log::debug('task export offline create-db start');
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $this->create('sentence.sql','wikipali-offline');
+        $this->create('sentence.sql','wikipali-offline-index');
+
+        return 0;
+    }
+
+    private function create($sqlFile,$dbFile){
+        $sqlPath = database_path('export/'.$sqlFile);
+        $exportDir = storage_path('app/public/export/offline');
+        $exportFile = $exportDir.'/'.$dbFile.'-'.date("Y-m-d").'.db3';
+        $file = fopen($exportFile,'w');
+        fclose($file);
+        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
+        //建立数据库
+        $_sql = file_get_contents($sqlPath);
+        $_arr = explode(';', $_sql);
+        //执行sql语句
+        foreach ($_arr as $_value) {
+            $dbh->query($_value . ';');
+        }
+        Log::debug('task export offline create-db finished');
+    }
+}

+ 109 - 0
app/Console/Commands/ExportFtsPali.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Api\DictApi;
+use App\Models\UserDict;
+use Illuminate\Support\Facades\Redis;
+
+class ExportFtsPali extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:fts.pali';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '导出全文搜索用的巴利语词汇表';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        //irregular
+        $dictId = ['4d3a0d92-0adc-4052-80f5-512a2603d0e8'];
+         //regular
+        $dictId[] = DictApi::getSysDict('system_regular');
+        $long = ["ā","ī","ū"];
+        $path = storage_path('app/export/fts');
+        if(!is_dir($path)){
+            $res = mkdir($path,0700,true);
+            if(!$res){
+                Log::error('mkdir fail path='.$exportDir);
+                return 1;
+            }
+        }
+
+        $pageSize = 10000;
+        $currPage = 1;
+        $filename = "/pali-{$currPage}.syn";
+        $fp = fopen($path.$filename,'w') or die("Unable to open file!");
+        $count = 0;
+        foreach ($dictId as $key => $value) {
+            $words = UserDict::where('dict_id',$value)
+                             ->select('word')
+                             ->groupBy('word')->cursor();
+            $this->info('word count='.count($words));
+            foreach ($words as $key => $word) {
+                $count++;
+                if($count % 1000 === 0){
+                    $this->info($count);
+                }
+                if($count % 10000 === 0){
+                    fclose($fp);
+                    $redisKey = 'export/fts/pali'.$filename;
+                    $content = file_get_contents($path.$filename);
+                    Redis::set($redisKey,$content);
+                    Redis::expire($redisKey,3600*24*10);
+                    $currPage++;
+                    $filename = "/pali-{$currPage}.syn";
+                    $this->info('new file filename='.$filename);
+                    $fp = fopen($path.$filename,'w') or die("Unable to open file!");
+                }
+                $parent = UserDict::where('dict_id',$value)
+                             ->where('word',$word->word)
+                             ->selectRaw('parent,char_length("parent")')
+                             ->groupBy('parent')->orderBy('char_length','asc')->first();
+
+                if($parent && !empty($parent->parent)){
+                    $end = mb_substr($parent->parent,-1,null,"UTF-8");
+                    if(in_array($end,["ā","ī","ū"])){
+                        $head = mb_substr($parent->parent,0,mb_strlen($parent->parent)-1,"UTF-8");
+                        $newEnd = str_replace(["ā","ī","ū"],["a","i","u"],$end);
+                        $parentWord = $head.$newEnd;
+
+                    }else{
+                        $parentWord = $parent->parent;
+                    }
+                    fwrite($fp, $word->word.' '.$parentWord.PHP_EOL);
+                }else{
+                    $this->error('word no parent word='.$word->word);
+                }
+            }
+        }
+        fclose($fp);
+
+
+        return 0;
+    }
+}

+ 3 - 0
app/Console/Commands/ExportNissaya.php

@@ -42,6 +42,9 @@ class ExportNissaya extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $nissaya_channel = Channel::where('type','nissaya')->select('uid')->get();
         $channels = [];
         foreach ($nissaya_channel as $key => $value) {

+ 77 - 15
app/Console/Commands/ExportOffline.php

@@ -3,22 +3,27 @@
 namespace App\Console\Commands;
 
 use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
+use App\Tools\RedisClusters;
+use Illuminate\Support\Facades\Redis;
 
 class ExportOffline extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan export:offline lzma
      * @var string
      */
-    protected $signature = 'export:offline';
+    protected $signature = 'export:offline {format?  : zip file format 7z,lzma,gz } {--shortcut}  {--driver=morus}';
 
     /**
      * The console command description.
      *
      * @var string
      */
-    protected $description = 'Command description';
+    protected $description = 'export  offline data for app';
 
     /**
      * Create a new command instance.
@@ -37,19 +42,76 @@ class ExportOffline extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $exportDir = storage_path('app/public/export/offline');
+        if(!is_dir($exportDir)){
+            $res = mkdir($exportDir,0755,true);
+            if(!$res){
+                Log::error('mkdir fail path='.$exportDir);
+                return 1;
+            }
+        }
+
+        //删除全部的旧文件
+        foreach (scandir($exportDir) as $key => $file) {
+            if(is_file($exportDir.'/'.$file)){
+                unlink($exportDir.'/'.$file);
+            }
+        }
+        //添加 .stop
+        $exportStop = $exportDir.'/.stop';
+        $file = fopen($exportStop,'w');
+        fclose($file);
+
+        //建表
+        $this->info('create db');
+        $this->call('export:create.db');
+
+        //term
+        $this->info('export term start');
+        $this->call('export:term');
+
         //导出channel
-        $this->call('export:channel');
-        //导出channel
-        $this->call('export:tag');
-        $this->call('export:tag.map');
-        $this->call('export:pali.text');
-        //导出章节索引
-        $this->call('export:chapter.index');
-        //导出译文
-        $this->call('export:sentence');
-        //导出原文
-        $this->call('export:sentence',['--type'=>'original']);
-        shell_exec("XZ_OPT=-9 tar jcvf ".storage_path("app/public/export/offline.tar.xz")." ".storage_path("app/public/export/offline"));
+        $this->info('export channel start');
+        $this->call('export:channel',['db'=>'wikipali-offline']);
+        $this->call('export:channel',['db'=>'wikipali-offline-index']);
+
+        if(!$this->option('shortcut')){
+            //tag
+            $this->info('export tag start');
+            $this->call('export:tag',['db'=>'wikipali-offline']);
+            $this->call('export:tag.map',['db'=>'wikipali-offline']);
+            //
+            $this->info('export pali text start');
+            $this->call('export:pali.text');
+            //导出章节索引
+            $this->info('export chapter start');
+            $this->call('export:chapter.index',['db'=>'wikipali-offline']);
+            $this->call('export:chapter.index',['db'=>'wikipali-offline-index']);
+            //导出译文
+            $this->info('export sentence start');
+            $this->call('export:sentence',['--type'=>'translation','--driver'=>$this->option('driver')]);
+            $this->call('export:sentence',['--type'=>'nissaya','--driver'=>$this->option('driver')]);
+            //导出原文
+            $this->call('export:sentence',['--type'=>'original','--driver'=>$this->option('driver')]);
+        }
+
+        $this->info('zip');
+        Log::info('export offline: db写入完毕 开始压缩');
+
+        sleep(5);
+        $this->call('export:zip',[
+            'db'=>'wikipali-offline-index',
+            'format'=>$this->argument('format'),
+        ]);
+        $this->call('export:zip',[
+            'db'=>'wikipali-offline',
+            'format'=>$this->argument('format'),
+        ]);
+
+        unlink($exportStop);
         return 0;
     }
 }

+ 28 - 12
app/Console/Commands/ExportPalitext.php

@@ -5,6 +5,7 @@ namespace App\Console\Commands;
 use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Storage;
 use App\Models\PaliText;
+use Illuminate\Support\Facades\Log;
 
 class ExportPalitext extends Command
 {
@@ -39,32 +40,47 @@ class ExportPalitext extends Command
      */
     public function handle()
     {
-        $filename = "public/export/offline/pali_text.csv";
-        Storage::disk('local')->put($filename, "");
-        $file = fopen(storage_path("app/{$filename}"),"w");
-        fputcsv($file,['id','book','paragraph','level','toc','length','chapter_len','next_chapter','prev_chapter','parent','chapter_strlen']);
+        Log::debug('task export offline palitext-table start');
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $exportFile = storage_path('app/public/export/offline/wikipali-offline-'.date("Y-m-d").'.db3');
+        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
+        $dbh->beginTransaction();
+
+        $query = "INSERT INTO pali_text ( id , book , paragraph, level, toc,
+                                    chapter_len , parent   )
+                                    VALUES ( ? , ? , ? , ? , ? , ? , ? )";
+        try{
+            $stmt = $dbh->prepare($query);
+        }catch(PDOException $e){
+            Log::info($e);
+            return 1;
+        }
+
         $bar = $this->output->createProgressBar(PaliText::count());
-        foreach (PaliText::select(['uid','book','paragraph','level','toc','lenght','chapter_len','next_chapter','prev_chapter','parent','chapter_strlen'])
+        foreach (PaliText::select(['uid','book','paragraph',
+                    'level','toc','lenght','chapter_len',
+                    'next_chapter','prev_chapter','parent','chapter_strlen'])
                     ->orderBy('book')
                     ->orderBy('paragraph')
                     ->cursor() as $chapter) {
-            fputcsv($file,[
+            $currData = array(
                             $chapter->uid,
                             $chapter->book,
                             $chapter->paragraph,
                             $chapter->level,
                             $chapter->toc,
-                            $chapter->lenght,
                             $chapter->chapter_len,
-                            $chapter->next_chapter,
-                            $chapter->prev_chapter,
                             $chapter->parent,
-                            $chapter->chapter_strlen,
-                            ]);
+                            );
+            $stmt->execute($currData);
             $bar->advance();
         }
-        fclose($file);
+        $dbh->commit();
         $bar->finish();
+        Log::debug('task: export offline palitext-table finished');
 
         return 0;
     }

+ 59 - 22
app/Console/Commands/ExportSentence.php

@@ -7,6 +7,9 @@ use Illuminate\Support\Facades\Storage;
 use App\Models\Sentence;
 use App\Models\Channel;
 use App\Http\Api\ChannelApi;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
+use App\Http\Api\MdRender;
 
 class ExportSentence extends Command
 {
@@ -15,7 +18,7 @@ class ExportSentence extends Command
      *
      * @var string
      */
-    protected $signature = 'export:sentence {--channel=} {--type=translation}';
+    protected $signature = 'export:sentence {--channel=} {--type=translation} {--driver=morus}';
 
     /**
      * The console command description.
@@ -41,6 +44,11 @@ class ExportSentence extends Command
      */
     public function handle()
     {
+        Log::debug('task export offline sentence-table start');
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        \App\Tools\Markdown::driver($this->option('driver'));
         $channels = [];
         $channel_id = $this->option('channel');
         if($channel_id){
@@ -63,32 +71,61 @@ class ExportSentence extends Command
                 }
             }
         }
+
+
+        $exportFile = storage_path('app/public/export/offline/wikipali-offline-'.date("Y-m-d").'.db3');
+        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
+        $dbh->beginTransaction();
+
+        if($channel_type === "original"){
+            $table = 'sentence';
+        }else{
+            $table = 'sentence_translation';
+        }
+
+        $query = "INSERT INTO {$table} ( book , paragraph ,
+                                    word_start , word_end , content , channel_id  )
+                                    VALUES ( ? , ? , ? , ? , ? , ? )";
+        try{
+            $stmt = $dbh->prepare($query);
+        }catch(PDOException $e){
+            Log::info($e);
+            return 1;
+        }
+
         $db = Sentence::whereIn('channel_uid',$channels);
-        $file_name = "public/export/offline/sentence_{$file_suf}.csv";
-        Storage::disk('local')->put($file_name, "");
-        $file = fopen(storage_path("app/{$file_name}"),"w");
-        fputcsv($file,['id','book','paragraph','word_start','word_end','content','content_type','html','channel_id','editor_id','language','updated_at']);
         $bar = $this->output->createProgressBar($db->count());
-        foreach ($db->select(['uid','book_id','paragraph','word_start','word_end','content','content_type','channel_uid','editor_uid','language','updated_at'])->cursor() as $chapter) {
-            $content = str_replace("\n","<br />",$chapter->content);
-            fputcsv($file,[
-                            $chapter->uid,
-                            $chapter->book_id,
-                            $chapter->paragraph,
-                            $chapter->word_start,
-                            $chapter->word_end,
-                            $content,
-                            $chapter->content_type,
-                            $content,
-                            $chapter->channel_uid,
-                            $chapter->editor_uid,
-                            $chapter->language,
-                            $chapter->updated_at,
-                            ]);
+        $srcDb = $db->select(['uid','book_id','paragraph',
+                                'word_start','word_end',
+                                'content','content_type','channel_uid',
+                                'editor_uid','language','updated_at'])->cursor();
+        foreach ($srcDb as $sent) {
+            if(Str::isUuid($sent->channel_uid)){
+                $channel = ChannelApi::getById($sent->channel_uid);
+                $currData = array(
+                        $sent->book_id,
+                        $sent->paragraph,
+                        $sent->word_start,
+                        $sent->word_end,
+                        MdRender::render($sent->content,
+                                        [$sent->channel_uid],
+                                        null,
+                                        'read',
+                                        $channel['type'],
+                                        $sent->content_type,
+                                        'unity',
+                                        ),
+                        $sent->channel_uid,
+                    );
+                $stmt->execute($currData);
+
+            }
             $bar->advance();
         }
-        fclose($file);
+        $dbh->commit();
         $bar->finish();
+        Log::debug('task export sentence finished');
         return 0;
     }
 }

+ 33 - 14
app/Console/Commands/ExportTag.php

@@ -5,6 +5,7 @@ namespace App\Console\Commands;
 use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Storage;
 use App\Models\Tag;
+use Illuminate\Support\Facades\Log;
 
 class ExportTag extends Command
 {
@@ -13,7 +14,7 @@ class ExportTag extends Command
      *
      * @var string
      */
-    protected $signature = 'export:tag';
+    protected $signature = 'export:tag {db}';
 
     /**
      * The console command description.
@@ -39,23 +40,41 @@ class ExportTag extends Command
      */
     public function handle()
     {
-        $filename = "public/export/offline/tag.csv";
-        Storage::disk('local')->put($filename, "");
-        $file = fopen(storage_path("app/{$filename}"),"w");
-        fputcsv($file,['id','name','description','color','owner_id']);
+        Log::debug('task: export offline data tag-table start');
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $exportFile = storage_path('app/public/export/offline/'.$this->argument('db').'-'.date("Y-m-d").'.db3');
+        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
+        $dbh->beginTransaction();
+
+        $query = "INSERT INTO tag ( id , name ,
+                                    description , color , owner_id  )
+                                    VALUES ( ? , ? , ? , ? , ?  )";
+        try{
+            $stmt = $dbh->prepare($query);
+        }catch(PDOException $e){
+            Log::info($e);
+            return 1;
+        }
+
         $bar = $this->output->createProgressBar(Tag::count());
-        foreach (Tag::select(['id','name','description','color','owner_id'])->cursor() as $chapter) {
-            fputcsv($file,[
-                            $chapter->id,
-                            $chapter->name,
-                            $chapter->description,
-                            $chapter->color,
-                            $chapter->owner_id,
-                            ]);
+        foreach (Tag::select(['id','name','description','color','owner_id'])->cursor() as $row) {
+            $currData = array(
+                $row->id,
+                $row->name,
+                $row->description,
+                $row->color,
+                $row->owner_id,
+            );
+            $stmt->execute($currData);
             $bar->advance();
         }
-        fclose($file);
+        $dbh->commit();
         $bar->finish();
+        Log::debug('task: export offline data tag-table start');
+
         return 0;
     }
 }

+ 29 - 13
app/Console/Commands/ExportTagmap.php

@@ -5,6 +5,8 @@ namespace App\Console\Commands;
 use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Storage;
 use App\Models\TagMap;
+use Illuminate\Support\Facades\Log;
+
 class ExportTagmap extends Command
 {
     /**
@@ -12,7 +14,7 @@ class ExportTagmap extends Command
      *
      * @var string
      */
-    protected $signature = 'export:tag.map';
+    protected $signature = 'export:tag.map {db}';
 
     /**
      * The console command description.
@@ -38,22 +40,36 @@ class ExportTagmap extends Command
      */
     public function handle()
     {
-        $filename = "public/export/offline/tag_map.csv";
-        Storage::disk('local')->put($filename, "");
-        $file = fopen(storage_path("app/{$filename}"),"w");
-        fputcsv($file,['id','table_name','anchor_id','tag_id']);
+        Log::debug('task: export offline tagmap-table start');
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $exportFile = storage_path('app/public/export/offline/'.$this->argument('db').'-'.date("Y-m-d").'.db3');
+        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
+        $dbh->beginTransaction();
+
+        $query = "INSERT INTO tag_map ( anchor_id , tag_id )
+                                    VALUES ( ? , ? )";
+        try{
+            $stmt = $dbh->prepare($query);
+        }catch(PDOException $e){
+            Log::info($e);
+            return 1;
+        }
+
         $bar = $this->output->createProgressBar(TagMap::count());
-        foreach (TagMap::select(['id','table_name','anchor_id','tag_id'])->cursor() as $chapter) {
-            fputcsv($file,[
-                            $chapter->id,
-                            $chapter->table_name,
-                            $chapter->anchor_id,
-                            $chapter->tag_id,
-                            ]);
+        foreach (TagMap::select(['id','table_name','anchor_id','tag_id'])->cursor() as $row) {
+            $currData = array(
+                            $row->anchor_id,
+                            $row->tag_id,
+                            );
+            $stmt->execute($currData);
             $bar->advance();
         }
-        fclose($file);
+        $dbh->commit();
         $bar->finish();
+        Log::debug('task: export offline tagmap-table finished');
         return 0;
     }
 }

+ 100 - 0
app/Console/Commands/ExportTerm.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use App\Models\DhammaTerm;
+use Illuminate\Support\Facades\Log;
+
+class ExportTerm extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:term';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        Log::info('task export offline term-table start');
+        $startAt = time();
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $exportFile = storage_path('app/public/export/offline/wikipali-offline-'.date("Y-m-d").'.db3');
+        $dbh = new \PDO('sqlite:'.$exportFile, "", "", array(\PDO::ATTR_PERSISTENT => true));
+        $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
+        $dbh->beginTransaction();
+
+        $query = "INSERT INTO dhamma_terms ( uuid , word , word_en , meaning ,
+                                    other_meaning , note , tag , channel_id,
+                                    language, owner, editor_id,
+                                    created_at,updated_at,deleted_at)
+                                    VALUES ( ? , ? , ? , ? ,
+                                            ? , ? , ? , ? ,
+                                            ?, ?, ?,
+                                            ?, ?, ? )";
+        try{
+            $stmt = $dbh->prepare($query);
+        }catch(PDOException $e){
+            Log::info($e);
+            return 1;
+        }
+
+        $bar = $this->output->createProgressBar(DhammaTerm::count());
+        foreach (DhammaTerm::select(['guid','word','word_en','meaning',
+                          'other_meaning','note','tag','channal',
+                          'language',"owner","editor_id",
+                          "created_at","updated_at","deleted_at"
+                          ])
+                          ->cursor() as $row) {
+                $currData = array(
+                            $row->guid,
+                            $row->word,
+                            $row->word_en,
+                            $row->meaning,
+                            $row->other_meaning,
+                            $row->note,
+                            $row->tag,
+                            $row->channal,
+                            $row->language,
+                            $row->owner,
+                            $row->editor_id,
+                            $row->created_at,
+                            $row->updated_at,
+                            $row->deleted_at,
+                            );
+            $stmt->execute($currData);
+            $bar->advance();
+        }
+        $dbh->commit();
+        $bar->finish();
+        $this->info(' time='.(time()-$startAt).'s');
+        Log::info('task export offline term-table finished');
+        return 0;
+    }
+}

+ 149 - 0
app/Console/Commands/ExportZip.php

@@ -0,0 +1,149 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Log;
+use App\Tools\RedisClusters;
+use Illuminate\Support\Facades\App;
+
+class ExportZip extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:zip {db : db filename} {format?  : zip file format 7z,lzma,gz }';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        Log::debug('export offline: 开始压缩');
+        $this->info('export offline: 开始压缩');
+        $exportPath = 'app/public/export/offline';
+        $exportFile = $this->argument('db').'-'.date("Y-m-d").'.db3';
+
+        Log::debug('export offline: zip file {filename} {format}',
+                    [
+                        'filename'=>$exportFile,
+                        'format'=>$this->argument('format')
+                    ]);
+        switch ($this->argument('format')) {
+            case '7z':
+                $zipFile = $exportFile . ".7z";
+                break;
+            case 'lzma':
+                $zipFile = $exportFile . ".lzma";
+                break;
+            default:
+                $zipFile = $exportFile . ".gz";
+                break;
+        }
+        //
+        $exportFullFileName = storage_path($exportPath.'/'.$exportFile);
+        if(!file_exists($exportFullFileName)){
+            Log::error('export offline: no db file {filename}',['filename'=>$exportFullFileName]);
+            $this->error('export offline: no db file {filename}'.$exportFullFileName);
+            return 1;
+        }
+
+        $zipFullFileName = storage_path($exportPath.'/'.$zipFile);
+        if(file_exists($zipFullFileName)){
+            Log::debug('export offline: delete old zip file:'.$zipFullFileName);
+            unlink($zipFullFileName);
+        }
+
+        shell_exec("cd ".storage_path($exportPath));
+        if($this->argument('format')==='7z'){
+            $command = "7z a -t7z -m0=lzma -mx=9 -mfb=64 -md=32m -ms=on {$zipFullFileName} {$exportFullFileName}";
+        }else if($this->argument('format')==='lzma'){
+            $command = "xz -k -9 --format=lzma {$exportFullFileName}";
+        }else{
+            $command = "gzip -k -q --best -c {$exportFullFileName} > {$zipFullFileName}";
+        }
+        $this->info($command);
+        Log::debug('export offline: zip command:'.$command);
+        shell_exec($command);
+        $this->info('压缩完成');
+        Log::debug('zip file {filename} in {format} saved.',
+                    [
+                        'filename'=>$exportFile,
+                        'format'=>$this->argument('format')
+                    ]);
+        $info = array();
+        $url = array();
+        foreach (config('mint.server.cdn_urls') as $key => $cdn) {
+            $url[] = [
+                    'link' => $cdn . '/' . $zipFile,
+                    'hostname' =>'cdn-' . $key,
+                ];
+        }
+
+        $bucket = config('mint.attachments.bucket_name.temporary');
+        $tmpFile =  $bucket.'/'. $zipFile ;
+
+        $this->info('upload file='.$tmpFile);
+        Log::debug('export offline: upload file {filename}',['filename'=>$tmpFile]);
+
+        Storage::put($tmpFile, file_get_contents($zipFullFileName));
+
+        $this->info('upload done file='.$tmpFile);
+        Log::debug('export offline: upload done {filename}',['filename'=>$tmpFile]);
+
+        if (App::environment('local')) {
+            $link = Storage::url($tmpFile);
+        }else{
+            try{
+                $link = Storage::temporaryUrl($tmpFile, now()->addDays(2));
+            }catch(\Exception $e){
+                $this->error('generate temporaryUrl fail');
+                Log::error('export offline: generate temporaryUrl fail {Exception}',
+                            [
+                                'exception'=>$e,
+                                'file'=>$tmpFile
+                            ]);
+                return 1;
+            }
+        }
+        $this->info('link = '.$link);
+        Log::info('export offline: link='.$link);
+
+        $url[] = [
+            'link'=>$link,
+            'hostname'=>'Amazon cloud storage(Hongkong)',
+        ];
+        $info[] = ['filename'=>$zipFile,
+                    'url' => $url,
+                   'create_at'=>date("Y-m-d H:i:s"),
+                   'chapter'=>RedisClusters::get("/export/chapter/count"),
+                   'filesize'=>filesize($zipFullFileName),
+                   'min_app_ver'=>'1.3',
+                    ];
+        RedisClusters::put('/offline/index/'.$this->argument('db'),$info);
+        unlink($exportFullFileName);
+        return 0;
+    }
+}

+ 147 - 0
app/Console/Commands/ImportArticle.php

@@ -0,0 +1,147 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+use App\Http\Api\StudioApi;
+use App\Models\Article;
+
+class ImportArticle extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan import:article --studio=visuddhinanda --anthology=eb9e3f7f-b942-4ca4-bd6f-b7876b59a523 --token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJuYmYiOjE2OTc3Mjg2ODUsImV4cCI6MTcyOTI2NDY4NSwidWlkIjoiYmE1NDYzZjMtNzJkMS00NDEwLTg1OGUtZWFkZDEwODg0NzEzIiwiaWQiOjR9.fiXhnY2LczZ9kKVHV0FfD3AJPZt-uqM5wrDe4EhToVexdd007ebPFYssZefmchfL0mx9nF0rgHSqjNhx4P0yDA
+     * @var string
+     */
+    protected $signature = 'import:article {--studio=} {--anthology=} {--token=}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '导入缅文tipitaka sarupa文章';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     * 分两个步骤导入
+     * 1. 导入文章到文集
+     * 2. 重新生成目录结构
+     * @return int
+     */
+    public function handle()
+    {
+        if (!$this->confirm('Do you wish to continue?')) {
+            return 0;
+        }
+        $token = $this->option('token');
+        $studioName = $this->option('studio');
+        $anthologyId = $this->option('anthology');
+
+        //先获取文章列表,建立全部目录
+        $head = array();
+        $strFileName = __DIR__."/tipitaka-sarupa.csv";
+        if(!file_exists($strFileName)){
+            $this->error($strFileName.'文件不存在');
+            return 1;
+        }
+
+        if (($fp = fopen($strFileName, "r")) === false) {
+            $this->error("can not open csv {$strFileName}");
+            return 0;
+        }
+        $this->info('打开csv文件成功');
+
+        $studioId = StudioApi::getIdByName($studioName);
+        if(!$studioId){
+            $this->error("can not found studio name {$studioName}");
+            return 0;
+        }
+        //导入文章
+        $url = config('app.url').'/api/v2/article';
+        $inputRow = 0;
+        fseek($fp, 0);
+        $count = 0;
+        $success = 0;
+        $fail = 0;
+        while (($data = fgetcsv($fp, 0, ',')) !== false) {
+            if($inputRow>0){
+                $id = $data[0];
+                $dir = $data[1];
+                $title = $data[2];
+                $realTitle = "[{$id}]{$title}";
+                $content = str_replace('\n',"\n",$data[4]) ;
+                $reference = str_replace(['(',')'],['({{ql|type=m|title=','}})'],$data[5]);
+                $contentCombine = "{$title}\n\n{$content}\n\n{$reference}";
+                $percent = (int)($inputRow*100/7000);
+                $this->info("[{$percent}%] doing ".$realTitle);
+                //先查是否有
+                $hasArticle = Article::where('owner',$studioId)
+                              ->where('title',$realTitle)
+                              ->exists();
+                if($hasArticle){
+                    $this->error('文章已经存在 title='.$realTitle);
+                    continue;
+                }
+                $count++;
+                $this->info('新建 title='.$realTitle);
+                sleep(2);
+                $response = Http::withToken($token)->post($url,
+                                [
+                                    'title'=> $realTitle,
+                                    'lang'=> 'my',
+                                    'studio'=> $studioName,
+                                    'anthologyId'=> $anthologyId,
+                                ]);
+                if($response->ok()){
+                    $this->info('create ok');
+                    $articleId = $response->json('data')['uid'];
+                }else{
+                    $this->error('create article fail.'.$realTitle);
+                    Log::error('create article fail title='.$realTitle);
+                    $fail++;
+                    continue;
+                }
+                sleep(2);
+                $this->info('修改 id='.$articleId);
+                $response = Http::withToken($token)->put($url.'/'.$articleId,
+                                    [
+                                        'title'=> $realTitle,
+                                        'summary'=> $title.'#'.$id,
+                                        'lang'=> 'my',
+                                        'content'=> $contentCombine,
+                                        'anthology_id'=>$anthologyId,
+                                        'to_tpl'=>true,
+                                        'status'=>30,
+                                    ]);
+
+                if($response->ok()){
+                    $this->info('edit ok');
+                    $success++;
+                }else{
+                    $this->error('edit article fail');
+                    Log::error('edit article fail ',['id'=>$articleId,'title'=>$realTitle]);
+                    $fail++;
+                }
+            }
+            $inputRow++;
+        }
+
+        fclose($fp);
+
+        $this->info('成功='.$success.' 失败='.$fail);
+        return 0;
+    }
+}

+ 213 - 0
app/Console/Commands/ImportArticleMap.php

@@ -0,0 +1,213 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+use App\Http\Api\StudioApi;
+use App\Models\Article;
+use App\Models\Collection;
+
+class ImportArticleMap extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan import:article.map visuddhinanda --studio=visuddhinanda --size=30000 --anthology=4c6b661b-fd68-44c5-8918-2e327c870b9a --token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJuYmYiOjE2OTc3Mjg2ODUsImV4cCI6MTcyOTI2NDY4NSwidWlkIjoiYmE1NDYzZjMtNzJkMS00NDEwLTg1OGUtZWFkZDEwODg0NzEzIiwiaWQiOjR9.fiXhnY2LczZ9kKVHV0FfD3AJPZt-uqM5wrDe4EhToVexdd007ebPFYssZefmchfL0mx9nF0rgHSqjNhx4P0yDA
+     *
+     * @var string
+     */
+    protected $signature = 'import:article.map {src_studio} {--token=} {--studio=} {--anthology=} {--size=}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '重置缅文tipitaka sarupa文章目录';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $token = $this->option('token');
+        $studioName = $this->option('studio');
+        $anthologyId = $this->option('anthology');
+        $srcStudio = $this->argument('src_studio');
+        if (!$this->confirm('Do you wish to continue?')) {
+            return 0;
+        }
+        $studioId = StudioApi::getIdByName($studioName);
+        if(!$studioId){
+            $this->error("can not found studio name {$studioName}");
+            return 0;
+        }
+        $srcStudioId = StudioApi::getIdByName($srcStudio);
+        if(!$srcStudioId){
+            $this->error("can not found src studio name {$srcStudio}");
+            return 0;
+        }
+
+        //先获取文章列表,建立全部目录
+        $url = config('app.url').'/api/v2/article-map';
+
+        $this->info('打开csv文件并读取数据');
+        $head = array();
+        $strFileName = __DIR__."/tipitaka-sarupa.csv";
+        if(!file_exists($strFileName)){
+            $this->error($strFileName.'文件不存在');
+            return 1;
+        }
+
+        if (($fp = fopen($strFileName, "r")) === false) {
+            $this->error("can not open csv {$strFileName}");
+            return 0;
+        }
+        //查询文集语言
+        $srcAnthology = Collection::where('uid',$anthologyId)->first();
+        if(!$srcAnthology){
+            $this->error("文集不存在 anthologyId=".$anthologyId);
+            return 0;
+        }
+        $lang = $srcAnthology->lang;
+        if(empty($lang)){
+            $this->error("文集语言不能为空 anthologyId=".$anthologyId);
+            return 0;
+        }
+        $inputRow = 0;
+        $currSize=0;
+        $currBlock=1;
+        $currDir='';
+        $success = 0;
+        $fail = 0;
+        $articleMap = array();
+        while (($data = fgetcsv($fp, 0, ',')) !== false) {
+            if($inputRow>0){
+                $id = $data[0];
+                $dir = $data[1];
+                $title = $data[2];
+                $realTitle = "[{$id}]{$title}";
+                $realTitle = mb_substr($realTitle,0,128,'UTF-8');
+                $reference = $data[5];
+
+                $percent = (int)($inputRow*100/6984);
+                $this->info("[{$percent}%] doing ".$realTitle);
+
+                if($this->option('size')){
+                    $currDir = $srcAnthology->title . '-' . $currBlock;
+                    if($currSize > $this->option('size')){
+                        $currBlock++;
+                        $currSize=0;
+                    }
+                }else{
+                    $currDir = $dir;
+                }
+                //查找目录文章是否存在
+                $dirArticle = Article::where('owner',$studioId)
+                              ->where('title',$currDir)
+                              ->first();
+                if($dirArticle){
+                    $dirId = $dirArticle->uid;
+                }else{
+                    $this->info('不存在目录'.$currDir.'新建');
+                    $url = config('app.url').'/api/v2/article';
+                    sleep(2);
+                    $response = Http::withToken($token)->post($url,
+                    [
+                        'title'=> $currDir,
+                        'lang'=> $lang,
+                        'studio'=> $studioName,
+                        'anthologyId'=> $anthologyId,
+                    ]);
+                    if($response->ok()){
+                        $this->info('dir create ok title='.$currDir);
+                        $dirId = $response->json('data.uid');
+                    }else{
+                        $this->error('create dir fail.'.$currDir);
+                        Log::error('create dir fail title='.$currDir);
+                        $fail++;
+                        continue;
+                    }
+                }
+                //创建目录结束
+                if(!isset($articleMap[$dirId])){
+                    $articleMap[$dirId] = ['name'=>$currDir,'children'=>[]];
+                }
+                //查找文章
+                $article = Article::where('owner',$srcStudioId)
+                              ->where('title',$realTitle)
+                              ->first();
+                if(!$article){
+                    $this->error('文章没找到.'.$realTitle);
+                    Log::error('文章没找到 title='.$realTitle);
+                    $fail++;
+                    continue;
+                }
+                $articleMap[$dirId]['children'][] = [
+                    'id'=>$article->uid,
+                    'title'=>$article->title,
+                ];
+                if($this->option('size')){
+                    $currSize += mb_strlen($title,'UTF-8') +
+                                mb_strlen($data[4],'UTF-8') +
+                                mb_strlen($reference,'UTF-8');
+                }
+                $success++;
+            }
+            $inputRow++;
+        }
+        $this->info("找到文章=" .$success);
+        $this->info("目录=" .count($articleMap));
+
+        $this->info('正在准备map数据');
+
+        $data = array();
+        foreach ($articleMap as $dirId => $dir) {
+            $data[] = [
+                    'article_id'=> $dirId,
+                    'level'=> 1,
+                    'title'=> $dir['name'],
+                    'children'=> count($dir['children']),
+                    'deleted_at'=> null,
+            ];
+            foreach ($dir['children'] as $key => $child) {
+                $data[] = [
+                        'article_id'=> $child['id'],
+                        'level'=> 2,
+                        'title'=> $child['title'],
+                        'children'=> 0,
+                        'deleted_at'=> null,
+                ];
+            }
+        }
+        $this->info('map data='.count($data));
+
+        //目录写入db
+        $url = config('app.url').'/api/v2/article-map/'.$anthologyId;
+        $response = Http::withToken($token)->put($url,
+        [
+            'data'=> $data,
+            'operation' => "anthology",
+        ]);
+        if($response->ok()){
+            $this->info('map update ok ');
+        }else{
+            $this->error('map update  fail.');
+            Log::error('map update  fail ');
+        }
+        return 0;
+    }
+}

+ 18 - 6
app/Console/Commands/InitCs6sentence.php

@@ -43,12 +43,16 @@ class InitCs6sentence extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$start = time();
         $channelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
         if($channelId === false){
             $this->error('no channel');
             return 1;
         }
+        $this->info($channelId);
 		$pali = new PaliSentence;
 		if(!empty($this->argument('book'))){
 			$pali = $pali->where('book',$this->argument('book'));
@@ -58,7 +62,7 @@ class InitCs6sentence extends Command
 		}
 		$bar = $this->output->createProgressBar($pali->count());
 		$pali = $pali->select('book','paragraph','word_begin','word_end')->cursor();
-
+        $pageHead = ['M','P','T','V','O'];
 		foreach ($pali as $value) {
 			# code...
 			$words = WbwTemplate::where("book",$value->book)
@@ -74,8 +78,7 @@ class InitCs6sentence extends Command
 			foreach ($words as $word) {
 				# code...
 				//if($word->style != "note" && $word->type != '.ctl.')
-				if( $word->type != '.ctl.')
-                {
+				if( $word->type != '.ctl.'){
                     if($lastWord !== null){
                         if($word->real !== "ti" ){
 
@@ -86,7 +89,7 @@ class InitCs6sentence extends Command
                         }
                     }
 
-					if(strpos($word->word,'{') >=0 ){
+					if(strpos($word->word,'{') !== false ){
                         //一个单词里面含有黑体字的
 						$paliWord = \str_replace("{","<strong>",$word->word) ;
 						$paliWord = \str_replace("}","</strong>",$paliWord) ;
@@ -99,7 +102,16 @@ class InitCs6sentence extends Command
                         }
 					}
 
-				}
+				}else{
+                    $type = substr($word->word,0,1);
+                    if(in_array($type,$pageHead)){
+                        $arrPage = explode('.',$word->word);
+                        if(count($arrPage)===2){
+                            $pageNumber = $arrPage[0].'.'.(int)$arrPage[1];
+                           $sent .= "<code>{$pageNumber}</code>";
+                        }
+                    }
+                }
                 $lastWord = $word;
 			}
 
@@ -125,7 +137,7 @@ class InitCs6sentence extends Command
                     'create_time' => time()*1000,
 				]
 				);
-            $newRow->editor_uid = config("app.admin.root_uuid");
+            $newRow->editor_uid = config("mint.admin.root_uuid");
             $newRow->content = "<span>{$sent}</span>";
             $newRow->strlen = mb_strlen($sent,"UTF-8");
             $newRow->status = 10;

+ 6 - 3
app/Console/Commands/InitDependence.php

@@ -39,16 +39,19 @@ class InitDependence extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		#克隆依赖的数据仓库到本地
-		$depDir = $this->info(config("app.path.dependence"));
-		foreach ($this->info(config("app.dependence")) as $key => $value) {
+		$depDir = $this->info(config("mint.path.dependence"));
+		foreach ($this->info(config("mint.dependence")) as $key => $value) {
 			# code...
 			$process = new Process(['git','clone',$value->url,$depDir.'/'.$value->path]);
 			$process->run();
 			if(!$process->isSuccessful()){
 				throw new ProcessFailedException($process);
 			}
-			$this->info($process->getOutput());				
+			$this->info($process->getOutput());
 		}
         return 0;
     }

+ 31 - 2
app/Console/Commands/InitSystemChannel.php

@@ -47,6 +47,11 @@ class InitSystemChannel extends Command
             'type'=>'translation',
             'lang'=>'en',
         ],
+        [
+            "name"=>'_System_Grammar_Term_my_',
+            'type'=>'translation',
+            'lang'=>'my',
+        ],
         [
             "name"=>'_community_term_zh-hans_',
             'type'=>'translation',
@@ -62,6 +67,26 @@ class InitSystemChannel extends Command
             'type'=>'translation',
             'lang'=>'en',
         ],
+        [
+            "name"=>'_community_translation_zh-hans_',
+            'type'=>'translation',
+            'lang'=>'zh-Hans',
+        ],
+        [
+            "name"=>'_community_translation_zh-hant_',
+            'type'=>'translation',
+            'lang'=>'zh-Hant',
+        ],
+        [
+            "name"=>'_community_translation_en_',
+            'type'=>'translation',
+            'lang'=>'en',
+        ],
+        [
+            "name"=>'_System_Quote_',
+            'type'=>'original',
+            'lang'=>'en',
+        ],
     ];
 
     /**
@@ -81,12 +106,15 @@ class InitSystemChannel extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $this->info("start");
         foreach ($this->channels as $key => $value) {
             # code...
             $channel = Channel::firstOrNew([
                 'name' => $value['name'],
-                'owner_uid' => config("app.admin.root_uuid"),
+                'owner_uid' => config("mint.admin.root_uuid"),
             ]);
             if(empty($channel->id)){
                 $channel->id = app('snowflake')->id();
@@ -94,9 +122,10 @@ class InitSystemChannel extends Command
             $channel->type = $value['type'];
             $channel->lang = $value['lang'];
             $channel->editor_id = 0;
-            $channel->owner_uid = config("app.admin.root_uuid");
+            $channel->owner_uid = config("mint.admin.root_uuid");
             $channel->create_time = time()*1000;
             $channel->modify_time = time()*1000;
+            $channel->is_system = true;
             $channel->save();
             $this->info("created". $value['name']);
         }

+ 4 - 1
app/Console/Commands/InitSystemDict.php

@@ -72,12 +72,15 @@ class InitSystemDict extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $this->info("start");
         foreach ($this->dictionary as $key => $value) {
             # code...
             $channel = DictInfo::firstOrNew([
                 'name' => $value['name'],
-                'owner_id' => config("app.admin.root_uuid"),
+                'owner_id' => config("mint.admin.root_uuid"),
             ]);
             $channel->shortname = $value['shortname'];
             $channel->description = $value['description'];

+ 14 - 33
app/Console/Commands/Install.php

@@ -35,49 +35,30 @@ class Install extends Command
      *
      * @return int
      */
-	/*
-	php ../../public/app/install/db_insert_templet_cli.php 1 217
-	php ../../public/app/install/db_update_toc_cli.php 1 217 pali
-	php ../../public/app/install/db_update_toc_cli.php 1 217 zh-hans
-	php ../../public/app/install/db_update_toc_cli.php 1 217 zh-hant
-	php ../../public/app/install/db_insert_palitext_cli.php 1 217
-	php ../../public/app/install/db_update_palitext_cli.php 1 217
-	php ../../public/app/install/db_insert_bookword_from_csv_cli.php 1 217
-	php ../../public/app/install/db_insert_word_from_csv_cli.php 1 217
-	php ../../public/app/install/db_insert_wordindex_from_csv_cli.php
 
-	php ./migrations/20211202084900_init_pali_serieses.php
-	php ./migrations/20211125155600_word_statistics.php
-	php ./migrations/20211125155700_pali_sent_org.php
-	php ./migrations/20211125165700-pali_sent-upgrade.php
-	php ./migrations/20211126220400-pali_sent_index-upgrade.php
-	php ./migrations/20211127214800_sent_sim.php
-	php ./migrations/20211127214900-sent_sim_index.php
-
-	php ../../public/app/fts/sql.php
-
-	php ../../public/app/admin/word_index_weight_refresh.php 1 217
-	*/
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$isTest = $this->option('test');
 		if($isTest){
 			$this->call('install:wbwtemplate', ['from' => 1]);
 		}else{
-			$this->call('install:wbwtemplate');	
-			$this->call('install:palitext');	
-			$this->call('install:wordbook');	
-			$this->call('install:wordall');	
+			$this->call('install:wbwtemplate');
+			$this->call('install:palitext');
+			$this->call('install:wordbook');
+			$this->call('install:wordall');
 			$this->call('install:wordindex');
 
-			$this->call('upgrade:palitext');	
-			$this->call('upgrade:palitoc',['lang'=>'pali']);	
-			$this->call('upgrade:palitoc',['lang'=>'zh-hans']);	
-			$this->call('upgrade:palitoc',['lang'=>'zh-hant']);	
+			$this->call('upgrade:palitext');
+			$this->call('upgrade:palitoc',['lang'=>'pali']);
+			$this->call('upgrade:palitoc',['lang'=>'zh-hans']);
+			$this->call('upgrade:palitoc',['lang'=>'zh-hant']);
+
+			$this->call('install:paliseries');
+			$this->call('install:wordstatistics');
 
-			$this->call('install:paliseries');	
-			$this->call('install:wordstatistics');	
-			
 		}
 
         return 0;

+ 9 - 5
app/Console/Commands/InstallPaliSeries.php

@@ -14,7 +14,7 @@ class InstallPaliSeries extends Command
      *
      * @var string
      */
-    protected $signature = 'install:paliseries';
+    protected $signature = 'install:pali.series';
 
     /**
      * The console command description.
@@ -40,6 +40,9 @@ class InstallPaliSeries extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$this->info("upgrade pali serieses");
 		$startTime = time();
 
@@ -47,22 +50,23 @@ class InstallPaliSeries extends Command
 			#删除目标数据库中数据
 			BookTitle::where('book','>',0)->delete();
 
-		// 打开csv文件并读取数据										
-			$strFileName = config("app.path.pali_title") . "/pali_serieses.csv";
+		// 打开csv文件并读取数据
+			$strFileName = config("mint.path.pali_title") . "/pali_serieses.csv";
 			if(!file_exists($strFileName)){
 				return 1;
-			}		
+			}
 			$inputRow = 0;
 			if (($fp = fopen($strFileName, "r")) !== false) {
 				while (($data = fgetcsv($fp, 0, ',')) !== false) {
 					if($inputRow>0){
 						$newData = [
+							'sn'=>$data[0],
 							'book'=>$data[1],
 							'paragraph'=>$data[2],
 							'title'=>$data[3],
 						];
 
-						BookTitle::create($newData);							
+						BookTitle::create($newData);
 					}
 					$inputRow++;
 				}

+ 18 - 15
app/Console/Commands/InstallPaliText.php

@@ -40,6 +40,9 @@ class InstallPaliText extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$this->info("instert pali text");
 		$startTime = time();
 
@@ -51,7 +54,7 @@ class InstallPaliText extends Command
 		}else if(empty($_to)){
 			$_to = $_from;
 		}
-		$fileListFileName = config("app.path.palitext_filelist");
+		$fileListFileName = config("mint.path.palitext_filelist");
 
 		$filelist = array();
 
@@ -61,18 +64,18 @@ class InstallPaliText extends Command
 		}
 		$bar = $this->output->createProgressBar($_to-$_from+1);
 
-		for ($from=$_from; $from <=$_to ; $from++) { 
+		for ($from=$_from; $from <=$_to ; $from++) {
 			# code...
-			
+
 			$fileSn = $from-1;
 			$FileName = $filelist[$fileSn][1];
-	
-			$dirXmlBase = config("app.path.palicsv") . "/";
+
+			$dirXmlBase = config("mint.path.palicsv") . "/";
 			$GLOBALS['data'] = array();
-    
+
 			// 打开vri html文件并读取数据
 			$pali_text_array = array();
-			$htmlFile = config("app.path.palitext") .'/'. $FileName.'.htm';
+			$htmlFile = config("mint.path.palitext") .'/'. $FileName.'.htm';
 			if (($fpPaliText = fopen($htmlFile, "r")) !== false) {
 				while (($data = fgets($fpPaliText)) !== false) {
 					if (substr($data, 0, 2) === "<p") {
@@ -85,9 +88,9 @@ class InstallPaliText extends Command
 				$this->error( "can not pali text file. filename=" . $htmlFile . PHP_EOL) ;
 				Log::error( "can not pali text file. filename=" . $htmlFile . PHP_EOL) ;
 			}
-			
+
 			$inputRow = 0;
-			$csvFile = config("app.path.palicsv") .'/'. $FileName .'/'. $FileName.'_pali.csv';
+			$csvFile = config("mint.path.palicsv") .'/'. $FileName .'/'. $FileName.'_pali.csv';
 			if (($fp = fopen($csvFile, "r")) !== false) {
 				while (($data = fgetcsv($fp, 0, ',')) !== false) {
 					if ($inputRow > 0) {
@@ -106,19 +109,19 @@ class InstallPaliText extends Command
 				Log::error( "can not open csv file. filename=" . $csvFile. PHP_EOL) ;
 				continue;
 			}
-			
+
 			if (($inputRow - 1) != count($pali_text_array)) {
 				$this->error( "line count error $FileName ".PHP_EOL);
 				Log::error( "line count error $FileName ".PHP_EOL);
 			}
-							 
-		
+
+
 			#删除目标数据库中数据
 			PaliText::where('book', $from)->delete();
 
 
 			// 打开文件并读取数据
-			
+
 
 			DB::transaction(function () {
 				foreach ($GLOBALS['data'] as $oneParam) {
@@ -139,9 +142,9 @@ class InstallPaliText extends Command
 					];
 					PaliText::create($params);
 				}
-				
+
 			});
-			
+
 			$bar->advance();
 		}
 		$bar->finish();

+ 13 - 10
app/Console/Commands/InstallWbwTemplate.php

@@ -40,6 +40,9 @@ class InstallWbwTemplate extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$this->info("instert wbw template");
 
 
@@ -61,22 +64,22 @@ class InstallWbwTemplate extends Command
 		}
 		$bar = $this->output->createProgressBar($_to-$_from+1);
 
-		for ($from=$_from; $from <=$_to ; $from++) { 
+		for ($from=$_from; $from <=$_to ; $from++) {
 			# code...
-			
+
 			$fileSn = $from-1;
 			$outputFileNameHead = $filelist[$fileSn][1];
-	
+
 			$dirXmlBase = public_path('/tmp/palicsv') . "/";
 			$dirXml = $outputFileNameHead . "/";
-		
-		
+
+
 			#删除目标数据库中数据
 			WbwTemplate::where('book', $from)->delete();
 
 
 			// 打开文件并读取数据
-			
+
 			if (($GLOBALS["fp"] = fopen($dirXmlBase . $dirXml . $outputFileNameHead . ".csv", "r")) !== false) {
 				$GLOBALS["row"]=0;
 				DB::transaction(function () {
@@ -95,7 +98,7 @@ class InstallWbwTemplate extends Command
 							'type'=>$data[6],
 							'gramma'=>$data[7],
 							'part'=>$data[10],
-							'style'=>$data[15]	
+							'style'=>$data[15]
 						];
 						WbwTemplate::insert($params);
 					}
@@ -105,11 +108,11 @@ class InstallWbwTemplate extends Command
 				$this->error("can not open csv file. filename=" . $dirXmlBase . $dirXml . $outputFileNameHead . ".csv".PHP_EOL) ;
 				Log::error("can not open csv file. filename=" . $dirXmlBase . $dirXml . $outputFileNameHead . ".csv".PHP_EOL) ;
 			}
-			
+
 			$bar->advance();
 		}
 		$bar->finish();
         return 0;
-    
+
 	}
-}
+}

+ 9 - 6
app/Console/Commands/InstallWordAll.php

@@ -40,6 +40,9 @@ class InstallWordAll extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$startTime = time();
 
 		$this->info("instert word in palibook ");
@@ -56,14 +59,14 @@ class InstallWordAll extends Command
 
 		$bar = $this->output->createProgressBar($_to-$_from+1);
 
-		for ($book=$_from; $book <= $_to; $book++) { 
+		for ($book=$_from; $book <= $_to; $book++) {
 			Log::info("doing ".($book));
 			DB::transaction(function ()use($book) {
 				$fileSn = $book-1;
-				if (($fpoutput = fopen(config("app.path.paliword_book") . "/{$fileSn}_words.csv", "r")) !== false){
+				if (($fpoutput = fopen(config("mint.path.paliword_book") . "/{$fileSn}_words.csv", "r")) !== false){
 					#删除目标数据库中数据
-					WordList::where('book', $book)->delete();			
-					while (($data = fgetcsv($fpoutput, 0, ',')) !== false)  
+					WordList::where('book', $book)->delete();
+					while (($data = fgetcsv($fpoutput, 0, ',')) !== false)
 					{
 						$newData = [
 							'sn'=>$data[0],
@@ -72,9 +75,9 @@ class InstallWordAll extends Command
 							'wordindex'=>$data[3],
 							'bold'=>$data[4],
 						];
-						WordList::create($newData);				
+						WordList::create($newData);
 					}
-					return 0;			
+					return 0;
 				}else{
 					Log::error("open csv fail");
 					return 1;

+ 8 - 5
app/Console/Commands/InstallWordBook.php

@@ -40,6 +40,9 @@ class InstallWordBook extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$startTime = time();
 
 		$this->info("instert word in palibook ");
@@ -56,7 +59,7 @@ class InstallWordBook extends Command
 
 		$bar = $this->output->createProgressBar($_to-$_from+1);
 
-		for ($book=$_from; $book <= $_to; $book++) { 
+		for ($book=$_from; $book <= $_to; $book++) {
 			Log::info("doing ".($book));
 
 			#删除目标数据库中数据
@@ -65,7 +68,7 @@ class InstallWordBook extends Command
 			//分类汇总得到单词表
 			$bookword = array();
 			$fileId = $book-1;
-			if (($fpoutput = fopen(config("app.path.paliword_book") . "/{$fileId}_words.csv", "r")) !== false) {
+			if (($fpoutput = fopen(config("mint.path.paliword_book") . "/{$fileId}_words.csv", "r")) !== false) {
 				$count = 0;
 				while (($data = fgetcsv($fpoutput, 0, ',')) !== false) {
 					$book = $data[1];
@@ -74,7 +77,7 @@ class InstallWordBook extends Command
 					} else {
 						$bookword[$data[3]] = 1;
 					}
-		
+
 					$count++;
 				}
 			}else{
@@ -88,7 +91,7 @@ class InstallWordBook extends Command
 						'wordindex'=>$key,
 						'count'=>$value,
 					];
-					BookWord::create($newData);				
+					BookWord::create($newData);
 				}
 			});
 			$bar->advance();
@@ -98,7 +101,7 @@ class InstallWordBook extends Command
 		$msg = "all done in ". time()-$startTime . "s";
 		$this->info($msg.PHP_EOL);
 		Log::info($msg);
-		
+
         return 0;
     }
 }

+ 8 - 5
app/Console/Commands/InstallWordIndex.php

@@ -40,6 +40,9 @@ class InstallWordIndex extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$startTime = time();
 
 		$info = "instert word in palibook ";
@@ -47,13 +50,13 @@ class InstallWordIndex extends Command
 		Log::info($info);
 
 		#删除目标数据库中数据
-		WordIndex::where('id', '>',-1)->delete();	
+		WordIndex::where('id', '>',-1)->delete();
 
-		$scan = scandir(config("app.path.paliword_index"));
+		$scan = scandir(config("mint.path.paliword_index"));
 		$bar = $this->output->createProgressBar(count($scan));
 		foreach($scan as $filename) {
 			$bar->advance();
-			$filename = config("app.path.paliword_index")."/".$filename;
+			$filename = config("mint.path.paliword_index")."/".$filename;
 			if (is_file($filename)) {
 				Log::info("doing ".$filename);
 				DB::transaction(function ()use($filename) {
@@ -69,12 +72,12 @@ class InstallWordIndex extends Command
 								'is_base'=>$data[5],
 								'len'=>$data[6],
 							];
-							WordIndex::create($newData);	
+							WordIndex::create($newData);
 							$count++;
 						}
 						Log::info("insert ".$count);
 					}
-				});				
+				});
 			}
 		}
 		$bar->finish();

+ 8 - 5
app/Console/Commands/InstallWordStatistics.php

@@ -40,6 +40,9 @@ class InstallWordStatistics extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$startTime = time();
 
 		$info = "instert wordstatistics ";
@@ -47,13 +50,13 @@ class InstallWordStatistics extends Command
 		Log::info($info);
 
 		#删除目标数据库中数据
-		WordStatistic::where('id', '>',-1)->delete();	
+		WordStatistic::where('id', '>',-1)->delete();
 
-		$scan = scandir(config("app.path.word_statistics"));
+		$scan = scandir(config("mint.path.word_statistics"));
 		$bar = $this->output->createProgressBar(count($scan));
 		foreach($scan as $filename) {
 			$bar->advance();
-			$filename = config("app.path.word_statistics")."/".$filename;
+			$filename = config("mint.path.word_statistics")."/".$filename;
 			if (is_file($filename)) {
 				Log::info("doing ".$filename);
 				DB::transaction(function ()use($filename) {
@@ -70,12 +73,12 @@ class InstallWordStatistics extends Command
 								'type'=>$data[6],
 								'length'=>$data[7],
 							];
-							WordStatistic::create($newData);	
+							WordStatistic::create($newData);
 							$count++;
 						}
 						Log::info("insert ".$count);
 					}
-				});				
+				});
 			}
 		}
 		$bar->finish();

+ 160 - 0
app/Console/Commands/MqDiscussion.php

@@ -0,0 +1,160 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\Sentence;
+use App\Models\WebHook;
+use App\Models\Discussion;
+use App\Models\Article;
+use App\Http\Api\Mq;
+use App\Tools\WebHook as WebHookSend;
+use App\Http\Api\MdRender;
+use App\Http\Api\UserApi;
+use Illuminate\Support\Facades\Log;
+
+class MqDiscussion extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan mq:discussion
+     * @var string
+     */
+    protected $signature = 'mq:discussion';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $exchange = 'router';
+        $queue = 'discussion';
+        $this->info(" [*] Waiting for {$queue}. To exit press CTRL+C");
+        Log::info("discussion worker start .");
+        Mq::worker($exchange,$queue,function ($message){
+            Log::info('mq discussion receive {message}',['message'=>json_encode($message,JSON_UNESCAPED_UNICODE)]);
+            $result = 0;
+            switch ($message->res_type) {
+                case 'sentence':
+                    $sentence = Sentence::where('uid',$message->res_id)->first();
+                    if(!$sentence){
+                        return 0;
+                    }
+                    $contentHtml = MdRender::render($sentence->content,
+                                             [$sentence->channel_uid],
+                                             null,
+                                             'read',
+                                             'translation',
+                                             $sentence->content_type);
+                    $contentTxt = strip_tags($contentHtml);
+                    /**生成消息内容 */
+
+                    $msgParam = array();
+                    $msgParam['anchor-content'] = $contentTxt;
+                    $msgParam['nickname'] = $message->editor->nickName;
+                    $link = config('app.url')."/pcd/discussion/topic/";
+                    if($message->parent){
+                        $msgParam['topic-title'] = Discussion::where('id',$message->parent)->value('title');
+                        $id = $message->id;
+                        $msgParam['link'] = $link . $message->parent.'#'.$id;
+                        $msgTitle = "回复讨论";
+                        $type = 'reply';
+                    }else{
+                        $msgParam['title'] = $message->title;
+                        $msgParam['link'] = $link . $message->id;
+                        $msgTitle = "创建讨论";
+                        $type = 'create';
+                    }
+                    if($message->content){
+                        $msgParam['content'] = $message->content;
+                    }
+
+                    $rootId = UserApi::getById(0)['uid'];
+                    $articleTitle = "webhook://discussion/{$type}/zh-hans";
+                    $tpl = Article::where('owner',$rootId)
+                                  ->where('title',$articleTitle)
+                                  ->value('content');
+                    if(empty($tpl)){
+                        Log::error('mq:discussion 模版不能为空',['tpl_title'=>$articleTitle]);
+                        return 1;
+                    }
+                    $m = new \Mustache_Engine(array('entity_flags'=>ENT_QUOTES,
+                                                'delimiters' => '{% %}',));
+                    $msgContent = $m->render($tpl,$msgParam);
+
+                    $webhooks = WebHook::where('res_id',$sentence->channel_uid)
+                                    ->where('status','active')
+                                    ->get();
+                    foreach ($webhooks as $key => $hook) {
+                        $event = json_decode($hook->event);
+
+                        if(is_array($event)){
+                            if(!in_array('discussion',$event)){
+                                continue;
+                            }
+                        }else{
+                            continue;
+                        }
+                        $command = '';
+                        $whSend = new WebHookSend;
+                        $ok = 0;
+                        switch ($hook->receiver) {
+                            case 'dingtalk':
+                                $ok = $whSend->dingtalk($hook->url,$msgTitle,$msgContent);
+                                break;
+                            case 'wechat':
+                                $ok = $whSend->wechat($hook->url,null,$msgContent);
+                                break;
+                            default:
+                                $ok=2;
+                                break;
+                        }
+                        $result += $ok;
+                        $logMsg = "{$command}  ok={$ok}";
+                        if($ok === 0){
+                            $this->info($logMsg);
+                        }else{
+                            $this->error($logMsg);
+                        }
+
+                        if($ok === 0){
+                            Log::debug('mq:discussion: send success {url}',['url'=>$hook->url]);
+                            WebHook::where('id',$hook->id)->increment('success');
+                        }else{
+                            Log::error('mq:discussion: send fail {url}',['url'=>$hook->url]);
+                            WebHook::where('id',$hook->id)->increment('fail');
+                        }
+                    }
+                    break;
+                default:
+                    # code...
+                    break;
+            }
+            return $result;
+        });
+
+        return 0;
+    }
+}

+ 71 - 0
app/Console/Commands/MqExport.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Api\Mq;
+use Illuminate\Support\Facades\Log;
+
+class MqExport extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan mq:export
+     * @var string
+     */
+    protected $signature = 'mq:export';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '导出功能用的消息队列';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $exchange = 'router';
+        $queue = 'export';
+        $this->info(" [*] Waiting for {$queue}. To exit press CTRL+C");
+        Log::debug("mq:progress start.");
+        Mq::worker($exchange,$queue,function ($message){
+            $data = [
+                        'book'=>$message->book,
+                        'para'=>$message->para,
+                        'channel'=>$message->channel,
+                        '--format'=>$message->format,
+                        'filename'=>$message->filename,
+                    ];
+            if(isset($message->origin) && is_string($message->origin)){
+                $data['--origin'] = $message->origin;
+            }
+            if(isset($message->translation) && is_string($message->translation)){
+                $data['--translation'] = $message->translation;
+            }
+            $ok = $this->call('export:chapter',$data);
+            if($ok !== 0){
+                Log::error('mq:progress upgrade:progress fail',$data);
+            }else{
+                $this->info("Received book=".$message->book.' result='.$ok);
+                Log::debug("mq:export: done ",$data);
+                return $ok;
+            }
+        });
+        return 0;
+    }
+}

+ 73 - 0
app/Console/Commands/MqExportArticle.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Api\Mq;
+use Illuminate\Support\Facades\Log;
+
+class MqExportArticle extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan mq:export.article
+     * @var string
+     */
+    protected $signature = 'mq:export.article';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '导出文章';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $exchange = 'router';
+        $queue = 'export_article';
+        $this->info(" [*] Waiting for {$queue}. To exit press CTRL+C");
+        Log::debug("mq:export_article start.");
+        Mq::worker($exchange,$queue,function ($message){
+            $data = [
+                        'id'=>$message->id,
+                        '--format'=>$message->format,
+                        'query_id'=>$message->queryId,
+                    ];
+            if(isset($message->token) && is_string($message->token)){
+                $data['--token'] = $message->token;
+            }
+            if(isset($message->anthology) && is_string($message->anthology)){
+                $data['--anthology'] = $message->anthology;
+            }
+            if(isset($message->channel) && is_string($message->channel)){
+                $data['--channel'] = $message->channel;
+            }
+            $ok = $this->call('export:article',$data);
+            if($ok !== 0){
+                Log::error('mq:export.article fail',$data);
+            }else{
+                $this->info("Received article id=".$message->id.' result='.$ok);
+                Log::debug("mq:export.article done ",$data);
+                return $ok;
+            }
+        });
+
+        return 0;
+    }
+}

+ 74 - 0
app/Console/Commands/MqExportPaliChapter.php

@@ -0,0 +1,74 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Api\Mq;
+use Illuminate\Support\Facades\Log;
+
+class MqExportPaliChapter extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan mq:export.pali.chapter
+     * @var string
+     */
+    protected $signature = 'mq:export.pali.chapter';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '导出巴利文章节';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $exchange = 'router';
+        $queue = 'export_pali_chapter';
+        $this->info(" [*] Waiting for {$queue}. To exit press CTRL+C");
+        Log::debug("mq:export_pali_chapter start.");
+        Mq::worker($exchange,$queue,function ($message){
+            $data = [
+                        'book'=>$message->book,
+                        'para'=>$message->para,
+                        'channel'=>$message->channel,
+                        '--format'=>$message->format,
+                        'query_id'=>$message->queryId,
+                    ];
+            if(isset($message->origin) && is_string($message->origin)){
+                $data['--origin'] = $message->origin;
+            }
+            if(isset($message->translation) && is_string($message->translation)){
+                $data['--translation'] = $message->translation;
+            }
+            if(isset($message->token) && is_string($message->token)){
+                $data['--token'] = $message->token;
+            }
+            $ok = $this->call('export:chapter',$data);
+            if($ok !== 0){
+                Log::error('mq:export.pali.chapter upgrade:progress fail',$data);
+            }else{
+                $this->info("Received book=".$message->book.' result='.$ok);
+                Log::debug("mq:export.pali.chapter done ",$data);
+                return $ok;
+            }
+        });
+        return 0;
+    }
+}

+ 53 - 0
app/Console/Commands/MqIssues.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Api\Mq;
+
+class MqIssues extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan mq:issues
+     * @var string
+     */
+    protected $signature = 'mq:issues';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $exchange = 'router';
+        $queue = 'issues';
+        $this->info(" [*] Waiting for {$queue}. To exit press CTRL+C");
+        Mq::worker($exchange,$queue,function ($message){
+            print_r($message);
+            return 0;
+        });
+        return 0;
+    }
+}

+ 163 - 0
app/Console/Commands/MqPr.php

@@ -0,0 +1,163 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+
+use App\Http\Api\Mq;
+use App\Models\Sentence;
+use App\Models\WebHook;
+use App\Models\PaliSentence;
+use App\Tools\WebHook as WebHookSend;
+use App\Http\Api\MdRender;
+use App\Http\Api\PaliTextApi;
+use App\Http\Controllers\NotificationController;
+
+class MqPr extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan mq:pr
+     * @var string
+     */
+    protected $signature = 'mq:pr';
+
+    protected $ver = '2024-1-2';
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'push pr message to mq';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $exchange = 'router';
+        $queue = 'suggestion';
+        $this->info(" [*] Waiting for {$queue}. Ver. ".$this->ver);
+        Log::debug("mq:pr start. ver=".$this->ver);
+        Mq::worker($exchange,$queue,function ($message){
+            /**生成消息内容 */
+
+            $msgTitle = '修改建议';
+            $prData = $message->data;
+            $sent_num = "{$prData->book}-{$prData->paragraph}-{$prData->word_start}-{$prData->word_end}";
+            $this->info('ver='.$this->ver.' request'.$sent_num);
+
+            $username = $prData->editor->nickName;
+            $palitext = PaliSentence::where('book',$prData->book)
+                                ->where('paragraph',$prData->paragraph)
+                                ->where('word_begin',$prData->word_start)
+                                ->where('word_end',$prData->word_end)
+                                ->value('text');
+            $orgText = Sentence::where('book_id',$prData->book)
+                                ->where('paragraph',$prData->paragraph)
+                                ->where('word_start',$prData->word_start)
+                                ->where('word_end',$prData->word_end)
+                                ->where('channel_uid',$prData->channel->id)
+                                ->first();
+            $prtext = mb_substr($prData->content,0,140,"UTF-8");
+
+            $link = config('app.url')."/pcd/article/para/{$prData->book}-{$prData->paragraph}";
+            $link .= "?book={$prData->book}&par={$prData->paragraph}&channel={$prData->channel->id}";
+
+            $msgContent = "{$username} 就文句`{$palitext}`提出了修改建议:\n";
+            $msgContent .= ">内容摘要:<font color=\"comment\">{$prtext}</font>,\n";
+            $msgContent .= ">句子编号:<font color=\"info\">{$sent_num}</font>\n";
+            $msgContent .= "欢迎大家[点击链接]({$link})查看并讨论。";
+
+
+            $result=0;
+            //发送站内信
+            try{
+                $sendTo = array();
+                if($prData->editor->id !== $prData->channel->studio_id){
+                    $sendTo[] = $prData->channel->studio_id;
+                }
+                if($orgText){
+                    //原文作者
+                    if(!in_array($orgText->editor_uid,$sendTo) &&
+                        $orgText->editor_uid !== $prData->editor->id){
+                        $sendTo[] = $orgText->editor_uid;
+                    }
+                    //原文采纳者
+                    if(!empty($orgText->acceptor_uid) &&
+                       !in_array($orgText->acceptor_uid,$sendTo) &&
+                       $orgText->acceptor_uid !== $prData->editor->id){
+                        $sendTo[] = $orgText->acceptor_uid;
+                    }
+                }
+                if(count($sendTo) > 0){
+                    $sendCount = NotificationController::insert($prData->editor->id,
+                                                    $sendTo,
+                                                    'suggestion',
+                                                    $prData->uid,
+                                                    $prData->channel->id);
+                }
+
+                $this->info("send notification success to [".count($sendTo).'] users');
+            }catch(\Exception $e){
+                $this->error('send notification failed');
+                Log::error('send notification failed',['exception'=>$e]);
+            }
+
+            //发送webhook
+
+            $webhooks = WebHook::where('res_id',$prData->channel->id)
+                            ->where('status','active')
+                            ->get();
+
+
+            foreach ($webhooks as $key => $hook) {
+                $event = json_decode($hook->event);
+                if(!in_array('pr',$event)){
+                    continue;
+                }
+                $command = '';
+                $whSend = new WebHookSend;
+                switch ($hook->receiver) {
+                    case 'dingtalk':
+                        $ok = $whSend->dingtalk($hook->url,$msgTitle,$msgContent);
+                        break;
+                    case 'wechat':
+                        $ok = $whSend->wechat($hook->url,null,$msgContent);
+                        break;
+                    default:
+                        $ok=2;
+                        break;
+                }
+                $this->info("{$command}  ok={$ok}");
+                $result+=$ok;
+                if($ok === 0){
+                    Log::debug('mq:pr: send success {url}',['url'=>$hook->url]);
+                    WebHook::where('id',$hook->id)->increment('success');
+                }else{
+                    Log::error('mq:pr: send fail {url}',['url'=>$hook->url]);
+                    WebHook::where('id',$hook->id)->increment('fail');
+                }
+            }
+            return $result;
+        });
+        return 0;
+    }
+}

+ 70 - 0
app/Console/Commands/MqProgress.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Api\Mq;
+use Illuminate\Support\Facades\Log;
+
+class MqProgress extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan mq:progress
+     * @var string
+     */
+    protected $signature = 'mq:progress';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $exchange = 'router';
+        $queue = 'progress';
+        $this->info(" [*] Waiting for {$queue}. To exit press CTRL+C");
+        Log::debug("mq:progress start.");
+        Mq::worker($exchange,$queue,function ($message){
+            $data = [
+                        '--book'=>$message->book,
+                        '--para'=>$message->para,
+                        '--channel'=>$message->channel,
+                    ];
+            $ok1 = $this->call('upgrade:progress',$data);
+            if($ok1 !== 0){
+                Log::error('mq:progress upgrade:progress fail',$data);
+            }
+            $ok2 = $this->call('upgrade:progress.chapter',$data);
+            if($ok2 !== 0){
+                Log::error('mq:progress upgrade:progress.chapter fail',$data);
+            }
+            $this->info("Received book=".$message->book.' progress='.$ok1.' chapter='.$ok2);
+            Log::debug("mq:progress: done book=".$message->book.' progress='.$ok1.' chapter='.$ok2);
+            return $ok1+$ok2;
+        });
+        return 0;
+
+    }
+}

+ 57 - 0
app/Console/Commands/MqTask.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Api\Mq;
+use Illuminate\Support\Facades\Log;
+
+class MqTask extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'mq:task';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'run task';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $this->info('env='.env("RABBITMQ_HOST"));
+        $this->info('config='.config("queue.connections.rabbitmq.host"));
+        $exchange = 'router';
+        $queue = 'task';
+        $this->info(" [*] Waiting for {$queue}. To exit press CTRL+C");
+        Mq::worker($exchange,$queue,function ($message){
+            $message = json_decode(json_encode($message), true);
+            $this->info('name=',$message['name']);
+            return $this->call($message['name'],$message['param']);
+        });
+        return 0;
+    }
+}

+ 63 - 0
app/Console/Commands/MqWbwAnalyses.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Api\Mq;
+use Illuminate\Support\Facades\Log;
+
+class MqWbwAnalyses extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'mq:wbw.analyses';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $exchange = 'router';
+        $queue = 'wbw-analyses';
+        $this->info(" [*] Waiting for {$queue}. To exit press CTRL+C");
+        Log::debug("mq:wbw.analyses start.");
+        Mq::worker($exchange,$queue,function ($message){
+            $data = ['id'=>implode(',',$message)];
+            $ok = $this->call('upgrade:wbw.analyses',$data);
+            if($ok === 0){
+                $this->info("Received count=".count($message).' ok='.$ok);
+                Log::debug('mq:wbw.analyses done count='.count($message));
+            }else{
+                Log::error('mq:wbw.analyses',$data);
+            }
+            return $ok;
+        });
+
+        return 0;
+    }
+}

+ 44 - 0
app/Console/Commands/PatchWbwPageNumber.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class PatchWbwPageNumber extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'command:name';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '修补vri原文件中的页码错误';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        /**
+         */
+        return 0;
+    }
+}

+ 61 - 0
app/Console/Commands/RemoveTermCache.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\DhammaTerm;
+use App\Models\Channel;
+use Illuminate\Support\Facades\Cache;
+
+class RemoveTermCache extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'remove:term.cache {word?}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $word = $this->argument('word');
+        $channels = Channel::select('uid')->get();
+        if(empty($word)){
+
+        }else{
+            foreach ($channels as $key => $channel) {
+                $key = "/term/{$channel}/{$word}";
+                if(Cache::has($key)){
+                    $this->info('has:'.$key);
+                    Cache::forget($key);
+                }
+            }
+        }
+        return 0;
+    }
+}

+ 3 - 0
app/Console/Commands/StatisticsDict.php

@@ -40,6 +40,9 @@ class StatisticsDict extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $file = "public/statistics/lookup-monthly.csv";
         Storage::disk('local')->put($file, "");
         #按月获取数据

+ 3 - 0
app/Console/Commands/StatisticsExp.php

@@ -40,6 +40,9 @@ class StatisticsExp extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $file = "public/statistics/exp-monthly.csv";
         Storage::disk('local')->put($file, "");
         #按月获取数据

+ 3 - 0
app/Console/Commands/StatisticsNissaya.php

@@ -41,6 +41,9 @@ class StatisticsNissaya extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $nissaya_channel = Channel::where('type','nissaya')->select('uid')->get();
         $this->info('channel:'.count($nissaya_channel));
         $maxDay = 360;

+ 3 - 0
app/Console/Commands/StatisticsWbw.php

@@ -40,6 +40,9 @@ class StatisticsWbw extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $file = "public/statistics/wbw-monthly.csv";
         Storage::disk('local')->put($file, "");
         #按月获取数据

+ 11 - 4
app/Console/Commands/TestCaseMan.php

@@ -40,10 +40,17 @@ class TestCaseMan extends Command
      */
     public function handle()
     {
-		$caseman = new CaseMan();
-		$parents = $caseman->WordToBase($this->argument('word'),1);
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+		$caseMan = new CaseMan();
+        $case = $caseMan->Declension($this->argument('word'),'.n:base.','.nt.',0.5);
+        print_r($case);
+        return 0;
+
+		$parents = $caseMan->WordToBase($this->argument('word'),1);
 			# code...
-			
+
 		foreach ($parents as $base => $rows) {
 			# code...
 			if(count($rows)==0){
@@ -51,7 +58,7 @@ class TestCaseMan extends Command
 			}else{
 				$this->warn("base={$base}-(".count($rows).")");
 			}
-			
+
 			foreach ($rows as $value) {
 				# code...
 				$this->info($value['word'].'-'.$value['type'].'-'.$value['grammar'].'-'.$base);

+ 3 - 0
app/Console/Commands/TestJsonToXml.php

@@ -38,6 +38,9 @@ class TestJsonToXml extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $array = [
             'pali'=>['status'=>'7','value'=>'bārāṇasiyaṃ'],
             'real'=>['status'=>'7','value'=>'bārāṇasiyaṃ'],

+ 83 - 0
app/Console/Commands/TestMarkdownToTpl.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Controllers\ArticleController;
+use Illuminate\Support\Facades\Log;
+
+class TestMarkdownToTpl extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:markdown.tpl {item?}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        Log::info('md render start item='.$this->argument('item'));
+        $data = array();
+        $data['basic'] = <<<md
+        # 去除烦恼的五种方法(一种分类)
+
+        1 为了自己的利益而从别人处听法;
+        2 为了自己的利益而开示自己听闻过的法;
+        3 念诵听闻过、学习过的法;
+        4 一次又一次地用心思维听闻过、学习过的法;
+        5 在心中忆念自己适合的禅修业处,如十遍、十不净等。
+
+        (无碍解道,义注,1,63)
+        md;
+
+        $data['tpl'] = <<<md
+        为了自己的利益而从别人处听法;
+
+        {{168-916-2-9}}
+        md;
+
+        $article = new ArticleController;
+
+        foreach ($data as $key => $value) {
+            $_item = $this->argument('item');
+            if(!empty($_item) && $key !==$_item){
+                continue;
+            }
+            $tpl = $article->toTpl($value,
+                        'eb9e3f7f-b942-4ca4-bd6f-b7876b59a523',
+                        [
+                            'user_uid'=>'ba5463f3-72d1-4410-858e-eadd10884713',
+                            'user_id'=>4,
+                        ]
+                    );
+            var_dump($tpl);
+        }
+        return 0;
+    }
+}

+ 103 - 47
app/Console/Commands/TestMdRender.php

@@ -5,15 +5,17 @@ namespace App\Console\Commands;
 use Illuminate\Console\Command;
 use App\Http\Api\MdRender;
 use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Log;
+use App\Tools\Markdown;
 
 class TestMdRender extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan test:md.render term unity --driver=str
      * @var string
      */
-    protected $signature = 'test:md.render';
+    protected $signature = 'test:md.render {item?} {--format=} {--driver=morus}';
 
     /**
      * The console command description.
@@ -39,51 +41,105 @@ class TestMdRender extends Command
      */
     public function handle()
     {
-        $markdown = "";
-        //$markdown .= "[[isipatana]] `bla` [[dhammacakka]]\n\n";
-        //$markdown .= "前面的\n";
-        //$markdown .= "{{note|\n";
-        //$markdown .= "多**行注**释\n\n";
-        //$markdown .= "多行注释\n";
-        //$markdown .= "}}\n\n";
-        $markdown .= "## heading \n\n";
-        $markdown .= "ddd \n\n";
-        $markdown .= "- title \n";
-        $markdown .= "  \n";
-        $markdown .= "  content-1\n";
-        $markdown .= "- title-2 \n";
-        $markdown .= "  \n";
-        $markdown .= "  content-2\n";
-        $markdown .= "  \n";
-        $markdown .= "\n";
-        $markdown .= "\n";
-        $markdown .= "aaa bbb\n";
-        /*
-        $markdown .= "```\n";
-        $markdown .= "content **content**\n";
-        $markdown .= "content **content**\n";
-        $markdown .= "```\n\n";
-        */
-        /*
-        $markdown .= "{{168-916-10-37}}";
-        $markdown .= "{{exercise|1|((168-916-10-37))}}";
-
-        $markdown2 = "# heading [[isipatana]] \n\n";
-        $markdown2 .= "{{exercise\n|id=1\n|content={{168-916-10-37}}}}";
-        $markdown2 .= "{{exercise\n|id=2\n|content=# ddd}}";
-
-        $markdown2 .= "{{note|trigger=kacayana|text={{99-556-8-12}}}}";
-        $markdown2 = "aaa=bbb\n";
-        $markdown2 .= "ccc=ddd\n";
-*/
-        //echo MdRender::render($markdown,'00ae2c48-c204-4082-ae79-79ba2740d506');
-        //$wiki = MdRender::markdown2wiki($markdown2);
-        //$xml = MdRender::wiki2xml($wiki);
-        //$html = MdRender::xmlQueryId($xml, "1");
-        //$sent = MdRender::take_sentence($html);
-        //print_r($sent);
-        echo Str::markdown($markdown);
-        echo MdRender::render2($markdown,'00ae2c48-c204-4082-ae79-79ba2740d506',null,'read','nissaya');
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        Log::info('md render start item='.$this->argument('item'));
+        $data = array();
+        $data['bold'] = <<<md
+        **三十位** 经在[中间]六处为**[licchavi]**,在极果为**慧解脱**
+        md;
+
+        $data['sentence'] = <<<md
+        {{168-916-2-9}}
+        md;
+
+        $data['link'] = <<<md
+        aa `[link](wikipali.org/aa.php?view=b&c=d)` bb
+        md;
+
+        $data['term'] = <<<md
+        ## term
+        [[bhagavantu]]
+        md;
+        $data['noteMulti'] = <<<md
+        ## heading
+
+        [点击](http://127.0.0.1:3000/my/article/para/168-876?mode=edit&channel=00ae2c48-c204-4082-ae79-79ba2740d506&book=168&par=876)
+
+        ----
+
+        dfef
+
+        ```
+        bla **content**
+        {{99-556-8-12}}
+        bla **content**
+        ```
+        md;
+
+        $data['note'] = '`bla **bold** _em_ bla`';
+        $data['noteTpl'] = <<<md
+        {{note|trigger=kacayana|text=bla **bold** _em_ bla}}
+        md;
+
+        $data['noteTpl2'] = <<<md
+        {{note|trigger=kacayana|text={{99-556-8-12}}}}
+        md;
+
+        $data['trigger'] = <<<md
+        ## heading
+        ddd
+        - title
+          content-1
+        - title-2
+
+          content-2
+
+        aaa bbb
+        md;
+        $data['exercise'] = <<<md
+        {{168-916-10-37}}
+        {{exercise|1|((168-916-10-37))}}
+        {{exercise|
+        id=1|
+        content={{168-916-10-37}}
+        }}
+        {{exercise|
+        id=2|
+        content=# ddd}}
+        md;
+
+        $data['article'] = <<<md
+        {{article|
+        type=article|
+        id=27ade9ad-2d0c-4f66-b857-e9335252cc08|
+        title=第一章 戒律概说(Vinaya)|
+        style=modal}}
+        md;
+        $data['empty'] = '';
+
+        Markdown::driver($this->option('driver'));
+
+        $format = $this->option('format');
+        if(empty($format)){
+            $formats = ['react','unity','text','tex','html','simple'];
+        }else{
+            $formats = [$format];
+        }
+        foreach ($formats as $format) {
+            $this->info("format:{$format}");
+            foreach ($data as $key => $value) {
+                $_item = $this->argument('item');
+                if(!empty($_item) && $key !==$_item){
+                    continue;
+                }
+                echo MdRender::render($value,
+                                    ['00ae2c48-c204-4082-ae79-79ba2740d506'],
+                                    null,'read','translation',
+                                    $contentType="markdown",$format);
+            }
+        }
         return 0;
     }
 }

+ 21 - 21
app/Console/Commands/TestMq.php

@@ -3,18 +3,22 @@
 namespace App\Console\Commands;
 
 use Illuminate\Console\Command;
-use PhpAmqpLib\Connection\AMQPStreamConnection;
-use PhpAmqpLib\Message\AMQPMessage;
+use Illuminate\Support\Str;
 
+use App\Http\Api\Mq;
+use App\Models\Discussion;
+use App\Http\Resources\DiscussionResource;
+use App\Models\SentPr;
+use App\Http\Resources\SentPrResource;
 
 class TestMq extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan test:mq
      * @var string
      */
-    protected $signature = 'test:mq ';
+    protected $signature = 'test:mq {--discussion=} {--pr=}';
 
     /**
      * The console command description.
@@ -40,23 +44,19 @@ class TestMq extends Command
      */
     public function handle()
     {
-        //一对一
-		$connection = new AMQPStreamConnection(MQ_HOST, MQ_PORT, MQ_USERNAME, MQ_PASSWORD);
-		$channel = $connection->channel();
-		$channel->queue_declare('hello', false, true, false, false);
-
-		$msg = new AMQPMessage('Hello World!');
-		$channel->basic_publish($msg, '', 'hello');
-
-		echo " [x] Sent 'Hello World!'\n";
-		$channel->close();
-		$connection->close();
-
-        //一对多
-        $connection = new AMQPStreamConnection(MQ_HOST, MQ_PORT, MQ_USERNAME, MQ_PASSWORD);
-        $channel->exchange_declare('hello_exchange','fanout',false,true);
-        $channel->queue_declare('hello', false, true, false, false);
-        $channel->exchange_bind('hello','exchange',"");
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+		Mq::publish('hello',['hello world']);
+        $discussion = $this->option('discussion');
+        if($discussion && Str::isUuid($discussion)){
+            Mq::publish('discussion',new DiscussionResource(Discussion::find($discussion)));
+        }
+
+        $pr = $this->option('pr');
+        if($pr && Str::isUuid($pr)){
+            Mq::publish('suggestion',new SentPrResource(SentPr::where('uid',$pr)->first()));
+        }
 
         return 0;
     }

+ 85 - 14
app/Console/Commands/TestMqWorker.php

@@ -3,17 +3,17 @@
 namespace App\Console\Commands;
 
 use Illuminate\Console\Command;
-
 use PhpAmqpLib\Connection\AMQPStreamConnection;
+use PhpAmqpLib\Exchange\AMQPExchangeType;
 
 class TestMqWorker extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan test:mq.worker
      * @var string
      */
-    protected $signature = 'test:mqworker';
+    protected $signature = 'test:mq.worker';
 
     /**
      * The console command description.
@@ -39,22 +39,93 @@ class TestMqWorker extends Command
      */
     public function handle()
     {
-		$connection = new AMQPStreamConnection(MQ_HOST, MQ_PORT, MQ_USERNAME, MQ_PASSWORD);
-		$channel = $connection->channel();
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $exchange = 'router';
+        $queue = 'hello';
+        $consumerTag = 'consumer';
+        $connection = new AMQPStreamConnection(config("queue.connections.rabbitmq.host"),
+                                            config("queue.connections.rabbitmq.port"),
+                                            config("queue.connections.rabbitmq.user"),
+                                            config("queue.connections.rabbitmq.password"),
+                                            config("queue.connections.rabbitmq.virtual_host"));
+        $channel = $connection->channel();
+
+        /*
+            The following code is the same both in the consumer and the producer.
+            In this way we are sure we always have a queue to consume from and an
+                exchange where to publish messages.
+        */
+
+        /*
+            name: $queue
+            passive: false
+            durable: true // the queue will survive server restarts
+            exclusive: false // the queue can be accessed in other channels
+            auto_delete: false //the queue won't be deleted once the channel is closed.
+        */
+        $channel->queue_declare($queue, false, true, false, false);
+
+        /*
+            name: $exchange
+            type: direct
+            passive: false
+            durable: true // the exchange will survive server restarts
+            auto_delete: false //the exchange won't be deleted once the channel is closed.
+        */
+
+        $channel->exchange_declare($exchange, AMQPExchangeType::DIRECT, false, true, false);
+
+        $channel->queue_bind($queue, $exchange);
+
+        /**
+         * @param \PhpAmqpLib\Message\AMQPMessage $message
+         */
+        $process_message = function ($message)
+        {
+            echo "\n--------\n";
+            echo $message->body;
+            echo "\n--------\n";
+
+            $message->ack();
+
+            // Send a message with the string "quit" to cancel the consumer.
+            if ($message->body === 'quit') {
+                $message->getChannel()->basic_cancel($message->getConsumerTag());
+            }
+        };
 
-		$channel->queue_declare('hello', false, true, false, false);
+        /*
+            queue: Queue from where to get the messages
+            consumer_tag: Consumer identifier
+            no_local: Don't receive messages published by this consumer.
+            no_ack: If set to true, automatic acknowledgement mode will be used by this consumer. See https://www.rabbitmq.com/confirms.html for details.
+            exclusive: Request exclusive consumer access, meaning only this consumer can access the queue
+            nowait:
+            callback: A PHP Callback
+        */
 
-		echo " [*] Waiting for messages. To exit press CTRL+C\n";
+        $channel->basic_consume($queue, $consumerTag, false, false, false, false, $process_message);
 
-		$callback = function ($msg) {
-			echo ' [x] Received ', $msg->body, "\n";
-		  };
+        /**
+         * @param \PhpAmqpLib\Channel\AMQPChannel $channel
+         * @param \PhpAmqpLib\Connection\AbstractConnection $connection
+         */
+        $shutdown = function ($channel, $connection)
+        {
+            $channel->close();
+            $connection->close();
+        };
 
-		$channel->basic_consume('hello', '', false, true, false, false, $callback);
+        register_shutdown_function($shutdown, $channel, $connection);
 
-		while ($channel->is_open()) {
-			  $channel->wait();
-		  }
+        // Loop as long as the channel has callbacks registered
+        while ($channel->is_consuming()) {
+            $channel->wait(null, true);
+            // do something else
+            usleep(300000);
+        }
         return 0;
     }
 }

+ 60 - 3
app/Console/Commands/TestRedis.php

@@ -5,6 +5,7 @@ namespace App\Console\Commands;
 use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Redis;
 use Illuminate\Support\Facades\Cache;
+use App\Tools\RedisClusters;
 
 class TestRedis extends Command
 {
@@ -39,15 +40,34 @@ class TestRedis extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$value='this is a test';
 		$this->info("test redis start");
-		Redis::set("test-redis",$value);
-		if(Redis::get("test-redis")==$value){
+        $remember = Cache::store('redis')->remember('dd',10,function(){
+            return 'remember ok';
+        });
+        $this->info("test store remember value=".$remember);
+
+        $key = "test-redis";
+		Redis::set($key,$value);
+        if(Redis::exists($key)){
+            $this->info("has key ".$key);
+        }else{
+            $this->error("no key ".$key);
+        }
+        $expire = Redis::expire($key,10);
+        $this->info("key expire ".$expire);
+        $this->info('del key '.Redis::del($key));
+		Redis::set($key,$value);
+        $getValue = Redis::get($key);
+		if($getValue === $value){
 			$this->info("redis set ok ");
 		}else{
 			$this->error("redis set fail ");
 		}
-		
+
 
 		Redis::hSet("test-redis-hash",'hash',$value);
 		if(Redis::hGet("test-redis-hash",'hash')==$value){
@@ -97,6 +117,43 @@ class TestRedis extends Command
 		}else{
 			$this->error("cache::remember() fail.");
 		}
+
+        $key = 'cache-key-clusters';
+		$this->info("testing RedisClusters remember()");
+        $this->info('get='.RedisClusters::get($key));
+        RedisClusters::put($key,'RedisClusters');
+        if(RedisClusters::has($key)){
+            $this->info("RedisClusters has key value=".RedisClusters::get($key));
+        }
+        RedisClusters::forget($key);
+        if(RedisClusters::has($key)){
+            $this->error("RedisClusters forget fail ");
+        }else{
+            $this->info("RedisClusters forget successful ");
+        }
+        $null = null;
+		$value = RedisClusters::remember($key,2,function() use($null){
+			return $null;
+		});
+        $this->info("null=".$value);
+        RedisClusters::forget($key);
+        $value1 = ['data'=>'cache-key-clusters'];
+		$value = RedisClusters::remember($key,2,function() use($value1){
+			return $value1;
+		});
+
+		if(RedisClusters::has($key)){
+			$this->info("{$key} exist value=");
+            var_dump(RedisClusters::get($key));
+            sleep(3);
+            if(RedisClusters::has($key)){
+                $this->error('exp fail');
+            }else{
+                $this->info('exp successful');
+            }
+		}else{
+			$this->error("cache::remember() fail.");
+		}
         return 0;
     }
 }

+ 48 - 0
app/Console/Commands/TestSchedule.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Log;
+
+class TestSchedule extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:schedule';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        Log::info('schedule test start');
+        $this->info('schedule test start');
+        return 0;
+    }
+}

+ 77 - 0
app/Console/Commands/TestSearchPali.php

@@ -0,0 +1,77 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Tools\PaliSearch;
+
+class TestSearchPali extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan test:search.pali
+     * @var string
+     */
+    protected $signature = 'test:search.pali {word?}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $word = $this->argument('word');
+        if(empty($word)){
+            $word = 'citta';
+        }
+        $words = str_replace('_',' ',$word);
+        $words = explode(',',$words);
+        $this->info("searching word={$word} limit=10,offset=0");
+        $result = PaliSearch::search($words,[],'case',0,10);
+        if($result){
+            $this->info("word={$word} total=".$result['total']);
+        }else{
+            $this->error("word={$word} search fail");
+        }
+
+        $rpc_result = PaliSearch::book_list($words,
+                                            [],
+                                            'case');
+        $this->info('book list count='.count($rpc_result['rows']));
+
+        $this->info("searching word={$word} limit=10,offset=10");
+        $result = PaliSearch::search($words,[],'case',10,10);
+        if($result){
+            $this->info("word={$word} total=".$result['total']);
+        }else{
+            $this->error("word={$word} search fail");
+        }
+        $this->info("searching word={$word} book=267");
+        $result = PaliSearch::search($words,[267],'case',0,3);
+        if($result){
+            $this->info("word={$word} book=267 total=".$result['total']);
+        }else{
+            $this->error("word={$word} book=267 search fail");
+        }
+
+        return 0;
+    }
+}

+ 54 - 0
app/Console/Commands/TestSendMail.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Mail;
+use App\Mail\InviteMail;
+use Illuminate\Support\Str;
+
+class TestSendMail extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:send.mail';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $uuid = Str::uuid();
+        Mail::to("visuddhinanda@gmail.com")
+            ->send(new InviteMail($uuid));
+        if(Mail::failures()){
+            $this->error('send email fail');
+        }
+        return 0;
+    }
+}

+ 102 - 0
app/Console/Commands/TestTex.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use App\Tools\Export;
+
+class TestTex extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:tex';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $tex = array();
+        $content = <<<'EOF'
+% 导言区
+\documentclass[a4paper, 12pt, fontset=ubuntu]{article} % book, report, letter
+\usepackage{ctex} % Use chinese package
+
+\title{\heiti 一级标题}
+\author{\kaishu 半闲}
+\date{\today}
+
+% 正文区
+
+\begin{document}
+    \maketitle % 头部信息在正文显示
+    \newpage
+    \tableofcontents % 显示索引列
+
+    \include{section-1.tex}
+    \include{section-2.tex}
+
+\end{document}
+
+EOF;
+$tex[] = ['name'=>'main.tex','content'=>$content];
+$content = <<<'EOF'
+\section{三十位经}
+
+住在王舍城的竹林园。
+那时,三十位波婆城的比丘全是住林野者、全是常乞食者、全是穿粪扫衣者、全是但三衣者、全是尚有结缚者,他们去见世尊。
+\subsubsection{子章节1.1 标题}
+子章节1-1 正文
+\subsection{子章节1.2 标题}
+子章节1-2 正文
+EOF;
+$tex[] = ['name'=>'section-1.tex','content'=>$content];
+
+$content = <<<'EOF'
+\section{章节2 标题}
+章节2 正文
+\subsection{子章节2.1 标题}
+子章节2-1 正文
+\subsection{子章节2.2 标题}
+子章节2-2 正文
+EOF;
+
+$tex[] = ['name'=>'section-2.tex','content'=>$content];
+
+        $data = Export::ToPdf($tex);
+        if($data['ok']){
+            $filename = "export/test.pdf";
+            $this->info($data['content-type']);
+            Storage::disk('local')->put($filename, $data['data']);
+        }else{
+            $this->error($data['code'].'-'.$data['message']);
+        }
+        return 0;
+    }
+}

+ 62 - 0
app/Console/Commands/UpdateRelationTo.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\Relation;
+
+class UpdateRelationTo extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'update:relation.to';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $count=0;
+        $all=0;
+        foreach (Relation::select(['id','to'])->cursor() as $relation) {
+            $all++;
+            if(!empty($relation->to)){
+                $old = json_decode($relation->to,true);
+                if(count(array_filter(array_keys($old),'is_string'))===0){
+                    //索引数组,需要转换
+                    $new = ['case'=>$old];
+                    Relation::where('id',$relation->id)->update(['to'=>json_encode($new)]);
+                    $count++;
+                }
+            }
+        }
+        $this->info("{$count} of {$all}");
+
+        return 0;
+    }
+}

+ 89 - 0
app/Console/Commands/UpdateSentenceUnique.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\Sentence;
+use App\Models\SentHistory;
+use App\Models\Discussion;
+
+use Illuminate\Support\Facades\DB;
+
+class UpdateSentenceUnique extends Command
+{
+    /**
+     * 将channel+book+paragraph+start+end重复的数据筛查,合并
+     * 与此句相关的资源也要合并,包括,pr,history,discussion
+     * 多的句子软删除
+     * php artisan update:sentence.unique
+     * @var string
+     */
+    protected $signature = 'update:sentence.unique';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '将sentence中的重复数据合并';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $queryCount = "SELECT count(*) from (SELECT * from (SELECT book_id ,paragraph ,word_start ,word_end ,channel_uid , count(*) as co from sentences s where ver = 2  group by book_id ,paragraph ,word_start ,word_end ,channel_uid) T where co>1) TT ";
+        $total = DB::select($queryCount);
+        $querySame = "SELECT * from (SELECT book_id ,paragraph ,word_start ,word_end ,channel_uid , count(*) as co from sentences s where ver = 2  group by book_id ,paragraph ,word_start ,word_end ,channel_uid) T where co>1";
+        $query = DB::select($querySame);
+
+        $count = 0;
+        foreach ($query as $key => $value) {
+            $count++;
+            $same = Sentence::where('book_id',$value->book_id)
+                            ->where('paragraph',$value->paragraph)
+                            ->where('word_start',$value->word_start)
+                            ->where('word_end',$value->word_end)
+                            ->where('channel_uid',$value->channel_uid)
+                            ->orderBy('updated_at','desc')
+                            ->get();
+            $per = (int)($count*100 / $total[0]->count);
+            $this->info("[{$per}]-{$count} ".$same[0]->updated_at.' '.$same[1]->updated_at.' '.count($same));
+
+            for ($i=1; $i < count($same); $i++) {
+                //将旧数据的历史记录 重新定位到新数据
+                $history = SentHistory::where('sent_uid',$same[$i]->uid)
+                                      ->update(['sent_uid'=>$same[0]->uid]);
+                //将旧数据的discussion 重新定位到新数据
+                $discussion = Discussion::where('res_id',$same[$i]->uid)
+                                        ->update(['res_id'=>$same[0]->uid]);
+                $this->info("{$history}-$discussion");
+                //将旧数据的 pr 重新定位到新数据
+                //删除旧数据
+                $same[$i]->delete();
+                if($same[$i]->trashed()){
+                    $this->info('软删除成功!');
+                }else{
+                    $this->error('软删除失败!');
+                }
+            }
+
+            if($count >= 1){
+                break;
+            }
+        }
+        return 0;
+    }
+}

+ 58 - 0
app/Console/Commands/UpdateSentenceVer.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\Sentence;
+use Illuminate\Support\Str;
+
+class UpdateSentenceVer extends Command
+{
+    /**
+     * 将无channel_uid的旧版句子数据的ver修改为1.
+     * php artisan update:sentence.ver
+     * @var string
+     */
+    protected $signature = 'update:sentence.ver';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '将无channel_uid的旧版句子数据的ver修改为1';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $count = 0;
+        $total = Sentence::whereNull('channel_uid')->orWhere('channel_uid','')->count();
+        foreach (Sentence::whereNull('channel_uid')->orWhere('channel_uid','')->cursor() as $key => $value) {
+            # code...
+            $value->ver = 1;
+            $value->channel_uid = Str::uuid();
+            $value->save();
+            $count++;
+            if($count % 1000 === 0){
+                $per = (int)($count*100 / $total);
+                $this->info("[{$per}%]-{$count}");
+            }
+        }
+        $this->info("all done [{$count}]");
+        return 0;
+    }
+}

+ 3 - 0
app/Console/Commands/UpgradeAt20230227.php

@@ -37,6 +37,9 @@ class UpgradeAt20230227 extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $this->call('init:system.channel');
         $this->call('init:system.dict');
         $this->call('upgrade:dict');

+ 8 - 5
app/Console/Commands/UpgradeChapterDynamic.php

@@ -16,7 +16,7 @@ class UpgradeChapterDynamic extends Command
      *
      * @var string
      */
-    protected $signature = 'upgrade:chapterdynamic {--test}';
+    protected $signature = 'upgrade:chapter.dynamic {--test}';
 
     /**
      * The console command description.
@@ -42,6 +42,9 @@ class UpgradeChapterDynamic extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$this->info('upgrade:chapterdynamic start.');
 
         $startAt = time();
@@ -51,7 +54,7 @@ class UpgradeChapterDynamic extends Command
         $min = 30;
         $linewidth = 2;
 
-        
+
 //更新总动态
 		$this->info("更新总动态");
         $chapters = ProgressChapter::select('book','para')
@@ -65,11 +68,11 @@ class UpgradeChapterDynamic extends Command
             #章节长度
             $paraEnd = PaliText::where('book',$chapter->book)
                             ->where('paragraph',$chapter->para)
-                            ->value('chapter_len')+$chapter->para-1;            
+                            ->value('chapter_len')+$chapter->para-1;
 
             $svg = "<svg xmlns='http://www.w3.org/2000/svg'  fill='currentColor' viewBox='0 0 $img_width $img_height'>";
             $svg .= "<polyline points='";
-            for ($i=$days; $i >0 ; $i--) { 
+            for ($i=$days; $i >0 ; $i--) {
                 # code...
 
                 #这一天有多少次更新
@@ -136,7 +139,7 @@ class UpgradeChapterDynamic extends Command
 
             $svg = "<svg xmlns='http://www.w3.org/2000/svg'  fill='currentColor' viewBox='0 0 $img_width $img_height'>";
             $svg .= "<polyline points='";
-            for ($i=$days; $i >0 ; $i--) { 
+            for ($i=$days; $i >0 ; $i--) {
                 # code...
 
                 #这一天有多少次更新

+ 20 - 7
app/Console/Commands/UpgradeChapterDynamicWeekly.php

@@ -8,7 +8,8 @@ use App\Models\SentHistory;
 use App\Models\Sentence;
 use App\Models\ProgressChapter;
 use App\Models\PaliText;
-use Illuminate\Support\Facades\Cache;
+use App\Tools\RedisClusters;
+use Illuminate\Support\Facades\Log;
 
 class UpgradeChapterDynamicWeekly extends Command
 {
@@ -43,6 +44,10 @@ class UpgradeChapterDynamicWeekly extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+		Log::debug('upgrade:chapter.dynamic.weekly start.');
 		$this->info('upgrade:chapter.dynamic.weekly start.');
 
         $startAt = time();
@@ -50,6 +55,7 @@ class UpgradeChapterDynamicWeekly extends Command
 
 //更新总动态
 		$this->info("更新总动态");
+		Log::debug("task:更新总动态开始");
         $table = ProgressChapter::select('book','para')
                                     ->groupBy('book','para')
                                     ->orderBy('book');
@@ -58,6 +64,7 @@ class UpgradeChapterDynamicWeekly extends Command
         }
         $chapters = $table->get();
         $bar = $this->output->createProgressBar(count($chapters));
+        Log::debug('chapter {count} ',['count',count($chapters)]);
         foreach ($chapters as $key => $chapter) {
             #章节长度
             $paraEnd = PaliText::where('book',$chapter->book)
@@ -78,18 +85,20 @@ class UpgradeChapterDynamicWeekly extends Command
                 $progress[] = $count;
             }
             $key="/chapter_dynamic/{$chapter->book}/{$chapter->para}/global";
-            Cache::put($key,$progress,3600*24*7);
+            RedisClusters::put($key,$progress,3600*24*7);
+
             $bar->advance();
 
             if($this->option('test')){
                 $this->info("key:{$key}");
-                if(Cache::has($key)){
+                if(RedisClusters::has($key)){
                     $this->info('has key '.$key);
                 }
                 break; //调试代码
             }
         }
         $bar->finish();
+		Log::debug("task:更新总动态结束");
 
 		$time = time()- $startAt;
         $this->info("用时 {$time}");
@@ -98,14 +107,15 @@ class UpgradeChapterDynamicWeekly extends Command
 
 		$startAt = time();
         //更新chennel动态
-        $this->info('更新chennel动态');
+        $this->info('更新channel动态');
+		Log::debug("task:更新channel动态开始");
 
         $table = ProgressChapter::select('book','para','channel_id');
         if($this->option('book')){
             $table = $table->where('book',$this->option('book'));
         }
         $bar = $this->output->createProgressBar($table->count());
-
+		Log::debug("更新channel动态 {count}",['count'=>$table->count()]);
         foreach ($table->cursor() as $chapter) {
             # code...
             $max=0;
@@ -128,22 +138,25 @@ class UpgradeChapterDynamicWeekly extends Command
                 $progress[] = $count;
             }
             $key="/chapter_dynamic/{$chapter->book}/{$chapter->para}/ch_{$chapter->channel_id}";
-            Cache::put($key,$progress,3600*24*7);
+            RedisClusters::put($key,$progress,3600*24*7);
             $bar->advance();
 
             if($this->option('test')){
                 $this->info("key:{$key}");
-                if(Cache::has($key)){
+                if(RedisClusters::has($key)){
                     $this->info('has key '.$key);
                 }
                 break; //调试代码
             }
         }
         $bar->finish();
+		Log::debug("task:更新channel动态结束");
+
 		$time = time()- $startAt;
         $this->info("用时 {$time}");
 
         $this->info("upgrade:chapter.dynamic done");
+        Log::debug("task: upgrade:chapter.dynamic done");
 
         return 0;
     }

+ 20 - 12
app/Console/Commands/UpgradeCommunityTerm.php

@@ -18,7 +18,7 @@ class UpgradeCommunityTerm extends Command
      *
      * @var string
      */
-    protected $signature = 'upgrade:community.term {lang}';
+    protected $signature = 'upgrade:community.term {lang} {word?}';
 
     /**
      * The console command description.
@@ -44,23 +44,28 @@ class UpgradeCommunityTerm extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $lang = strtolower($this->argument('lang'));
         $langFamily = explode('-',$lang)[0];
-        $localTerm = ChannelApi::getSysChannel(
-            "_community_term_{$lang}_"
-        );
+        $localTerm = ChannelApi::getSysChannel("_community_term_{$lang}_");
         if(!$localTerm){
             return 1;
         }
+
         $channelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
         if($channelId === false){
             $this->error('no channel');
             return 1;
         }
-        $table = DhammaTerm::select('word')->whereIn('language',[$this->argument('lang'),$lang,$langFamily])
-                            ->groupBy('word');
-
+        $table = DhammaTerm::select(['word','tag'])
+                            ->whereIn('language',[$this->argument('lang'),$lang,$langFamily])
+                            ->groupBy(['word','tag']);
 
+        if($this->argument('word')){
+            $table = $table->where('word',$this->argument('word'));
+        }
         $words = $table->get();
         $bar = $this->output->createProgressBar(count($words));
         foreach ($words as $key => $word) {
@@ -71,6 +76,7 @@ class UpgradeCommunityTerm extends Command
              */
             $bestNote = "" ;
             $allTerm = DhammaTerm::where('word',$word->word)
+                                ->where('tag',$word->tag)
                                 ->whereIn('language',[$this->argument('lang'),$lang,$langFamily])
                                 ->get();
             $score = [];
@@ -80,12 +86,13 @@ class UpgradeCommunityTerm extends Command
                                         ->where('date_int','<=',date_timestamp_get(date_create($term->updated_at))*1000)
                                         ->sum('duration');
                 $iExp = (int)($exp/1000);
-                $noteStrLen = mb_strlen($term->note);
+                $noteStrLen = $term->note? mb_strlen($term->note,'UTF-8'):0;
                 $paliStrLen = 0;
                 $tranStrLen = 0;
                 $noteWithoutPali = "";
-                if(!empty(trim($term->note))){
-                    #查找句子模版
+                if($term->note && !empty(trim($term->note))){
+                    //计算note得分
+                    //查找句子模版
                     $pattern = "/\{\{[0-9].+?\}\}/";
                     //获取去掉句子模版的剩余部分
                     $noteWithoutPali = preg_replace($pattern,"",$term->note);
@@ -116,7 +123,6 @@ class UpgradeCommunityTerm extends Command
                             }
                         }
                     }
-
                 }
                 //计算该术语总得分
                 $score["{$key}"] = $iExp*$noteStrLen;
@@ -138,6 +144,7 @@ class UpgradeCommunityTerm extends Command
                 $term = DhammaTerm::where('channal',$localTerm)->firstOrNew(
                         [
                             "word" => $word->word,
+                            "tag" => $word->tag,
                             "channal" => $localTerm,
                         ],
                         [
@@ -146,11 +153,12 @@ class UpgradeCommunityTerm extends Command
                             'word_en' =>Tools::getWordEn($word->word),
                             'meaning' => '',
                             'language' => $this->argument('lang'),
-                            'owner' => config("app.admin.root_uuid"),
+                            'owner' => config("mint.admin.root_uuid"),
                             'editor_id' => 0,
                             'create_time' => time()*1000,
                         ]
                     );
+                    $term->tag = $word->tag;
                     $term->meaning = $hotMeaning->meaning;
                     $term->note = $bestNote;
                     $term->modify_time = time()*1000;

+ 118 - 69
app/Console/Commands/UpgradeCompound.php

@@ -5,25 +5,26 @@ use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Storage;
 use App\Models\WordIndex;
 use App\Models\WbwTemplate;
-use App\Models\UserDict;
 use App\Tools\TurboSplit;
 use App\Http\Api\DictApi;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Http;
 
 class UpgradeCompound extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php -d memory_limit=1024M artisan upgrade:compound  --api=https://next.wikipali.org/api --from=182852 --to=30000
      * @var string
      */
-    protected $signature = 'upgrade:compound {word?} {--book=} {--debug} {--test}';
+    protected $signature = 'upgrade:compound {word?} {--book=} {--debug} {--test} {--continue} {--api=} {--from=} {--to=}';
 
     /**
      * The console command description.
      *
      * @var string
      */
-    protected $description = 'Command description';
+    protected $description = 'auto split compound word';
 
 
 
@@ -44,6 +45,12 @@ class UpgradeCompound extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            $this->info('.stop exists');
+            return 0;
+        }
+        $this->info('['.date('Y-m-d H:i:s', time()).'] upgrade:compound start');
+
         $dict_id = DictApi::getSysDict('robot_compound');
         if(!$dict_id){
             $this->error('没有找到 robot_compound 字典');
@@ -52,22 +59,7 @@ class UpgradeCompound extends Command
 
 		$start = \microtime(true);
 
-		$_word = $this->argument('word');
-		if(!empty($_word)){
-			$ts = new TurboSplit();
-            if($this->option('debug')){
-                $ts->debug(true);
-            }
-			$results = $ts->splitA($_word);
-			Storage::disk('local')->put("tmp/compound1.csv", "word,type,grammar,parent,factors");
-			foreach ($results as $key => $value) {
-				# code...
-                $output = "{$value['word']},{$value['type']},{$value['grammar']},{$value['parent']},{$value['factors']}";
-                $this->info($output);
-				Storage::disk('local')->append("tmp/compound1.csv", $output);
-			}
-			return 0;
-		}
+
 
 		//
 		if($this->option('test')){
@@ -83,7 +75,10 @@ class UpgradeCompound extends Command
 			];
 			foreach ($list as $take) {
 				# code...
-				$words = WordIndex::where('final',0)->whereBetween('len',[$take[0],$take[1]])->select('word')->take($take[2])->get();
+				$words = WordIndex::where('final',0)
+                            ->whereBetween('len',[$take[0],$take[1]])
+                            ->select('word')
+                            ->take($take[2])->get();
 				foreach ($words as $word) {
 					$this->info($word->word);
 					Storage::disk('local')->append("tmp/compound.md", "## {$word->word}");
@@ -100,75 +95,129 @@ class UpgradeCompound extends Command
 			return 0;
 		}
 
-        if($this->option('book')){
+
+		$_word = $this->argument('word');
+		if(!empty($_word)){
+            $words = array((object)array('real'=>$_word));
+            $count = 1;
+		}else if($this->option('book')){
             $words = WbwTemplate::select('real')
                             ->where('book',$this->option('book'))
                             ->where('type','<>','.ctl.')
                             ->where('real','<>','')
+                            ->orderBy('real')
                             ->groupBy('real')->cursor();
+            $query = DB::select('SELECT count(*) from (
+                                    SELECT "real" from wbw_templates where book = ? and type <> ? and real <> ? group by real) T',
+                                    [$this->option('book'),'.ctl.','']);
+            $count = $query[0]->count;
         }else{
-            $words = WbwTemplate::select('real')
-                            ->where('type','<>','.ctl.')
-                            ->where('real','<>','')
-                            ->groupBy('real')->cursor();
+            $min = WordIndex::min('id');
+            $max = WordIndex::max('id');
+            if($this->option('from')){
+                $from = $min + $this->option('from');
+            }else{
+                $from = $min;
+            }
+            if($this->option('to')){
+                $to = $min + $this->option('to');
+            }else{
+                $to = $max;
+            }
+            $words = WordIndex::whereBetween('id',[$from,$to])
+                            ->where('len','>',7)
+                            ->where('len','<',51)
+                            ->orderBy('id')
+                            ->selectRaw('word as real')
+                            ->cursor();
+            $count = $to - $from + 1;
         }
 
-		$count = 0;
+		$sn = 0;
+        $wordIndex = array();
+        $result = array();
 		foreach ($words as $key => $word) {
-            UserDict::where('word',$word->real)
-                    ->where('dict_id',$dict_id)
-                    ->update(['flag'=>2]);
-			//先看目前字典里有没有
-			$isExists = UserDict::where('word',$word->real)
-								->where('dict_id',"<>",$dict_id)
-								->exists();
-
-			if($isExists){
-				$this->info("Exists:{$word->real}");
-				//continue;
-			}
-			# code...
-			$count++;
-			$this->info("{$count}:{$word->real}");
-			$ts = new TurboSplit();
+            if(\App\Tools\Tools::isStop()){
+                return 0;
+            }
+            $sn++;
+            $startAt = microtime(true);
 
+			$ts = new TurboSplit();
+            if($this->option('debug')){
+                $ts->debug(true);
+            }
+            $wordIndex[] = $word->real;
             $parts = $ts->splitA($word->real);
+            $time = round(microtime(true) - $startAt,2);
+            $percent = (int)($sn * 100 / $count);
+
+            $this->info("[{$percent}%][{$sn}] {$word->real}  {$time}s");
+
+            $resultCount = 0;
             foreach ($parts as $part) {
                 if(isset($part['type']) && $part['type'] === ".v."){
                     continue;
                 }
-
-                $new = UserDict::firstOrNew(
-                    [
-                        'word' => $part['word'],
-                        'factors' => $part['factors'],
-                        'dict_id' => $dict_id,
-                    ],
-                    [
-                        'id' => app('snowflake')->id(),
-                        'source' => '_ROBOT_',
-                        'create_time'=>(int)(microtime(true)*1000),
-                    ]
-                );
+                $resultCount++;
+                $new = array();
+                $new['word'] = $part['word'];
+                $new['factors'] = $part['factors'];
                 if(isset($part['type'])){
-                    $new->type = $part['type'];
+                    $new['type'] = $part['type'];
+                }else{
+                    $new['type'] = ".cp.";
+                }
+                if(isset($part['grammar'])){
+                    $new['grammar'] = $part['grammar'];
+                }else{
+                    $new['grammar'] = null;
+                }
+                if(isset($part['parent'])){
+                    $new['parent'] = $part['parent'];
                 }else{
-                    $new->type = ".cp.";
+                    $new['parent'] = null;
                 }
-                if(isset($part['grammar'])) $new->grammar = $part['grammar'];
-                if(isset($part['parent'])) $new->parent = $part['parent'];
-                $new->confidence = 50*$part['confidence'];
-                $new->note = $part['confidence'];
-                $new->language = 'cm';
-                $new->creator_id = 1;
-                $new->flag = 1;
-                $new->save();
+                $new['confidence'] = 50*$part['confidence'];
+                $result[] = $new;
+
+                if(!empty($_word)){
+                    $output = "[{$resultCount}],{$part['word']},{$part['type']},{$part['grammar']},{$part['parent']},{$part['factors']},{$part['confidence']}";
+                    $this->info($output);
+                }
+            }
+
+            if(count($wordIndex) % 100 ===0){
+                $this->upload($wordIndex,$result,$this->option('api'));
+                $wordIndex = array();
+                $result = array();
             }
 		}
-		//删除旧数据
-		UserDict::where('dict_id',$dict_id)->where('flag',2)->delete();
-		UserDict::where('dict_id',$dict_id)->where('flag',1)->update(['flag'=>0]);
+        $this->upload($wordIndex,$result,$this->option('api'));
+
+        $this->info('['.date('Y-m-d H:i:s', time()).'] upgrade:compound finished');
 
         return 0;
     }
+
+    private function upload($index,$words,$url=null){
+
+        if(!$url){
+            $url = config('app.url').'/api/v2/compound';
+        }else{
+            $url = $url.'/v2/compound';
+        }
+        $this->info('url = '.$url);
+        $this->info('uploading size='.strlen(json_encode($words,JSON_UNESCAPED_UNICODE)));
+        $response = Http::post($url,
+                                [
+                                    'index'=> $index,
+                                    'words'=> $words,
+                                ]);
+        if($response->ok()){
+            $this->info('upload ok');
+        }else{
+            $this->error('upload fail.');
+        }
+    }
 }

+ 27 - 14
app/Console/Commands/UpgradeDaily.php

@@ -4,6 +4,7 @@ namespace App\Console\Commands;
 
 use Illuminate\Console\Command;
 use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\Log;
 
 
 class UpgradeDaily extends Command
@@ -39,41 +40,54 @@ class UpgradeDaily extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $env = config('app.env');
+        Log::info('daily task start ',['app.env'=>$env]);
         $start = time();
 		if(app()->isLocal()==false){
+
 			$this->call('message:webhook',[
 				'listener' => 'dingtalk',
 				'url' => 'dingtalk1',
 				'title' => "后台任务",
-				'message' => " wikipali: 每日统计后台任务开始执行。",
+				'message' => " wikipali: 每日统计后台任务开始执行。app.env=".$env,
 			]);
 		}
-
-        //巴利原文段落库目录结构改变时运行
-        //$this->call('upgrade:palitext');
-        #巴利段落标签
-        //$this->call('upgrade:palitexttag');
+        Log::info('wikipali: 每日统计后台任务开始执行{app.env}',['app.env'=>$env]);
+        $message = "wikipali: 每日统计后台任务执行完毕。".$env;
 
         //更新单词首选意思
         $this->call('upgrade:dict.default.meaning');
+        $time = time()-$start;
+        $message .= "dict.default.meaning:{$time}; ";
+        $currTime = time();
+        Log::info('更新单词首选意思完毕'.$env);
 
-        #译文进度
-        $this->call('upgrade:progress');
-        $this->call('upgrade:progresschapter');
         //社区术语表
-        $this->call('upgrade:community.term',['zh-Hans']);
+        $this->call('upgrade:community.term',['lang'=>'zh-Hans']);
+        $time = time()-$currTime;
+        $message .= "community.term:{$time}; ";
+        $currTime = time();
+        Log::info('社区术语表完毕 {app.env}',['app.env'=>$env]);
 
-        # 逐词译数据库分析
-        $this->call('upgrade:wbwanalyses');
+        # 导出离线数据
+
+        $this->call('export:offline',['format'=>'lzma','--driver'=>'str']);
+        $time = time()-$currTime;
+        $message .= "export:offline:{$time}; ";
+        Log::info('导出离线数据完毕{app.env}',['app.env'=>$env]);
 
         $time = time()-$start;
+        $message .= "总时间:{$time}; ";
 
 		if(app()->isLocal()==false){
 			$this->call('message:webhook',[
 				'listener' => 'dingtalk',
 				'url' => 'dingtalk1',
 				'title' => "后台任务",
-				'message' => "wikipali: 每日统计后台任务执行完毕。用时{$time}",
+				'message' => $message.' app.env='.$env,
 			]);
 			//发送dingding消息
 			$this->call('message:webhookarticlenew',[
@@ -87,7 +101,6 @@ class UpgradeDaily extends Command
 			]);
 		}
 
-
         return 0;
     }
 }

+ 15 - 5
app/Console/Commands/UpgradeDict.php

@@ -14,7 +14,7 @@ class UpgradeDict extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan upgrade:dict
      * @var string
      */
     protected $signature = 'upgrade:dict {uuid?} {--part}';
@@ -67,17 +67,24 @@ class UpgradeDict extends Command
 									$this->error("not uuid");
 									continue;
 								}
+                                //读取 description
+                                $desFile = $dir."/description.md";
+                                if(file_exists($desFile)){
+                                    $description = file_get_contents($desFile);
+                                }else{
+                                    $description = $this->dictInfo['meta']['description'];
+                                }
 								$tableDict = DictInfo::firstOrNew([
 									"id" => $this->dictInfo['meta']['uuid']
 								]);
 								$tableDict->id = $this->dictInfo['meta']['uuid'];
 								$tableDict->name = $this->dictInfo['meta']['dictname'];
 								$tableDict->shortname = $this->dictInfo['meta']['shortname'];
-								$tableDict->description = $this->dictInfo['meta']['description'];
+								$tableDict->description = $description;
 								$tableDict->src_lang = $this->dictInfo['meta']['src_lang'];
 								$tableDict->dest_lang = $this->dictInfo['meta']['dest_lang'];
 								$tableDict->rows = $this->dictInfo['meta']['rows'];
-								$tableDict->owner_id = config("app.admin.root_uuid");
+								$tableDict->owner_id = config("mint.admin.root_uuid");
 								$tableDict->meta = json_encode($this->dictInfo['meta']);
 								$tableDict->save();
 
@@ -122,7 +129,7 @@ class UpgradeDict extends Command
 														if(isset($newPart[$part])){
 															$newPart[$part][0]++;
 														}else{
-															$partExists = Cache::remember('dict/part/'.$part,1000,function() use($part){
+															$partExists = Cache::remember('dict/part/'.$part,config('cache.expire',1000),function() use($part){
 																return UserDict::where('word',$part)->exists();
 															});
 															if(!$partExists){
@@ -208,8 +215,11 @@ class UpgradeDict extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$this->info("upgrade dict start");
-		$this->scandict(config("app.path.dict_text"));
+		$this->scandict(config("mint.path.dict_text"));
 		$this->info("upgrade dict done");
 
         return 0;

+ 6 - 2
app/Console/Commands/UpgradeDictDefaultMeaning.php

@@ -13,6 +13,7 @@ use Illuminate\Console\Command;
 use App\Models\UserDict;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Log;
+use App\Tools\RedisClusters;
 
 class UpgradeDictDefaultMeaning extends Command
 {
@@ -87,6 +88,9 @@ class UpgradeDictDefaultMeaning extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $_word = $this->argument('word');
         # 获取字典中所有的语言
         $langInDict = UserDict::select('language')->groupBy('language')->get();
@@ -107,7 +111,7 @@ class UpgradeDictDefaultMeaning extends Command
                                 ->select('word','note')
                                 ->cursor() as $word) {
                 if(!empty($word['note'])){
-                    Cache::put("dict_first_mean/{$thisLang}/{$word['word']}", mb_substr($word['note'],0,50,"UTF-8") ,30*24*3600);
+                    RedisClusters::put("dict_first_mean/{$thisLang}/{$word['word']}", mb_substr($word['note'],0,50,"UTF-8"));
                 }
                 $bar->advance();
             }
@@ -126,7 +130,7 @@ class UpgradeDictDefaultMeaning extends Command
                             if(!empty($_word) && $word['word'] === $_word ){
                                 Log::info($cacheKey.':'.$cacheValue);
                             }
-                            Cache::put($cacheKey, $cacheValue ,30*24*3600);
+                            RedisClusters::put($cacheKey, $cacheValue);
                         }
 
                         if($count % 1000 === 0){

+ 62 - 0
app/Console/Commands/UpgradeDictId.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+use App\Http\Api\DictApi;
+
+class UpgradeDictId extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:wbw.dict.id';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '修改wbw字典id';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $this->info($this->description);
+        $user_dict_id = DictApi::getSysDict('community');
+        if($user_dict_id){
+            $result = DB::select('UPDATE "user_dicts" set "dict_id"=? where "source"=? ',[$user_dict_id,'_USER_WBW_']);
+        }else{
+            $this->error('没有找到 community 字典');
+        }
+
+        $user_dict_extract_id = DictApi::getSysDict('community_extract');
+        if($user_dict_extract_id){
+            $result = DB::select('UPDATE "user_dicts" set "dict_id"=? where "source"=? ',[$user_dict_extract_id,'_SYS_USER_WBW_']);
+        }else{
+            $this->error('没有找到 community_extract 字典');
+        }
+        $this->info('all done');
+        return 0;
+    }
+}

+ 156 - 0
app/Console/Commands/UpgradeDictSysRegular.php

@@ -0,0 +1,156 @@
+<?php
+/**
+ * 生成系统规则变形词典
+ * 算法: 扫描字典里的所有单词。根据语尾表变形。
+ * 并在词库中查找是否在三藏中出现。出现的保存。
+ */
+namespace App\Console\Commands;
+
+use App\Models\UserDict;
+use App\Models\WbwTemplate;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\DB;
+use App\Http\Api\DictApi;
+use App\Tools\CaseMan;
+
+class UpgradeDictSysRegular extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan upgrade:regular jāta
+     * @var string
+     */
+    protected $signature = 'upgrade:regular {word?} {--debug}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'upgrade regular';
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $dict_id = DictApi::getSysDict('system_regular');
+        if(!$dict_id){
+            $this->error('没有找到 system_regular 字典');
+            return 1;
+        }else{
+            $this->info("system_regular :{$dict_id}");
+        }
+
+		if(empty($this->argument('word'))){
+			$words = UserDict::where('type','.n:base.')
+							->orWhere('type','.v:base.')
+							->orWhere('type','.adj:base.')
+							->orWhere('type','.ti:base.');
+            $init = UserDict::where('dict_id',$dict_id)
+                            ->update(['flag'=>0]);
+		}else{
+			$words = UserDict::where('word',$this->argument('word'))
+							->where(function($query) {
+								$query->where('type','.n:base.')
+								->orWhere('type','.v:base.')
+								->orWhere('type','.adj:base.')
+								->orWhere('type','.ti:base.');
+							});
+            $init = UserDict::where('dict_id',$dict_id)
+                            ->where('word',$this->argument('word'))
+                            ->update(['flag'=>0]);
+		}
+		$words = $words->select(['word','type','grammar'])
+						->groupBy(['word','type','grammar'])
+						->orderBy('word');
+		$query = "
+		select count(*) from (select count(*) from user_dicts ud where
+			\"type\" = '.v:base.' or
+			\"type\" = '.n:base.' or
+			\"type\" = '.ti:base.' or
+			\"type\" = '.adj:base.'
+			group by word,type,grammar) as t;
+		";
+		$count = DB::select($query);
+		$bar = $this->output->createProgressBar($count[0]->count);
+        $caseMan = new CaseMan();
+		foreach ($words->cursor() as $word) {
+            if($this->option('debug')){$this->info("{$word->word}:{$word->type}");}
+            $newWords = $caseMan->Declension($word->word,$word->type,$word->grammar,0.5);
+            if($this->option('debug')){$this->info("{$word->word}:".count($newWords));}
+            foreach ($newWords as $newWord) {
+                if(isset($newWord['type'])){
+                    $type = $newWord['type'];
+                }else{
+                    $type = \str_replace(':base','',$word->type);
+                }
+
+                $new = UserDict::firstOrNew(
+                    [
+                        'word' => $newWord['word'],
+                        'type' => $type,
+                        'grammar' => $newWord['grammar'],
+                        'parent' => $word->word,
+                        'factors' => $newWord['factors'],
+                        'dict_id' => $dict_id,
+                    ],
+                    [
+                        'id' => app('snowflake')->id(),
+                        'source' => '_ROBOT_',
+                        'create_time'=>(int)(microtime(true)*1000)
+                    ]
+                );
+                $new->confidence = 80;
+                $new->language = 'cm';
+                $new->creator_id = 1;
+                $new->flag = 1;
+                $new->save();
+            }
+
+			$bar->advance();
+		}
+		$bar->finish();
+        if(!empty($this->argument('word'))){
+			$declensions = UserDict::where('dict_id',$dict_id)
+                            ->where('parent',$this->argument('word'))
+                            ->select('word')
+                            ->groupBy('word')
+                            ->get();
+            foreach ($declensions as $key => $word) {
+                Log::debug($word->word);
+            }
+		}
+		//删除旧数据
+		$table = UserDict::where('dict_id',$dict_id);
+		if(!empty($this->argument('word'))){
+			$table = $table->where('parent',$this->argument('word'));
+		}
+		$table->where('flag',0)->delete();
+
+        //DB::enableQueryLog();
+        $newRecord = UserDict::where('dict_id',$dict_id);
+		if(!empty($this->argument('word'))){
+			$newRecord = $newRecord->where('parent',$this->argument('word'));
+		}
+		$newRecord->where('flag',1)->update(['flag'=>0]);
+        //print_r(DB::getQueryLog());
+        return 0;
+    }
+}

+ 3 - 0
app/Console/Commands/UpgradeDictSysWbwExtract.php

@@ -43,6 +43,9 @@ class UpgradeDictSysWbwExtract extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $user_dict_id = DictApi::getSysDict('community');
         if(!$user_dict_id){
             $this->error('没有找到 community 字典');

+ 4 - 1
app/Console/Commands/UpgradeDictVocabulary.php

@@ -11,7 +11,7 @@ class UpgradeDictVocabulary extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan upgrade:dict.vocabulary
      * @var string
      */
     protected $signature = 'upgrade:dict.vocabulary';
@@ -40,6 +40,9 @@ class UpgradeDictVocabulary extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $words = UserDict::where('source','_PAPER_')->selectRaw('word,count(*)')->groupBy('word')->cursor();
 
 		$bar = $this->output->createProgressBar(230000);

+ 71 - 24
app/Console/Commands/UpgradeFts.php

@@ -6,6 +6,7 @@ use Illuminate\Console\Command;
 use App\Models\BookTitle;
 use App\Models\FtsText;
 use App\Models\WbwTemplate;
+use App\Tools\PaliSearch;
 
 class UpgradeFts extends Command
 {
@@ -14,9 +15,9 @@ class UpgradeFts extends Command
      *
      * @var string
      */
-    protected $signature = 'upgrade:fts {--content : upgrade col content}
+    protected $signature = 'upgrade:fts {--content : upgrade col content only}
         {para?}
-        {--test : output log}';
+        {--test : output log only}';
 
     /**
      * The console command description.
@@ -42,37 +43,83 @@ class UpgradeFts extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 
-        if($this->option('content')){
-            if(!empty($this->argument('para'))){
-                $para = explode('-',$this->argument('para'));
+        if(!empty($this->argument('para'))){
+            $para = explode('-',$this->argument('para'));
+        }
+        for ($iBook=1; $iBook <= 217; $iBook++) {
+            if(isset($para[0]) && $para[0] != $iBook){
+                continue;
             }
-            for ($iBook=1; $iBook <= 217; $iBook++) {
-                if(isset($para[0]) && $para[0] != $iBook){
+            # code...
+            $this->info('book:'.$iBook);
+            $maxParagraph = WbwTemplate::where('book',$iBook)->max('paragraph');
+            $bar = $this->output->createProgressBar($maxParagraph-1);
+            for($iPara=1; $iPara <= $maxParagraph; $iPara++){
+                if(isset($para[1]) && $para[1] != $iPara){
+                    $bar->advance();
                     continue;
                 }
-                # code...
-                $this->info('book:'.$iBook);
-                $maxParagraph = WbwTemplate::where('book',$iBook)->max('paragraph');
-                $bar = $this->output->createProgressBar($maxParagraph-1);
-                for($iPara=1; $iPara <= $maxParagraph; $iPara++){
-                    if(isset($para[1]) && $para[1] != $iPara){
-                        $bar->advance();
-                        continue;
-                    }
-                    $content = $this->getContent($iBook,$iPara);
-                    if($this->option('test')){
-                        $this->info($content);
+                $content = $this->getContent($iBook,$iPara);
+                //查找黑体字
+                $words = WbwTemplate::where('book',$iBook)
+                                    ->where('paragraph',$iPara)
+                                    ->orderBy('wid')->get();
+                $bold1 = array();
+                $bold2 = array();
+                $bold3 = array();
+                $currBold = array();
+                foreach ($words as $word) {
+                    if($word->style==='bld'){
+                        $currBold[] = $word->real;
                     }else{
-                        //TODO update bold
-                        FtsText::where('book',$iBook)->where('paragraph',$iPara)->update(['content'=>$content]);
+                        $countBold = count($currBold);
+                        if($countBold === 1){
+                            $bold1[] = $currBold[0];
+                        }else if($countBold === 2){
+                            $bold2 = array_merge($bold2,$currBold);
+                        }else if($countBold > 0){
+                            $bold3 = array_merge($bold3,$currBold);
+                        }
+                        $currBold = [];
                     }
-                    $bar->advance();
                 }
-                $bar->finish();
-                $this->info('done');
+                $pcd_book = BookTitle::where('book',$iBook)
+                        ->where('paragraph','<=',$iPara)
+                        ->orderBy('paragraph','desc')
+                        ->first();
+                if($pcd_book){
+                    $pcd_book_id = $pcd_book->sn;
+                }else{
+                    $pcd_book_id = BookTitle::where('book',$iBook)
+                                            ->orderBy('paragraph')
+                                            ->value('sn');
+                }
+                if($this->option('test')){
+                    $this->info($content.
+                                ' pcd_book='.$pcd_book_id.
+                                ' bold1='.implode(' ',$bold1).
+                                ' bold2='.implode(' ',$bold2).
+                                ' bold3='.implode(' ',$bold3).PHP_EOL
+                                );
+                }else{
+                    $update = PaliSearch::update($iBook,
+                                                 $iPara,
+                                                implode(' ',$bold1),
+                                                implode(' ',$bold2),
+                                                implode(' ',$bold3),
+                                                $content,
+                                                $pcd_book_id);
+                }
+                $bar->advance();
             }
+            $bar->finish();
+            $this->info('done');
         }
+
         return 0;
     }
 

+ 141 - 0
app/Console/Commands/UpgradeGrammarBook.php

@@ -0,0 +1,141 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Http;
+
+use App\Http\Api\ChannelApi;
+use App\Models\DhammaTerm;
+use App\Models\Relation;
+use App\Tools\Tools;
+
+class UpgradeGrammarBook extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan upgrade:grammar.book
+     * @var string
+     */
+    protected $signature = 'upgrade:grammar.book';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $lang = 'zh-Hans';
+        $channelId = ChannelApi::getSysChannel('_System_Grammar_Term_'.strtolower($lang).'_');
+        if($channelId === false){
+            $this->error('no channel');
+            return 1;
+        }
+
+        $relations = Relation::get();
+        $result = [];
+        foreach ($relations as $key => $relation) {
+            $from = json_decode($relation->from,true);
+            $words = [];
+            if(isset($from['spell']) && !empty($from['spell'])){
+                $words[] =  $from['spell'];
+            }
+            if(isset($from['case']) && count($from['case'])>0){
+                $words = array_merge($words,$from['case']);
+            }
+            if(count($words)===0){
+                continue;
+            }
+            $word = implode('.',$words);
+            if(!isset($result[$word])){
+                $result[$word] = array();
+            }
+            $result[$word][] = $relation;
+        }
+
+        foreach ($result as $key => $rows) {
+            $this->info('## '.$key);
+
+            $caseLocal = DhammaTerm::where('channal',$channelId)
+                                ->where('word',$key)
+                                ->value('meaning');
+            if($caseLocal){
+                $title = "**[[{$key}]]** 用法表\n\n";
+            }else{
+                $title = "**{$key}** 用法表\n\n";
+            }
+            $relations = [];
+            foreach ($rows as $row) {
+                if(!isset($relations[$row['name']])){
+                    $relations[$row['name']] = array();
+                    $local = DhammaTerm::where('channal',$channelId)
+                                        ->where('word',$row['name'])
+                                        ->first();
+                    if($local){
+                        $relations[$row['name']]['meaning']=$local->meaning;
+                        $relations[$row['name']]['note']=$local->note;
+                    }else{
+                        $relations[$row['name']]['meaning']='';
+                        $relations[$row['name']]['note']='';
+                    }
+                    $relations[$row['name']]['to']=array();
+                }
+                $relations[$row['name']]['to'][] = $row['to'];
+            }
+            $table = "|名称|解释|\n";
+            $table .= "| -- | -- |\n";
+            foreach ($relations as $relation => $value) {
+                $table .= "| [[{$relation}]] | ". $value['note']." |\n";
+            }
+            $table .= "\n\n";
+            echo $title.$table;
+            //更新字典
+            $newWord = $key.'.relations';
+            $new = DhammaTerm::firstOrNew([
+                                'channal' => $channelId,
+                                'word' => $newWord,
+                            ],[
+                                'id'=>app('snowflake')->id(),
+                                'guid'=>Str::uuid(),
+                                'create_time'=>time()*1000,
+                            ]);
+            if(empty($caseLocal)){
+                $caseLocal = $key;
+            }
+            $owner = ChannelApi::getById($channelId);
+            if(!$owner){
+                $this->error('channel id error '.$channelId);
+                continue;
+            }
+            $new->word_en = strtolower($newWord);
+            $new->meaning =$caseLocal.'用法表';
+            $new->note = $table;
+            $new->language = $lang;
+            $new->editor_id = 1;
+            $new->owner = $owner['studio_id'];
+            $new->modify_time = time()*1000;
+            $new->save();
+        }
+
+        return 0;
+    }
+}

+ 75 - 0
app/Console/Commands/UpgradePageNumber.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\WbwTemplate;
+use App\Models\PageNumber;
+
+class UpgradePageNumber extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:page.number';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $table = WbwTemplate::where('type','.ctl.')->orderBy('book')->orderBy('paragraph')->cursor();
+        $pageHead = ['M','P','T','V','O'];
+        $bar = $this->output->createProgressBar(WbwTemplate::where('type','.ctl.')->count());
+        foreach ($table as $key => $value) {
+            $type = substr($value->word,0,1);
+            if(in_array($type,$pageHead)){
+                $arrPage = explode('.',$value->word);
+                if(count($arrPage)!==2){
+                    continue;
+                }
+                $page = PageNumber::firstOrNew(
+                    [
+                        'book'=>$value->book,
+                        'paragraph'=>$value->paragraph,
+                        'wid'=>$value->wid,
+                    ],
+                    [
+                        'type'=>$type,
+                        'volume'=>(int)substr($arrPage[0],1),
+                        'page'=>(int)$arrPage[1],
+                        'pcd_book_id'=>$value->pcd_book_id,
+                    ]
+                    );
+                    $page->save();
+            }
+            $bar->advance();
+        }
+        $bar->finish();
+        return 0;
+    }
+}

+ 29 - 8
app/Console/Commands/UpgradePaliText.php

@@ -15,7 +15,7 @@ class UpgradePaliText extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan upgrade:palitext 168
      * @var string
      */
     protected $signature = 'upgrade:palitext {from?} {to?}';
@@ -44,6 +44,9 @@ class UpgradePaliText extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$this->info("upgrade pali text");
 		$startTime = time();
 
@@ -56,7 +59,7 @@ class UpgradePaliText extends Command
 			$_to = $_from;
 		}
 #载入文件列表
-		$fileListFileName = config("app.path.palitext_filelist");
+		$fileListFileName = config("mint.path.palitext_filelist");
 
 		$filelist = array();
 
@@ -72,7 +75,7 @@ class UpgradePaliText extends Command
 			$arrInserString = array();
 			#载入csv数据
 			$FileName = $filelist[$from-1][1];
-			$csvFile = config("app.path.pali_title") .'/'. $from.'_pali.csv';
+			$csvFile = config("mint.path.pali_title") .'/'. $from.'_pali.csv';
 			if (($fp = fopen($csvFile, "r")) !== false) {
                 Log::info("csv load:" . $csvFile);
 				while (($data = fgetcsv($fp, 0, ',')) !== false) {
@@ -177,6 +180,10 @@ class UpgradePaliText extends Command
                     'parent' => $parent,
                     'chapter_strlen'=> $iChapter_strlen,
                 ];
+                if((int)$arrInserString[$iPar][3] < 8){
+                    $newData['title'] = strtolower($arrInserString[$iPar][6]);
+                    $newData['title_en'] = \App\Tools\Tools::getWordEn($newData['title']);
+                }
 
                 $path = [];
 
@@ -198,12 +205,26 @@ class UpgradePaliText extends Command
                     $currParent = $title_data[$currParent-1]["parent"];
                     $iLoop++;
                 }
+
+                //插入书名
                 if(count($path)>0){
-                    //插入书名
-                    $bookTitle = BookTitle::where('book',$book)
-                                        ->where('paragraph',end($path)['paragraph'])
-                                        ->value('title');
-                    $path[] = ["book"=>0,"paragraph"=>0,"title"=>$bookTitle,"level"=>0];
+                    $bookPara = end($path)['paragraph'];
+                }else{
+                    $bookPara = $paragraph;
+                }
+
+                $pcd_book = BookTitle::where('book',$book)
+                                    ->where('paragraph',$bookPara)
+                                    ->first();
+                if($pcd_book){
+                    if(empty($pcd_book)){
+                        Log::error('no pcd book:'.$book.'-'.$bookPara);
+                    }
+                    $book_id = $pcd_book->sn;
+                    if(!empty($book_id)){
+                        $newData['pcd_book_id'] = $book_id;
+                    }
+                    $path[] = ["book"=>$book_id,"paragraph"=>$book_id,"title"=>$pcd_book->title,"level"=>0];
                 }
 
                 # 将路径反向

+ 4 - 1
app/Console/Commands/UpgradePaliTextId.php

@@ -40,12 +40,15 @@ class UpgradePaliTextId extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $this->info("upgrade pali text uuid");
         $startTime = time();
 
         $bar = $this->output->createProgressBar(PaliText::count());
         #载入csv数据
-        $csvFile = config("app.path.pali_title") .'/pali_text_uuid.csv';
+        $csvFile = config("mint.path.pali_title") .'/pali_text_uuid.csv';
         if (($fp = fopen($csvFile, "r")) === false) {
             $this->error( "can not open csv file. filename=" . $csvFile. PHP_EOL) ;
             Log::error( "can not open csv file. filename=" . $csvFile) ;

+ 7 - 4
app/Console/Commands/UpgradePaliTextTag.php

@@ -41,11 +41,14 @@ class UpgradePaliTextTag extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $this->info("upgrade pali text tag");
         $startTime = time();
 
         #载入csv数据
-        $csvFile = config("app.path.pali_title") .'/pali_text_tag.csv';
+        $csvFile = config("mint.path.pali_title") .'/pali_text_tag.csv';
         if (($fp = fopen($csvFile, "r")) === false) {
             $this->error( "can not open csv file. filename=" . $csvFile. PHP_EOL) ;
             Log::error( "can not open csv file. filename=" . $csvFile) ;
@@ -58,7 +61,7 @@ class UpgradePaliTextTag extends Command
             if($inputRow%100==0){
                 $this->info($inputRow);
             }
-            
+
             //略过第一行标题行
             if ($inputRow == 1){
                 continue;
@@ -85,7 +88,7 @@ class UpgradePaliTextTag extends Command
                 foreach ($tags as $key => $tag) {
                     # code...
                     if(!empty($tag)){
-                        $tagRow = Tag::firstOrCreate(['name'=>$tag],['owner_id'=>config("app.admin.root_uuid")]);
+                        $tagRow = Tag::firstOrCreate(['name'=>$tag],['owner_id'=>config("mint.admin.root_uuid")]);
                         $tagmap = TagMap::firstOrCreate([
                                     'table_name' => 'pali_texts',
                                     'anchor_id' => $paliTextUuid,
@@ -99,7 +102,7 @@ class UpgradePaliTextTag extends Command
             }else{
                     $this->error("no palitext uuid book=$book para=$para ");
             }
-            
+
         }
         fclose($fp);
         $this->info(" $inputRow para $tagCount tags  finished. in ". time()-$startTime . "s");

+ 6 - 3
app/Console/Commands/UpgradePaliToc.php

@@ -41,6 +41,9 @@ class UpgradePaliToc extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$this->info("upgrade pali text");
 		$startTime = time();
 
@@ -62,7 +65,7 @@ class UpgradePaliToc extends Command
 		$bar = $this->output->createProgressBar($_to-$_from+1);
 		for ($from=$_from; $from <= $_to; $from++) {
 			// 打开csv文件并读取数据
-			$strFileName = config("app.path.pali_title") . "/{$from}_{$_lang}.csv";
+			$strFileName = config("mint.path.pali_title") . "/{$from}_{$_lang}.csv";
 			if(!file_exists($strFileName)){
 				continue;
 			}
@@ -81,7 +84,7 @@ class UpgradePaliToc extends Command
 								$author = "cscd4";
 							}
 							$data[6] = mb_substr($data[6],0,1024);
-							
+
 							$newData = [
 								'book'=>$from,
 								'paragraph'=>$data[2],
@@ -91,7 +94,7 @@ class UpgradePaliToc extends Command
 								'type'=>$type,
 								'language'=>$_lang,
 								'author'=>$author,
-								'share'=>1,				
+								'share'=>1,
 								'create_time'=>time()*1000,
 								'update_time'=>time()*1000,
 							];

+ 18 - 3
app/Console/Commands/UpgradePcdBookId.php

@@ -6,6 +6,8 @@ use Illuminate\Console\Command;
 use App\Models\FtsText;
 use App\Models\WbwTemplate;
 use App\Models\BookTitle;
+use App\Models\PaliText;
+use App\Models\PageNumber;
 
 class UpgradePcdBookId extends Command
 {
@@ -40,20 +42,33 @@ class UpgradePcdBookId extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $table = $this->option('table');
-        $bookTitles = BookTitle::orderBy('id')->get();
+        $bookTitles = BookTitle::orderBy('sn')->get();
         $bar = $this->output->createProgressBar(count($bookTitles));
         foreach ($bookTitles as $key => $value) {
             # code...
             if($table === 'all' || $table ==='fts'){
                 FtsText::where('book',$value->book)
                     ->where('paragraph','>=',$value->paragraph)
-                    ->update(['pcd_book_id'=>$value->id]);
+                    ->update(['pcd_book_id'=>$value->sn]);
             }
             if($table === 'all' || $table ==='wbw'){
                 WbwTemplate::where('book',$value->book)
                     ->where('paragraph','>=',$value->paragraph)
-                    ->update(['pcd_book_id'=>$value->id]);
+                    ->update(['pcd_book_id'=>$value->sn]);
+            }
+            if($table === 'all' || $table ==='pali_text'){
+                PaliText::where('book',$value->book)
+                    ->where('paragraph','>=',$value->paragraph)
+                    ->update(['pcd_book_id'=>$value->sn]);
+            }
+            if($table === 'all' || $table ==='page_number'){
+                PageNumber::where('book',$value->book)
+                    ->where('paragraph','>=',$value->paragraph)
+                    ->update(['pcd_book_id'=>$value->sn]);
             }
             $bar->advance();
         }

+ 45 - 29
app/Console/Commands/UpgradeProgress.php

@@ -8,15 +8,16 @@ use App\Models\PaliSentence;
 use App\Models\Progress;
 use App\Models\ProgressChapter;
 use App\Models\PaliText;
+use Illuminate\Support\Facades\Log;
 
 class UpgradeProgress extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan upgrade:progress --book=168 --para=916 --channel=19f53a65-81db-4b7d-8144-ac33f1217d34
      * @var string
      */
-    protected $signature = 'upgrade:progress';
+    protected $signature = 'upgrade:progress {--book=} {--para=} {--channel=}';
 
     /**
      * The console command description.
@@ -42,36 +43,51 @@ class UpgradeProgress extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$this->info('upgrade:progress start');
 		$startTime = time();
-
-        $channels = Sentence::where('strlen','>',0)
+        $book = $this->option('book');
+        $para = $this->option('para');
+        $channelId = $this->option('channel');
+        if($book && $para && $channelId){
+            $sentences = Sentence::where('strlen','>',0)
+                          ->where('book_id',$book)
+                          ->where('paragraph',$para)
+                          ->where('channel_uid',$channelId)
+                          ->groupby('book_id','paragraph','channel_uid')
+                          ->select('book_id','paragraph','channel_uid');
+        }else{
+            $sentences = Sentence::where('strlen','>',0)
                           ->where('book_id','<',1000)
                           ->where('channel_uid','<>','')
                           ->groupby('book_id','paragraph','channel_uid')
-                          ->select('book_id','paragraph','channel_uid')
-                          ->cursor();
-        $this->info('channels:',count($channels));
+                          ->select('book_id','paragraph','channel_uid');
+        }
+        $count = $sentences->count();
+        $sentences = $sentences->cursor();
+        $this->info('sentences:'.$count);
         #第二步 更新段落表
-        $bar = $this->output->createProgressBar(count($channels));
-        foreach ($channels as $channel) {
+        $bar = $this->output->createProgressBar($count);
+        foreach ($sentences as $sentence) {
             # 第二步 生成para progress 1,2,15,zh-tw
             # 计算此段落完成时间
             $finalAt = Sentence::where('strlen','>',0)
-                        ->where('book_id',$channel->book_id)
-                        ->where('paragraph',$channel->paragraph)
-                        ->where('channel_uid',$channel->channel_uid)
+                        ->where('book_id',$sentence->book_id)
+                        ->where('paragraph',$sentence->paragraph)
+                        ->where('channel_uid',$sentence->channel_uid)
                         ->max('created_at');
             $updateAt = Sentence::where('strlen','>',0)
-                        ->where('book_id',$channel->book_id)
-                        ->where('paragraph',$channel->paragraph)
-                        ->where('channel_uid',$channel->channel_uid)
+                        ->where('book_id',$sentence->book_id)
+                        ->where('paragraph',$sentence->paragraph)
+                        ->where('channel_uid',$sentence->channel_uid)
                         ->max('updated_at');
             # 查询每个段落的等效巴利语字符数
             $result_sent = Sentence::where('strlen','>',0)
-                                    ->where('book_id',$channel->book_id)
-                                    ->where('paragraph',$channel->paragraph)
-                                    ->where('channel_uid',$channel->channel_uid)
+                                    ->where('book_id',$sentence->book_id)
+                                    ->where('paragraph',$sentence->paragraph)
+                                    ->where('channel_uid',$sentence->channel_uid)
                                     ->select('word_start')
                                     ->get();
             if (count($result_sent) > 0) {
@@ -79,25 +95,25 @@ class UpgradeProgress extends Command
                 $para_strlen = 0;
                 foreach ($result_sent as $sent) {
                     # code...
-                    $para_strlen += PaliSentence::where('book',$channel->book_id)
-                                ->where('paragraph',$channel->paragraph)
+                    $para_strlen += PaliSentence::where('book',$sentence->book_id)
+                                ->where('paragraph',$sentence->paragraph)
                                 ->where('word_begin',$sent->word_start)
                                 ->value('length');
                 }
-
-                Progress::updateOrInsert(
-                    [
-                        'book'=>$channel->book_id,
-                        'para'=>$channel->paragraph,
-                        'channel_id'=>$channel->channel_uid
-                    ],
-                    [
+                $paraInfo = [
+                        'book'=>$sentence->book_id,
+                        'para'=>$sentence->paragraph,
+                        'channel_id'=>$sentence->channel_uid
+                ];
+                $paraData = [
                         'lang'=>'en',
                         'all_strlen'=>$para_strlen,
                         'public_strlen'=>$para_strlen,
                         'created_at'=>$finalAt,
                         'updated_at'=>$updateAt,
-                    ]);
+                ];
+                Log::debug('Progress updateOrInsert',['para'=>$paraInfo,'data'=>$paraData]);
+                Progress::updateOrInsert($paraInfo,$paraData);
             }
             $bar->advance();
         }

+ 62 - 31
app/Console/Commands/UpgradeProgressChapter.php

@@ -12,15 +12,16 @@ use App\Models\PaliText;
 use App\Models\Tag;
 use App\Models\TagMap;
 use App\Models\Channel;
+use App\Http\Api\MdRender;
 
 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:progresschapter';
+    protected $signature = 'upgrade:progress.chapter  {--book=} {--para=} {--channel=} {--driver=str}';
 
     /**
      * The console command description.
@@ -46,27 +47,48 @@ class UpgradeProgressChapter extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$this->info("upgrade:progresschapter start.");
 		$startTime = time();
+        $book = $this->option('book');
+        $para = $this->option('para');
+        $channelId = $this->option('channel');
+
+        \App\Tools\Markdown::driver($this->option('driver'));
 
         $tagCount=0;
         #第一步 查询有多少书有译文
-		$books = Sentence::where('strlen','>',0)
-                          ->where('book_id','<',1000)
-                          ->where('channel_uid','<>','')
-                          ->groupby('book_id')
-                          ->select('book_id')
-                          ->get();
+        if($book){
+            $books = Sentence::where('book_id',$book)
+                            ->groupby('book_id')
+                            ->select('book_id')
+                            ->get();
+        }else{
+            $books = Sentence::where('strlen','>',0)
+                ->where('book_id','<',1000)
+                ->where('channel_uid','<>','')
+                ->groupby('book_id')
+                ->select('book_id')
+                ->get();
+        }
+
 
         $bar = $this->output->createProgressBar(count($books));
 
         foreach ($books as $book) {
-            # code...
-            $chapters = PaliText::where('book',$book->book_id)
-                                ->where('level','>',0)
-                                ->where('level','<',8)
-                                ->select('paragraph','chapter_strlen','chapter_len')
-                                ->get();
+            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)
@@ -76,11 +98,14 @@ class UpgradeProgressChapter extends Command
                     $this->error('chapter_strlen is 0 book:'.$book->book_id.' paragraph:'.$chapter->paragraph.'-'.($chapter->paragraph+$chapter->chapter_len-1));
                     continue;
                 }
-                $strlen = Progress::where('book',$book->book_id)
-                                  ->whereBetween('para',[$chapter->paragraph,$chapter->paragraph+$chapter->chapter_len-1])
-                                  ->groupby('channel_id')
-                                  ->selectRaw('channel_id, sum(all_strlen) as cp_len')
-                                  ->get();
+                $table = Progress::where('book',$book->book_id)
+                                  ->whereBetween('para',[$chapter->paragraph,$chapter->paragraph+$chapter->chapter_len-1]);
+                if($channelId){
+                    $table->where('channel_id',$channelId);
+                }
+                $strlen = $table->groupby('channel_id')
+                        ->selectRaw('channel_id, sum(all_strlen) as cp_len')
+                        ->get();
                 foreach ($strlen as $final) {
                     # code...
                     # 计算此段落完成时间
@@ -99,19 +124,25 @@ class UpgradeProgressChapter extends Command
                                 ->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...
-                        $summaryText .= str_replace("\n","",$text->content);
+                        $textContent = $mdRender->convert($text->content,[$final->channel_id]);
+                        $summaryText .= str_replace("\n","",$textContent);
                         if(mb_strlen($summaryText,"UTF-8")>255){
                             break;
                         }
                     }
-                    #查询标题
-                    $title = Sentence::where('book_id',$book->book_id)
-                          ->where('paragraph',$chapter->paragraph)
-                          ->where('channel_uid',$final->channel_id)
-                          ->value('content');
 
                     //查询语言
                     $channelLang = Channel::where('uid',$final->channel_id)->value('lang');
@@ -120,7 +151,7 @@ class UpgradeProgressChapter extends Command
                             'book'=>$book->book_id,
                             'para'=>$chapter->paragraph,
                             'channel_id'=>$final->channel_id];
-                    
+
                     $rules = array(
                         'book' => 'integer',
                         'para' => 'integer',
@@ -145,20 +176,20 @@ class UpgradeProgressChapter extends Command
                     $chapterData->all_trans = $final->cp_len/$chapter_strlen;
                     $chapterData->public = $final->cp_len/$chapter_strlen;
                     $chapterData->progress = $final->cp_len/$chapter_strlen;
-                    $chapterData->title = mb_substr($title,0,255,"UTF-8");
-                    $chapterData->summary = mb_substr($summaryText,0,255,"UTF-8");
+                    $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();
 
-                    $wasCreated = $chapterData->wasRecentlyCreated; 
+                    $wasCreated = $chapterData->wasRecentlyCreated;
                     $wasChanged = $chapterData->wasChanged();
                     #查询路径
                     $path = json_decode(
                                 PaliText::where('book',$book->book_id)
                                 ->where('paragraph',$chapter->paragraph)
                                 ->value('path'));
-                    
+
                     if($path){
                         //查询标签
                         $tags = [];
@@ -176,7 +207,7 @@ class UpgradeProgressChapter extends Command
                                     # code...
                                     $tags[$taguuid['tag_id']]=1;
                                 }
-                                
+
                             }
                         }
 

+ 141 - 0
app/Console/Commands/UpgradeQuote.php

@@ -0,0 +1,141 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\PaliSentence;
+use App\Models\WbwTemplate;
+use App\Models\Sentence;
+use App\Http\Api\ChannelApi;
+use Illuminate\Support\Str;
+
+class UpgradeQuote extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:quote {book?}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $start = time();
+        $channelId = ChannelApi::getSysChannel('_System_Quote_');
+        if($channelId===false){
+            $this->error('no channel');
+            return 1;
+        }
+
+
+        for ($i=1; $i <= 217; $i++) {
+            if(!empty($this->argument('book'))){
+                if($i != $this->argument('book')){
+                    continue;
+                }
+            }
+            $pts_book=0;
+            $pts_page=0;
+            $myanmar_book=0;
+            $myanmar_page=0;
+            $cs_para=0;
+
+            $pali = PaliSentence::where('book',$i);
+            $bar = $this->output->createProgressBar(PaliSentence::where('book',$i)->count());
+            $pali = $pali->select('book','paragraph','word_begin','word_end')->cursor();
+            foreach ($pali as $value) {
+                # code...
+                $wbwContent=[];
+                $words = WbwTemplate::where("book",$value->book)
+                                    ->where("paragraph",$value->paragraph)
+                                    ->where("wid",">=",$value->word_begin)
+                                    ->where("wid","<=",$value->word_end)
+                                    ->orderBy('wid','asc')
+                                    ->get();
+                $sent = '';
+                foreach ($words as $wbw_word) {
+                    # code...
+                    if($wbw_word->type === '.ctl.'){
+                        if(substr($wbw_word->word,0,1) === 'M'){
+                            $m = explode('.',substr($wbw_word->word,1));
+                            $myanmar_book=(int)$m[0];
+                            $myanmar_page=(int)$m[1];
+                        }
+                        if(substr($wbw_word->word,0,1) === 'P'){
+                            $m = explode('.',substr($wbw_word->word,1));
+                            $pts_book=(int)$m[0];
+                            $pts_page=(int)$m[1];
+                        }
+                    }
+                    if($wbw_word->style === 'paranum'){
+                        $cs_para=(int)$wbw_word->word;
+                    }
+                }
+                $wbwContent = [
+                    'pts_book'=>$pts_book,
+                    'pts_page'=>$pts_page,
+                    'myanmar_book'=> $myanmar_book,
+                    'myanmar_page'=> $myanmar_page,
+                    'cs_para'=> $cs_para,
+                ];
+                $sent = \json_encode($wbwContent,JSON_UNESCAPED_UNICODE);
+
+                $newRow = Sentence::firstOrNew(
+                    [
+                        "book_id" => $value->book,
+                        "paragraph" => $value->paragraph,
+                        "word_start" => $value->word_begin,
+                        "word_end" => $value->word_end,
+                        "channel_uid" => $channelId,
+                    ],
+                    [
+                        'id' =>app('snowflake')->id(),
+                        'uid' =>Str::uuid(),
+                    ]
+                    );
+                $newRow->editor_uid = config("mint.admin.root_uuid");
+                $newRow->content = trim($sent);
+                $newRow->content_type = "json";
+                $newRow->strlen = mb_strlen($sent,"UTF-8");
+                $newRow->status = 10;
+                $newRow->create_time = time()*1000;
+                $newRow->modify_time = time()*1000;
+                $newRow->language = 'en';
+                $newRow->save();
+
+                $bar->advance();
+            }
+            $bar->finish();
+        }
+
+
+
+		$this->info("finished ".(time()-$start)."s");
+        return 0;
+    }
+}

+ 0 - 226
app/Console/Commands/UpgradeRegular.php

@@ -1,226 +0,0 @@
-<?php
-/**
- * 生成系统规则变形词典
- * 算法: 扫描字典里的所有单词。根据语尾表变形。
- * 并在词库中查找是否在三藏中出现。出现的保存。
- */
-namespace App\Console\Commands;
-
-use App\Models\UserDict;
-use App\Models\WbwTemplate;
-use Illuminate\Console\Command;
-use Illuminate\Support\Facades\Cache;
-use Illuminate\Support\Facades\Log;
-use Illuminate\Support\Facades\DB;
-use App\Http\Api\DictApi;
-
-class UpgradeRegular extends Command
-{
-    /**
-     * The name and signature of the console command.
-     *
-     * @var string
-     */
-    protected $signature = 'upgrade:regular {word?} {--debug}';
-
-    /**
-     * The console command description.
-     *
-     * @var string
-     */
-    protected $description = 'upgrade regular';
-    /**
-     * Create a new command instance.
-     *
-     * @return void
-     */
-    public function __construct()
-    {
-        parent::__construct();
-    }
-
-    /**
-     * Execute the console command.
-     *
-     * @return int
-     */
-    public function handle()
-    {
-        $dict_id = DictApi::getSysDict('system_regular');
-        if(!$dict_id){
-            $this->error('没有找到 system_regular 字典');
-            return 1;
-        }
-		$nounEnding = array();
-		$rowCount=0;
-		if(($handle=fopen(public_path('app/public/ending/noun.csv'),'r'))!==FALSE){
-			while(($data=fgetcsv($handle,0,','))!==FALSE){
-				$rowCount++;
-				if($rowCount==1) continue;//忽略首行
-				array_push($nounEnding,$data);
-			}
-		}
-		fclose($handle);
-
-		$adjEnding = array();
-		$rowCount=0;
-		if(($handle=fopen(public_path('app/public/ending/adj.csv'),'r'))!==FALSE){
-			while(($data=fgetcsv($handle,0,','))!==FALSE){
-				$rowCount++;
-				if($rowCount==1) continue;//忽略首行
-				array_push($adjEnding,$data);
-			}
-		}
-		fclose($handle);
-
-		$verbEnding = array();
-		$rowCount=0;
-		if(($handle=fopen(public_path('app/public/ending/verb.csv'),'r'))!==FALSE){
-			while(($data=fgetcsv($handle,0,','))!==FALSE){
-				$rowCount++;
-				if($rowCount==1) continue;//忽略首行
-				array_push($verbEnding,$data);
-			}
-		}
-		fclose($handle);
-
-		if(empty($this->argument('word'))){
-			$words = UserDict::where('type','.n:base.')
-							->orWhere('type','.v:base.')
-							->orWhere('type','.adj:base.')
-							->orWhere('type','.ti:base.');
-		}else{
-			$words = UserDict::where('word',$this->argument('word'))
-							->where(function($query) {
-								$query->where('type','.n:base.')
-								->orWhere('type','.v:base.')
-								->orWhere('type','.adj:base.')
-								->orWhere('type','.ti:base.');
-							});
-		}
-		$words = $words->select(['word','type','grammar'])
-						->groupBy(['word','type','grammar'])
-						->orderBy('word');
-		$query = "
-		select count(*) from (select count(*) from user_dicts ud where
-			\"type\" = '.v:base.' or
-			\"type\" = '.n:base.' or
-			\"type\" = '.ti:base.' or
-			\"type\" = '.adj:base.'
-			group by word,type,grammar) as t;
-		";
-		$count = DB::select($query);
-		$bar = $this->output->createProgressBar($count[0]->count);
-
-/*
-		$words = UserDict::where('word','ābandhattalakkhaṇa')
-				->select(['word','type','grammar'])
-				->groupBy(['word','type','grammar']);
-		$bar = $this->output->createProgressBar(1);
-*/
-		foreach ($words->cursor() as $word) {
-			# code...
-			switch($word->type){
-				case ".v:base.":
-					$casetable=$verbEnding;
-					break;
-				case ".n:base.":
-					$casetable = $nounEnding;
-						break;
-				case ".ti:base.":
-				case ".adj:base.":
-					$casetable = $adjEnding;
-					break;
-				case "":
-					$casetable=false;
-					break;
-				default:
-					$casetable=false;
-					break;
-			}
-			if($casetable === false){
-				continue;
-			}
-			if($this->option('debug'))  $this->info("{$word->word}:{$word->type}");
-			foreach($casetable as $thiscase){
-				if($word->type==".v:base."){
-					$endLen = (int)$thiscase[0];
-					$head = mb_substr($word->word,0,(0-$endLen),"UTF-8");//原词剩余的部分
-					$newEnding = $thiscase[1];
-					$newGrammar = $thiscase[2];
-					$newword=$head.$thiscase[1];
-					//动词不做符合规则判定
-					$isMatch = true;
-				}else{
-					$endLen = (int)$thiscase[5];
-					$end = mb_substr($word->word,0-$endLen,NULL,"UTF-8");//原词被切下来的部分
-					$head = mb_substr($word->word,0,(0-$endLen),"UTF-8");//原词剩余的部分
-					$newEnding = $thiscase[3];
-					$newGrammar = $thiscase[4];
-					$newword=$head.$thiscase[2];
-					if($word->type==".n:base."){
-						//名词
-						if($thiscase[0]==$word->grammar  && $thiscase[1]==$end){
-							//符合规则判定成功
-							$isMatch = true;
-						}else{
-							$isMatch = false;
-						}
-					}else{
-						//形容词
-						if($thiscase[1]==$end){
-							//符合规则判定成功
-							$isMatch = true;
-						}else{
-							$isMatch = false;
-						}
-					}
-
-				}
-
-				if($isMatch){
-					if($this->option('debug'))  $this->error($newword.':match');
-					//查询这个词是否在三藏存在
-					$exist = Cache::remember('palicanon/word/exists/'.$newword, 100 , function() use($newword) {
-						return WbwTemplate::where('real',$newword)->exists();
-					});
-					if($exist){
-						if($this->option('debug'))  $this->info('exist');
-						$new = UserDict::firstOrNew(
-							[
-								'word' => $newword,
-								'type' => \str_replace(':base','',$word->type),
-								'grammar' => $newGrammar,
-								'parent' => $word->word,
-								'factors' => "{$word->word}+[{$newEnding}]",
-								'dict_id' => $dict_id,
-							],
-							[
-								'id' => app('snowflake')->id(),
-								'source' => '_ROBOT_',
-								'create_time'=>(int)(microtime(true)*1000)
-							]
-						);
-						$new->confidence = 80;
-						$new->language = 'cm';
-						$new->creator_id = 1;
-						$new->flag = 1;
-						$new->save();
-					}else{
-						if($this->option('debug'))  $this->info('not exist');
-					}
-				}
-			}
-			$bar->advance();
-		}
-		$bar->finish();
-		//删除旧数据
-		$delOld = UserDict::where('dict_id',$dict_id);
-		if(!empty($this->argument('word'))){
-			$delOld = $delOld->where('word',$this->argument('word'));
-		}
-		$delOld->where('flag',0)->delete();
-		$delOld->where('flag',1)->update(['flag'=>0]);
-        return 0;
-    }
-}

+ 5 - 2
app/Console/Commands/UpgradeRelatedParagraph.php

@@ -43,12 +43,15 @@ class UpgradeRelatedParagraph extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $this->info("upgrade related.paragraph");
 		$startTime = time();
         #删除目标数据库中数据
         RelatedParagraph::where('book','>',0)->delete();
 		// 打开csv文件并读取数据
-        $strFileName = config("app.path.pali_title") . "/cs6_para.csv";
+        $strFileName = config("mint.path.pali_title") . "/cs6_para.csv";
         if(!file_exists($strFileName)){
             return 1;
         }
@@ -58,7 +61,7 @@ class UpgradeRelatedParagraph extends Command
             $this->error("can not open csv $strFileName");
             Log::error("can not open csv $strFileName");
         }
-        $bookTitles = BookTitle::orderBy('id','desc')->get();
+        $bookTitles = BookTitle::orderBy('sn','desc')->get();
 
         while (($data = fgetcsv($fp, 0, ',')) !== false) {
             if($inputRow>0){

+ 59 - 0
app/Console/Commands/UpgradeSimIndex.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\SentSim;
+use App\Models\SentSimIndex;
+use Illuminate\Support\Facades\DB;
+
+class UpgradeSimIndex extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:sim.index';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '刷新相似句索引';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $result = DB::select('select count(*) from (select sent1 from sent_sims where sim>0.5  group by sent1) T');
+        $bar = $this->output->createProgressBar($result[0]->count);
+        foreach (SentSim::selectRaw('sent1,count(*)')
+                        ->where('sim','>',0.5)
+                        ->groupBy('sent1')->cursor() as $sent) {
+            SentSimIndex::updateOrInsert(
+                ['sent_id'=>$sent->sent1],
+                ['count'=>$sent->count,]);
+            $bar->advance();
+        }
+        $bar->finish();
+        return 0;
+    }
+}

+ 3 - 0
app/Console/Commands/UpgradeTestData.php

@@ -39,6 +39,9 @@ class UpgradeTestData extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $this->call('init:cs6sentence',[$this->argument('book')]);
         $this->call('upgrade:wbw.template',[$this->argument('book')]);
         $this->call('upgrade:chapter.dynamic.weekly',["--book"=>$this->argument('book'),"--offset"=>300]);

+ 3 - 0
app/Console/Commands/UpgradeVocabulary.php

@@ -40,6 +40,9 @@ class UpgradeVocabulary extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$words = UserDict::select('word')->groupBy('word')->cursor();
 		$count=0;
 		foreach ($words as $word) {

+ 20 - 6
app/Console/Commands/UpgradeWbwAnalyses.php

@@ -11,10 +11,10 @@ class UpgradeWbwAnalyses extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan upgrade:wbw.analyses 13607580802879488
      * @var string
      */
-    protected $signature = 'upgrade:wbwanalyses {id?}';
+    protected $signature = 'upgrade:wbw.analyses {id?}';
 
     /**
      * The console command description.
@@ -40,6 +40,9 @@ class UpgradeWbwAnalyses extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		$startAt = time();
 		$this->info("upgrade:wbwanalyses start");
 
@@ -48,7 +51,8 @@ class UpgradeWbwAnalyses extends Command
         if(empty($this->argument('id'))){
             $it = Wbw::orderby('id')->cursor();
         }else{
-            $it = Wbw::where('id',$this->argument('id'))->orderby('id')->cursor();
+            $arrId = explode(',',$this->argument('id'));
+            $it = Wbw::whereIn('id',$arrId)->orderby('id')->cursor();
         }
 
         foreach ($it as $wbwrow) {
@@ -70,8 +74,14 @@ class UpgradeWbwAnalyses extends Command
                 $pali = $word->real->__toString();
                 $factors = [];
                 foreach ($word as $key => $value) {
-                    $strValue = $value->__toString();
-                    if ($strValue !== "?" && $strValue !== "" && $strValue !== ".ctl." && $strValue !== ".a." && $strValue !== " " && mb_substr($strValue, 0, 3, "UTF-8") !== "[a]" && $strValue !== "_un_auto_factormean_" && $strValue !== "_un_auto_mean_") {
+                    $strValue = trim($value->__toString());
+                    if ($strValue !== "?" &&
+                        $strValue !== "" &&
+                        $strValue !== ".ctl." &&
+                        $strValue !== ".a." &&
+                        mb_substr($strValue, 0, 3, "UTF-8") !== "[a]" &&
+                        $strValue !== "_un_auto_factormean_" &&
+                        $strValue !== "_un_auto_mean_") {
                         $iType = 0;
                         $lang = 'pali';
                         $newData = [
@@ -127,11 +137,15 @@ class UpgradeWbwAnalyses extends Command
                                 $newData['type']=6;
                                 WbwAnalysis::insert($newData);
                                 break;
+                            case 'case':
+                                $newData['type']=8;
+                                WbwAnalysis::insert($newData);
+                                break;
                             case 'rela':
                             /*
                             <rela>[{"sour_id":"p199-764-6","sour_spell":"dhammacakkappavattanatthaṃ","dest_id":"p199-764-8","dest_spell":"āmantanā","relation":"ADV","note":""}]</rela>
                             */
-                                $newData['type']=7;
+                                $newData['type']=9;
                                 $rlt = json_decode($strValue);
                                 foreach ($rlt as $rltValue) {
                                     # code...

+ 122 - 0
app/Console/Commands/UpgradeWbwParaNum.php

@@ -0,0 +1,122 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\WbwTemplate;
+
+class UpgradeWbwParaNum extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *  php artisan upgrade:wbw.para.num 130
+     * @var string
+     */
+    protected $signature = 'upgrade:wbw.para.num {book?}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(empty($this->argument('book'))){
+            $start = 1;
+            $end = 217;
+        }else{
+            $start = $this->argument('book');
+            $end = $this->argument('book');
+        }
+
+        for ($iBook=$start; $iBook <=$end ; $iBook++) {
+            $this->info("book = {$iBook}");
+
+            $startAt = time();
+            $count = WbwTemplate::where('book',$iBook)->count();
+            $bar = $this->output->createProgressBar($count);
+            $rows = WbwTemplate::where('book',$iBook)
+                                ->orderBy('paragraph')
+                                ->orderBy('wid')->cursor();
+            $start = false;
+            $bookCode='';
+            $count=0;
+            $currPara = 0;
+            $bookCodeStack = [];
+            foreach ($rows as $key => $row) {
+                $bar->advance();
+                if($row->paragraph !== $currPara){
+                    $currPara = $row->paragraph;
+                    $start = false;
+                    $bookCode='';
+                    $bookCodeStack = [];
+                }
+                if($row->word==='('){
+                    $start = true;
+                    $bookCode='';
+                    $bookCodeStack = [];
+                    continue;
+                }
+                if($start){
+                    if(is_numeric(str_replace('-','',$row->word))){
+                        if(empty($bookCode) && count($bookCodeStack)>0){
+                            //继承之前的
+                            $bookCodeList=[];
+                            foreach ($bookCodeStack as $key => $value) {
+                                $bookCodeList[] = $key;
+                            }
+                            $bookCode = $bookCodeList[0];
+                        }
+                        $dot = mb_strrpos($bookCode,'.',0,'UTF-8');
+                        if($dot === false){
+                            $bookName = $bookCode;
+                            $paraNum = $row->word;
+                        }else{
+                            $bookName = mb_substr($bookCode,0,$dot+1,'UTF-8');
+                            $paraNum = mb_substr($bookCode,$dot+1,null,'UTF-8').$row->word;
+                        }
+                        $bookName = mb_strtolower(mb_substr($bookName,0,64,'UTF-8'),'UTF-8');
+                        $bookCodeStack[$bookName] = 1;
+                        if(!empty($bookName)){
+                            WbwTemplate::where('id',$row->id)->update([
+                                'type'=>':cs.para:',
+                                'gramma'=>$bookName,
+                                'part'=>$paraNum,
+                            ]);
+                        }
+                        $count++;
+                    }else if($row->word===';'){
+                        $bookCode = '';
+                        continue;
+                    }else if($row->word===')'){
+                        $start = false;
+                        continue;
+                    }
+                    $bookCode .= $row->word;
+                }
+            }
+            $bar->finish();
+            $time = time() - $startAt;
+            $this->info(" {$time}s {$count}");
+        }
+
+        return 0;
+    }
+}

+ 6 - 3
app/Console/Commands/UpgradeWbwTemplate.php

@@ -13,7 +13,7 @@ class UpgradeWbwTemplate extends Command
 {
     /**
      * The name and signature of the console command.
-     *
+     * php artisan upgrade:wbw.template 107
      * @var string
      */
     protected $signature = 'upgrade:wbw.template {book?} {para?}';
@@ -42,6 +42,9 @@ class UpgradeWbwTemplate extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $start = time();
 		$pali = new PaliSentence;
 		if(!empty($this->argument('book'))){
@@ -88,7 +91,7 @@ class UpgradeWbwTemplate extends Command
                     'style'=> ['value'=>$wbw_word->style,'status'=>0],
                     'factors'=> ['value'=>$part,'status'=>0],
                     'factorMeaning'=> ['value'=>'','status'=>0],
-                    'confidence'=> 0.5
+                    'confidence'=> 1.0
                 ];
 
             }
@@ -107,7 +110,7 @@ class UpgradeWbwTemplate extends Command
 					'uid' =>Str::uuid(),
 				]
 				);
-            $newRow->editor_uid = config("app.admin.root_uuid");
+            $newRow->editor_uid = config("mint.admin.root_uuid");
             $newRow->content = trim($sent);
             $newRow->content_type = "json";
             $newRow->strlen = mb_strlen($sent,"UTF-8");

+ 133 - 0
app/Console/Commands/UpgradeWbwWeight.php

@@ -0,0 +1,133 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\WbwTemplate;
+
+class UpgradeWbwWeight extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan upgrade:wbw.weight 66
+     * @var string
+     */
+    protected $signature = 'upgrade:wbw.weight {book?} {para?}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'upgrade wbw template weight by word bold ';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $book = $this->argument('book');
+        $para = $this->argument('para');
+
+        for ($iBook=1; $iBook <= 217 ; $iBook++) {
+            if($book && $book != $iBook ){
+                continue;
+            }
+            $this->info('running book='.$iBook);
+            $maxPara = WbwTemplate::where("book",$iBook)->max('paragraph');
+		    $bar = $this->output->createProgressBar($maxPara);
+            //以段落为单元计算权重
+            for ($iPara=1; $iPara <= $maxPara ; $iPara++) {
+                if($para && $para != $iPara ){
+                    continue;
+                }
+                $bar->advance();
+                $words = WbwTemplate::where("book",$iBook)
+                                    ->where("paragraph",$iPara)
+                                    ->orderBy('wid','asc')
+                                    ->get();
+
+                $katama = false; //单词前面是否有katama
+                $katamaDis = 0; //单词与katama的距离
+                $katamaWeight = 1; //katama 权重
+                $arrKatama = array();//katama 权重结果
+                //
+                /**
+                 * 先计算katama权重
+                 * 将每个单词的katama权重放在arrKatama数组里面
+                 */
+                for ($iWord=0; $iWord < count($words); $iWord++) {
+                    //计算katama加分
+                    if(empty($words[$iWord]->real)){
+                        $katama = false;
+                        $katamaWeight = 0;
+                    }
+                    if($katama){
+                        $katamaDis++;
+                        $katamaWeight = 463 * (pow(0.2, ($katamaDis-1))+1);
+                    }
+                    $arrKatama[] = $katamaWeight;
+                    if(mb_substr($words[$iWord]->real,0,5,"UTF-8") === 'katam'){
+                        $katama = true;
+                        $katamaDis = 0;
+                    }
+                }
+                $start = -1; //黑体字开始
+                $bold = 0; //连续黑体字计数器
+                for ($iWord=0; $iWord < count($words); $iWord++) {
+                    $wid = $words[$iWord]->wid;
+                    $weight = 1.01;
+                    WbwTemplate::where('id',$words[$iWord]->id)->update(['weight'=>$weight*1000]);
+
+                    if($words[$iWord]->style === 'bld' && !empty($words[$iWord]->real)){
+                        //是黑体字
+                        if($start === -1){
+                            //黑体计数尚未开始
+                            $start = $iWord;
+                            $bold = 1;
+                        }else{
+                            $bold++;
+                        }
+                    }else{
+                        if($start>=0){
+                            /**
+                             * 某词自己不是黑体,但是前面的词是黑体字
+                             * 先保存这个词
+                             * 然后,修改前面的黑体字单词的权重
+                             *
+                            */
+                            $result = WbwTemplate::where('id',$words[$iWord]->id)
+                                                    ->update(['weight'=>floor(($weight+$arrKatama[$iWord])*1000)]);
+                            $weight = 1.01 + 23 * pow(10,(2-$bold));
+                            for ($i=$start; $i < $iWord ; $i++) {
+                                $result = WbwTemplate::where('id',$words[$i]->id)
+                                                    ->update(['weight'=>floor(($weight+$arrKatama[$i])*1000)]);
+                            }
+
+                            $start = -1;
+                            $bold = 0;
+                        }else{
+                            //前词不是黑体
+                            $result = WbwTemplate::where('id',$words[$iWord]->id)
+                                        ->update(['weight'=>floor(($weight+$arrKatama[$iWord])*1000)]);
+                        }
+                    }
+                }
+            }
+            $bar->finish();
+        }
+        return 0;
+    }
+}

+ 20 - 3
app/Console/Commands/UpgradeWeekly.php

@@ -37,11 +37,28 @@ class UpgradeWeekly extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        #译文进度
+        $this->call('upgrade:progress');
+        $time = time()-$currTime;
+        $message .= "progress:{$time}; ";
+        $currTime = time();
+
+        $this->call('upgrade:progress.chapter');
+        $time = time()-$currTime;
+        $message .= "progress.chapter:{$time}; ";
+        $currTime = time();
+
+        # 逐词译数据库分析
+        $this->call('upgrade:wbw.analyses');
+        $time = time()-$currTime;
+        $message .= "wbw.analyses:{$time}; ";
+
         # 段落更新图
-        $this->call('upgrade:chapterdynamic');
+        $this->call('upgrade:chapter.dynamic');
         $this->call('upgrade:chapter.dynamic.weekly');
-        $this->call('export:offline');
-
         return 0;
     }
 }

+ 25 - 2
app/Console/Commands/UpgradeWordPart.php

@@ -3,8 +3,11 @@
 namespace App\Console\Commands;
 
 use App\Models\WordPart;
+use App\Models\UserDict;
+
 use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\DB;
 
 class UpgradeWordPart extends Command
 {
@@ -13,7 +16,7 @@ class UpgradeWordPart extends Command
      *
      * @var string
      */
-    protected $signature = 'upgrade:wordpart';
+    protected $signature = 'upgrade:word.part';
 
     /**
      * The console command description.
@@ -39,8 +42,28 @@ class UpgradeWordPart extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
+        $delete = WordPart::where('id','>',0)->delete();
+        #载入纸质词典数据
+        $paper = UserDict::selectRaw('word,count(*)')->where("source",'_PAPER_')->groupBy('word')->cursor();
+        $sql = "select
+                      count(*) from (
+                        select word, count(*) from user_dicts ud where source = '_PAPER_' group by word) as T";
+        $count = DB::select($sql);
+        $bar = $this->output->createProgressBar($count[0]->count);
+        foreach ($paper as $key => $word) {
+            $newWord = new WordPart;
+            $newWord->word = $word->word;
+            $newWord->weight = $word->count;
+            $newWord->save();
+            $bar->advance();
+        }
+        $bar->finish();
+
 		#载入csv数据
-		$csvFile = config("app.path.dict_text") .'/system/part.csv';
+		$csvFile = config("mint.path.dict_text") .'/system/part2.csv';
 		if (($fp = fopen($csvFile, "r")) !== false) {
 			Log::info("csv load:" . $csvFile);
 			while (($data = fgetcsv($fp, 0, ',')) !== false) {

+ 3 - 0
app/Console/Commands/UuidViranyani.php

@@ -50,6 +50,9 @@ class UuidViranyani extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
         $old = "C1AB2ABF-EAA8-4EEF-B4D9-3854321852B4";
 		$result = DB::select('UPDATE "articles" set "owner"=? where "owner"=? ',[strtolower($old),$old]);
 		$result = DB::select('UPDATE "channels" set "owner_uid"=? where "owner_uid"=? ',[strtolower($old),$old]);

+ 7 - 5
app/Console/Commands/WebHook.php

@@ -42,21 +42,23 @@ class WebHook extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		switch ($this->argument('listener')) {
 			case 'weixin':
-				# code...
 				break;
 			case 'dingtalk':
 				# code...
 				$url = $this->url[$this->argument('url')];
 				$param = [
 				"markdown"=> [
-					"title"=> $this->argument('title'), 
-					"text"=> $this->argument('message'), 
-				], 
+					"title"=> $this->argument('title'),
+					"text"=> $this->argument('message'),
+				],
 				"msgtype"=>"markdown"
 				];
-				break;				
+				break;
 			default:
 				# code...
 				break;

+ 14 - 11
app/Console/Commands/WebHookArticleNew.php

@@ -38,8 +38,11 @@ class WebHookArticleNew extends Command
      */
     public function handle()
     {
+        if(\App\Tools\Tools::isStop()){
+            return 0;
+        }
 		# 获取最新文章数据
-		$url = env('APP_URL',"http://127.0.0.1:8000")."/api/v2/progress?view=chapter&channel_type=translation";
+		$url = config('app.url')."/api/v2/progress?view=chapter&channel_type=translation";
 
 		$response = Http::get($url);
 		if($response->successful()){
@@ -47,7 +50,7 @@ class WebHookArticleNew extends Command
 			$data = $response['data']['rows'];
 			$title = "2022-7-3更新";
 			$message = "# wikipali:最新更新\n\n";
-			for ($i=0; $i < 4; $i++) { 
+			for ($i=0; $i < 4; $i++) {
 				# code...
 				$row = $data[$i];
 				$book = $row['book'];
@@ -58,11 +61,11 @@ class WebHookArticleNew extends Command
 				}else{
 					$title = $row['toc'];
 				}
-                
-				$link = env('APP_URL',"http://127.0.0.1:8000")."/app/article/index.php?view=chapter&book={$book}&par={$para}&channel={$channel_id}";
-				$message .= "1. [{$title}]({$link})\n";				
+
+				$link = config('app.url')."/app/article/index.php?view=chapter&book={$book}&par={$para}&channel={$channel_id}";
+				$message .= "1. [{$title}]({$link})\n";
 			}
-			$link = env('APP_URL',"http://127.0.0.1:8000")."/app/palicanon";
+			$link = config('app.url')."/app/palicanon";
 			$message .= "\n [更多]({$link})";
 			$this->info($message);
 			$url = $this->argument('host');
@@ -70,9 +73,9 @@ class WebHookArticleNew extends Command
 				case "dingtalk":
 					$param = [
 						"markdown"=> [
-							"title"=> $title, 
-							"text"=> $message, 
-						], 
+							"title"=> $title,
+							"text"=> $message,
+						],
 						"msgtype"=>"markdown"
 						];
 					break;
@@ -80,8 +83,8 @@ class WebHookArticleNew extends Command
 					$param = [
 						"msgtype"=>"markdown",
 						"markdown"=> [
-							"content"=> $message, 
-						], 
+							"content"=> $message,
+						],
 						];
 					break;
 			}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است