Bläddra i källkod

Merge pull request #1157 from visuddhinanda/laravel

Laravel
visuddhinanda 2 år sedan
förälder
incheckning
676bff315a
100 ändrade filer med 12778 tillägg och 721 borttagningar
  1. 8 0
      .env.example
  2. 11 0
      about.md
  3. 66 0
      app/Console/Commands/ExportChannel.php
  4. 64 0
      app/Console/Commands/ExportChapterIndex.php
  5. 76 0
      app/Console/Commands/ExportNissaya.php
  6. 55 0
      app/Console/Commands/ExportOffline.php
  7. 71 0
      app/Console/Commands/ExportPalitext.php
  8. 94 0
      app/Console/Commands/ExportSentence.php
  9. 61 0
      app/Console/Commands/ExportTag.php
  10. 59 0
      app/Console/Commands/ExportTagmap.php
  11. 40 43
      app/Console/Commands/InitCs6sentence.php
  12. 105 0
      app/Console/Commands/InitSystemChannel.php
  13. 92 0
      app/Console/Commands/InitSystemDict.php
  14. 86 0
      app/Console/Commands/StatisticsNissaya.php
  15. 62 0
      app/Console/Commands/TestCaseMan.php
  16. 50 0
      app/Console/Commands/TestJsonToXml.php
  17. 75 0
      app/Console/Commands/TestMdRender.php
  18. 63 0
      app/Console/Commands/TestMq.php
  19. 60 0
      app/Console/Commands/TestMqWorker.php
  20. 49 0
      app/Console/Commands/UpgradeAt20230227.php
  21. 150 0
      app/Console/Commands/UpgradeChapterDynamicWeekly.php
  22. 165 0
      app/Console/Commands/UpgradeCommunityTerm.php
  23. 90 36
      app/Console/Commands/UpgradeCompound.php
  24. 7 3
      app/Console/Commands/UpgradeDaily.php
  25. 102 38
      app/Console/Commands/UpgradeDict.php
  26. 142 0
      app/Console/Commands/UpgradeDictDefaultMeaning.php
  27. 136 0
      app/Console/Commands/UpgradeDictSysWbwExtract.php
  28. 61 0
      app/Console/Commands/UpgradeDictVocabulary.php
  29. 97 0
      app/Console/Commands/UpgradeFts.php
  30. 124 116
      app/Console/Commands/UpgradePaliText.php
  31. 64 0
      app/Console/Commands/UpgradePcdBookId.php
  32. 51 22
      app/Console/Commands/UpgradeRegular.php
  33. 106 0
      app/Console/Commands/UpgradeRelatedParagraph.php
  34. 49 0
      app/Console/Commands/UpgradeTestData.php
  35. 16 2
      app/Console/Commands/UpgradeWbwAnalyses.php
  36. 125 0
      app/Console/Commands/UpgradeWbwTemplate.php
  37. 47 0
      app/Console/Commands/UpgradeWeekly.php
  38. 72 0
      app/Console/Commands/UuidViranyani.php
  39. 4 0
      app/Console/Kernel.php
  40. 30 0
      app/Console/Workers/Workers.php
  41. 34 0
      app/Http/Api/AuthApi.php
  42. 43 0
      app/Http/Api/ChannelApi.php
  43. 81 0
      app/Http/Api/DictApi.php
  44. 18 0
      app/Http/Api/GroupApi.php
  45. 246 0
      app/Http/Api/MdRender.php
  46. 18 0
      app/Http/Api/PaliTextApi.php
  47. 176 0
      app/Http/Api/ShareApi.php
  48. 41 0
      app/Http/Api/StudioApi.php
  49. 31 0
      app/Http/Api/SuggestionApi.php
  50. 266 0
      app/Http/Api/TemplateRender.php
  51. 39 0
      app/Http/Api/UserApi.php
  52. 395 0
      app/Http/Controllers/ArticleController.php
  53. 178 0
      app/Http/Controllers/ArticleMapController.php
  54. 130 0
      app/Http/Controllers/ArticleProgressController.php
  55. 109 0
      app/Http/Controllers/AuthController.php
  56. 67 0
      app/Http/Controllers/CaseController.php
  57. 485 6
      app/Http/Controllers/ChannelController.php
  58. 265 0
      app/Http/Controllers/CollectionController.php
  59. 3 2
      app/Http/Controllers/Controller.php
  60. 858 0
      app/Http/Controllers/CorpusController.php
  61. 255 0
      app/Http/Controllers/CourseController.php
  62. 282 0
      app/Http/Controllers/CourseMemberController.php
  63. 456 85
      app/Http/Controllers/DhammaTermController.php
  64. 225 0
      app/Http/Controllers/DictController.php
  65. 127 0
      app/Http/Controllers/DictMeaningController.php
  66. 255 0
      app/Http/Controllers/DiscussionController.php
  67. 171 0
      app/Http/Controllers/ExerciseController.php
  68. 143 0
      app/Http/Controllers/ExportWbwController.php
  69. 83 0
      app/Http/Controllers/GrammarGuideController.php
  70. 237 0
      app/Http/Controllers/GroupController.php
  71. 164 0
      app/Http/Controllers/GroupMemberController.php
  72. 340 0
      app/Http/Controllers/NissayaEndingController.php
  73. 105 21
      app/Http/Controllers/PaliTextController.php
  74. 136 73
      app/Http/Controllers/ProgressChapterController.php
  75. 113 0
      app/Http/Controllers/RelatedParagraphController.php
  76. 237 0
      app/Http/Controllers/RelationController.php
  77. 341 0
      app/Http/Controllers/SearchController.php
  78. 80 25
      app/Http/Controllers/SentPrController.php
  79. 116 0
      app/Http/Controllers/SentSimController.php
  80. 228 94
      app/Http/Controllers/SentenceController.php
  81. 196 0
      app/Http/Controllers/ShareController.php
  82. 88 0
      app/Http/Controllers/StudioController.php
  83. 74 0
      app/Http/Controllers/SuggestionController.php
  84. 94 1
      app/Http/Controllers/TagController.php
  85. 95 0
      app/Http/Controllers/TermVocabularyController.php
  86. 81 0
      app/Http/Controllers/UploadController.php
  87. 76 0
      app/Http/Controllers/UserController.php
  88. 131 82
      app/Http/Controllers/UserDictController.php
  89. 113 0
      app/Http/Controllers/UserOperationDailyController.php
  90. 150 0
      app/Http/Controllers/UserStatisticController.php
  91. 57 30
      app/Http/Controllers/ViewController.php
  92. 80 0
      app/Http/Controllers/VocabularyController.php
  93. 189 0
      app/Http/Controllers/WbwController.php
  94. 463 0
      app/Http/Controllers/WbwLookupController.php
  95. 94 0
      app/Http/Controllers/WordIndexController.php
  96. 3 2
      app/Http/Kernel.php
  97. 32 0
      app/Http/Middleware/ApiLog.php
  98. 0 40
      app/Http/Middleware/EnableCrossRequestMiddleware.php
  99. 256 0
      app/Http/Middleware/UserOperation.php
  100. 44 0
      app/Http/Requests/CollectionRequest.php

+ 8 - 0
.env.example

@@ -7,6 +7,7 @@ APP_ENV=local
 APP_KEY=
 APP_DEBUG=true
 APP_URL=http://localhost
+API_URL=http://localhost/api
 
 LOG_CHANNEL=stack
 LOG_DEPRECATIONS_CHANNEL=null
@@ -63,4 +64,11 @@ ASSETS_SERVER="https://assets-hk.wikipali.org"
 SNOWFLAKE_DATA_CENTER_ID=1
 SNOWFLAKE_WORKER_ID=1
 
+MQ_HOST="localhost"
+MQ_PORT=5672
+MQ_USERNAME="guest"
+MQ_PASSWORD="guest"
+
+
+
 

+ 11 - 0
about.md

@@ -0,0 +1,11 @@
+# wikipali
+
+巴利圣典教学与研究社区
+
+- 巴利圣典阅读与翻译
+- 在线课程发布与学习
+- 在线巴利语学习工具
+
+wikipali是一个在线巴利圣典学习,翻译,研究平台。也是一个巴利知识分享社区。
+不断完善的学习工具让巴利圣典的学习变得更容易。并为专业的翻译和研究者提供便利的创作和成功发布功能。
+我们希望,人人都能从巴利圣典中受益。

+ 66 - 0
app/Console/Commands/ExportChannel.php

@@ -0,0 +1,66 @@
+<?php
+/**
+ * 导出离线用的channel数据
+ */
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use App\Models\Channel;
+
+class ExportChannel extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:channel';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '导出离线用的channel数据';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    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']);
+        $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,
+                            ]);
+            $bar->advance();
+        }
+        fclose($file);
+        $bar->finish();
+        return 0;
+    }
+}

+ 64 - 0
app/Console/Commands/ExportChapterIndex.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use App\Models\ProgressChapter;
+
+class ExportChapterIndex extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:chapter.index';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'export chapter index';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    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,
+                            ]);
+            $bar->advance();
+        }
+        fclose($file);
+        $bar->finish();
+        return 0;
+    }
+}

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

@@ -0,0 +1,76 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use App\Models\Channel;
+use App\Models\Sentence;
+
+class ExportNissaya extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:nissaya';
+    protected $my = ["ႁႏၵ","ခ္","ဃ္","ဆ္","ဈ္","ည္","ဌ္","ဎ္","ထ္","ဓ္","ဖ္","ဘ္","က္","ဂ္","စ္","ဇ္","ဉ္","ဠ္","ဋ္","ဍ္","ဏ္","တ္","ဒ္","န္","ဟ္","ပ္","ဗ္","မ္","ယ္","ရ္","လ္","ဝ္","သ္","င္","င်္","ဿ","ခ","ဃ","ဆ","ဈ","စျ","ည","ဌ","ဎ","ထ","ဓ","ဖ","ဘ","က","ဂ","စ","ဇ","ဉ","ဠ","ဋ","ဍ","ဏ","တ","ဒ","န","ဟ","ပ","ဗ","မ","ယ","ရ","႐","လ","ဝ","သ","aျ္","aွ္","aြ္","aြ","ၱ","ၳ","ၵ","ၶ","ၬ","ၭ","ၠ","ၡ","ၢ","ၣ","ၸ","ၹ","ၺ","႓","ၥ","ၧ","ၨ","ၩ","်","ျ","ႅ","ၼ","ွ","ႇ","ႆ","ၷ","ၲ","႒","႗","ၯ","ၮ","႑","kaၤ","gaၤ","khaၤ","ghaၤ","aှ","aိံ","aုံ","aော","aေါ","aအံ","aဣံ","aဥံ","aံ","aာ","aါ","aိ","aီ","aု","aဳ","aူ","aေ","အါ","အာ","အ","ဣ","ဤ","ဥ","ဦ","ဧ","ဩ","ႏ","ၪ","a္","္","aံ","ေss","ေkh","ေgh","ေch","ေjh","ေññ","ေṭh","ေḍh","ေth","ေdh","ေph","ေbh","ေk","ေg","ေc","ေj","ေñ","ေḷ","ေṭ","ေḍ","ေṇ","ေt","ေd","ေn","ေh","ေp","ေb","ေm","ေy","ေr","ေl","ေv","ေs","ေy","ေv","ေr","ea","eā","၁","၂","၃","၄","၅","၆","၇","၈","၉","၀","း","့","။","၊"];
+    protected $en = ["ndra","kh","gh","ch","jh","ññ","ṭh","ḍh","th","dh","ph","bh","k","g","c","j","ñ","ḷ","ṭ","ḍ","ṇ","t","d","n","h","p","b","m","y","r","l","v","s","ṅ","ṅ","ssa","kha","gha","cha","jha","jha","ñña","ṭha","ḍha","tha","dha","pha","bha","ka","ga","ca","ja","ña","ḷa","ṭa","ḍa","ṇa","ta","da","na","ha","pa","ba","ma","ya","ra","ra","la","va","sa","ya","va","ra","ra","္ta","္tha","္da","္dha","္ṭa","္ṭha","္ka","္kha","္ga","္gha","္pa","္pha","္ba","္bha","္ca","္cha","္ja","္jha","္a","္ya","္la","္ma","္va","္ha","ssa","na","ta","ṭṭha","ṭṭa","ḍḍha","ḍḍa","ṇḍa","ṅka","ṅga","ṅkha","ṅgha","ha","iṃ","uṃ","o","o","aṃ","iṃ","uṃ","aṃ","ā","ā","i","ī","u","u","ū","e","ā","ā","a","i","ī","u","ū","e","o","n","ñ","","","aṃ","sse","khe","ghe","che","jhe","ññe","ṭhe","ḍhe","the","dhe","phe","bhe","ke","ge","ce","je","ñe","ḷe","ṭe","ḍe","ṇe","te","de","ne","he","pe","be","me","ye","re","le","ve","se","ye","ve","re","e","o","1","2","3","4","5","6","7","8","9","0","”","’",".",","];
+
+    /**
+     * 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()
+    {
+        $nissaya_channel = Channel::where('type','nissaya')->select('uid')->get();
+        $channels = [];
+        foreach ($nissaya_channel as $key => $value) {
+            # code...
+            $channels[] = $value->uid;
+        }
+        $this->info('channel:'.count($channels));
+        $filename = "public/export/nissaya.csv";
+        Storage::disk('local')->put($filename, "");
+        $file = fopen(storage_path("app/$filename"),"w");
+        $bar = $this->output->createProgressBar(Sentence::whereIn('channel_uid',$channels)->count());
+        foreach (Sentence::whereIn('channel_uid',$channels)->select('content')->cursor() as $sent) {
+            $lines = explode("\n",$sent->content);
+            foreach ($lines as $key => $line) {
+                # code...
+                if(substr_count(trim($line),'=') === 1){
+                    $nissaya_str = explode('=',$line);
+                    $pali = $this->my2en($nissaya_str[0]);
+                    fputcsv($file,[$pali,$nissaya_str[1]]);
+                }
+            }
+            $bar->advance();
+        }
+        fclose($file);
+        $bar->finish();
+        return 0;
+    }
+
+    public function my2en($my){
+        return str_replace($this->my,$this->en,$my);
+    }
+}

+ 55 - 0
app/Console/Commands/ExportOffline.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class ExportOffline extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:offline';
+
+    /**
+     * 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()
+    {
+        //导出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"));
+        return 0;
+    }
+}

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

@@ -0,0 +1,71 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use App\Models\PaliText;
+
+class ExportPalitext extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:pali.text';
+
+    /**
+     * 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()
+    {
+        $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']);
+        $bar = $this->output->createProgressBar(PaliText::count());
+        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,[
+                            $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,
+                            ]);
+            $bar->advance();
+        }
+        fclose($file);
+        $bar->finish();
+
+        return 0;
+    }
+}

+ 94 - 0
app/Console/Commands/ExportSentence.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use App\Models\Sentence;
+use App\Models\Channel;
+use App\Http\Api\ChannelApi;
+
+class ExportSentence extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:sentence {--channel=} {--type=translation}';
+
+    /**
+     * 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()
+    {
+        $channels = [];
+        $channel_id = $this->option('channel');
+        if($channel_id){
+            $file_suf = $channel_id;
+            $channels[] = $channel_id;
+        }else{
+            $channel_type = $this->option('type');
+            $file_suf = $channel_type;
+            if($channel_type === "original"){
+                $pali_channel = ChannelApi::getSysChannel("_System_Pali_VRI_");
+                if($pali_channel === false){
+                    return 0;
+                }
+                $channels[] = $pali_channel;
+            }else{
+                $nissaya_channel = Channel::where('type',$channel_type)->where('status',30)->select('uid')->get();
+                foreach ($nissaya_channel as $key => $value) {
+                    # code...
+                    $channels[] = $value->uid;
+                }
+            }
+        }
+        $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,
+                            ]);
+            $bar->advance();
+        }
+        fclose($file);
+        $bar->finish();
+        return 0;
+    }
+}

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

@@ -0,0 +1,61 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use App\Models\Tag;
+
+class ExportTag extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:tag';
+
+    /**
+     * 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()
+    {
+        $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']);
+        $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,
+                            ]);
+            $bar->advance();
+        }
+        fclose($file);
+        $bar->finish();
+        return 0;
+    }
+}

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

@@ -0,0 +1,59 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use App\Models\TagMap;
+class ExportTagmap extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'export:tag.map';
+
+    /**
+     * 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()
+    {
+        $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']);
+        $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,
+                            ]);
+            $bar->advance();
+        }
+        fclose($file);
+        $bar->finish();
+        return 0;
+    }
+}

+ 40 - 43
app/Console/Commands/InitCs6sentence.php

@@ -7,6 +7,8 @@ use App\Models\PaliSentence;
 use App\Models\WbwTemplate;
 use App\Models\Sentence;
 use Illuminate\Support\Str;
+use App\Http\Api\ChannelApi;
+
 
 class InitCs6sentence extends Command
 {
@@ -42,6 +44,11 @@ class InitCs6sentence extends Command
     public function handle()
     {
 		$start = time();
+        $channelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
+        if($channelId === false){
+            $this->error('no channel');
+            return 1;
+        }
 		$pali = new PaliSentence;
 		if(!empty($this->argument('book'))){
 			$pali = $pali->where('book',$this->argument('book'));
@@ -64,71 +71,61 @@ class InitCs6sentence extends Command
 			$boldCount = 0;
 			foreach ($words as $word) {
 				# code...
-				if($word->style != "note" && $word->type != '.ctl.'){
-					if($word->style=='bld'){
-						if(!$boldStart){
-							#黑体字开始
-							$boldStart = true;
-							$sent .= ' **';
-						}
+				//if($word->style != "note" && $word->type != '.ctl.')
+				if( $word->type != '.ctl.')
+                {
+					if(strpos($word->word,'{') >=0 ){
+                        //一个单词里面含有黑体字的
+						$paliWord = \str_replace("{","<strong>",$word->word) ;
+						$paliWord = \str_replace("}","</strong>",$paliWord) ;
+                        $sent .= $paliWord;
 					}else{
-						if($boldStart){
-							#黑体字结束
-							$boldStart = false;
-							$boldCount = 0;
-							$sent .= '**';
-						}
-					}
-					if($boldStart){
-						$boldCount++;
+                        if($word->style=='bld'){
+                            $sent .= "<strong>{$word->word}</strong>";
+                        }else{
+                            $sent .= $word->word;
+                        }
 					}
-					if(!empty($word->real) && $boldCount != 1){
-						#如果不是标点符号,在词的前面加空格 。第一个黑体字前不加空格
+
+					if(!empty($word->real) ){
+						#如果不是标点符号,在词的前面加空格 。
 						$sent .= " ";
 					}
-					
-					if(strpos($word->word,'{') >=0 ){
-						$paliWord = \str_replace("{","",$word->word) ;
-						$paliWord = \str_replace("}","**",$paliWord) ;
-					}else{
-						$paliWord = $word->word;
-					}
-					$sent .= $paliWord;
+
 				}
 			}
-			if($boldStart){
-				#句子结尾是黑体字 加黑体结束符号
-				$boldStart = false;
-				$sent .= '** ';
-			}
+
 			#将wikipali风格的引用 改为缅文风格
+            /*
 			$sent = \str_replace('n’’’ ti','’’’nti',$sent);
 			$sent = \str_replace('n’’ ti','’’nti',$sent);
 			$sent = \str_replace('n’ ti','’nti',$sent);
 			$sent = \str_replace('**ti**','**ti',$sent);
 			$sent = \str_replace('‘ ','‘',$sent);
-			$sent = trim($sent);			
-			$snowId = app('snowflake')->id();
-			$newRow = Sentence::updateOrCreate(
+            */
+			$newRow = Sentence::firstOrNew(
 				[
 					"book_id" => $value->book,
 					"paragraph" => $value->paragraph,
 					"word_start" => $value->word_begin,
 					"word_end" => $value->word_end,
-					"channel_uid" => config("app.admin.cs6_channel"),
+					"channel_uid" => $channelId,
 				],
 				[
-					'id' =>$snowId,
+					'id' =>app('snowflake')->id(),
 					'uid' =>Str::uuid(),
-					'editor_uid'=>config("app.admin.root_uuid"),
-					'content'=>trim($sent),
-					'strlen'=>mb_strlen($sent,"UTF-8"),
-					'status' => 30,
-					'create_time'=>time()*1000,
-					'modify_time'=>time()*1000,
-					'language'=>'en'
 				]
 				);
+            $newRow->editor_uid = config("app.admin.root_uuid");
+            $newRow->content = "<span>{$sent}</span>";
+            $newRow->strlen = mb_strlen($sent,"UTF-8");
+            $newRow->status = 10;
+            $newRow->content_type = "html";
+            $newRow->create_time = time()*1000;
+            $newRow->modify_time = time()*1000;
+            $newRow->language = 'en';
+            $newRow->save();
+
 			$bar->advance();
 		}
 		$bar->finish();

+ 105 - 0
app/Console/Commands/InitSystemChannel.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Models\Channel;
+use Illuminate\Console\Command;
+
+class InitSystemChannel extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'init:system.channel';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'create system channel. like pali text , wbw template ect.';
+
+    protected $channels =[
+        [
+            "name"=>'_System_Pali_VRI_',
+            'type'=>'original',
+            'lang'=>'pali',
+        ],
+        [
+            "name"=>'_System_Wbw_VRI_',
+            'type'=>'original',
+            'lang'=>'pali',
+        ],
+        [
+            "name"=>'_System_Grammar_Term_zh-hans_',
+            'type'=>'translation',
+            'lang'=>'zh-Hans',
+        ],
+        [
+            "name"=>'_System_Grammar_Term_zh-hant_',
+            'type'=>'translation',
+            'lang'=>'zh-Hant',
+        ],
+        [
+            "name"=>'_System_Grammar_Term_en_',
+            'type'=>'translation',
+            'lang'=>'en',
+        ],
+        [
+            "name"=>'_community_term_zh-hans_',
+            'type'=>'translation',
+            'lang'=>'zh-Hans',
+        ],
+        [
+            "name"=>'_community_term_zh-hant_',
+            'type'=>'translation',
+            'lang'=>'zh-Hant',
+        ],
+        [
+            "name"=>'_community_term_en_',
+            'type'=>'translation',
+            'lang'=>'en',
+        ],
+    ];
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $this->info("start");
+        foreach ($this->channels as $key => $value) {
+            # code...
+            $channel = Channel::firstOrNew([
+                'name' => $value['name'],
+                'owner_uid' => config("app.admin.root_uuid"),
+            ]);
+            if(empty($channel->id)){
+                $channel->id = app('snowflake')->id();
+            }
+            $channel->type = $value['type'];
+            $channel->lang = $value['lang'];
+            $channel->editor_id = 0;
+            $channel->owner_uid = config("app.admin.root_uuid");
+            $channel->create_time = time()*1000;
+            $channel->modify_time = time()*1000;
+            $channel->save();
+            $this->info("created". $value['name']);
+        }
+        return 0;
+    }
+}

+ 92 - 0
app/Console/Commands/InitSystemDict.php

@@ -0,0 +1,92 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Models\DictInfo;
+use Illuminate\Console\Command;
+
+class InitSystemDict extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'init:system.dict';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'create system dict. like sys_regular  ect.';
+
+    /**
+     * name 不要修改。因为在程序其他地方,用name 查询词典id
+     */
+    protected $dictionary =[
+        [
+            "name"=>'robot_compound',
+            'shortname'=>'compound',
+            'description'=>'split compound by AI',
+            'src_lang'=>'pa',
+            'dest_lang'=>'cm',
+        ],
+        [
+            "name"=>'system_regular',
+            'shortname'=>'regular',
+            'description'=>'system regular',
+            'src_lang'=>'pa',
+            'dest_lang'=>'cm',
+        ],
+        [
+            "name"=>'community',
+            'shortname'=>'社区',
+            'description'=>'由用户贡献词条的社区字典',
+            'src_lang'=>'pa',
+            'dest_lang'=>'cm',
+        ],
+        [
+            "name"=>'community_extract',
+            'shortname'=>'社区汇总',
+            'description'=>'由用户贡献词条的社区字典汇总统计',
+            'src_lang'=>'pa',
+            'dest_lang'=>'cm',
+        ],
+    ];
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $this->info("start");
+        foreach ($this->dictionary as $key => $value) {
+            # code...
+            $channel = DictInfo::firstOrNew([
+                'name' => $value['name'],
+                'owner_id' => config("app.admin.root_uuid"),
+            ]);
+            $channel->shortname = $value['shortname'];
+            $channel->description = $value['description'];
+            $channel->src_lang = $value['src_lang'];
+            $channel->dest_lang = $value['dest_lang'];
+            $channel->meta = json_encode($value,JSON_UNESCAPED_UNICODE);
+            $channel->save();
+            $this->info("updated {$value['name']}");
+        }
+        return 0;
+    }
+}

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

@@ -0,0 +1,86 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\Channel;
+use App\Models\Sentence;
+use Illuminate\Support\Facades\Storage;
+
+class StatisticsNissaya extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'statistics:nissaya';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '统计nissaya 每日录入进度';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $nissaya_channel = Channel::where('type','nissaya')->select('uid')->get();
+        $channels = [];
+        foreach ($nissaya_channel as $key => $value) {
+            # code...
+            $channels[] = $value->uid;
+        }
+        $this->info('channel:'.count($channels));
+        $maxDay = 360;
+        $file = "public/export/nissaya-daily.csv";
+        Storage::disk('local')->put($file, "");
+        #按天获取数据
+        for($i = 1; $i <= $maxDay; $i++){
+            $day = strtotime("today -{$i} day");
+            $date = date("Y-m-d",$day);
+            $strlen = Sentence::whereIn('channel_uid',$channels)
+                    ->whereDate('created_at','=',$date)
+                    ->sum('strlen');
+            $editor = Sentence::whereIn('channel_uid',$channels)
+                    ->whereDate('created_at','=',$date)
+                    ->groupBy('editor_uid')
+                    ->select('editor_uid')->get();
+            $info = $date.','.$strlen.','.count($editor);
+            $this->info($info);
+            Storage::disk('local')->append($file, $info);
+        }
+        $file = "public/export/nissaya-week.csv";
+        Storage::disk('local')->put($file, "");
+        for($i = 1; $i <= $maxDay; $i=$i+7){
+            $day1 = strtotime("today -{$i} day");
+            $date1 = date("Y-m-d",$day1);
+            $j = $i - 7;
+            $date2 = date("Y-m-d",strtotime("today -{$j} day"));
+            $editor = Sentence::whereIn('channel_uid',$channels)
+                    ->whereDate('created_at','>',$date1)
+                    ->whereDate('created_at','<=',$date2)
+                    ->groupBy('editor_uid')
+                    ->select('editor_uid')->get();
+            $info = $date2.','.$date1.','.count($editor);
+            $this->info($info);
+            Storage::disk('local')->append($file, $info);
+        }
+        return 0;
+    }
+}

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

@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Tools\CaseMan;
+use App\Models\UserDict;
+
+
+class TestCaseMan extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:case {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()
+    {
+		$caseman = new CaseMan();
+		$parents = $caseman->WordToBase($this->argument('word'),1);
+			# code...
+			
+		foreach ($parents as $base => $rows) {
+			# code...
+			if(count($rows)==0){
+				$this->error("base={$base}-(".count($rows).")");
+			}else{
+				$this->warn("base={$base}-(".count($rows).")");
+			}
+			
+			foreach ($rows as $value) {
+				# code...
+				$this->info($value['word'].'-'.$value['type'].'-'.$value['grammar'].'-'.$base);
+			}
+		}
+        return 0;
+    }
+}

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

@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Tools\Tools;
+
+class TestJsonToXml extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:json.to.xml';
+
+    /**
+     * 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()
+    {
+        $array = [
+            'pali'=>['status'=>'7','value'=>'bārāṇasiyaṃ'],
+            'real'=>['status'=>'7','value'=>'bārāṇasiyaṃ'],
+            'id'=>'p171-2475-10'
+        ];
+        $xml = Tools::JsonToXml($array);
+        $this->info($xml);
+        return 0;
+    }
+}

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

@@ -0,0 +1,75 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Http\Api\MdRender;
+
+class TestMdRender extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:md.render';
+
+    /**
+     * 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()
+    {
+        $markdown = "# heading \n\n";
+        //$markdown .= "[[isipatana]] `bla` [[dhammacakka]]\n\n";
+        $markdown .= "前面的\n";
+        $markdown .= "{{note|\n";
+        $markdown .= "多**行注**释\n\n";
+        $markdown .= "多行注释\n";
+        $markdown .= "}}\n\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 MdRender::render2($markdown,'00ae2c48-c204-4082-ae79-79ba2740d506',null,'read','nissaya');
+        return 0;
+    }
+}

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

@@ -0,0 +1,63 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use PhpAmqpLib\Connection\AMQPStreamConnection;
+use PhpAmqpLib\Message\AMQPMessage;
+
+
+class TestMq extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:mq ';
+
+    /**
+     * 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()
+    {
+        //一对一
+		$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',"");
+
+        return 0;
+    }
+}

+ 60 - 0
app/Console/Commands/TestMqWorker.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+use PhpAmqpLib\Connection\AMQPStreamConnection;
+
+class TestMqWorker extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:mqworker';
+
+    /**
+     * 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()
+    {
+		$connection = new AMQPStreamConnection(MQ_HOST, MQ_PORT, MQ_USERNAME, MQ_PASSWORD);
+		$channel = $connection->channel();
+
+		$channel->queue_declare('hello', false, true, false, false);
+
+		echo " [*] Waiting for messages. To exit press CTRL+C\n";
+
+		$callback = function ($msg) {
+			echo ' [x] Received ', $msg->body, "\n";
+		  };
+
+		$channel->basic_consume('hello', '', false, true, false, false, $callback);
+
+		while ($channel->is_open()) {
+			  $channel->wait();
+		  }
+        return 0;
+    }
+}

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

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class UpgradeAt20230227 extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:at20230227';
+
+    /**
+     * 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->call('init:system.channel');
+        $this->call('init:system.dict');
+        $this->call('upgrade:dict');
+        $this->call('upgrade:dict.vocabulary');
+        $this->call('upgrade:dict.default.meaning');
+        $this->call('upgrade:related.paragraph');
+        $this->call('upgrade:fts',['--book'=>'']);
+        return 0;
+    }
+}

+ 150 - 0
app/Console/Commands/UpgradeChapterDynamicWeekly.php

@@ -0,0 +1,150 @@
+<?php
+
+namespace App\Console\Commands;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Console\Command;
+use Illuminate\Support\Carbon;
+use App\Models\SentHistory;
+use App\Models\Sentence;
+use App\Models\ProgressChapter;
+use App\Models\PaliText;
+use Illuminate\Support\Facades\Cache;
+
+class UpgradeChapterDynamicWeekly extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:chapter.dynamic.weekly {--test} {--book=} {--offset=}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '更新章节活跃程度svg';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+		$this->info('upgrade:chapter.dynamic.weekly start.');
+
+        $startAt = time();
+        $weeks = 60; //统计多少周
+
+//更新总动态
+		$this->info("更新总动态");
+        $table = ProgressChapter::select('book','para')
+                                    ->groupBy('book','para')
+                                    ->orderBy('book');
+        if($this->option('book')){
+            $table = $table->where('book',$this->option('book'));
+        }
+        $chapters = $table->get();
+        $bar = $this->output->createProgressBar(count($chapters));
+        foreach ($chapters as $key => $chapter) {
+            #章节长度
+            $paraEnd = PaliText::where('book',$chapter->book)
+                            ->where('paragraph',$chapter->para)
+                            ->value('chapter_len')+$chapter->para-1;
+
+            $progress = [];
+            for ($i=$weeks; $i > 0 ; $i--) {
+                #这一周有多少次更新
+                $currDay = $i*7+$this->option('offset',0);
+                $count = SentHistory::whereBetween('sent_histories.created_at',
+                                                  [Carbon::today()->subDays($currDay)->startOfWeek(),
+                                                  Carbon::today()->subDays($currDay)->endOfWeek()])
+                           ->leftJoin('sentences', 'sent_histories.sent_uid', '=', 'sentences.uid')
+                             ->where('book_id',$chapter->book)
+                             ->whereBetween('paragraph',[$chapter->para,$paraEnd])
+                             ->count();
+                $progress[] = $count;
+            }
+            $key="/chapter_dynamic/{$chapter->book}/{$chapter->para}/global";
+            Cache::put($key,$progress,3600*24*7);
+            $bar->advance();
+
+            if($this->option('test')){
+                $this->info("key:{$key}");
+                if(Cache::has($key)){
+                    $this->info('has key '.$key);
+                }
+                break; //调试代码
+            }
+        }
+        $bar->finish();
+
+		$time = time()- $startAt;
+        $this->info("用时 {$time}");
+
+        $startAt = time();
+
+		$startAt = time();
+        //更新chennel动态
+        $this->info('更新chennel动态');
+
+        $table = ProgressChapter::select('book','para','channel_id');
+        if($this->option('book')){
+            $table = $table->where('book',$this->option('book'));
+        }
+        $bar = $this->output->createProgressBar($table->count());
+
+        foreach ($table->cursor() as $chapter) {
+            # code...
+            $max=0;
+            #章节长度
+            $paraEnd = PaliText::where('book',$chapter->book)
+                            ->where('paragraph',$chapter->para)
+                            ->value('chapter_len')+$chapter->para-1;
+            $progress = [];
+            for ($i=$weeks; $i > 0 ; $i--) {
+                #这一周有多少次更新
+                $currDay = $i*7+$this->option('offset',0);
+                $count = SentHistory::whereBetween('sent_histories.created_at',
+                                                [Carbon::today()->subDays($currDay)->startOfWeek(),
+                                                Carbon::today()->subDays($currDay)->endOfWeek()])
+                           ->leftJoin('sentences', 'sent_histories.sent_uid', '=', 'sentences.uid')
+                             ->where('book_id',$chapter->book)
+                             ->whereBetween('paragraph',[$chapter->para,$paraEnd])
+                             ->where('sentences.channel_uid',$chapter->channel_id)
+                             ->count();
+                $progress[] = $count;
+            }
+            $key="/chapter_dynamic/{$chapter->book}/{$chapter->para}/ch_{$chapter->channel_id}";
+            Cache::put($key,$progress,3600*24*7);
+            $bar->advance();
+
+            if($this->option('test')){
+                $this->info("key:{$key}");
+                if(Cache::has($key)){
+                    $this->info('has key '.$key);
+                }
+                break; //调试代码
+            }
+        }
+        $bar->finish();
+		$time = time()- $startAt;
+        $this->info("用时 {$time}");
+
+        $this->info("upgrade:chapter.dynamic done");
+
+        return 0;
+    }
+}

+ 165 - 0
app/Console/Commands/UpgradeCommunityTerm.php

@@ -0,0 +1,165 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Tools\Tools;
+use App\Models\DhammaTerm;
+use App\Models\UserOperationDaily;
+use App\Models\Sentence;
+
+use App\Http\Api\ChannelApi;
+use Illuminate\Support\Str;
+
+class UpgradeCommunityTerm extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:community.term {lang}';
+
+    /**
+     * 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 = strtolower($this->argument('lang'));
+        $langFamily = explode('-',$lang)[0];
+        $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');
+
+
+        $words = $table->get();
+        $bar = $this->output->createProgressBar(count($words));
+        foreach ($words as $key => $word) {
+            /**
+             * 最优算法
+             * 1. 找到最常见的意思
+             * 2. 找到分数最高的
+             */
+            $bestNote = "" ;
+            $allTerm = DhammaTerm::where('word',$word->word)
+                                ->whereIn('language',[$this->argument('lang'),$lang,$langFamily])
+                                ->get();
+            $score = [];
+            foreach ($allTerm as $key => $term) {
+                //经验值
+                $exp = UserOperationDaily::where('user_id',$term->editor_id)
+                                        ->where('date_int','<=',date_timestamp_get(date_create($term->updated_at))*1000)
+                                        ->sum('duration');
+                $iExp = (int)($exp/1000);
+                $noteStrLen = mb_strlen($term->note);
+                $paliStrLen = 0;
+                $tranStrLen = 0;
+                $noteWithoutPali = "";
+                if(!empty(trim($term->note))){
+                    #查找句子模版
+                    $pattern = "/\{\{[0-9].+?\}\}/";
+                    //获取去掉句子模版的剩余部分
+                    $noteWithoutPali = preg_replace($pattern,"",$term->note);
+                    $sentences = [];
+                    $iSent = preg_match_all($pattern,$term->note,$sentences);
+                    if($iSent>0){
+                        foreach ($sentences[0] as  $sentence) {
+                            $sentId = explode("-",trim($sentence,"{}"));
+                            if(count($sentId) === 4){
+                                $hasTran = Sentence::where('book_id',$sentId[0])
+                                                    ->where('paragraph',$sentId[1])
+                                                    ->where('word_start',$sentId[2])
+                                                    ->where('word_end',$sentId[3])
+                                                    ->exists();
+
+                                $sentLen = Sentence::where('book_id',$sentId[0])
+                                                    ->where('paragraph',$sentId[1])
+                                                    ->where('word_start',$sentId[2])
+                                                    ->where('word_end',$sentId[3])
+                                                    ->where("channel_uid", $channelId)
+                                                    ->value('strlen');
+                                if($sentLen){
+                                    $paliStrLen += $sentLen;
+                                    if($hasTran){
+                                        $tranStrLen += $sentLen;
+                                    }
+                                }
+                            }
+                        }
+                    }
+
+                }
+                //计算该术语总得分
+                $score["{$key}"] = $iExp*$noteStrLen;
+            }
+
+            $hotMeaning = DhammaTerm::selectRaw('meaning,count(*) as co')
+                        ->where('word',$word->word)
+                        ->whereIn('language',[$this->argument('lang'),$lang,$langFamily])
+                        ->groupBy('meaning')
+                        ->orderBy('co','desc')
+                        ->first();
+            if($hotMeaning){
+                $bestNote = "";
+                if(count($score)>0){
+                    arsort($score);
+                    $bestNote = $allTerm[(int)key($score)]->note;
+                }
+
+                $term = DhammaTerm::where('channal',$localTerm)->firstOrNew(
+                        [
+                            "word" => $word->word,
+                            "channal" => $localTerm,
+                        ],
+                        [
+                            'id' =>app('snowflake')->id(),
+                            'guid' =>Str::uuid(),
+                            'word_en' =>Tools::getWordEn($word->word),
+                            'meaning' => '',
+                            'language' => $this->argument('lang'),
+                            'owner' => config("app.admin.root_uuid"),
+                            'editor_id' => 0,
+                            'create_time' => time()*1000,
+                        ]
+                    );
+                    $term->meaning = $hotMeaning->meaning;
+                    $term->note = $bestNote;
+                    $term->modify_time = time()*1000;
+                    $term->save();
+            }
+            $bar->advance();
+        }
+        $bar->finish();
+
+        return 0;
+    }
+}

+ 90 - 36
app/Console/Commands/UpgradeCompound.php

@@ -1,14 +1,13 @@
 <?php
 namespace App\Console\Commands;
 
-require_once __DIR__."/../../../public/app/dict/turbo_split.php";
-
 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;
 
 class UpgradeCompound extends Command
 {
@@ -17,7 +16,7 @@ class UpgradeCompound extends Command
      *
      * @var string
      */
-    protected $signature = 'upgrade:compound {word?} {--test}';
+    protected $signature = 'upgrade:compound {word?} {--book=} {--debug} {--test}';
 
     /**
      * The console command description.
@@ -26,6 +25,8 @@ class UpgradeCompound extends Command
      */
     protected $description = 'Command description';
 
+
+
     /**
      * Create a new command instance.
      *
@@ -43,18 +44,35 @@ class UpgradeCompound extends Command
      */
     public function handle()
     {
-		$ts = new TurboSplit();
+        $dict_id = DictApi::getSysDict('robot_compound');
+        if(!$dict_id){
+            $this->error('没有找到 robot_compound 字典');
+            return 1;
+        }
+
 		$start = \microtime(true);
 
 		$_word = $this->argument('word');
 		if(!empty($_word)){
-			var_dump($ts->splitA($_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')){
 			//调试代码
+            $ts = new TurboSplit();
 			Storage::disk('local')->put("tmp/compound.md", "# Turbo Split");
 			//获取需要拆的词
 			$list = [
@@ -69,52 +87,88 @@ class UpgradeCompound extends Command
 				foreach ($words as $word) {
 					$this->info($word->word);
 					Storage::disk('local')->append("tmp/compound.md", "## {$word->word}");
-
 					$parts = $ts->splitA($word->word);
 					foreach ($parts as $part) {
 						# code...
-						$this->info("{$part['word']},{$part['factors']},{$part['confidence']}");
-						Storage::disk('local')->append("tmp/compound.md", "- `{$part['word']}`,{$part['factors']},{$part['confidence']}");
+                        $info = "`{$part['word']}`,{$part['factors']},{$part['confidence']}";
+						$this->info($info);
+						Storage::disk('local')->append("tmp/compound.md", "- {$info}");
 					}
 				}
-			}		
-			$this->info("耗时:".\microtime(true)-$start);		
-			return 0;	
+			}
+			$this->info("耗时:".\microtime(true)-$start);
+			return 0;
 		}
 
-		$words = WbwTemplate::select('real')->where('type','<>','.ctl.')->where('real','<>','')->groupBy('real')->cursor();
+        if($this->option('book')){
+            $words = WbwTemplate::select('real')
+                            ->where('book',$this->option('book'))
+                            ->where('type','<>','.ctl.')
+                            ->where('real','<>','')
+                            ->groupBy('real')->cursor();
+        }else{
+            $words = WbwTemplate::select('real')
+                            ->where('type','<>','.ctl.')
+                            ->where('real','<>','')
+                            ->groupBy('real')->cursor();
+        }
+
 		$count = 0;
 		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}");
-			$parts = $ts->splitA($word->real);
-			foreach ($parts as $part) {
-				$new = UserDict::firstOrNew(
-					[
-						'word' => $part['word'],
-						'type' => ".cp.",
-						'factors' => $part['factors'],
-						'dict_id' => 'c42980f0-5967-4833-b695-84183344f68f'
-					],
-					[
-						'id' => app('snowflake')->id(),
-						'source' => '_ROBOT_',
-						'create_time'=>(int)(microtime(true)*1000),
-					]
-				);
-				$new->confidence = 50;
-				$new->language = 'cm';
-				$new->creator_id = 1;
-				$new->flag = 1;
-				$new->save();
+			$ts = new TurboSplit();
 
-			}
+            $parts = $ts->splitA($word->real);
+            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),
+                    ]
+                );
+                if(isset($part['type'])){
+                    $new->type = $part['type'];
+                }else{
+                    $new->type = ".cp.";
+                }
+                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();
+            }
 		}
 		//删除旧数据
-		UserDict::where('flag',0)->delete();
-		UserDict::where('flag',1)->update(['flag'=>0]);
-	
+		UserDict::where('dict_id',$dict_id)->where('flag',2)->delete();
+		UserDict::where('dict_id',$dict_id)->where('flag',1)->update(['flag'=>0]);
+
         return 0;
     }
 }

+ 7 - 3
app/Console/Commands/UpgradeDaily.php

@@ -52,14 +52,18 @@ class UpgradeDaily extends Command
         # 刷巴利语句子uuid 仅调用一次
         //$this->call('upgrade:palitextid');
         //巴利原文段落库目录结构改变时运行
-        $this->call('upgrade:palitext'); 
+        $this->call('upgrade:palitext');
         #巴利段落标签
         $this->call('upgrade:palitexttag');
+
+        //更新单词首选意思
+        $this->call('upgrade:dict.default.meaning');
+
         #译文进度
         $this->call('upgrade:progress');
         $this->call('upgrade:progresschapter');
-        # 段落更新图
-        $this->call('upgrade:chapterdynamic');
+        $this->call('upgrade:community.term',['zh-Hans']);
+
         # 逐词译数据库分析
         $this->call('upgrade:wbwanalyses');
 

+ 102 - 38
app/Console/Commands/UpgradeDict.php

@@ -4,6 +4,9 @@ namespace App\Console\Commands;
 
 use Illuminate\Support\Str;
 use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Storage;
+
 use App\Models\UserDict;
 use App\Models\DictInfo;
 
@@ -14,7 +17,7 @@ class UpgradeDict extends Command
      *
      * @var string
      */
-    protected $signature = 'upgrade:dict {uuid?}';
+    protected $signature = 'upgrade:dict {uuid?} {--part}';
 
     /**
      * The console command description.
@@ -23,6 +26,9 @@ class UpgradeDict extends Command
      */
     protected $description = '导入csv字典';
 
+	protected $dictInfo;
+	protected $cols;
+
     /**
      * Create a new command instance.
      *
@@ -48,80 +54,125 @@ class UpgradeDict extends Command
 						//是文件,查看是否是字典信息文件
 						$infoFile = $fullPath;
 						if(pathinfo($infoFile,PATHINFO_EXTENSION) === 'ini'){
-							$dictInfo = parse_ini_file($infoFile,true);
-							if(isset($dictInfo['meta']['dictname'])){
+							$this->dictInfo = parse_ini_file($infoFile,true);
+							if(isset($this->dictInfo['meta']['dictname'])){
 								//是字典信息文件
-								$this->info($dictInfo['meta']['dictname']);
+								$this->info($this->dictInfo['meta']['dictname']);
 								if(Str::isUuid($this->argument('uuid'))){
-									if($this->argument('uuid') !== $dictInfo['meta']['uuid']){
+									if($this->argument('uuid') !== $this->dictInfo['meta']['uuid']){
 										continue;
 									}
 								}
-								if(!Str::isUuid($dictInfo['meta']['uuid'])){
+								if(!Str::isUuid($this->dictInfo['meta']['uuid'])){
 									$this->error("not uuid");
 									continue;
 								}
 								$tableDict = DictInfo::firstOrNew([
-									"id" => $dictInfo['meta']['uuid']
+									"id" => $this->dictInfo['meta']['uuid']
 								]);
-								$tableDict->id = $dictInfo['meta']['uuid'];
-								$tableDict->name = $dictInfo['meta']['dictname'];
-								$tableDict->shortname = $dictInfo['meta']['shortname'];
-								$tableDict->description = $dictInfo['meta']['description'];
-								$tableDict->src_lang = $dictInfo['meta']['src_lang'];
-								$tableDict->dest_lang = $dictInfo['meta']['dest_lang'];
-								$tableDict->rows = $dictInfo['meta']['rows'];
+								$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->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->meta = json_encode($dictInfo['meta']);
+								$tableDict->meta = json_encode($this->dictInfo['meta']);
 								$tableDict->save();
 
-								UserDict::where("dict_id",$dictInfo['meta']['uuid'])->delete();
+								if($this->option('part')){
+									$this->info(" dict id = ".$this->dictInfo['meta']['uuid']);
+								}else{
+									$del = UserDict::where("dict_id",$this->dictInfo['meta']['uuid'])->delete();
+									$this->info("delete {$del} rows dict id = ".$this->dictInfo['meta']['uuid']);
+								}
+                                /**
+                                 * 允许一个字典拆成若干个小文件
+                                 * 文件名 为 ***.csv , ***-1.csv , ***-2.csv
+                                 *
+                                 */
 								$filename = $dir.'/'.pathinfo($infoFile,PATHINFO_FILENAME);
 								$csvFile = $filename . ".csv";
 								$count = 0;
-								$bar = $this->output->createProgressBar($dictInfo['meta']['rows']);
+								$bar = $this->output->createProgressBar($this->dictInfo['meta']['rows']);
 								while (file_exists($csvFile)) {
 									# code...
 									$this->info("runing:{$csvFile}");
 									$inputRow = 0;
 									if (($fp = fopen($csvFile, "r")) !== false) {
-										$cols = array();
+										$this->cols = array();
 										while (($data = fgetcsv($fp, 0, ',')) !== false) {
 											if ($inputRow == 0) {
 												foreach ($data as $key => $colname) {
 													# 列名列表
-													$cols[$colname] = $key;
+													$this->cols[$colname] = $key;
 												}
 											}else{
-												$word["id"]=app('snowflake')->id();
-												$word["word"] = $data[$cols['word']];
-												if(isset($cols['type'])) $word["type"] = $data[$cols['type']];
-												if(isset($cols['grammar'])) $word["grammar"] = $data[$cols['grammar']];
-												if(isset($cols['parent'])) $word["parent"] = $data[$cols['parent']];
-												if(isset($cols['mean'])) $word["mean"] = $data[$cols['mean']];
-												if(isset($cols['note'])) $word["note"] = $data[$cols['note']];
-												if(isset($cols['factors'])) $word["factors"] = $data[$cols['factors']];
-												if(isset($cols['factormean'])) $word["factormean"] = $data[$cols['factormean']];
-												if(isset($cols['status'])) $word["status"] = $data[$cols['status']];
-												if(isset($cols['language'])) $word["language"] = $data[$cols['language']];
-												if(isset($cols['confidence'])) $word["confidence"] = $data[$cols['confidence']];
-												$word["source"]='_PAPER_RICH_';
-												$word["create_time"]=(int)(microtime(true)*1000);
-												$word["creator_id"]=1;
-												$word["dict_id"] = $dictInfo['meta']['uuid'];
-												$id = UserDict::insert($word);
+												if($this->option('part')){
+													//仅仅提取拆分零件
+													$word = $this->get($data,'word');
+													$factor1 = $this->get($data,'factors');
+													$factor1 = \str_replace([' ','(',')','=','-','$'],"+",$factor1);
+													foreach (\explode('+',$factor1)  as $part) {
+														# code...
+														if(empty($part)){
+															continue;
+														}
+														if(isset($newPart[$part])){
+															$newPart[$part][0]++;
+														}else{
+															$partExists = Cache::remember('dict/part/'.$part,1000,function() use($part){
+																return UserDict::where('word',$part)->exists();
+															});
+															if(!$partExists){
+															$count++;
+															$newPart[$part] = [1,$word];
+															$this->info("{$count}:{$part}-{$word}");
+															}
+														}
+
+													}
+												}else{
+													$newDict = new UserDict();
+													$newDict->id = app('snowflake')->id();
+													$newDict->word = $data[$this->cols['word']];
+													$newDict->type = $this->get($data,'type');
+													$newDict->grammar = $this->get($data,'grammar');
+													$newDict->parent = $this->get($data,'parent');
+													$newDict->mean = $this->get($data,'mean');
+													$newDict->note = $this->get($data,'note');
+													$newDict->factors = $this->get($data,'factors');
+													$newDict->factormean = $this->get($data,'factormean');
+													$newDict->status = $this->get($data,'status');
+													$newDict->language = $this->get($data,'language');
+													$newDict->confidence = $this->get($data,'confidence');
+													$newDict->source = $this->get($data,'source');
+													$newDict->create_time =(int)(microtime(true)*1000);
+													$newDict->creator_id = 1;
+													$newDict->dict_id = $this->dictInfo['meta']['uuid'];
+													$newDict->save();
+												}
+
 												$bar->advance();
 											}
 											$inputRow++;
 										}
 									}
 									$count++;
-									$csvFile = $filename . "{$count}.csv";
+									$csvFile = $filename . "-{$count}.csv";
 								}
 								$bar->finish();
+								Storage::disk('local')->put("tmp/pm-part.csv", "part,count,word");
+                                if(isset($newPart)){
+                                    foreach ($newPart as $part => $info) {
+                                        # 写入磁盘文件
+                                        Storage::disk('local')->append("tmp/pm-part.csv", "{$part},{$info[0]},{$info[1]}");
+                                    }
+                                }
 								$this->info("done");
 							}
-							
+
 						}
 					}
 				}
@@ -137,6 +188,19 @@ class UpgradeDict extends Command
 			return;
 		}
 	}
+
+	/**
+	 * 获取列的值
+	 */
+	protected function get($data,$colname,$defualt=""){
+		if(isset($this->cols[$colname])){
+			return $data[$this->cols[$colname]];
+		}else if(isset($this->dictInfo['cols'][$colname])){
+			return $this->dictInfo['cols'][$colname];
+		}else{
+			return $defualt;
+		}
+	}
     /**
      * Execute the console command.
      *

+ 142 - 0
app/Console/Commands/UpgradeDictDefaultMeaning.php

@@ -0,0 +1,142 @@
+<?php
+/**
+ * 刷新字典单词的默认意思
+ * 目标:
+ *    可以查询到某个单词某种语言的首选意思
+ * 算法:
+ * 1. 某种语言会有多个字典。按照字典重要程度人工排序
+ * 2. 按照顺序搜索这些字典。找到第一个意思就停止。
+ */
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\UserDict;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
+
+class UpgradeDictDefaultMeaning extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:dict.default.meaning {word?}';
+    protected $dict = [
+        "zh-Hans"=>[
+            "8833de18-0978-434c-b281-a2e7387f69be",	/*巴汉字典明法尊者修订版*/
+            "f364d3dc-b611-471b-9a4f-531286b8c2c3",	/*《巴汉词典》Mahāñāṇo Bhikkhu编著*/
+            "0e4dc5c8-a228-4693-92ba-7d42918d8a91",	/*汉译パーリ语辞典-黃秉榮*/
+            "6aa9ec8b-bba4-4bcd-abd2-9eae015bad2b",	/*汉译パーリ语辞典-李瑩*/
+            "eb99f8b4-c3e5-43af-9102-6a93fcb97db6",	/*パーリ语辞典--勘误表*/
+            "0d79e8e8-1430-4c99-a0f1-b74f2b4b26d8",	/*《巴汉词典》增订*/
+        ],
+        "zh-Hant"=>[
+            "3acf0c0f-59a7-4d25-a3d9-bf394a266ebd",	/*汉译パーリ语辞典-黃秉榮*/
+            "5293ffb9-887e-4cf2-af78-48bf52a85304",	/*巴利詞根*/
+        ],
+        "jp"=>[
+            "91d3ec93-3811-4973-8d84-ced99179a0aa",	/*パーリ语辞典*/
+            "6d6c6812-75e7-457d-874f-5b049ad4b6de",	/*パーリ语辞典-增补*/
+        ],
+        "en"=>[
+            "c6e70507-4a14-4687-8b70-2d0c7eb0cf21",	/*	Concise P-E Dict*/
+            "eae9fd6f-7bac-4940-b80d-ad6cd6f433bf",	/*	Concise P-E Dict*/
+            "2f93d0fe-3d68-46ee-a80b-11fa445a29c6",	/*	unity*/
+            "b9163baf-2bca-41a5-a936-5a0834af3945",	/*	Pali-Dict Vri*/
+            "b089de57-f146-4095-b886-057863728c43",	/*	Buddhist Dictionary*/
+            "6afb8c05-5cbe-422e-b691-0d4507450cb7",	/*	PTS P-E dictionary*/
+            "0bfd87ec-f3ac-49a2-985e-28388779078d",	/*	Pali Proper Names Dict*/
+            "1cdc29e0-6783-4241-8784-5430b465b79c",	/*	Pāḷi Root In Saddanīti*/
+            "5718cbcf-684c-44d4-bbf2-4fa12f2588a4",	/*	Critical Pāli Dictionary*/
+        ],
+        "my"=>[
+            "e740ef40-26d7-416e-96c2-925d6650ac6b",	/*	Tipiṭaka Pāḷi-Myanmar*/
+            "beb45062-7c20-4047-bcd4-1f636ba443d1",	/*	U Hau Sein’s Pāḷi-Myanmar Dictionary*/
+            "1e299ccb-4fc4-487d-8d72-08f63d84c809",	/*	Pali Roots Dictionary*/
+            "6f9caea1-17fa-41f1-92e5-bd8e6e70e1d7",	/*	U Hau Sein’s Pāḷi-Myanmar*/
+        ],
+        "vi"=>[
+            "23f67523-fa03-48d9-9dda-ede80d578dd2",	/*	Pali Viet Dictionary*/
+            "4ac8a0d5-9c6f-4b9f-983d-84288d47f993",	/*	Pali Viet Abhi-Terms*/
+            "7c7ee287-35ba-4cf3-b87b-30f1fa6e57c9",	/*	Pali Viet Vinaya Terms*/
+        ],
+        "cm"=>[],
+    ];
+    /**
+     * 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()
+    {
+        $_word = $this->argument('word');
+        # 获取字典中所有的语言
+        $langInDict = UserDict::select('language')->groupBy('language')->get();
+        $languages = [];
+        foreach ($langInDict as $lang) {
+            if(!empty($lang["language"])){
+                $languages[] = $lang["language"];
+            }
+        }
+		//print_r($languages);
+        foreach ($this->dict as $thisLang=>$dictId) {
+            $this->info("running $thisLang");
+
+            $bar = $this->output->createProgressBar(UserDict::where('source','_PAPER_')
+                                                        ->where('language',$thisLang)->count());
+            foreach (UserDict::where('source','_PAPER_')
+                                ->where('language',$thisLang)
+                                ->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);
+                }
+                $bar->advance();
+            }
+            $bar->finish();
+
+            for ($i=count($dictId)-1; $i >=0 ; $i--) {
+                # code...
+                $this->info("running $thisLang - {$dictId[$i]}");
+                $count = 0;
+                foreach (UserDict::where('dict_id',$dictId[$i])
+                    ->select('word','note')
+                    ->cursor() as $word) {
+                        $cacheKey = "dict_first_mean/{$thisLang}/{$word['word']}";
+                        if(!empty($word['note'])){
+                            $cacheValue = mb_substr($word['note'],0,50,"UTF-8");
+                            if(!empty($_word) && $word['word'] === $_word ){
+                                Log::info($cacheKey.':'.$cacheValue);
+                            }
+                            Cache::put($cacheKey, $cacheValue ,30*24*3600);
+                        }
+
+                        if($count % 1000 === 0){
+                            $this->info("{$count}");
+                        }
+                        $count++;
+                    }
+            }
+        }
+
+        return 0;
+    }
+}

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

@@ -0,0 +1,136 @@
+<?php
+/**
+ * 将用户词典中的数据进行汇总。
+ * 算法:
+ * 同样词性的合并为一条记录。意思按照出现的次数排序
+ */
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\UserDict;
+use App\Http\Api\DictApi;
+
+class UpgradeDictSysWbwExtract extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:syswbwextract';
+
+    /**
+     * 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()
+    {
+        $user_dict_id = DictApi::getSysDict('community');
+        if(!$user_dict_id){
+            $this->error('没有找到 community 字典');
+            return 1;
+        }
+        $user_dict_extract_id = DictApi::getSysDict('community_extract');
+        if(!$user_dict_extract_id){
+            $this->error('没有找到 community_extract 字典');
+            return 1;
+        }
+		$dict  = UserDict::select('word')->where('word','!=','')->where('dict_id',$user_dict_id)->groupBy('word');
+		$bar = $this->output->createProgressBar($dict->count());
+		foreach ($dict->cursor() as  $word) {
+			# code...
+			//case
+			$wordtype = '';
+			$wordgrammar = '';
+			$wordparent = '';
+			$wordfactors = '';
+
+			$case = UserDict::selectRaw('type,grammar, sum(confidence)')
+					->where('word',$word->word)
+					->where('dict_id',$user_dict_id)
+					->where('type','!=','.part.')
+					->where('type','<>','')
+					->whereNotNull('type')
+					->groupBy(['type','grammar'])
+					->orderBy('sum','desc')
+					->first();
+			if($case){
+				$wordtype = $case->type;
+				$wordgrammar = $case->grammar;
+			}
+
+			//parent
+			$parent = UserDict::selectRaw('parent, sum(confidence)')
+					->where('word',$word->word)
+					->where('dict_id',$user_dict_id)
+					->where('type','!=','.part.')
+					->where('parent','!=','')
+					->whereNotNull('parent')
+					->groupBy('parent')
+					->orderBy('sum','desc')
+					->first();
+			if($parent){
+				$wordparent = $parent->parent;
+			}
+
+
+				//factors
+				$factor = UserDict::selectRaw('factors, sum(confidence)')
+						->where('word',$word->word)
+						->where('dict_id',$user_dict_id)
+						->where('type','!=','.part.')
+						->where('factors','<>','')
+						->whereNotNull('factors')
+						->groupBy('factors')
+						->orderBy('sum','desc')
+						->first();
+				if($factor){
+					$wordfactors = $factor->factors;
+				}
+				$new = UserDict::firstOrNew(
+					[
+						'word' => $word->word,
+						'type' => $wordtype,
+						'grammar' => $wordgrammar,
+						'parent' => $wordparent,
+						'factors' => $wordfactors,
+						'dict_id' => $user_dict_extract_id,
+					],
+					[
+						'id' => app('snowflake')->id(),
+						'source' => '_ROBOT_',
+						'create_time'=>(int)(microtime(true)*1000)
+					]
+				);
+				$new->confidence = 90;
+				$new->language = 'cm';
+				$new->creator_id = 1;
+				$new->flag = 1;
+				$new->save();
+
+				$bar->advance();
+		}
+		$bar->finish();
+
+        //TODO 删除旧数据
+        return 0;
+    }
+}

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

@@ -0,0 +1,61 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\Vocabulary;
+use App\Models\UserDict;
+use App\Tools\Tools;
+
+class UpgradeDictVocabulary extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:dict.vocabulary';
+
+    /**
+     * 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()
+    {
+        $words = UserDict::where('source','_PAPER_')->selectRaw('word,count(*)')->groupBy('word')->cursor();
+
+		$bar = $this->output->createProgressBar(230000);
+		foreach ($words as $word) {
+			$update = Vocabulary::firstOrNew(
+                ['word' => $word->word],
+                ['word_en'=>Tools::getWordEn($word->word)]
+            );
+            $update->count = $word->count;
+            $update->flag = 1;
+            $update->strlen = mb_strlen($word->word,"UTF-8");
+            $update->save();
+            $bar->advance();
+		}
+        $bar->finish();
+        Vocabulary::where('flag',0)->delete();
+        return 0;
+    }
+}

+ 97 - 0
app/Console/Commands/UpgradeFts.php

@@ -0,0 +1,97 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\BookTitle;
+use App\Models\FtsText;
+use App\Models\WbwTemplate;
+
+class UpgradeFts extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:fts {--content} {para?} {--test}';
+
+    /**
+     * 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($this->option('content')){
+            if(!empty($this->argument('para'))){
+                $para = explode('-',$this->argument('para'));
+                $content = $this->getContent($para[0],$para[1]);
+                if($this->option('test')){
+                    $this->info($content);
+                }else{
+                    FtsText::where('book',$para[0])->where('paragraph',$para[1])->update(['content'=>$content]);
+                }
+            }else{
+                for ($iBook=1; $iBook <= 217; $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++){
+                        $content = $this->getContent($iBook,$iPara);
+                        FtsText::where('book',$iBook)->where('paragraph',$iPara)->update(['content'=>$content]);
+                        $bar->advance();
+                    }
+                    $bar->finish();
+                    $this->info('done');
+                }
+            }
+
+
+        }
+        return 0;
+    }
+
+    private function getContent($book,$para){
+        $words = WbwTemplate::where('book',$book)
+                            ->where('paragraph',$para)
+                            ->where('type',"<>",".ctl.")
+                            ->orderBy('wid')->get();
+        $content = '';
+        foreach ($words as  $word) {
+            if($word->style === 'bld'){
+                if(strpos($word->word,"{")===FALSE){
+                    $content .= "**{$word->word}** ";
+                }else{
+                    $content .= str_replace(['{','}'],['**','** '],$word->word);
+                }
+            }else if($word->style === 'note'){
+                $content .= " _{$word->word}_ ";
+            }else{
+                $content .= $word->word . " ";
+            }
+        }
+        return $content;
+    }
+}
+
+

+ 124 - 116
app/Console/Commands/UpgradePaliText.php

@@ -1,9 +1,13 @@
 <?php
-
+/**
+ * 计算章节的父子,前后关系
+ * 输入: csv文件
+ */
 namespace App\Console\Commands;
 
 use Illuminate\Console\Command;
 use App\Models\PaliText;
+use App\Models\BookTitle;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 
@@ -42,7 +46,7 @@ class UpgradePaliText extends Command
     {
 		$this->info("upgrade pali text");
 		$startTime = time();
-        
+
 		$_from = $this->argument('from');
 		$_to = $this->argument('to');
 		if(empty($_from) && empty($_to)){
@@ -78,7 +82,7 @@ class UpgradePaliText extends Command
 					$inputRow++;
 				}
 				fclose($fp);
-				
+
 			} else {
 				$this->error( "can not open csv file. filename=" . $csvFile. PHP_EOL) ;
 				Log::error( "can not open csv file. filename=" . $csvFile) ;
@@ -86,140 +90,144 @@ class UpgradePaliText extends Command
 			}
 			$title_data = PaliText::select(['book','paragraph','level','parent','toc','lenght'])
 								->where('book',$from)->orderby('paragraph','asc')->get();
-            {
-				$paragraph_count = count($title_data);
-				$paragraph_info = array();
-				$paragraph_info[] = array($from, -1, $paragraph_count, -1, -1, -1);
 
+            $paragraph_count = count($title_data);
+            $paragraph_info = array();
+            $paragraph_info[] = array($from, -1, $paragraph_count, -1, -1, -1);
 
-                for ($iPar = 0; $iPar < count($title_data); $iPar++) {
-                    $title_data[$iPar]["level"] = $arrInserString[$iPar][3];
-                }
 
+            for ($iPar = 0; $iPar < count($title_data); $iPar++) {
+                $title_data[$iPar]["level"] = $arrInserString[$iPar][3];
+            }
 
-				for ($iPar = 0; $iPar < count($title_data); $iPar++) {
-					$book = $from ;
-					$paragraph = $title_data[$iPar]["paragraph"];
+            for ($iPar = 0; $iPar < count($title_data); $iPar++) {
+                $book = $from ;
+                $paragraph = $title_data[$iPar]["paragraph"];
+                $true_level = (int) $title_data[$iPar]["level"];
 
-					if ((int) $title_data[$iPar]["level"] == 8) {
-						$title_data[$iPar]["level"] = 100;
-					}
+                if ((int) $title_data[$iPar]["level"] == 8) {
+                    $title_data[$iPar]["level"] = 100;
+                }
+                $curr_level = (int) $title_data[$iPar]["level"];
+                # 计算这个chapter的段落数量
+                $length = -1;
 
-					$curr_level = (int) $title_data[$iPar]["level"];
-					# 计算这个chapter的段落数量
-					$length = -1;
-				
-					
-					for ($iPar1 = $iPar + 1; $iPar1 < count($title_data); $iPar1++) {
-						$thislevel = (int) $title_data[$iPar1]["level"];
-						if ($thislevel <= $curr_level) {
-							$length = (int) $title_data[$iPar1]["paragraph"] - $paragraph;
-							break;
-						}
-					}
 
-					if ($length == -1) {
-						$length = $paragraph_count - $paragraph + 1;
-					}
+                for ($iPar1 = $iPar + 1; $iPar1 < count($title_data); $iPar1++) {
+                    $thislevel = (int) $title_data[$iPar1]["level"];
+                    if ($thislevel <= $curr_level) {
+                        $length = (int) $title_data[$iPar1]["paragraph"] - $paragraph;
+                        break;
+                    }
+                }
+
+                if ($length == -1) {
+                    $length = $paragraph_count - $paragraph + 1;
+                }
 
-                    /*
-                    上一个段落
-                    算法:查找上一个标题段落。而且该标题段落的下一个段落不是标题段落
-                    */
-                    $prev = -1;
-                    if ($iPar > 0) {
-                        for ($iPar1 = $iPar - 1; $iPar1 >= 0; $iPar1--) {
-                            if ($title_data[$iPar1]["level"] < 8 && $title_data[$iPar1+1]["level"]==100) {
-                                $prev = $title_data[$iPar1]["paragraph"];
-                                break;
-                            }
+                /*
+                上一个段落
+                算法:查找上一个标题段落。而且该标题段落的下一个段落不是标题段落
+                */
+                $prev = -1;
+                if ($iPar > 0) {
+                    for ($iPar1 = $iPar - 1; $iPar1 >= 0; $iPar1--) {
+                        if ($title_data[$iPar1]["level"] < 8 && $title_data[$iPar1+1]["level"]==100) {
+                            $prev = $title_data[$iPar1]["paragraph"];
+                            break;
                         }
                     }
-                    /*
-                    下一个段落
-                    算法:查找下一个标题段落。而且该标题段落的下一个段落不是标题段落
-                    */
-                    $next = -1;
-                    if ($iPar < count($title_data) - 1) {
-                        for ($iPar1 = $iPar + 1; $iPar1 < count($title_data)-1; $iPar1++) {
-                            if ($title_data[$iPar1]["level"] <8 && $title_data[$iPar1+1]["level"]==100) {
-                                $next = $title_data[$iPar1]["paragraph"];
-                                break;
-                            }
+                }
+                /*
+                下一个段落
+                算法:查找下一个标题段落。而且该标题段落的下一个段落不是标题段落
+                */
+                $next = -1;
+                if ($iPar < count($title_data) - 1) {
+                    for ($iPar1 = $iPar + 1; $iPar1 < count($title_data)-1; $iPar1++) {
+                        if ($title_data[$iPar1]["level"] <8 && $title_data[$iPar1+1]["level"]==100) {
+                            $next = $title_data[$iPar1]["paragraph"];
+                            break;
                         }
                     }
+                }
+                //查找parent
+                $parent = -1;
+                if ($iPar > 0) {
+                    for ($iPar1 = $iPar - 1; $iPar1 >= 0; $iPar1--) {
+                        if ($title_data[$iPar1]["level"] < $true_level) {
+                            $parent = $title_data[$iPar1]["paragraph"];
+                            break;
+                        }
+                    }
+                }
+                //计算章节包含总字符数
+                $iChapter_strlen = 0;
 
-					$parent = -1;
-					if ($iPar > 0) {
-						for ($iPar1 = $iPar - 1; $iPar1 >= 0; $iPar1--) {
-							if ($title_data[$iPar1]["level"] < $curr_level) {
-								$parent = $title_data[$iPar1]["paragraph"];
-								break;
-							}
-						}
-					}
-					//计算章节包含总字符数
-					$iChapter_strlen = 0;
-
-					for ($i = $iPar; $i < $iPar + $length; $i++) {
-						$iChapter_strlen += $title_data[$i]["lenght"];
-					}
+                for ($i = $iPar; $i < $iPar + $length; $i++) {
+                    $iChapter_strlen += $title_data[$i]["lenght"];
+                }
 
-					$newData = [
-						'level' => $arrInserString[$iPar][3],
-						'toc' => $arrInserString[$iPar][5],
-						'chapter_len' => $length,
-						'next_chapter' => $next,
-						'prev_chapter' => $prev,
-						'parent' => $parent,
-						'chapter_strlen'=> $iChapter_strlen,
-					];
-
-                    $path = [];
-
-                    $title_data[$iPar]["level"] = $newData["level"];
-                    $title_data[$iPar]["toc"] = $newData["toc"];
-                    $title_data[$iPar]["parent"] = $newData["parent"];
-
-					/*
-                    *获取路径
-                    */
-                    $currParent = $parent;
-                    
-                    $iLoop = 0;
-                    while ($currParent != -1 && $iLoop<7) {
-                        # code...
-                        $pathTitle = $title_data[$currParent-1]["toc"];
-                        $pathLevel = $title_data[$currParent-1]['level'];
-                        $path[] = ["book"=>$book,"paragraph"=>$currParent,"title"=>$pathTitle,"level"=>$pathLevel];
-                        $currParent = $title_data[$currParent-1]["parent"];
-                        $iLoop++;
-                    }
-                    # 将路径反向
-                    $path1 = [];
-                    for ($i=count($path)-1; $i >=0 ; $i--) { 
-                        # code...
-                        $path1[] = $path[$i];
-                    }
-                    $newData['path'] = $path1;
+                $newData = [
+                    'level' => $arrInserString[$iPar][3],
+                    'toc' => $arrInserString[$iPar][5],
+                    'chapter_len' => $length,
+                    'next_chapter' => $next,
+                    'prev_chapter' => $prev,
+                    'parent' => $parent,
+                    'chapter_strlen'=> $iChapter_strlen,
+                ];
+
+                $path = [];
+
+                $title_data[$iPar]["level"] = $newData["level"];
+                $title_data[$iPar]["toc"] = $newData["toc"];
+                $title_data[$iPar]["parent"] = $newData["parent"];
+
+                /*
+                *获取路径
+                */
+                $currParent = $parent;
+
+                $iLoop = 0;
+                while ($currParent != -1 && $iLoop<7) {
+                    # code...
+                    $pathTitle = $title_data[$currParent-1]["toc"];
+                    $pathLevel = $title_data[$currParent-1]['level'];
+                    $path[] = ["book"=>$book,"paragraph"=>$currParent,"title"=>$pathTitle,"level"=>$pathLevel];
+                    $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];
+                }
 
+                # 将路径反向
+                $path1 = [];
+                for ($i=count($path)-1; $i >=0 ; $i--) {
+                    # code...
+                    $path1[] = $path[$i];
+                }
+                $newData['path'] = $path1;
 
-					PaliText::where('book',$book)
-							->where('paragraph',$paragraph)
-							->update($newData);
 
-					if ($curr_level > 0 && $curr_level < 8) {
-						$paragraph_info[] = array($book, $paragraph, $length, $prev, $next, $parent);
-					}
-				}
-			}
+                PaliText::where('book',$book)
+                        ->where('paragraph',$paragraph)
+                        ->update($newData);
 
-            
+                if ($curr_level > 0 && $curr_level < 8) {
+                    $paragraph_info[] = array($book, $paragraph, $length, $prev, $next, $parent);
+                }
+            }
 			$bar->advance();
 		}
 		$bar->finish();
-	
-		$this->info("instert pali text finished. in ". time()-$startTime . "s" .PHP_EOL);
+
+		$this->info("instert pali text finished. in ". time()-$startTime . "s" );
 		Log::info("instert pali text finished. in ". time()-$startTime . "s");
         return 0;
     }

+ 64 - 0
app/Console/Commands/UpgradePcdBookId.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\FtsText;
+use App\Models\WbwTemplate;
+use App\Models\BookTitle;
+
+class UpgradePcdBookId extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:pcd.book.id {--table=all}';
+
+    /**
+     * 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()
+    {
+        $table = $this->option('table');
+        $bookTitles = BookTitle::orderBy('id')->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]);
+            }
+            if($table === 'all' || $table ==='wbw'){
+                WbwTemplate::where('book',$value->book)
+                    ->where('paragraph','>=',$value->paragraph)
+                    ->update(['pcd_book_id'=>$value->id]);
+            }
+            $bar->advance();
+        }
+        $bar->finish();
+
+        return 0;
+    }
+}

+ 51 - 22
app/Console/Commands/UpgradeRegular.php

@@ -1,5 +1,9 @@
 <?php
-
+/**
+ * 生成系统规则变形词典
+ * 算法: 扫描字典里的所有单词。根据语尾表变形。
+ * 并在词库中查找是否在三藏中出现。出现的保存。
+ */
 namespace App\Console\Commands;
 
 use App\Models\UserDict;
@@ -8,6 +12,7 @@ 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
 {
@@ -16,15 +21,14 @@ class UpgradeRegular extends Command
      *
      * @var string
      */
-    protected $signature = 'upgrade:regular';
+    protected $signature = 'upgrade:regular {word?} {--debug}';
 
     /**
      * The console command description.
      *
      * @var string
      */
-    protected $description = 'Command description';
-
+    protected $description = 'upgrade regular';
     /**
      * Create a new command instance.
      *
@@ -42,6 +46,11 @@ class UpgradeRegular extends Command
      */
     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){
@@ -75,19 +84,28 @@ class UpgradeRegular extends Command
 		}
 		fclose($handle);
 
-		
-		$words = UserDict::where('type','.n:base.')
-						->orWhere('type','.v:base.')
-						->orWhere('type','.adj:base.')
-						->orWhere('type','.ti:base.')
-						->select(['word','type','grammar'])
+		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 
+		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;
 		";
@@ -123,7 +141,7 @@ class UpgradeRegular extends Command
 			if($casetable === false){
 				continue;
 			}
-			//$this->info("{$word->word}:{$word->type}");
+			if($this->option('debug'))  $this->info("{$word->word}:{$word->type}");
 			foreach($casetable as $thiscase){
 				if($word->type==".v:base."){
 					$endLen = (int)$thiscase[0];
@@ -139,7 +157,7 @@ class UpgradeRegular extends Command
 					$head = mb_substr($word->word,0,(0-$endLen),"UTF-8");//原词剩余的部分
 					$newEnding = $thiscase[3];
 					$newGrammar = $thiscase[4];
-					$newword=$head.$thiscase[2];					
+					$newword=$head.$thiscase[2];
 					if($word->type==".n:base."){
 						//名词
 						if($thiscase[0]==$word->grammar  && $thiscase[1]==$end){
@@ -147,7 +165,7 @@ class UpgradeRegular extends Command
 							$isMatch = true;
 						}else{
 							$isMatch = false;
-						}						
+						}
 					}else{
 						//形容词
 						if($thiscase[1]==$end){
@@ -155,19 +173,19 @@ class UpgradeRegular extends Command
 							$isMatch = true;
 						}else{
 							$isMatch = false;
-						}							
+						}
 					}
 
 				}
 
 				if($isMatch){
-					//$this->error($newword.':match');
+					if($this->option('debug'))  $this->error($newword.':match');
 					//查询这个词是否在三藏存在
-					$exist = Cache::remember('palicanon/word/exists/'.$newword, 10 , function() use($newword) {
+					$exist = Cache::remember('palicanon/word/exists/'.$newword, 100 , function() use($newword) {
 						return WbwTemplate::where('real',$newword)->exists();
 					});
 					if($exist){
-						//$this->info("{$newword} exists");
+						if($this->option('debug'))  $this->info('exist');
 						$new = UserDict::firstOrNew(
 							[
 								'word' => $newword,
@@ -175,23 +193,34 @@ class UpgradeRegular extends Command
 								'grammar' => $newGrammar,
 								'parent' => $word->word,
 								'factors' => "{$word->word}+[{$newEnding}]",
-								'source' => '_SYS_REGULAR_'
+								'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;
     }
 }

+ 106 - 0
app/Console/Commands/UpgradeRelatedParagraph.php

@@ -0,0 +1,106 @@
+<?php
+/**
+ * 更新段落关联数据库
+ * 用于找到根本和义注复注的对应段落
+ */
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use App\Models\RelatedParagraph;
+use App\Models\BookTitle;
+use Illuminate\Support\Facades\Log;
+
+class UpgradeRelatedParagraph extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:related.paragraph {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()
+    {
+        $this->info("upgrade related.paragraph");
+		$startTime = time();
+        #删除目标数据库中数据
+        RelatedParagraph::where('book','>',0)->delete();
+		// 打开csv文件并读取数据
+        $strFileName = config("app.path.pali_title") . "/cs6_para.csv";
+        if(!file_exists($strFileName)){
+            return 1;
+        }
+        $inputRow = 0;
+        $fp = fopen($strFileName, "r");
+        if (!$fp ) {
+            $this->error("can not open csv $strFileName");
+            Log::error("can not open csv $strFileName");
+        }
+        $bookTitles = BookTitle::orderBy('id','desc')->get();
+
+        while (($data = fgetcsv($fp, 0, ',')) !== false) {
+            if($inputRow>0){
+                if(!empty($this->argument('book'))){
+                    if($this->argument('book') !=$data[0] ){
+                        continue;
+                    }
+                }
+                //获取书号
+                $bookId = 0;
+                foreach ($bookTitles as $bookTitle) {
+                    # code...
+                    if((int)$data[0] === $bookTitle->book){
+                        if((int)$data[1] >= $bookTitle->paragraph){
+                            $bookId = $bookTitle->id;
+                            break;
+                        }
+                    }
+                }
+                $begin = (int) $data[3];
+                $end = (int) $data[4];
+                $arrPara = array();
+                for ($i = $begin; $i <= $end; $i++) {
+                    $arrPara[] = $i;
+                }
+                foreach ($arrPara as $key => $para) {
+                    $newRow = new RelatedParagraph();
+                    $newRow->book = $data[0];
+                    $newRow->para = $data[1];
+                    $newRow->book_id = $bookId;
+                    $newRow->cs_para = $para;
+                    $newRow->book_name = $data[2];
+                    $newRow->save();
+                }
+            }
+            $inputRow++;
+            if($inputRow % 1000 == 0){
+                $this->info($inputRow);
+            }
+        }
+        fclose($fp);
+		$this->info("all done. in ". time()-$startTime . "s" );
+        return 0;
+    }
+}

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

@@ -0,0 +1,49 @@
+<?php
+/**
+ * 局部刷新语料库
+ */
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class UpgradeTestData extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:test.data {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()
+    {
+        $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]);
+        $this->call('upgrade:palitext',[$this->argument('book')]);
+        $this->call('upgrade:compound',["--book"=>$this->argument('book')]);
+        return 0;
+    }
+}

+ 16 - 2
app/Console/Commands/UpgradeWbwAnalyses.php

@@ -50,7 +50,7 @@ class UpgradeWbwAnalyses extends Command
         }else{
             $it = Wbw::where('id',$this->argument('id'))->orderby('id')->cursor();
         }
-        
+
         foreach ($it as $wbwrow) {
             $counter++;
             WbwAnalysis::where('wbw_id',$wbwrow->id)->delete();
@@ -64,10 +64,11 @@ class UpgradeWbwAnalyses extends Command
             }catch(Exception $e){
                 continue;
             }
-            
+
             $wordsList = $xmlWord->xpath('//word');
             foreach ($wordsList as $word) {
                 $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_") {
@@ -104,10 +105,23 @@ class UpgradeWbwAnalyses extends Command
                             case 'org':
                                 $newData['type']=4;
                                 WbwAnalysis::insert($newData);
+                                $factors=explode("+",$strValue);
                                 break;
                             case 'om':
                                 $newData['type']=5;
                                 WbwAnalysis::insert($newData);
+                                # 存储拆分意思
+                                $newData['type']=7;
+                                $factorMeaning = explode('+',$strValue);
+                                foreach ( $factors as $index => $factor) {
+                                    if(isset($factorMeaning[$index]) &&
+                                      !empty($factorMeaning[$index]) &&
+                                      $factorMeaning[$index] !== "↓↓" ){
+                                        $newData['wbw_word'] = $factor;
+                                        $newData['data'] = $factorMeaning[$index];
+                                        WbwAnalysis::insert($newData);
+                                    }
+                                }
                                 break;
                             case 'parent':
                                 $newData['type']=6;

+ 125 - 0
app/Console/Commands/UpgradeWbwTemplate.php

@@ -0,0 +1,125 @@
+<?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 UpgradeWbwTemplate extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:wbw.template {book?} {para?}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'upgrade wbw template by sentence';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $start = time();
+		$pali = new PaliSentence;
+		if(!empty($this->argument('book'))){
+			$pali = $pali->where('book',$this->argument('book'));
+		}
+		if(!empty($this->argument('para'))){
+			$pali = $pali->where('paragraph',$this->argument('para'));
+		}
+		$bar = $this->output->createProgressBar($pali->count());
+        $channelId = ChannelApi::getSysChannel('_System_Wbw_VRI_');
+        if($channelId===false){
+            $this->error('no channel');
+            return 1;
+        }
+		$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...
+                $type = $wbw_word->type=='?'? '':$wbw_word->type;
+                $grammar = $wbw_word->gramma=='?'? '':$wbw_word->gramma;
+                $part = $wbw_word->part=='?'? '':$wbw_word->part;
+                if(!empty($type) || !empty($grammar)){
+                    $case = "{$type}#$grammar";
+                }else{
+                    $case = "";
+                }
+                $wbwContent[] = [
+                    'sn'=>[$wbw_word->wid],
+                    'word'=>['value'=>$wbw_word->word,'status'=>0],
+                    'real'=> ['value'=>$wbw_word->real,'status'=>0],
+                    'meaning'=> ['value'=>'','status'=>0],
+                    'type'=> ['value'=>$type,'status'=>0],
+                    'grammar'=> ['value'=>$grammar,'status'=>0],
+                    'case'=> ['value'=>$case,'status'=>0],
+                    'style'=> ['value'=>$wbw_word->style,'status'=>0],
+                    'factors'=> ['value'=>$part,'status'=>0],
+                    'factorMeaning'=> ['value'=>'','status'=>0],
+                    'confidence'=> 0.5
+                ];
+
+            }
+            $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("app.admin.root_uuid");
+            $newRow->content = trim($sent);
+            $newRow->strlen = mb_strlen($sent,"UTF-8");
+            $newRow->status = 30;
+            $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;
+    }
+}

+ 47 - 0
app/Console/Commands/UpgradeWeekly.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class UpgradeWeekly extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'upgrade:weekly';
+
+    /**
+     * 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()
+    {
+        # 段落更新图
+        $this->call('upgrade:chapterdynamic');
+        $this->call('upgrade:chapter.dynamic.weekly');
+        $this->call('export:offline');
+
+        return 0;
+    }
+}

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

@@ -0,0 +1,72 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+use App\Models\Channel;
+use App\Models\Collection;
+use App\Models\DhammaTerm;
+use App\Models\GroupInfo;
+use App\Models\GroupMember;
+use App\Models\SentBlock;
+use App\Models\SentHistory;
+use App\Models\SentPr;
+use App\Models\Sentence;
+use App\Models\Share;
+use App\Models\WbwBlock;
+use App\Models\Wbw;
+
+class UuidViranyani extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'uuid:viranyani';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '修改各个表中的viranyani 的 user_uid 为小写';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $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]);
+		$result = DB::select('UPDATE "collections" set "owner"=? where "owner"=? ',[strtolower($old),$old]);
+		$result = DB::select('UPDATE "dhamma_terms" set "owner"=? where "owner"=? ',[strtolower($old),$old]);
+		$result = DB::select('UPDATE "group_infos" set "owner"=? where "owner"=? ',[strtolower($old),$old]);
+		$result = DB::select('UPDATE "group_members" set "user_id"=? where "user_id"=? ',[strtolower($old),$old]);
+		$result = DB::select('UPDATE "sent_blocks" set "owner_uid"=? where "owner_uid"=? ',[strtolower($old),$old]);
+		$result = DB::select('UPDATE "sent_blocks" set "editor_uid"=? where "editor_uid"=? ',[strtolower($old),$old]);
+		$result = DB::select('UPDATE "sent_histories" set "user_uid"=? where "user_uid"=? ',[strtolower($old),$old]);
+		$result = DB::select('UPDATE "sent_prs" set "editor_uid"=? where "editor_uid"=? ',[strtolower($old),$old]);
+		$result = DB::select('UPDATE "sentences" set "editor_uid"=? where "editor_uid"=? ',[strtolower($old),$old]);
+		$result = DB::select('UPDATE "shares" set "cooperator_id"=? where "cooperator_id"=? ',[strtolower($old),$old]);
+		$result = DB::select('UPDATE "wbw_blocks" set "creator_uid"=? where "creator_uid"=? ',[strtolower($old),$old]);
+		$result = DB::select('UPDATE "wbws" set "creator_uid"=? where "creator_uid"=? ',[strtolower($old),$old]);
+
+        $this->info('done');
+        return 0;
+    }
+}

+ 4 - 0
app/Console/Kernel.php

@@ -19,6 +19,10 @@ class Kernel extends ConsoleKernel
                  ->dailyAt('00:00')
                  ->emailOutputTo(config("app.email.ScheduleEmailOutputTo"))
 				 ->emailOutputOnFailure(config("app.email.ScheduleEmailOutputOnFailure"));
+
+        $schedule->command('upgrade:weekly')
+                 ->weekly()
+                 ->emailOutputOnFailure(config("app.email.ScheduleEmailOutputOnFailure"));
     }
 
     /**

+ 30 - 0
app/Console/Workers/Workers.php

@@ -0,0 +1,30 @@
+<?php
+namespace App\Console\Workers;
+class Workers{
+    protected $queue = 'hello';
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $connection = new AMQPStreamConnection(MQ_HOST, MQ_PORT, MQ_USERNAME, MQ_PASSWORD);
+		$channel = $connection->channel();
+
+		$channel->queue_declare($this->queue, false, true, false, false);
+
+		echo " [*] Waiting for messages. To exit press CTRL+C\n";
+
+		$channel->basic_consume($this->queue, '', false, true, false, false, $this->job);
+
+		while ($channel->is_open()) {
+			  $channel->wait();
+		  }
+        return 0;
+    }
+
+    public function job($msg){
+        echo ' [x] Received ', $msg->body, "\n";
+    }
+}

+ 34 - 0
app/Http/Api/AuthApi.php

@@ -0,0 +1,34 @@
+<?php
+namespace App\Http\Api;
+
+use Illuminate\Support\Facades\Log;
+use Illuminate\Http\Request;
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+class AuthApi{
+    public static function current(Request $request){
+        if($request->hasHeader('Authorization')){
+            $token = $request->header('Authorization');
+            if(\substr($token,0,6) === 'Bearer'){
+                $token = trim(substr($token,6));
+                if($token === "null"){
+                    return false;
+                }
+                $jwt = JWT::decode($token,new Key(env('APP_KEY'),'HS512'));
+                if($jwt->exp < time()){
+                    return false;
+                }else{
+                    //有效的token
+                    return ['user_uid'=>$jwt->uid,'user_id'=>$jwt->id];
+                }
+            }else{
+                return false;
+            }
+        }else if(isset($_COOKIE['user_uid'])){
+            return ['user_uid'=>$_COOKIE['user_uid'],'user_id'=>$_COOKIE['user_id']];
+        }else{
+            return false;
+        }
+    }
+}

+ 43 - 0
app/Http/Api/ChannelApi.php

@@ -0,0 +1,43 @@
+<?php
+namespace App\Http\Api;
+use App\Models\Channel;
+
+class ChannelApi{
+    public static function getById($id){
+        $channel = Channel::where("uid",$id)->first();
+        if($channel){
+            return [
+                    'id'=>$id,
+                    'name'=>$channel['name'],
+                    'type'=>$channel['type'],
+                    'lang'=>$channel['lang'],
+                    'studio_id'=>$channel['owner_uid'],
+                ];
+        }else{
+            return false;
+        }
+    }
+    public static function getListByUser(){
+
+    }
+    public static function getSysChannel($channel_name,$fallback=""){
+        $channel=  Channel::where('name',$channel_name)
+                    ->where('owner_uid',config("app.admin.root_uuid"))
+                    ->first();
+        if(!$channel){
+            if(!empty($fallback)){
+                $channel = Channel::where('name',$fallback)
+                                  ->where('owner_uid',config("app.admin.root_uuid"))
+                                  ->first();
+                if(!$channel){
+                    return false;
+                }else{
+                    return $channel->uid;
+                }
+            }
+            return false;
+        }else{
+            return $channel->uid;
+        }
+    }
+}

+ 81 - 0
app/Http/Api/DictApi.php

@@ -0,0 +1,81 @@
+<?php
+namespace App\Http\Api;
+use App\Models\DictInfo;
+
+class DictApi{
+    public static function langOrder(string $lang){
+        switch ($lang) {
+            case 'zh':
+                $output = ["zh","jp","en","my"];
+                break;
+            case 'en':
+                $output = ["en","my"];
+                break;
+            case 'my':
+                $output = ["my","en"];
+                break;
+            default:
+                $output = [$lang,"en","my"];
+                break;
+        }
+        $output[] = "others";
+        return $output;
+    }
+
+    public static function dictOrder($lang){
+        $output = [];
+        switch ($lang) {
+            case 'zh':
+                $output = [
+                "0d79e8e8-1430-4c99-a0f1-b74f2b4b26d8",	/*《巴汉词典》增订*/
+                "f364d3dc-b611-471b-9a4f-531286b8c2c3",	/*《巴汉词典》Mahāñāṇo Bhikkhu编著*/
+                "0e4dc5c8-a228-4693-92ba-7d42918d8a91",	/*汉译パーリ语辞典-黃秉榮*/
+                "6aa9ec8b-bba4-4bcd-abd2-9eae015bad2b",	/*汉译パーリ语辞典-李瑩*/
+                "eb99f8b4-c3e5-43af-9102-6a93fcb97db6",	/*パーリ语辞典--勘误表*/
+                ];
+                break;
+            case 'jp':
+                $output = [
+                "91d3ec93-3811-4973-8d84-ced99179a0aa",	/*パーリ语辞典*/
+                "6d6c6812-75e7-457d-874f-5b049ad4b6de",	/*パーリ语辞典-增补*/
+                ];
+                break;
+            case 'en':
+                $output = [
+                "c6e70507-4a14-4687-8b70-2d0c7eb0cf21",	/*	Concise P-E Dict*/
+                "6afb8c05-5cbe-422e-b691-0d4507450cb7",	/*	PTS P-E dictionary*/
+                ];
+                break;
+            case 'my':
+                $output =[
+                "e740ef40-26d7-416e-96c2-925d6650ac6b",	/*	Tipiṭaka Pāḷi-Myanmar*/
+                "beb45062-7c20-4047-bcd4-1f636ba443d1",	/*	U Hau Sein’s Pāḷi-Myanmar Dictionary*/
+                "1e299ccb-4fc4-487d-8d72-08f63d84c809",	/*	Pali Roots Dictionary*/
+                "6f9caea1-17fa-41f1-92e5-bd8e6e70e1d7",	/*	U Hau Sein’s Pāḷi-Myanmar*/
+                ];
+                break;
+            case 'vi':
+                $output = [
+                "23f67523-fa03-48d9-9dda-ede80d578dd2",	/*	Pali Viet Dictionary*/
+                "4ac8a0d5-9c6f-4b9f-983d-84288d47f993",	/*	Pali Viet Abhi-Terms*/
+                "7c7ee287-35ba-4cf3-b87b-30f1fa6e57c9",	/*	Pali Viet Vinaya Terms*/
+                ];
+                break;
+            default:
+                $output = [];
+                break;
+        };
+        $output[] = "others";
+        return $output;
+    }
+    public static function getSysDict($name){
+        $dict_info=  DictInfo::where('name',$name)
+                    ->where('owner_id',config("app.admin.root_uuid"))
+                    ->first();
+        if(!$dict_info){
+            return false;
+        }else{
+            return $dict_info->id;
+        }
+    }
+}

+ 18 - 0
app/Http/Api/GroupApi.php

@@ -0,0 +1,18 @@
+<?php
+namespace App\Http\Api;
+use App\Models\GroupInfo;
+
+class GroupApi{
+    public static function getById($id){
+        $group = GroupInfo::where("uid",$id)->first();
+        if($group){
+            return [
+                    'id'=>$id,
+                    'name'=>$group->name,
+                ];
+        }else{
+            return false;
+        }
+    }
+
+}

+ 246 - 0
app/Http/Api/MdRender.php

@@ -0,0 +1,246 @@
+<?php
+namespace App\Http\Api;
+
+use Illuminate\Support\Str;
+use mustache\mustache;
+use App\Models\DhammaTerm;
+use App\Models\PaliText;
+use App\Models\Channel;
+use App\Http\Controllers\CorpusController;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
+
+class MdRender{
+    public static function wiki2xml(string $wiki):string{
+        /**
+         * 替换{{}} 到xml之前 要先把换行符号去掉
+         */
+        $html = str_replace("\n","",$wiki);
+
+        $pattern = "/\{\{(.+?)\|/";
+        $replacement = '<MdTpl name="$1"><param>';
+        $html = preg_replace($pattern,$replacement,$html);
+        $html = str_replace("}}","</param></MdTpl>",$html);
+        $html = str_replace("|","</param><param>",$html);
+
+        /**
+         * 替换变量名
+         */
+
+        $pattern = "/<param>([a-z]+?)=/";
+        $replacement = '<param name="$1">';
+        $html = preg_replace($pattern,$replacement,$html);
+
+        $html = str_replace("<p>","<div>",$html);
+        $html = str_replace("</p>","</div>",$html);
+        $html = "<span>".$html."</span>";
+        return $html;
+    }
+    public static function xmlQueryId(string $xml, string $id):string{
+        try{
+            $dom = simplexml_load_string($xml);
+        }catch(\Exception $e){
+            Log::error($e);
+            return "<div></div>";
+        }
+        $tpl_list = $dom->xpath('//MdTpl');
+        foreach ($tpl_list as $key => $tpl) {
+            foreach ($tpl->children() as  $param) {
+                # 处理每个参数
+                if($param->getName() === "param"){
+                    foreach($param->attributes() as $pa => $pa_value){
+                        $pValue = $pa_value->__toString();
+                        if($pa === "name" && $pValue === "id"){
+                            if($param->__toString() === $id){
+                                return $tpl->asXML();
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return "<div></div>";
+    }
+    public static function take_sentence(string $xml):array{
+        $output = [];
+        try{
+            $dom = simplexml_load_string($xml);
+        }catch(\Exception $e){
+            Log::error($e);
+            return $output;
+        }
+        $tpl_list = $dom->xpath('//MdTpl');
+        foreach ($tpl_list as $key => $tpl) {
+            foreach($tpl->attributes() as $a => $a_value){
+                if($a==="name"){
+                    if($a_value->__toString() ==="sent"){
+                        foreach ($tpl->children() as  $param) {
+                            # 处理每个参数
+                            if($param->getName() === "param"){
+                                $sent = $param->__toString();
+                                if(!empty($sent)){
+                                    $output[] = $sent;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return $output;
+    }
+    public static function xml2tpl(string $xml, $channelId="",$mode='read'):string{
+        /**
+         * 解析xml
+         * 获取模版参数
+         * 生成react 组件参数
+         */
+        try{
+            $dom = simplexml_load_string($xml);
+        }catch(\Exception $e){
+            Log::error($e);
+            Log::error($xml);
+            return "<span>xml解析错误{$e}</span>";
+        }
+
+        $channelInfo = Channel::find($channelId);
+
+        $tpl_list = $dom->xpath('//MdTpl');
+        foreach ($tpl_list as $key => $tpl) {
+            /**
+             * 遍历 MdTpl 处理参数
+             */
+            $props = [];
+            $tpl_name = '';
+            foreach($tpl->attributes() as $a => $a_value){
+                if($a==="name"){
+                    $tpl_name = $a_value;
+                }
+            }
+            $param_id = 0;
+            foreach ($tpl->children() as  $param) {
+                # 处理每个参数
+                if($param->getName() === "param"){
+                    $param_id++;
+                    $props["{$param_id}"] = $param->__toString();
+                    foreach($param->attributes() as $pa => $pa_value){
+                        if($pa === "name"){
+                            $props["{$pa_value}"] = $param->__toString();
+                        }
+                    }
+                }
+            }
+            /**
+             * 生成模版参数
+             */
+            $tplRender = new TemplateRender($props,$channelInfo,$mode);
+            $tplProps = $tplRender->render($tpl_name);
+            if($tplProps){
+                $tpl->addAttribute("props",$tplProps['props']);
+                $tpl->addAttribute("tpl",$tplProps['tpl']);
+                $tpl->addChild($tplProps['tag'],$tplProps['html']);
+            }
+        }
+        $html = str_replace('<?xml version="1.0"?>','',$dom->asXML()) ;
+        $html = str_replace(['<xml>','</xml>'],['<span>','</span>'],$html);
+        return $html;
+    }
+
+    public static function render2($markdown,$channelId='',$queryId=null,$mode='read',$channelType,$contentType="markdown"){
+        $wiki = MdRender::markdown2wiki($markdown,$channelType,$contentType);
+        $html = MdRender::wiki2xml($wiki);
+        if(!is_null($queryId)){
+            $html = MdRender::xmlQueryId($html, $queryId);
+        }
+        $tpl = MdRender::xml2tpl($html,$channelId,$mode);
+        return $tpl;
+    }
+    public static function markdown2wiki(string $markdown,$channelType,$contentType): string{
+                /**
+         * nissaya
+         * aaa=bbb\n
+         * {{nissaya|aaa|bbb}}
+         */
+        if($channelType==='nissaya'){
+            if($contentType === "json"){
+                $json = json_decode($markdown);
+                $nissayaWord = [];
+                foreach ($json as $word) {
+                    if(count($word->sn) === 1){
+                        //只输出第一层级
+                        $str = "{{nissaya|";
+                        if(isset($word->word->value)){
+                            $str .= $word->word->value;
+                        }
+                        $str .= "|";
+                        if(isset($word->meaning->value)){
+                            $str .= $word->meaning->value;
+                        }
+                        $str .= "}}";
+                        $nissayaWord[] = $str;
+                    }
+
+                }
+                $markdown = implode('',$nissayaWord);
+            }else{
+                $pattern = '/(.+?)=(.+?)\n/';
+                $replacement = '{{nissaya|$1|$2}}';
+                $markdown = preg_replace($pattern,$replacement,$markdown);
+                $pattern = '/(.+?)=(.?)\n/';
+                $replacement = '{{nissaya|$1|$2}}';
+                $markdown = preg_replace($pattern,$replacement,$markdown);
+                $pattern = '/(.?)=(.+?)\n/';
+                $replacement = '{{nissaya|$1|$2}}';
+                $markdown = preg_replace($pattern,$replacement,$markdown);
+            }
+        }
+        $markdown = preg_replace("/\n\n/","<div></div>",$markdown);
+
+
+        /**
+         * 替换换行符
+         * react 无法处理 <br> 替换为<div></div>代替换行符作用
+         */
+        $markdown = str_replace('<br>','<div></div>',$markdown);
+
+        /**
+         * markdown -> html
+         */
+        $html = Str::markdown($markdown);
+
+        #替换术语
+        $pattern = "/\[\[(.+?)\]\]/";
+        $replacement = '{{term|$1}}';
+        $html = preg_replace($pattern,$replacement,$html);
+
+        #替换句子模版
+        $pattern = "/\{\{([0-9].+?)\}\}/";
+        $replacement = '{{sent|$1}}';
+        $html = preg_replace($pattern,$replacement,$html);
+
+        #替换单行注释
+        #<code>bla</code>
+        #{{note|bla}}
+        $pattern = '/<code>(.+?)<\/code>/';
+        $replacement = '{{note|$1}}';
+        $html = preg_replace($pattern,$replacement,$html);
+
+        #替换多行注释
+        #<pre><code>bla</code></pre>
+        #{{note|bla}}
+        $pattern = '/<pre><code>([\w\W]+?)<\/code><\/pre>/';
+        $replacement = '{{note|$1}}';
+        $html = preg_replace($pattern,$replacement,$html);
+
+        return $html;
+    }
+
+    /**
+     *
+     */
+    public static function render($markdown,$channelId,$queryId=null,$mode='read',$channelType='translation',$contentType="markdown"){
+        return MdRender::render2($markdown,$channelId,$queryId,$mode,$channelType,$contentType);
+    }
+
+}

+ 18 - 0
app/Http/Api/PaliTextApi.php

@@ -0,0 +1,18 @@
+<?php
+namespace App\Http\Api;
+
+use App\Models\PaliText;
+
+class PaliTextApi{
+    public static function getChapterStartEnd($book,$para){
+        $chapter = PaliText::where('book',$book)
+                        ->where('paragraph',$para)
+                        ->first();
+        if(!$chapter){
+            return false;
+        }
+        $start = $para;
+        $end = $para + $chapter->chapter_len -1;
+        return [$start,$end];
+    }
+}

+ 176 - 0
app/Http/Api/ShareApi.php

@@ -0,0 +1,176 @@
+<?php
+namespace App\Http\Api;
+use App\Models\GroupMember;
+use App\Models\Share;
+use App\Models\Article;
+use App\Models\Channel;
+use App\Models\Collection;
+use App\Http\Api\ChannelApi;
+
+class ShareApi{
+
+    /**
+     * 获取某用户的可见的协作资源
+     * $res_type 见readme.md#资源类型 -1全部类型资源
+     * ## 资源类型
+     *  1 PCS 文档
+     *  2 Channel 版本
+     *  3 Article 文章
+     *  4 Collection 文集
+     *  5 版本片段
+     * power 权限 10: 只读  20:编辑 30: 拥有者
+     */
+
+    public static function getResList($user_uid,$res_type=-1){
+        # 找我加入的群
+        $my_group = GroupMember::where("user_id",$user_uid)->select('group_id')->get();
+        $userList[] = $user_uid;
+        foreach ($my_group as $key => $value) {
+            # code...
+            $userList[]=$value["group_id"];
+        }
+
+        if($res_type==-1){
+            #所有类型资源
+            $Fetch =Share::whereIn("cooperator_id",$userList)->select(['res_id','res_type','power'])->get();
+        }
+        else{
+            #指定类型资源
+            $Fetch =Share::whereIn("cooperator_id",$userList)
+                        ->where('res_type',$res_type)
+                        ->select(['res_id','res_type','power'])->get();
+        }
+
+        $resOutput = array();
+        foreach ($Fetch as $key => $value) {
+            # 查重
+            if(isset($resOutput[$value["res_id"]])){
+                if($value["power"]>$resOutput[$value["res_id"]]["power"]){
+                    $resOutput[$value["res_id"]]["power"] = $value["power"];
+                }
+            }
+            else{
+                $resOutput[$value["res_id"]]= array("power"=> $value["power"],"type" => $value["res_type"]);
+            }
+        }
+        $resList=array();
+        foreach ($resOutput as $key => $value) {
+            # code...
+            $resList[]=array("res_id"=>$key,"res_type"=>(int)$value["type"],"power"=>(int)$value["power"]);
+        }
+
+        foreach ($resList as $key => $res) {
+            # 获取资源标题 和所有者
+            $resList[$key]["res_title"]="_unknown_";
+            $resList[$key]["res_owner_id"]="_unknown_";
+            $resList[$key]["type"]="_unknown_";
+            $resList[$key]["status"]="0";
+            $resList[$key]["lang"]="_unknown_";
+
+            switch ($res["res_type"]) {
+                case 1:
+                    # pcs 文档
+                    $resList[$key]["res_title"]="title";
+                    break;
+                case 2:
+                    # channel
+                    $channelInfo = Channel::where('uid',$res["res_id"])->first();
+                    if($channelInfo){
+                        $resList[$key]["res_title"]=$channelInfo["name"];
+                        $resList[$key]["res_owner_id"]=$channelInfo["owner_uid"];
+                        $resList[$key]["type"]=$channelInfo["type"];
+                        $resList[$key]["status"]=$channelInfo["status"];
+                        $resList[$key]["lang"]=$channelInfo["lang"];
+                    }
+                    break;
+                case 3:
+                    # 3 Article 文章
+                    $aInfo = Article::where('uid',$res["res_id"])->first();
+                    if($aInfo){
+                        $resList[$key]["res_title"]=$aInfo["title"];
+                        $resList[$key]["res_owner_id"]=$aInfo["owner"];
+                        $resList[$key]["status"]=$aInfo["status"];
+                        $resList[$key]["lang"]='';
+                    }
+                    break;
+                case 4:
+                    # 4 Collection 文集
+                    $aInfo = Collection::where('uid',$res["res_id"])->first();
+                    if($aInfo){
+                        $resList[$key]["res_title"]=$aInfo["title"];
+                        $resList[$key]["res_owner_id"]=$aInfo["owner"];
+                        $resList[$key]["status"]=$aInfo["status"];
+                        $resList[$key]["lang"]=$aInfo["lang"];
+                    }
+                    break;
+                case 5:
+                    # code...
+                    break;
+
+                default:
+                    # code...
+                    break;
+            }
+        }
+
+        return $resList;
+
+    }
+
+    /**
+     * 获取对某个共享资源的权限
+     */
+    public static function getResPower($user_uid,$res_id,$res_type=0){
+            if(empty($user_uid)){
+                #未登录用户 没有共享资源
+                return 0;
+            }
+            //查看是否为资源拥有者
+            if($res_type!=0){
+                switch ($res_type) {
+                    case 2:
+                        # channel
+                        $channel = ChannelApi::getById($res_id);
+                        if($channel){
+                            if($channel['studio_id'] === $user_uid){
+                                return 30;
+                            }
+                        }
+                        break;
+                    case 3:
+                        //Article
+                        $owner = Article::where('uid',$res_id)->value('owner');
+                        if($owner === $user_uid){
+                            return 30;
+                        }
+                        break;
+                    case 4:
+                        $owner = Collection::where('uid',$res_id)->value('owner');
+                        if($owner === $user_uid){
+                            return 30;
+                        }
+                        //文集
+                        break;
+                }
+            }
+            # 找我加入的群
+            $my_group = GroupMember::where("user_id",$user_uid)->select('group_id')->get();
+            $userList[] = $user_uid;
+            foreach ($my_group as $key => $value) {
+                $userList[]=$value["group_id"];
+            }
+            $Fetch =Share::whereIn("cooperator_id",$userList)
+                        ->where('res_id',$res_id)
+                        ->select(['power'])->get();
+            $power=0;
+            foreach ($Fetch as $key => $value) {
+                # code...
+                if((int)$value["power"]>$power){
+                    $power = $value["power"];
+                }
+            }
+            return $power;
+    }
+
+}
+

+ 41 - 0
app/Http/Api/StudioApi.php

@@ -0,0 +1,41 @@
+<?php
+namespace App\Http\Api;
+
+require_once __DIR__.'/../../../public/app/ucenter/function.php';
+
+class StudioApi{
+    public static function getIdByName($name){
+        /**
+         * 获取 uuid
+         */
+        //TODO 改为studio table
+        if(empty($name)){
+            return false;
+        }
+        $userinfo = new \UserInfo();
+        $studio = $userinfo->getUserByName($name);
+        if($studio){
+            return $userinfo->getUserByName($name)['userid'];
+        }else{
+            return false;
+        }
+
+    }
+    public static function getById($id){
+        //TODO 改为studio table
+        if(empty($id)){
+            return false;
+        }
+        $userinfo = new \UserInfo();
+        $studio = $userinfo->getName($id);
+        if(!$studio){
+            return false;
+        }
+        return [
+            'id'=>$id,
+            'nickName'=>$studio['nickname'],
+            'realName'=>$studio['username'],
+            'avatar'=>'',
+        ];
+    }
+}

+ 31 - 0
app/Http/Api/SuggestionApi.php

@@ -0,0 +1,31 @@
+<?php
+namespace App\Http\Api;
+
+use App\Models\SentPr;
+use App\Models\Discussion;
+use App\Models\Sentence;
+use App\Http\Api\PaliTextApi;
+
+class SuggestionApi{
+    public static function getCountBySent($book,$para,$start,$end,$channel,$type="suggestion"){
+        $count['suggestion'] = SentPr::where('book_id',$book)
+                                    ->where('paragraph',$para)
+                                    ->where('word_start',$start)
+                                    ->where('word_end',$end)
+                                    ->where('channel_uid',$channel)
+                                    ->count();
+        $sentId = Sentence::where('book_id',$book)
+                            ->where('paragraph',$para)
+                            ->where('word_start',$start)
+                            ->where('word_end',$end)
+                            ->where('channel_uid',$channel)
+                            ->value('uid');
+        if($sentId){
+            $count['discussion'] = Discussion::where('res_id',$sentId)
+                                            ->whereNull('parent')
+                                            ->count();
+        }
+
+        return $count;
+    }
+}

+ 266 - 0
app/Http/Api/TemplateRender.php

@@ -0,0 +1,266 @@
+<?php
+namespace App\Http\Api;
+
+use App\Models\DhammaTerm;
+use App\Models\PaliText;
+use App\Http\Controllers\CorpusController;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
+
+class TemplateRender{
+    protected $param = [];
+    protected $mode = "read";
+    protected $channel_id = "";
+
+    /**
+     * Create a new command instance.
+     * int $mode  'read' | 'edit'
+     * @return void
+     */
+    public function __construct($param, $channelInfo, $mode)
+    {
+        $this->param = $param;
+        $this->channel_id = $channelInfo->uid;
+        $this->channelInfo = $channelInfo;
+        $this->mode = $mode;
+    }
+
+    public function render($tpl_name){
+        switch ($tpl_name) {
+            case 'term':
+                # 术语
+                $result = $this->render_term();
+                break;
+            case 'note':
+                $result = $this->render_note();
+                break;
+            case 'sent':
+                $result = $this->render_sent();
+                break;
+            case 'quote':
+                $result = $this->render_quote();
+                break;
+            case 'exercise':
+                $result = $this->render_exercise();
+                break;
+            case 'article':
+                $result = $this->render_article();
+                break;
+            case 'nissaya':
+                $result = $this->render_nissaya();
+                break;
+            default:
+                # code...
+                $result = [
+                    'props'=>base64_encode(\json_encode([])),
+                    'html'=>'',
+                    'tag'=>'span',
+                    'tpl'=>'unknown',
+                ];
+                break;
+        }
+        return $result;
+    }
+
+    private function render_term(){
+        $word = $this->get_param($this->param,"word",1);
+        $channelId = $this->channel_id;
+        $channelInfo = $this->channelInfo;
+        $props = Cache::remember("/term/{$this->channel_id}/{$word}",
+              60,
+              function() use($word,$channelId,$channelInfo){
+                //先查属于这个channel 的
+                $tplParam = DhammaTerm::where("word",$word)->where('channal',$channelId)->first();
+                if(!$tplParam){
+                    //没有,再查这个studio的
+                    $tplParam = DhammaTerm::where("word",$word)
+                                          ->where('owner',$channelInfo->owner_uid)
+                                          ->first();
+                }
+                $output = [
+                    "word" => $word,
+                    "parentChannelId" => $channelId,
+                    "parentStudioId" => $channelInfo->owner_uid,
+                    ];
+                    $innerString = $output["word"];
+                if($tplParam){
+                    $output["id"] = $tplParam->guid;
+                    $output["meaning"] = $tplParam->meaning;
+                    $output["channel"] = $tplParam->channal;
+                    $innerString = "{$output["meaning"]}({$output["word"]})";
+                    if(!empty($tplParam->other_meaning)){
+                        $output["meaning2"] = $tplParam->other_meaning;
+                    }
+                    if($tplParam->note){
+                        $output["summary"] = $tplParam->note;
+                    }else{
+                        //使用社区note
+                        //获取channel 语言
+                        //查找社区解释
+                    }
+                }
+                $output['innerHtml'] = $innerString;
+                return $output;
+              });
+        return [
+            'props'=>base64_encode(\json_encode($props)),
+            'html'=>$props['innerHtml'],
+            'tag'=>'span',
+            'tpl'=>'term',
+            ];
+    }
+
+    private  function render_note(){
+
+        $props = ["note" => $this->get_param($this->param,"text",1)];
+        $trigger = $this->get_param($this->param,"trigger",2);
+        $innerString = "";
+        if(!empty($trigger)){
+            $props["trigger"] = $trigger;
+            $innerString = $props["trigger"];
+        }
+        return [
+            'props'=>base64_encode(\json_encode($props)),
+            'html'=>$innerString,
+            'tag'=>'span',
+            'tpl'=>'note',
+            ];
+    }
+    private  function render_nissaya(){
+
+        $pali =  $this->get_param($this->param,"pali",1);
+        $meaning = $this->get_param($this->param,"meaning",2);
+        $innerString = "";
+        $props = [
+            "pali" => $pali,
+            "meaning" => $meaning,
+        ];
+        return [
+            'props'=>base64_encode(\json_encode($props)),
+            'html'=>$innerString,
+            'tag'=>'span',
+            'tpl'=>'nissaya',
+            ];
+    }
+    private  function render_exercise(){
+
+        $id = $this->get_param($this->param,"id",1);
+        $title = $this->get_param($this->param,"title",1);
+        $props = [
+                    "id" => $id,
+                    "title" => $title,
+                    "channel" => $this->channel_id,
+                ];
+
+        return [
+            'props'=>base64_encode(\json_encode($props)),
+            'html'=>"",
+            'tag'=>'span',
+            'tpl'=>'exercise',
+            ];
+    }
+    private  function render_article(){
+
+        $type = $this->get_param($this->param,"type",1);
+        $id = $this->get_param($this->param,"id",2);
+        $title = $this->get_param($this->param,"title",3);
+        $channel = $this->get_param($this->param,"channel",4);
+        $props = [
+                    "type" => $type,
+                    "id" => $id,
+                    "channel" => $channel,
+                ];
+        if(empty($channel)){
+            $props['channel'] = $this->channel_id;
+        }
+        if(!empty($title)){
+            $props['title'] = $title;
+        }
+        return [
+            'props'=>base64_encode(\json_encode($props)),
+            'html'=>"",
+            'tag'=>'span',
+            'tpl'=>'article',
+            ];
+    }
+    private  function render_quote(){
+        $paraId = $this->get_param($this->param,"para",1);
+        $channelId = $this->channel_id;
+        $props = Cache::remember("/quote/{$channelId}/{$paraId}",
+              60,
+              function() use($paraId,$channelId){
+                $para = \explode('-',$paraId);
+                $output = [
+                    "paraId" => $paraId,
+                    "channel" => $channelId,
+                    "innerString" => $paraId,
+                    ];
+                if(count($para)<2){
+                    return $output;
+                }
+                $PaliText = PaliText::where("book",$para[0])
+                                    ->where("paragraph",$para[1])
+                                    ->select(['toc','path'])
+                                    ->first();
+
+                if($PaliText){
+                    $output["pali"] = $PaliText->toc;
+                    $output["paliPath"] = \json_decode($PaliText->path);
+                    $output["innerString"]= $PaliText->toc;
+                }
+                return $output;
+              });
+        return [
+            'props'=>base64_encode(\json_encode($props)),
+            'html'=>$props["innerString"],
+            'tag'=>'span',
+            'tpl'=>'quote',
+            ];
+    }
+    private  function render_sent(){
+
+        $sid = $this->get_param($this->param,"sid",1);
+        $channel = $this->get_param($this->param,"channel",2);
+        if(!empty($channel)){
+            $mChannel = $channel;
+        }else{
+            $mChannel = $this->channel_id;
+        }
+        $sentInfo = explode('@',trim($sid));
+        $sentId = $sentInfo[0];
+        if(empty($mChannel)){
+            $channels = [];
+        }else{
+            $channels = [$mChannel];
+        }
+        if(isset($sentInfo[1])){
+            $channels = [$sentInfo[1]];
+        }
+        $Sent = new CorpusController();
+        $props = $Sent->getSentTpl($sentId,$channels,$this->mode,true);
+        if($props === false){
+            $props['error']="句子模版渲染错误。句子参数个数不符。应该是四个。";
+        }
+        if($this->mode==='read'){
+            $tpl = "sentread";
+        }else{
+            $tpl = "sentedit";
+        }
+        return [
+            'props'=>base64_encode(\json_encode($props)),
+            'html'=>"",
+            'tag'=>'span',
+            'tpl'=>$tpl,
+            ];
+    }
+
+    private  function get_param(array $param,string $name,int $id,string $default=''){
+        if(isset($param[$name])){
+            return trim($param[$name]);
+        }else if(isset($param["{$id}"])){
+            return trim($param["{$id}"]);
+        }else{
+            return $default;
+        }
+    }
+}

+ 39 - 0
app/Http/Api/UserApi.php

@@ -0,0 +1,39 @@
+<?php
+namespace App\Http\Api;
+use App\Models\UserInfo;
+
+require_once __DIR__.'/../../../public/app/ucenter/function.php';
+
+class UserApi{
+    public static function getIdByName($name){
+        $userinfo = new \UserInfo();
+        return $userinfo->getUserByName($name)['userid'];
+    }
+    public static function getIdByUuid($uuid){
+        return UserInfo::where('userid',$uuid)->value('id');
+    }
+    public static function getIntIdByName($name){
+        $userinfo = new \UserInfo();
+        return $userinfo->getUserByName($name)['id'];
+    }
+    public static function getById($id){
+        $userinfo = new \UserInfo();
+        $studio = $userinfo->getName($id);
+        return [
+            'id'=>$id,
+            'nickName'=>$studio['nickname'],
+            'userName'=>$studio['username'],
+            'avatar'=>'',
+        ];
+    }
+    public static function getByUuid($id){
+        $userinfo = new \UserInfo();
+        $studio = $userinfo->getName($id);
+        return [
+            'id'=>$id,
+            'nickName'=>$studio['nickname'],
+            'userName'=>$studio['username'],
+            'avatar'=>'',
+        ];
+    }
+}

+ 395 - 0
app/Http/Controllers/ArticleController.php

@@ -0,0 +1,395 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Article;
+use App\Models\ArticleCollection;
+use App\Models\Collection;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+use App\Http\Resources\ArticleResource;
+use App\Http\Api\AuthApi;
+use App\Http\Api\ShareApi;
+use App\Http\Api\StudioApi;
+use Illuminate\Support\Facades\DB;
+
+class ArticleController extends Controller
+{
+    public static function userCanRead($user_uid,Article $article){
+        if($article->status === 30 ){
+            return true;
+        }
+        if(empty($user_uid)){
+            return false;
+        }
+            //私有文章,判断是否为所有者
+        if($user_uid === $article->owner){
+            return true;
+        }
+        //非所有者
+        //判断是否为文章协作者
+        $power = ShareApi::getResPower($user_uid,$article->uid);
+        if($power >= 10 ){
+            return true;
+        }
+        //无读取权限
+        //判断文集是否有读取权限
+        $inCollection = ArticleCollection::where('article_id',$article->uid)
+                                        ->select('collect_id')
+                                        ->groupBy('collect_id')->get();
+        if(!$inCollection){
+            return false;
+        }
+        //查找与文章同主人的文集
+        $collections = Collection::whereIn('uid',$inCollection)
+                                    ->where('owner',$article->owner)
+                                    ->select('uid')
+                                    ->get();
+        if(!$collections){
+            return false;
+        }
+        //查找与文章同主人的文集是否是共享的
+        $power = 0;
+        foreach ($collections as $collection) {
+            # code...
+            $currPower = ShareApi::getResPower($user_uid,$collection->uid);
+            if($currPower >= 10){
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static function userCanEdit($user_uid,$article){
+        if(empty($user_uid)){
+            return false;
+        }
+        //私有文章,判断是否为所有者
+        if($user_uid === $article->owner){
+            return true;
+        }
+        //非所有者
+        //判断是否为文章协作者
+        $power = ShareApi::getResPower($user_uid,$article->uid);
+        if($power >= 20 ){
+            return true;
+        }
+        //无读取权限
+        //判断文集是否有读取权限
+        $inCollection = ArticleCollection::where('article_id',$article->uid)
+                                        ->select('collect_id')
+                                        ->groupBy('collect_id')->get();
+        if(!$inCollection){
+            return false;
+        }
+        //查找与文章同主人的文集
+        $collections = Collection::whereIn('uid',$inCollection)
+                                    ->where('owner',$article->owner)
+                                    ->select('uid')
+                                    ->get();
+        if(!$collections){
+            return false;
+        }
+        //查找与文章同主人的文集是否是共享的
+        $power = 0;
+        foreach ($collections as $collection) {
+            # code...
+            $currPower = ShareApi::getResPower($user_uid,$collection->uid);
+            if($currPower >= 20){
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static function userCanManage($user_uid,$studioName){
+        if(empty($user_uid)){
+            return false;
+        }
+        //判断是否为所有者
+        if($user_uid === StudioApi::getIdByName($studioName)){
+            return true;
+        }else{
+            return false;
+        }
+    }
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $indexCol = ['uid','title','subtitle','summary','owner','lang','status','updated_at','created_at'];
+        switch ($request->get('view')) {
+            case 'studio':
+				# 获取studio内所有channel
+                $user = \App\Http\Api\AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //判断当前用户是否有指定的studio的权限
+                $studioId = StudioApi::getIdByName($request->get('name'));
+                if($user['user_uid'] !== $studioId){
+                    return $this->error(__('auth.failed'));
+                }
+                $table = Article::select($indexCol);
+                if($request->get('view2','my')==='my'){
+                    $table = $table->where('owner', $studioId);
+                }else{
+                    //协作
+                    $resList = ShareApi::getResList($studioId,3);
+                    $resId=[];
+                    foreach ($resList as $res) {
+                        $resId[] = $res['res_id'];
+                    }
+                    $table = $table->whereIn('uid', $resId)->where('owner','<>', $studioId);
+                }
+
+                //根据anthology过滤
+                if($request->has('anthology')){
+                    switch ($request->get('anthology')) {
+                        case 'all':
+                            break;
+                        case 'none':
+                            # 我的文集
+                            $myCollection = Collection::where('owner',$studioId)->select('uid')->get();
+                            //收录在我的文集里面的文章
+                            $articles = ArticleCollection::whereIn('collect_id',$myCollection)
+                                                         ->select('article_id')->groupBy('article_id')->get();
+                            //不在这些范围之内的文章
+                            $table =  $table->whereNotIn('uid',$articles);
+                            break;
+                        default:
+                            $articles = ArticleCollection::where('collect_id',$request->get('anthology'))
+                                                         ->select('article_id')->get();
+                            $table =  $table->whereIn('uid',$articles);
+                            break;
+                    }
+                }
+				break;
+        }
+        //处理搜索
+        if($request->has("search") && !empty($request->has("search"))){
+            $table = $table->where('title', 'like', "%".$request->get("search")."%");
+        }
+        //获取记录总条数
+        $count = $table->count();
+        //处理排序
+        if(isset($_GET["order"]) && isset($_GET["dir"])){
+            $table = $table->orderBy($_GET["order"],$_GET["dir"]);
+        }else{
+            //默认排序
+            $table = $table->orderBy('updated_at','desc');
+        }
+        //处理分页
+        if($request->has("limit")){
+
+            if($request->has("offset")){
+                $offset = $request->get("offset");
+            }else{
+                $offset = 0;
+            }
+            $table = $table->skip($offset)->take($request->get("limit"));
+        }
+        //获取数据
+        $result = $table->get();
+        if($result){
+			return $this->ok(["rows"=>ArticleResource::collection($result),"count"=>$count]);
+		}else{
+			return $this->error("没有查询到数据");
+		}
+    }
+
+        /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function showMyNumber(Request $request){
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的studio的权限
+        $studioId = StudioApi::getIdByName($request->get('studio'));
+        if($user['user_uid'] !== $studioId){
+            return $this->error(__('auth.failed'));
+        }
+        //我的
+        $my = Article::where('owner', $studioId)->count();
+        //协作
+        $resList = ShareApi::getResList($studioId,3);
+        $resId=[];
+        foreach ($resList as $res) {
+            $resId[] = $res['res_id'];
+        }
+        $collaboration = Article::whereIn('uid', $resId)->where('owner','<>', $studioId)->count();
+
+        return $this->ok(['my'=>$my,'collaboration'=>$collaboration]);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //判断权限
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'),[],401);
+        }else{
+            $user_uid=$user['user_uid'];
+        }
+
+        $canManage = ArticleController::userCanManage($user_uid,$request->get('studio'));
+        if(!$canManage){
+            return $this->error(__('auth.failed'),[],403);
+        }
+        //权限判断结束
+
+        //查询标题是否重复
+        /*
+        if(Article::where('title',$request->get('title'))->where('owner',$studioUuid)->exists()){
+            return $this->error(__('validation.exists'));
+        }*/
+        $newArticle = new Article;
+        DB::transaction(function() use($user,$request,$newArticle){
+            $studioUuid = StudioApi::getIdByName($request->get('studio'));
+            //新建文章,加入文集必须都成功。否则回滚
+            $newArticle->id = app('snowflake')->id();
+            $newArticle->uid = Str::uuid();
+            $newArticle->title = $request->get('title');
+            $newArticle->lang = $request->get('lang');
+            $newArticle->owner = $studioUuid;
+            $newArticle->owner_id = $user['user_id'];
+            $newArticle->editor_id = $user['user_id'];
+            $newArticle->create_time = time()*1000;
+            $newArticle->modify_time = time()*1000;
+            $newArticle->save();
+
+            if(Str::isUuid($request->get('anthologyId'))){
+                $articleMap = new ArticleCollection();
+                $articleMap->id = app('snowflake')->id();
+                $articleMap->article_id = $newArticle->uid;
+                $articleMap->collect_id = $request->get('anthologyId');
+                $articleMap->title = Article::find($newArticle->uid)->title;
+                $articleMap->level = 1;
+                $articleMap->save();
+            }
+        });
+        if(Str::isUuid($newArticle->uid)){
+            return $this->ok($newArticle);
+        }else{
+            return $this->error('fail');
+        }
+
+    }
+
+    /**
+     * Display the specified resource.
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Article  $article
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Request  $request,Article $article)
+    {
+        //
+        if(!$article){
+            return $this->error("no recorder");
+        }
+        //判断权限
+        $user = AuthApi::current($request);
+        if(!$user){
+            $user_uid="";
+        }else{
+            $user_uid=$user['user_uid'];
+        }
+
+        $canRead = ArticleController::userCanRead($user_uid,$article);
+        if(!$canRead){
+            return $this->error(__('auth.failed'),[],401);
+        }
+        return $this->ok(new ArticleResource($article));
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Article  $article
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Article $article)
+    {
+        //
+        if(!$article){
+            return $this->error("no recorder");
+        }
+        //鉴权
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'),[],401);
+        }else{
+            $user_uid=$user['user_uid'];
+        }
+
+        $canEdit = ArticleController::userCanEdit($user_uid,$article);
+        if(!$canEdit){
+            return $this->error(__('auth.failed'),[],401);
+        }
+
+        /*
+        //查询标题是否重复
+        if(Article::where('title',$request->get('title'))
+                  ->where('owner',$article->owner)
+                  ->where('uid',"<>",$article->uid)
+                  ->exists()){
+            return $this->error(__('validation.exists'));
+        }*/
+
+        $article->title = $request->get('title');
+        $article->subtitle = $request->get('subtitle');
+        $article->summary = $request->get('summary');
+        $article->content = $request->get('content');
+        $article->lang = $request->get('lang');
+        $article->status = $request->get('status',10);
+        $article->editor_id = $user['user_id'];
+        $article->modify_time = time()*1000;
+        $article->save();
+        return $this->ok($article);
+
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Article  $article
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Request $request,Article $article)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的studio的权限
+        if($user['user_uid'] !== $article->owner){
+            return $this->error(__('auth.failed'));
+        }
+        $delete = 0;
+        DB::transaction(function() use($article,$delete){
+            //TODO 删除文集中的文章
+            $delete = $article->delete();
+            ArticleMapController::deleteArticle($article->uid);
+        });
+
+        return $this->ok($delete);
+    }
+}

+ 178 - 0
app/Http/Controllers/ArticleMapController.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\ArticleCollection;
+use App\Models\Article;
+use App\Models\Collection;
+
+use Illuminate\Http\Request;
+use App\Http\Resources\ArticleMapResource;
+
+class ArticleMapController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        switch ($request->get('view')) {
+            case 'anthology':
+                $table = ArticleCollection::where('collect_id',$request->get('id'));
+                break;
+            case 'article':
+                $table = ArticleCollection::where('article_id',$request->get('id'));
+                break;
+        }
+        $result = $table->select(['id','collect_id','article_id','level','title','children','deleted_at'])
+                        ->orderBy('id')->get();
+        return $this->ok(["rows"=>ArticleMapResource::collection($result),"count"=>count($result)]);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        $validated = $request->validate([
+                'anthology_id' => 'required',
+                'operation' => 'required'
+            ]);
+        switch ($validated['operation']) {
+            case 'add':
+                # 添加多个文章到文集
+                $count=0;
+                foreach ($request->get('article_id') as $key => $article) {
+                    # code...
+
+                    if(!ArticleCollection::where('article_id',$article)
+                                        ->where('collect_id',$request->get('anthology_id'))
+                                        ->exists())
+                    {
+                        $new = new ArticleCollection;
+                        $new->id = app('snowflake')->id();
+                        $new->article_id = $article;
+                        $new->collect_id = $request->get('anthology_id');
+                        $new->title = Article::find($article)->title;
+                        $new->level = 1;
+                        $new->save();
+                        $count++;
+                    }
+                }
+                return $this->ok($count);
+                break;
+            default:
+                return $this->error('unknown operation');
+                break;
+        }
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\ArticleCollection  $articleCollection
+     * @return \Illuminate\Http\Response
+     */
+    public function show(ArticleCollection $articleCollection)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  string  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, string $id)
+    {
+        //
+        $validated = $request->validate([
+            'operation' => 'required'
+        ]);
+        switch ($validated['operation']) {
+            case 'anthology':
+                $delete = ArticleCollection::where('collect_id',$id)->delete();
+                $count=0;
+                foreach ($request->get('data') as $key => $row) {
+                    # code...
+                    $new = new ArticleCollection;
+                    $new->id = app('snowflake')->id();
+                    $new->article_id = $row["article_id"];
+                    $new->collect_id = $id;
+                    $new->title = $row["title"];
+                    $new->level = $row["level"];
+                    $new->children = $row["children"];
+                    $new->save();
+                    $count++;
+                }
+                ArticleMapController::updateCollection($id);
+                return $this->ok($count);
+                break;
+        }
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\ArticleCollection  $articleCollection
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(ArticleCollection $articleCollection)
+    {
+        //
+    }
+
+    public static function deleteArticle(string $articleId){
+        //查找有这个文章的文集
+        $collections = ArticleCollection::where('article_id',$articleId)
+                                        ->select('collect_id')
+                                        ->groupBy('collect_id')
+                                        ->get();
+        //设置为删除
+        ArticleCollection::where('article_id',$articleId)
+                         ->update(['deleted_at'=>now()]);
+        //查找没有下级文章的文集
+        $updateCollections = ArticleCollection::where('article_id',$articleId)
+                                            ->where('children',0)
+                                            ->select('collect_id')
+                                            ->groupBy('collect_id')
+                                            ->get();
+        //真的删除没有下级文章的文集中的文章
+        $count = ArticleCollection::where('article_id',$articleId)
+                                  ->where('children',0)
+                                  ->delete();
+        //更新改动的文集
+        foreach ($updateCollections as  $collection) {
+            # code...
+            ArticleMapController::updateCollection($collection->collect_id);
+        }
+        return [count($collections),$count];
+    }
+
+    public static function deleteCollection(string $collectionId){
+        $count = ArticleCollection::where('collect_id',$collectionId)
+                                  ->delete();
+        return $count;
+    }
+
+    /**
+     * 用表中的数据生成json,更新collection 表中的字段
+     */
+    public static function updateCollection(string $collectionId){
+        $result = ArticleCollection::where('collect_id',$collectionId)
+                        ->select(['article_id','level','title'])
+                        ->orderBy('id')->get();
+        Collection::where('uid',$collectionId)
+                  ->update(['article_list'=>json_encode($result,JSON_UNESCAPED_UNICODE)]);
+        return count($result);
+    }
+}

+ 130 - 0
app/Http/Controllers/ArticleProgressController.php

@@ -0,0 +1,130 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Channel;
+use App\Models\Sentence;
+use App\Models\PaliSentence;
+use App\Http\Api\PaliTextApi;
+use Illuminate\Support\Arr;
+
+use Illuminate\Http\Request;
+
+class ArticleProgressController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        switch ($request->get('view')) {
+            case 'chapter':
+                $chapter = PaliTextApi::getChapterStartEnd($request->get('book'),$request->get('para'));
+                $channels = Sentence::where('book_id',$request->get('book'))
+                                    ->whereBetween('paragraph',$chapter)
+                                    ->where('strlen','>',0)
+                                    ->groupBy('channel_uid')
+                                    ->select('channel_uid')
+                                    ->get();
+                //获取单句长度
+                $sentLen = PaliSentence::where('book',$request->get('book'))
+                            ->whereBetween('paragraph',$chapter)
+                            ->orderBy('word_begin')
+                            ->select(['book','paragraph','word_begin','word_end','length'])
+                            ->get();
+                //获取每个channel的完成度
+                foreach ($channels as $key => $value) {
+                    # code...
+                    $finished = Sentence::where('book_id',$request->get('book'))
+                    ->whereBetween('paragraph',$chapter)
+                    ->where('channel_uid',$value->channel_uid)
+                    ->where('strlen','>',0)
+                    ->select(['strlen','book_id','paragraph','word_start','word_end'])
+                    ->get();
+                    $final=[];
+                    foreach ($sentLen as  $sent) {
+                        # code...
+                        $first = Arr::first($finished, function ($value, $key) use($sent) {
+                            return ($value->book_id==$sent->book &&
+                                    $value->paragraph==$sent->paragraph &&
+                                    $value->word_start==$sent->word_begin &&
+                                    $value->word_end==$sent->word_end);
+                        });
+                        $final[] = [$sent->length,$first?true:false];
+                    }
+                    $value['final'] = $final;
+                }
+                return $this->ok($channels);
+                break;
+        }
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function create()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Channel  $channel
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Channel $channel)
+    {
+        //
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     *
+     * @param  \App\Models\Channel  $channel
+     * @return \Illuminate\Http\Response
+     */
+    public function edit(Channel $channel)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Channel  $channel
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Channel $channel)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\Channel  $channel
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Channel $channel)
+    {
+        //
+    }
+}

+ 109 - 0
app/Http/Controllers/AuthController.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace App\Http\Controllers;
+
+require_once __DIR__.'/../../../public/app/ucenter/function.php';
+
+use Illuminate\Http\Request;
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+use App\Http\Api;
+use Illuminate\Support\Facades\Log;
+
+class AuthController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function show($id)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, $id)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        //
+    }
+    public function signIn(Request $request){
+        $userInfo = new \UserInfo();
+        $user = $userInfo->signIn($request->get('username'),$request->get('password'));
+        if($user){
+            $ExpTime = time() + 60 * 60 * 24 * 365;
+            $key = env('APP_KEY');
+            $payload = [
+                'nbf' => time(),
+                'exp' => $ExpTime,
+                'uid' => $user['userid'],
+                'id' => $user['id'],
+            ];
+            $jwt = JWT::encode($payload,$key,'HS512');
+            return $this->ok($jwt);
+        }else{
+            Log::info($userInfo->getLog());
+            return $this->error('invalid token');
+        }
+    }
+    public function getUserInfoByToken(Request $request){
+        $curr = \App\Http\Api\AuthApi::current($request);
+        if($curr){
+            $userinfo = new \UserInfo();
+		    $username = $userinfo->getName($curr['user_uid']);
+            $user = [
+                "id"=>$curr['user_uid'],
+                "nickName"=> $username['nickname'],
+                "realName"=> $username['username'],
+                "avatar"=> "",
+                "roles"=> [],
+                "token"=>\substr($request->header('Authorization'),7) ,
+            ];
+            return $this->ok($user);
+        }else{
+            return $this->error('invalid token');
+        }
+    }
+
+}
+
+

+ 67 - 0
app/Http/Controllers/CaseController.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Tools\CaseMan;
+
+class CaseController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * 输入一个单词,输出三藏中所有可能的变形
+     *
+     * @param  string  $word
+     * @return \Illuminate\Http\Response
+     */
+    public function show($word)
+    {
+        //
+        $case  = new CaseMan();
+        $result = $case->BaseToWord($word);
+        return $this->ok(['rows'=>$result,'count'=>count($result)]);
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, $id)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        //
+    }
+}

+ 485 - 6
app/Http/Controllers/ChannelController.php

@@ -5,7 +5,19 @@ namespace App\Http\Controllers;
 require_once __DIR__.'/../../../public/app/ucenter/function.php';
 
 use App\Models\Channel;
+use App\Models\Sentence;
+use App\Models\DhammaTerm;
+use App\Models\WbwBlock;
+use App\Models\PaliSentence;
+use App\Http\Controllers\AuthController;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Log;
+use App\Http\Api\AuthApi;
+use App\Http\Api\StudioApi;
+use App\Http\Api\ShareApi;
+use App\Http\Api\PaliTextApi;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Facades\DB;
 
 class ChannelController extends Controller
 {
@@ -14,11 +26,397 @@ class ChannelController extends Controller
      *
      * @return \Illuminate\Http\Response
      */
-    public function index()
+    public function index(Request $request)
     {
         //
+        $userinfo = new \UserInfo();
+		$result=false;
+		$indexCol = ['uid','name','summary','type','owner_uid','lang','status','updated_at','created_at'];
+		switch ($request->get('view')) {
+            case 'studio':
+				# 获取studio内所有channel
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //判断当前用户是否有指定的studio的权限
+                $studioId = StudioApi::getIdByName($request->get('name'));
+                if($user['user_uid'] !== $studioId){
+                    return $this->error(__('auth.failed'));
+                }
+
+                $table = Channel::select($indexCol);
+                if($request->get('view2','my')==='my'){
+                    $table = $table->where('owner_uid', $studioId);
+                }else{
+                    //协作
+                    $resList = ShareApi::getResList($studioId,2);
+                    $resId=[];
+                    foreach ($resList as $res) {
+                        $resId[] = $res['res_id'];
+                    }
+                    $table = $table->whereIn('uid', $resId);
+                    if($request->get('collaborator','all') !== 'all'){
+                        $table = $table->where('owner_uid', $request->get('collaborator'));
+                    }else{
+                        $table = $table->where('owner_uid','<>', $studioId);
+                    }
+                }
+				break;
+            case 'studio-all':
+                /**
+                 * studio 的和协作的
+                 */
+                #获取user所有有权限的channel列表
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //判断当前用户是否有指定的studio的权限
+                if($user['user_uid'] !== \App\Http\Api\StudioApi::getIdByName($request->get('name'))){
+                    return $this->error(__('auth.failed'));
+                }
+                $channelById = [];
+                $channelId = [];
+                //获取共享channel
+                $allSharedChannels = ShareApi::getResList($user['user_uid'],2);
+                foreach ($allSharedChannels as $key => $value) {
+                    # code...
+                    $channelId[] = $value['res_id'];
+                    $channelById[$value['res_id']] = $value;
+                }
+                $table = Channel::select($indexCol)
+                            ->whereIn('uid', $channelId)
+                            ->orWhere('owner_uid',$user['user_uid']);
+                break;
+            case 'user-edit':
+                /**
+                 * 某用户有编辑权限的
+                 */
+                #获取user所有有权限的channel列表
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                $channelById = [];
+                $channelId = [];
+                //获取共享channel
+                $allSharedChannels = ShareApi::getResList($user['user_uid'],2);
+                foreach ($allSharedChannels as $key => $value) {
+                    # code...
+                    if($value['power']>=20){
+                        $channelId[] = $value['res_id'];
+                        $channelById[$value['res_id']] = $value;
+                    }
+                }
+                $table = Channel::select($indexCol)
+                            ->whereIn('uid', $channelId)
+                            ->orWhere('owner_uid',$user['user_uid']);
+                break;
+            case 'user-in-chapter':
+                #获取user 在某章节 所有有权限的channel列表
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                $channelById = [];
+                $channelId = [];
+                //获取共享channel
+                $allSharedChannels = ShareApi::getResList($user['user_uid'],2);
+                foreach ($allSharedChannels as $key => $value) {
+                    # code...
+                    $channelId[] = $value['res_id'];
+                    $channelById[$value['res_id']] = $value;
+                }
+                //获取全网公开channel
+                $chapter = PaliTextApi::getChapterStartEnd($request->get('book'),$request->get('para'));
+                $publicChannelsWithContent = Sentence::where('book_id',$request->get('book'))
+                                            ->whereBetween('paragraph',$chapter)
+                                            ->where('strlen','>',0)
+                                            ->where('status',30)
+                                            ->groupBy('channel_uid')
+                                            ->select('channel_uid')
+                                            ->get();
+                foreach ($publicChannelsWithContent as $key => $value) {
+                    # code...
+                    $value['res_id']=$value->channel_uid;
+                    $value['power'] = 10;
+                    $value['type'] = 2;
+                    if(!isset($channelById[$value['res_id']])){
+                        $channelId[] = $value['res_id'];
+                        $channelById[$value['res_id']] = $value;
+                    }
+                }
+                $table = Channel::select($indexCol)
+                        ->whereIn('uid', $channelId)
+                        ->orWhere('owner_uid',$user['user_uid']);
+
+                break;
+
+        }
+        //处理搜索
+        if($request->has("search")){
+            $table = $table->where('name', 'like', "%".$request->get("search")."%");
+        }
+        //获取记录总条数
+        $count = $table->count();
+        //处理排序
+        if($request->has("order") && $request->has("dir")){
+            $table = $table->orderBy($request->get("order"),$request->get("dir"));
+        }else{
+            //默认排序
+            $table = $table->orderBy('updated_at','desc');
+        }
+        //处理分页
+        if($request->has("limit")){
+            if($request->has("offset")){
+                $offset = $request->get("offset");
+            }else{
+                $offset = 0;
+            }
+            $table = $table->skip($offset)->take($request->get("limit"));
+        }
+        //获取数据
+        $result = $table->get();
+//TODO 将下面代码转移到resource
+        if($result){
+            if($request->has('progress')){
+                //获取进度
+                //获取单句长度
+                $sentLen = PaliSentence::where('book',$request->get('book'))
+                ->whereBetween('paragraph',$chapter)
+                ->orderBy('word_begin')
+                ->select(['book','paragraph','word_begin','word_end','length'])
+                ->get();
+            }
+            foreach ($result as $key => $value) {
+                if($request->has('progress')){
+                    //获取进度
+                    $finalTable = Sentence::where('book_id',$request->get('book'))
+                    ->whereBetween('paragraph',$chapter)
+                    ->where('channel_uid',$value->uid)
+                    ->where('strlen','>',0)
+                    ->select(['strlen','book_id','paragraph','word_start','word_end']);
+                    if($finalTable->count()>0){
+                        $finished = $finalTable->get();
+                        $final=[];
+                        foreach ($sentLen as  $sent) {
+                            # code...
+                            $first = Arr::first($finished, function ($value, $key) use($sent) {
+                                return ($value->book_id==$sent->book &&
+                                        $value->paragraph==$sent->paragraph &&
+                                        $value->word_start==$sent->word_begin &&
+                                        $value->word_end==$sent->word_end);
+                            });
+                            $final[] = [$sent->length,$first?true:false];
+                        }
+                        $value['final'] = $final;
+                    }
+
+                }
+                //角色
+                if($value->owner_uid===$user['user_uid']){
+                    $value['role'] = 'owner';
+                }else{
+                    if(isset($channelById[$value->uid])){
+                        switch ($channelById[$value->uid]['power']) {
+                            case 10:
+                                # code...
+                                $value['role'] = 'member';
+                                break;
+                            case 20:
+                                $value['role'] = 'editor';
+                                break;
+                            case 30:
+                                $value['role'] = 'owner';
+                                break;
+                            default:
+                                # code...
+                                $value['role'] = $channelById[$value->uid]['power'];
+                                break;
+                        }
+                    }
+                }
+                # 获取studio信息
+                $value->studio = \App\Http\Api\StudioApi::getById($value->owner_uid);
+            }
+			return $this->ok(["rows"=>$result,"count"=>$count]);
+		}else{
+			return $this->error("没有查询到数据");
+		}
+
     }
 
+    /**
+     * 获取我的,和协作channel数量
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function showMyNumber(Request $request){
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的studio的权限
+        $studioId = StudioApi::getIdByName($request->get('studio'));
+        if($user['user_uid'] !== $studioId){
+            return $this->error(__('auth.failed'));
+        }
+        //我的
+        $my = Channel::where('owner_uid', $studioId)->count();
+        //协作
+        $resList = ShareApi::getResList($studioId,2);
+        $resId=[];
+        foreach ($resList as $res) {
+            $resId[] = $res['res_id'];
+        }
+        $collaboration = Channel::whereIn('uid', $resId)->where('owner_uid','<>', $studioId)->count();
+
+        return $this->ok(['my'=>$my,'collaboration'=>$collaboration]);
+    }
+    /**
+     * 获取章节的进度
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function progress(Request $request){
+		$indexCol = ['uid','name','summary','type','owner_uid','lang','status','updated_at','created_at'];
+
+        $sent = $request->get('sentence') ;
+        $query = [];
+        $sentContainer = [];
+        $sentLenContainer = [];
+
+        foreach ($sent as $value) {
+            $ids = explode('-',$value);
+            if(count($ids)===4){
+                $sentContainer[$value] = false;
+                $query[] = $ids;
+            }
+        }
+        //获取单句长度
+        if(count($query)>0){
+            $table = PaliSentence::whereIns(['book','paragraph','word_begin','word_end'],$query)
+                                    ->select(['book','paragraph','word_begin','word_end','length']);
+            $sentLen = $table->get();
+
+            foreach ($sentLen as $value) {
+                $sentLenContainer["{$value->book}-{$value->paragraph}-{$value->word_begin}-{$value->word_end}"] = $value->length;
+            }
+        }
+
+        #获取 user 在某章节 所有有权限的 channel 列表
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        $channelById = [];
+        $channelId = [];
+        //获取共享channel
+        $allSharedChannels = ShareApi::getResList($user['user_uid'],2);
+        foreach ($allSharedChannels as $key => $value) {
+            # code...
+            $channelId[] = $value['res_id'];
+            $channelById[$value['res_id']] = $value;
+        }
+        //获取全网公开的有译文的channel
+        if(count($query)>0){
+            $publicChannelsWithContent = Sentence::whereIns(['book_id','paragraph','word_start','word_end'],$query)
+                                        ->where('strlen','>',0)
+                                        ->where('status',30)
+                                        ->groupBy('channel_uid')
+                                        ->select('channel_uid')
+                                        ->get();
+            foreach ($publicChannelsWithContent as $key => $value) {
+                # code...
+                $value['res_id']=$value->channel_uid;
+                $value['power'] = 10;
+                $value['type'] = 2;
+                if(!isset($channelById[$value['res_id']])){
+                    $channelId[] = $value['res_id'];
+                    $channelById[$value['res_id']] = $value;
+                }
+            }
+        }
+        //所有有这些句子译文的channel
+        if(count($query) > 0){
+            $allChannels = Sentence::whereIns(['book_id','paragraph','word_start','word_end'],$query)
+                                        ->where('strlen','>',0)
+                                        ->groupBy('channel_uid')
+                                        ->select('channel_uid')
+                                        ->get();
+        }
+
+
+
+        //所有需要查询的channel
+        $result = Channel::select(['uid','name','summary','type','owner_uid','lang','status','updated_at','created_at'])
+                        ->whereIn('uid', $channelId)
+                        ->orWhere('owner_uid',$user['user_uid'])
+                        ->get();
+
+        foreach ($result as $key => $value) {
+            //角色
+            if($value->owner_uid===$user['user_uid']){
+                $value['role'] = 'owner';
+            }else{
+                if(isset($channelById[$value->uid])){
+                    switch ($channelById[$value->uid]['power']) {
+                        case 10:
+                            # code...
+                            $value['role'] = 'member';
+                            break;
+                        case 20:
+                            $value['role'] = 'editor';
+                            break;
+                        case 30:
+                            $value['role'] = 'owner';
+                            break;
+                        default:
+                            # code...
+                            $value['role'] = $channelById[$value->uid]['power'];
+                            break;
+                    }
+                }
+            }
+            # 获取studio信息
+            $result[$key]["studio"] = \App\Http\Api\StudioApi::getById($value->owner_uid);
+
+            //获取进度
+            if(count($query) > 0){
+                $currChannelId = $value->uid;
+                $hasContent = Arr::first($allChannels, function ($value, $key) use($currChannelId) {
+                        return ($value->channel_uid===$currChannelId);
+                    });
+                if($hasContent && count($query)>0){
+                    $finalTable = Sentence::whereIns(['book_id','paragraph','word_start','word_end'],$query)
+                                            ->where('channel_uid',$currChannelId)
+                                            ->where('strlen','>',0)
+                                            ->select(['strlen','book_id','paragraph','word_start','word_end']);
+                    if($finalTable->count()>0){
+                        $finished = $finalTable->get();
+                        $currChannel = [];
+                        foreach ($finished as $rowFinish) {
+                            $currChannel["{$rowFinish->book_id}-{$rowFinish->paragraph}-{$rowFinish->word_start}-{$rowFinish->word_end}"] = 1;
+                        }
+                        $final=[];
+                        foreach ($sentContainer as $sentId=>$rowSent) {
+                            # code...
+                            if(isset($currChannel[$sentId])){
+                                $final[] = [$sentLenContainer[$sentId],true];
+                            }else{
+                                $final[] = [$sentLenContainer[$sentId],false];
+                            }
+                        }
+                        $result[$key]['final'] = $final;
+                    }
+                }
+            }
+        }
+        return $this->ok(["rows"=>$result,count($result)]);
+
+    }
     /**
      * Store a newly created resource in storage.
      *
@@ -28,6 +426,33 @@ class ChannelController extends Controller
     public function store(Request $request)
     {
         //
+        $user = AuthApi::current($request);
+        if($user){
+            //判断当前用户是否有指定的studio的权限
+            if($user['user_uid'] === StudioApi::getIdByName($request->get('studio'))){
+                //查询是否重复
+                if(Channel::where('name',$request->get('name'))->where('owner_uid',$user['user_uid'])->exists()){
+                    return $this->error(__('validation.exists',['name']));
+                }else{
+
+                    $channel = new Channel;
+                    $channel->id = app('snowflake')->id();
+                    $channel->name = $request->get('name');
+                    $channel->owner_uid = $user['user_uid'];
+                    $channel->type = $request->get('type');
+                    $channel->lang = $request->get('lang');
+                    $channel->editor_id = $user['user_id'];
+                    $channel->create_time = time()*1000;
+                    $channel->modify_time = time()*1000;
+                    $channel->save();
+                    return $this->ok($channel);
+                }
+            }else{
+                return $this->error(__('auth.failed'));
+            }
+        }else{
+            return $this->error(__('auth.failed'));
+        }
     }
 
     /**
@@ -39,9 +464,23 @@ class ChannelController extends Controller
     public function show($id)
     {
         //
-		$channel = Channel::where("uid",$id)->select(['name','owner_uid'])->first();
+        $indexCol = ['uid','name','summary','type','owner_uid','lang','status','updated_at','created_at'];
+		$channel = Channel::where("uid",$id)->select($indexCol)->first();
 		$userinfo = new \UserInfo();
-		$channel->owner_info = $userinfo->getName($channel->owner_uid);
+        $studio = $userinfo->getName($channel->owner_uid);
+		$channel->owner_info = $studio;
+        $channel->studio = [
+            'id'=>$channel->owner_uid,
+            'nickName'=>$studio['nickname'],
+            'studioName'=>$studio['username'],
+            'avastar'=>'',
+            'owner' => [
+                'id'=>$channel->owner_uid,
+                'nickName'=>$studio['nickname'],
+                'userName'=>$studio['username'],
+                'avastar'=>'',
+            ]
+        ];
 		return $this->ok($channel);
     }
 
@@ -54,17 +493,57 @@ class ChannelController extends Controller
      */
     public function update(Request $request, Channel $channel)
     {
-        //
+        //鉴权
+        $user = AuthApi::current($request);
+        if($user && $channel->owner_uid === $user["user_uid"]){
+            $channel->name = $request->get('name');
+            $channel->type = $request->get('type');
+            $channel->summary = $request->get('summary');
+            $channel->lang = $request->get('lang');
+            $channel->status = $request->get('status');
+            $channel->save();
+            return $this->ok($channel);
+        }else{
+            //非所有者鉴权失败
+            //TODO 判断是否为协作
+            return $this->error(__('auth.failed'));
+        }
+
     }
 
     /**
      * Remove the specified resource from storage.
-     *
+     * @param  \Illuminate\Http\Request  $request
      * @param  \App\Models\Channel  $channel
      * @return \Illuminate\Http\Response
      */
-    public function destroy(Channel $channel)
+    public function destroy(Request $request,Channel $channel)
     {
         //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的studio的权限
+        if($user['user_uid'] !== $channel->owner_uid){
+            return $this->error(__('auth.failed'));
+        }
+        //查询其他资源
+        if(Sentence::where("channel_uid",$channel->uid)->exists()){
+            return $this->error("译文有数据无法删除");
+        }
+        if(DhammaTerm::where("channal",$channel->uid)->exists()){
+            return $this->error("术语有数据无法删除");
+        }
+        if(WbwBlock::where("channel_uid",$channel->uid)->exists()){
+            return $this->error("逐词解析有数据无法删除");
+        }
+        $delete = 0;
+        DB::transaction(function() use($channel,$delete){
+            //TODO 删除相关资源
+            $delete = $channel->delete();
+        });
+
+        return $this->ok($delete);
     }
 }

+ 265 - 0
app/Http/Controllers/CollectionController.php

@@ -0,0 +1,265 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Collection;
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Log;
+use App\Http\Api\AuthApi;
+use App\Http\Api\StudioApi;
+use App\Http\Api\ShareApi;
+use App\Http\Resources\CollectionResource;
+use Illuminate\Support\Facades\DB;
+
+require_once __DIR__.'/../../../public/app/ucenter/function.php';
+
+
+class CollectionController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+                //
+        $userinfo = new \UserInfo();
+		$result=false;
+		$indexCol = ['uid','title','subtitle','summary','article_list','owner','status','lang','updated_at','created_at'];
+		switch ($request->get('view')) {
+            case 'studio_list':
+		        $indexCol = ['owner'];
+                $table = Collection::select($indexCol)->selectRaw('count(*) as count')->where('status', 30)->groupBy('owner');
+                break;
+			case 'studio':
+				# code...
+				//$table = Collection::select($indexCol)->where('owner', $_COOKIE["user_uid"]);
+                # 获取studio内所有channel
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                $studioId = StudioApi::getIdByName($request->get('name'));
+                //判断当前用户是否有指定的studio的权限
+                if($user['user_uid'] !== $studioId){
+                    return $this->error(__('auth.failed'));
+                }
+                $table = Collection::select($indexCol);
+                if($request->get('view2','my')==='my'){
+                    $table = $table->where('owner', $studioId);
+                }else{
+                    //协作
+                    $resList = ShareApi::getResList($studioId,4);
+                    $resId=[];
+                    foreach ($resList as $res) {
+                        $resId[] = $res['res_id'];
+                    }
+                    $table = $table->whereIn('uid', $resId)->where('owner','<>', $studioId);
+                }
+
+				break;
+			case 'public':
+                //全网公开
+				$table = Collection::select($indexCol)->where('status', 30);
+                if($request->has('studio')){
+                    $studioId = StudioApi::getIdByName($request->get('studio'));
+                    $table = $table->where('owner',$studioId);
+                }
+				break;
+			default:
+				# code...
+			    return $this->error("没有查询到数据");
+				break;
+		}
+        if($request->has("search") && !empty($request->has("search"))){
+            $table = $table->where('title', 'like', "%".$request->get("search")."%");
+        }
+        $count = $table->count();
+        if($request->has("order") && $request->has("dir")){
+            $table = $table->orderBy($request->get("order"),$request->get("dir"));
+        }else{
+            if($request->get('view') === 'studio_list'){
+                $table = $table->orderBy('count','desc');
+            }else{
+                $table = $table->orderBy('updated_at','desc');
+            }
+        }
+        if($request->has("limit")){
+            $table = $table->skip($request->get("offset",0))
+                           ->take($request->get("limit"));
+        }
+        $result = $table->get();
+		return $this->ok(["rows"=>CollectionResource::collection($result),"count"=>$count]);
+    }
+
+            /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function showMyNumber(Request $request){
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的studio的权限
+        $studioId = StudioApi::getIdByName($request->get('studio'));
+        if($user['user_uid'] !== $studioId){
+            return $this->error(__('auth.failed'));
+        }
+        //我的
+        $my = Collection::where('owner', $studioId)->count();
+        //协作
+        $resList = ShareApi::getResList($studioId,4);
+        $resId=[];
+        foreach ($resList as $res) {
+            $resId[] = $res['res_id'];
+        }
+        $collaboration = Collection::whereIn('uid', $resId)->where('owner','<>', $studioId)->count();
+
+        return $this->ok(['my'=>$my,'collaboration'=>$collaboration]);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        $user = \App\Http\Api\AuthApi::current($request);
+        if($user){
+            //判断当前用户是否有指定的studio的权限
+            if($user['user_uid'] === \App\Http\Api\StudioApi::getIdByName($request->get('studio'))){
+                //查询是否重复
+                if(Collection::where('title',$request->get('title'))->where('owner',$user['user_uid'])->exists()){
+                    return $this->error(__('validation.exists'));
+                }else{
+                    $newOne = new Collection;
+                    $newOne->id = app('snowflake')->id();
+                    $newOne->uid = Str::uuid();
+                    $newOne->title = $request->get('title');
+                    $newOne->lang = $request->get('lang');
+                    $newOne->article_list = "[]";
+                    $newOne->owner = $user['user_uid'];
+                    $newOne->owner_id = $user['user_id'];
+                    $newOne->editor_id = $user['user_id'];
+                    $newOne->create_time = time()*1000;
+                    $newOne->modify_time = time()*1000;
+                    $newOne->save();
+                    return $this->ok($newOne);
+                }
+            }else{
+                return $this->error(__('auth.failed'));
+            }
+        }else{
+            return $this->error(__('auth.failed'));
+        }
+
+    }
+
+    /**
+     * Display the specified resource.
+     * @param  \Illuminate\Http\Request  $request
+     * @param  string  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Request  $request,$id)
+    {
+        //
+		$indexCol = ['uid','title','subtitle','summary','article_list','status','owner','lang','updated_at','created_at'];
+
+		$result  = Collection::select($indexCol)->where('uid', $id)->first();
+		if($result){
+            if($result->status<30){
+                //私有文章,判断权限
+                $user = \App\Http\Api\AuthApi::current($request);
+                if($user){
+                    //判断当前用户是否有指定的studio的权限
+                    if($user['user_uid'] !== $result->owner){
+                        //非所有者
+                        //TODO 判断是否协作
+                        return $this->error(__('auth.failed'));
+                    }
+                }else{
+                    return $this->error(__('auth.failed'));
+                }
+            }
+			if(!empty($result->article_list)){
+				$result->article_list = \json_decode($result->article_list);
+			}
+			return $this->ok($result);
+		}else{
+			return $this->error("没有查询到数据");
+		}
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  string  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, string $id)
+    {
+        //
+        $collection  = Collection::find($id);
+        if($collection){
+            //鉴权
+            $user = \App\Http\Api\AuthApi::current($request);
+            if($user && $collection->owner === $user["user_uid"]){
+                $collection->title = $request->get('title');
+                $collection->subtitle = $request->get('subtitle');
+                $collection->summary = $request->get('summary');
+                if($request->has('aritcle_list')){
+                    $collection->article_list = \json_encode($request->get('aritcle_list'));
+                } ;
+                $collection->lang = $request->get('lang');
+                $collection->status = $request->get('status');
+                $collection->modify_time = time()*1000;
+                $collection->save();
+                return $this->ok($collection);
+            }else{
+                //鉴权失败
+
+                //TODO 判断是否为协作
+                return $this->error(__('auth.failed'));
+            }
+
+        }else{
+            return $this->error("no recorder");
+        }
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     * @param  \Illuminate\Http\Request  $request
+     * @param  string  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Request $request,string $id)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的studio的权限
+        $collection = Collection::find($id);
+        if($user['user_uid'] !== $collection['owner']){
+            return $this->error(__('auth.failed'));
+        }
+        $delete = 0;
+        DB::transaction(function() use($collection,$delete){
+            //TODO 删除文集中的文章
+            $delete = $collection->delete();
+        });
+
+        return $this->ok($delete);
+    }
+}

+ 3 - 2
app/Http/Controllers/Controller.php

@@ -17,7 +17,8 @@ class Controller extends BaseController
 			'data'=>$result,
 			'message'=> $message,
 		];
-		return response()->json($response,200);
+		return response()->json($response,200,['Content-Type' => 'application/json;charset=UTF-8',
+	'Charset' => 'utf-8'],JSON_UNESCAPED_UNICODE);
 	}
     public function ok($result,$message=""){
         return $this->sendResponse($result,$message);
@@ -31,7 +32,7 @@ class Controller extends BaseController
 		return response()->json($response,$code);
 	}
 
-    public function error($error, $errorMessages="", $code=404){
+    public function error($error, $errorMessages=[], $code=404){
         return $this->sendError($error, $errorMessages, $code);
     }
 }

+ 858 - 0
app/Http/Controllers/CorpusController.php

@@ -0,0 +1,858 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Sentence;
+use App\Models\Channel;
+use App\Models\PaliText;
+use App\Models\WbwTemplate;
+use App\Models\WbwBlock;
+use App\Models\Wbw;
+use App\Models\Discussion;
+use App\Models\PaliSentence;
+use App\Models\SentSimIndex;
+use Illuminate\Support\Str;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
+use App\Http\Api\MdRender;
+use App\Http\Api\SuggestionApi;
+use App\Http\Api\ChannelApi;
+use App\Http\Api\UserApi;
+use App\Http\Api\StudioApi;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Arr;
+use App\Http\Resources\TocResource;
+
+class CorpusController extends Controller
+{
+    protected $result = [
+        "uid"=> '',
+        "title"=> '',
+        "path"=>[],
+        "sub_title"=> '',
+        "summary"=> '',
+        "content"=> '',
+        "content_type"=> "html",
+        "toc" => [],
+        "status"=>30,
+        "lang"=> "",
+        "created_at"=> "",
+        "updated_at"=> "",
+    ];
+    protected $wbwChannels = [];
+    //句子需要查询的列
+    protected $selectCol = [
+        'uid',
+        'book_id',
+        'paragraph',
+        'word_start',
+        "word_end",
+        'channel_uid',
+        'content',
+        'content_type',
+        'editor_uid',
+        'acceptor_uid',
+        'pr_edit_at',
+        'updated_at'
+    ];
+    public function __construct()
+    {
+
+
+    }
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        switch ($request->get('view')) {
+            case 'para':
+                return $this->showPara($request);
+                break;
+            default:
+                # code...
+                break;
+        }
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Sentence  $sentence
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Sentence $sentence)
+    {
+        //
+    }
+    public function getSentTpl($id,$channels,$mode='edit',$onlyProps=false){
+        $sent = [];
+        $sentId = \explode('-',$id);
+        if(count($sentId) !== 4){
+            return false;
+        }
+        if($mode==='read'){
+            $channelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
+        }else{
+            $channelId = ChannelApi::getSysChannel('_System_Wbw_VRI_');
+        }
+
+        if($channelId !== false){
+            array_push($channels,$channelId);
+        }
+        $record = Sentence::select($this->selectCol)
+        ->where('book_id',$sentId[0])
+        ->where('paragraph',$sentId[1])
+        ->where('word_start',(int)$sentId[2])
+        ->where('word_end',(int)$sentId[3])
+        ->whereIn('channel_uid',$channels)
+        ->get();
+
+        $channelIndex = $this->getChannelIndex($channels);
+
+        //获取wbw channel
+        //目前默认的 wbw channel 是第一个translation channel
+        foreach ($channels as  $channel) {
+            # code...
+            if($channelIndex[$channel]->type==='translation'){
+                $this->wbwChannels[] = $channel;
+                break;
+            }
+        }
+        return $this->makeContent($record,$mode,$channelIndex,[],$onlyProps);
+    }
+    /**
+     * Display the specified resource.
+     *
+     * @param  string  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function showSent($id)
+    {
+        //
+        $param = \explode('_',$id);
+        if(count($param)>1){
+            $channels = array_slice($param,1);
+        }else{
+            $channels = [];
+        }
+        $this->result['content'] = getSentTpl($param[0],$channels);
+        return $this->ok($this->result);
+    }
+
+    public function showSentences($type,$id,$mode='read'){
+        $param = \explode('_',$id);
+        $sentId = \explode('-',$param[0]);
+        $channels = [];
+
+		#获取channel类型
+        $sentChannel = Sentence::select('channel_uid')
+                    ->where('book_id',$sentId[0])
+                    ->where('paragraph',$sentId[1])
+                    ->where('word_start',$sentId[2])
+                    ->where('word_end',$sentId[3])
+                    ->get();
+        foreach ($sentChannel as $key => $value) {
+            # code...
+            $channels[] = $value->channel_uid;
+        }
+		$channelInfo = Channel::whereIn("uid",$channels)->select(['uid','type','name'])->get();
+		$indexChannel = [];
+        $channels = [];
+		foreach ($channelInfo as $key => $value) {
+			# code...
+            if($value->type === $type){
+                $indexChannel[$value->uid] = $value;
+                $channels[] = $value->uid;
+            }
+		}
+		//获取句子数据
+        $record = Sentence::select($this->selectCol)
+                    ->where('book_id',$sentId[0])
+                    ->where('paragraph',$sentId[1])
+                    ->where('word_start',$sentId[2])
+                    ->where('word_end',$sentId[3])
+                    ->whereIn('channel_uid',$channels)
+                    ->orderBy('paragraph')
+                    ->orderBy('word_start')
+                    ->get();
+        if(count($record) ===0){
+            return $this->error("no data");
+        }
+
+        $this->result['content'] = $this->makeContent($record,$mode,$indexChannel);
+        return $this->ok($this->result);
+    }
+    /**
+     * Store a newly created resource in storage.
+
+     * @param  \Illuminate\Http\Request  $request
+     * @param string $id
+     * @param string $mode
+     * @return \Illuminate\Http\Response
+     */
+    public function showPara(Request $request)
+    {
+        //
+        $channels = [];
+        if($request->get('mode') === 'edit'){
+            //翻译模式加载json格式原文
+            $channels[] = ChannelApi::getSysChannel('_System_Wbw_VRI_');
+        }else{
+            //阅读模式加载html格式原文
+            $channels[] = ChannelApi::getSysChannel('_System_Pali_VRI_');
+        }
+
+        if($request->has('channels')){
+            $getChannel = explode(",",$request->get('channels'));
+            $channels = array_merge($channels,$getChannel );
+        }
+        $para = explode(",",$request->get('par'));
+
+        //段落所在章节
+        $parent = PaliText::where('book',$request->get('book'))
+                            ->where('paragraph',$para[0])->first();
+        $chapter = PaliText::where('book',$request->get('book'))
+                            ->where('paragraph',$parent->parent)->first();
+        if($chapter){
+            if(empty($chapter->toc)){
+                $this->result['title'] = "unknown";
+            }else{
+                $this->result['title'] = $chapter->toc;
+                $this->result['sub_title'] = $chapter->toc;
+                $this->result['path'] = json_decode($chapter->path);
+            }
+        }
+
+        $paraFrom = $para[0];
+        $paraTo = end($para);
+
+        $indexedHeading = [];
+
+		#获取channel索引表
+        $tranChannels = [];
+		$channelInfo = Channel::whereIn("uid",$channels)->select(['uid','type','name'])->get();
+		foreach ($channelInfo as $key => $value) {
+			# code...
+            if($value->type==="translation" ){
+                $tranChannels[] = $value->uid;
+            }
+		}
+        $indexChannel = [];
+        $indexChannel = $this->getChannelIndex($channels);
+        //获取wbw channel
+        //目前默认的 wbw channel 是第一个translation channel
+        foreach ($channels as $key => $value) {
+            # code...
+            if($indexChannel[$value]->type==='translation'){
+                $this->wbwChannels[] = $value;
+                break;
+            }
+        }
+        //章节译文标题
+        $title = Sentence::select($this->selectCol)
+                    ->where('book_id',$parent->book)
+                    ->where('paragraph',$parent->paragraph)
+                    ->whereIn('channel_uid',$tranChannels)
+                    ->first();
+        if($title){
+            $this->result['title'] = MdRender::render($title->content,$title->channel_uid);
+        }
+
+        /**
+         * 获取句子数据
+         */
+        $record = Sentence::select($this->selectCol)
+                    ->where('book_id',$request->get('book'))
+                    ->whereIn('paragraph',$para)
+                    ->whereIn('channel_uid',$channels)
+                    ->orderBy('paragraph')
+                    ->orderBy('word_start')
+                    ->get();
+        if(count($record) ===0){
+            $this->result['content'] = "<span>No Data</span>";
+        }else{
+            $this->result['content'] = $this->makeContent($record,$request->get('mode','read'),$indexChannel,$indexedHeading);
+        }
+
+        return $this->ok($this->result);
+    }
+    /**
+     * Store a newly created resource in storage.
+
+     * @param  \Illuminate\Http\Request  $request
+     * @param string $id
+     * @return \Illuminate\Http\Response
+     */
+    public function showChapter(Request $request, string $id)
+    {
+        //
+        $sentId = \explode('-',$id);
+        $channels = [];
+        if($request->has('channels')){
+            $channels = explode('_',$request->get('channels'));
+        }
+        $mode = $request->get('mode','read');
+        if($mode === 'read'){
+            //阅读模式加载html格式原文
+            $channelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
+        }else{
+            //翻译模式加载json格式原文
+            $channelId = ChannelApi::getSysChannel('_System_Wbw_VRI_');
+        }
+
+        if($channelId !== false){
+            $channels[] = $channelId;
+        }
+
+        $chapter = PaliText::where('book',$sentId[0])->where('paragraph',$sentId[1])->first();
+        if(!$chapter){
+            return $this->error("no data");
+        }
+        if(empty($chapter->toc)){
+            $this->result['title'] = "unknown";
+        }else{
+            $this->result['title'] = $chapter->toc;
+            $this->result['sub_title'] = $chapter->toc;
+            $this->result['path'] = json_decode($chapter->path);
+        }
+
+        $paraFrom = $sentId[1];
+        $paraTo = $sentId[1]+$chapter->chapter_len-1;
+        //获取标题
+        $heading = PaliText::select(["book","paragraph","level"])
+                            ->where('book',$sentId[0])
+                            ->whereBetween('paragraph',[$paraFrom,$paraTo])
+                            ->where('level','<',8)
+                            ->get();
+        //将标题段落转成索引数组 以便输出标题层级
+        $indexedHeading = [];
+        foreach ($heading as $key => $value) {
+            # code...
+            $indexedHeading["{$value->book}-{$value->paragraph}"] = $value->level;
+        }
+		#获取channel索引表
+        $tranChannels = [];
+		$channelInfo = Channel::whereIn("uid",$channels)->select(['uid','type','name'])->get();
+		foreach ($channelInfo as $key => $value) {
+			# code...
+            if($value->type==="translation" ){
+                $tranChannels[] = $value->uid;
+            }
+		}
+        $indexChannel = [];
+        $indexChannel = $this->getChannelIndex($channels);
+        //获取wbw channel
+        //目前默认的 wbw channel 是第一个translation channel
+        foreach ($channels as $key => $value) {
+            # code...
+            if($indexChannel[$value]->type==='translation'){
+                $this->wbwChannels[] = $value;
+                break;
+            }
+        }
+        $title = Sentence::select($this->selectCol)
+                    ->where('book_id',$sentId[0])
+                    ->where('paragraph',$sentId[1])
+                    ->whereIn('channel_uid',$tranChannels)
+                    ->first();
+        if($title){
+            $this->result['title'] = MdRender::render($title->content,$title->channel_uid);
+        }
+
+        /**
+         * 获取句子数据
+         * 算法:
+         * 1. 如果标题和下一级第一个标题之间有段落。只输出这些段落和子目录
+         * 2. 如果标题和下一级第一个标题之间没有间隔 且 chapter 长度大于10000个字符 且有子目录,只输出子目录
+         * 3. 如果二者都不是,lazy load
+         */
+		//1. 计算 标题和下一级第一个标题之间 是否有间隔
+        $nextChapter =  PaliText::where('book',$sentId[0])
+                                ->where('paragraph',">",$sentId[1])
+                                ->where('level','<',8)
+                                ->orderBy('paragraph')
+                                ->value('paragraph');
+        $between = $nextChapter - $sentId[1];
+        //输出子目录
+        $chapterLen = $chapter->chapter_len;
+        $toc = PaliText::where('book',$sentId[0])
+                        ->whereBetween('paragraph',[$paraFrom+1,$paraFrom+$chapterLen-1])
+                        ->where('level','<',8)
+                        ->orderBy('paragraph')
+                        ->select(['book','paragraph','level','toc'])
+                        ->get();
+
+        if($between > 1){
+            //有间隔
+            $paraTo = $nextChapter - 1;
+        }else{
+            if($chapter->chapter_strlen>2000){
+                if(count($toc)>0){
+                    //有子目录只输出标题和目录
+                    $paraTo = $paraFrom;
+                }else{
+                    //没有子目录 全部输出
+                }
+            }else{
+                //章节小。全部输出 不输出章节
+                $toc = [];
+            }
+        }
+        $record = Sentence::select($this->selectCol)
+                    ->where('book_id',$sentId[0])
+                    ->whereBetween('paragraph',[$paraFrom,$paraTo])
+                    ->whereIn('channel_uid',$channels)
+                    ->orderBy('paragraph')
+                    ->orderBy('word_start')
+                    ->get();
+        if(count($record) ===0){
+            return $this->error("no data");
+        }
+        $this->result['content'] = $this->makeContent($record,$mode,$indexChannel,$indexedHeading);
+        $this->result['toc'] = TocResource::collection($toc);
+        Log::info("show chapter");
+        return $this->ok($this->result);
+    }
+
+    private function getChannelIndex($channels,$type=null){
+        #获取channel索引表
+        $channelInfo = Channel::whereIn("uid",$channels)->select(['uid','type','name','owner_uid'])->get();
+        $indexChannel = [];
+        foreach ($channelInfo as $key => $value) {
+            # code...
+            if($type !== null && $value->type !== $type){
+                continue;
+            }
+            $indexChannel[$value->uid] = $value;
+        }
+        foreach ($indexChannel as $uid => $value) {
+            # 查询studio
+            $indexChannel[$uid]['studio'] = StudioApi::getById($value->owner_uid);
+        }
+        return $indexChannel;
+    }
+    /**
+     * 根据句子库数据生成文章内容
+     * $record 句子数据
+     * $mode read | edit | wbw
+     * $indexChannel channel索引
+     * $indexedHeading 标题索引 用于给段落加标题标签 <h1> ect.
+     */
+    private function makeContent($record,$mode,$indexChannel,$indexedHeading=[],$onlyProps=false,$paraMark=false){
+        $content = [];
+		$lastSent = "0-0";
+		$sentCount = 0;
+        $sent = [];
+        $sent["origin"] = [];
+        $sent["translation"] = [];
+
+        //获取句子编号列表
+        $sentList = [];
+        foreach ($record as $key => $value) {
+            $currSentId = "{$value->book_id}-{$value->paragraph}-{$value->word_start}-{$value->word_end}";
+            $sentList[$currSentId]=[$value->book_id ,$value->paragraph,$value->word_start,$value->word_end];
+            $value['sid'] = "{$currSentId}_{$value->channel_uid}";
+        }
+        //遍历列表查找每个句子的所有channel的数据,并填充
+        $currPara = "";
+        foreach ($sentList as $currSentId => $arrSentId) {
+            if($currPara === ""){
+                $currPara = $arrSentId[0]."-".$arrSentId[1];
+            }
+            $sent = $this->newSent($arrSentId[0],$arrSentId[1],$arrSentId[2],$arrSentId[3]);
+            foreach ($indexChannel as $channelId => $info) {
+                # code...
+                $sid = "{$currSentId}_{$channelId}";
+                $newSent = [
+                    "content"=>"",
+                    "html"=> "",
+                    "book"=> $arrSentId[0],
+                    "para"=> $arrSentId[1],
+                    "wordStart"=> $arrSentId[2],
+                    "wordEnd"=> $arrSentId[3],
+                    "channel"=> [
+                        "name"=>$info->name,
+                        "type"=>$info->type,
+                        "id"=> $info->uid,
+                    ],
+                    "studio" => $info['studio'],
+                    "updateAt"=> "",
+                    "suggestionCount" => SuggestionApi::getCountBySent($arrSentId[0],$arrSentId[1],$arrSentId[2],$arrSentId[3],$channelId),
+                ];
+
+                $row = Arr::first($record,function($value,$key) use($sid){
+                    return $value['sid']===$sid;
+                });
+                if($row){
+                    $newSent['id'] = $row->uid;
+                    $newSent['content'] = $row->content;
+                    $newSent['contentType'] = $row->content_type;
+                    $newSent['html'] = "";
+                    $newSent["editor"]=UserApi::getById($row->editor_uid);
+                    $newSent['updateAt'] = $row->updated_at;
+                    if($mode !== "read"){
+                        if(isset($row->acceptor_uid) && !empty($row->acceptor_uid)){
+                            $newSent["acceptor"]=UserApi::getById($row->acceptor_uid);
+                            $newSent["prEditAt"]=$row->pr_edit_at;
+                        }
+                    }
+                    switch ($info->type) {
+                        case 'wbw':
+                        case 'original':
+                            //
+                                // 在编辑模式下。
+                                // 如果是原文,查看是否有逐词解析数据,
+                                // 有的话优先显示。
+                                // 阅读模式直接显示html原文
+                                // 传过来的数据一定有一个原文channel
+                                //
+                            if($mode !== "read"){
+
+                                $newSent['channel']['type'] = "wbw";
+
+                                if(isset($this->wbwChannels[0])){
+                                    $newSent['channel']['name'] = $indexChannel[$this->wbwChannels[0]]->name;
+                                    $newSent['channel']['id'] = $this->wbwChannels[0];
+                                    //存在一个translation channel
+                                    //尝试查找逐词解析数据。找到,替换现有数据
+                                    $wbwData = $this->getWbw($arrSentId[0],$arrSentId[1],$arrSentId[2],$arrSentId[3],$this->wbwChannels[0]);
+                                    if($wbwData){
+                                        $newSent['content'] = $wbwData;
+                                        $newSent['html'] = "";
+                                    }
+                                }
+                            }else{
+                                $newSent['content'] = "";
+                                $newSent['html'] = $row->content;
+                            }
+
+                            break;
+                        case 'nissaya':
+                            $newSent['html'] = Cache::remember("/sent/{$channelId}/{$currSentId}",10,
+                            function() use($row,$mode){
+                                return MdRender::render($row->content,$row->channel_uid,null,$mode,"nissaya",$row->content_type);
+                            });
+                            break;
+                        default:
+                            //译文需要markdown渲染
+                            $newSent['html'] = Cache::remember("/sent/{$channelId}/{$currSentId}",10,
+                                                function() use($row){
+                                                    return MdRender::render($row->content,$row->channel_uid);
+                                                });
+                            break;
+                    }
+                }
+                switch ($info->type) {
+                    case 'wbw':
+                    case 'original':
+                        array_push($sent["origin"],$newSent);
+                        break;
+                    default:
+                        array_push($sent["translation"],$newSent);
+                        break;
+                }
+            }
+            if($onlyProps){
+                return $sent;
+            }
+            $content = $this->pushSent($content,$sent,0,$mode);
+        }
+
+        $output = \implode("",$content);
+        return "<div>{$output}</div>";
+    }
+    public function getWbw($book,$para,$start,$end,$channel){
+        /**
+         * 非阅读模式下。原文使用逐词解析数据。
+         * 优先加载第一个translation channel 如果没有。加载默认逐词解析。
+         */
+
+        //获取逐词解析数据
+        $wbwBlock = WbwBlock::where('channel_uid',$channel)
+                            ->where('book_id',$book)
+                            ->where('paragraph',$para)
+                            ->select('uid')
+                            ->first();
+        if(!$wbwBlock){
+            return false;
+        }
+        //找到逐词解析数据
+        $wbwData = Wbw::where('block_uid',$wbwBlock->uid)
+                      ->whereBetween('wid',[$start,$end])
+                      ->select(['book_id','paragraph','wid','data','uid'])
+                      ->orderBy('wid')
+                      ->get();
+        $wbwContent = [];
+        foreach ($wbwData as $wbwrow) {
+            $wbw = str_replace("&nbsp;",' ',$wbwrow->data);
+            $wbw = str_replace("<br>",' ',$wbw);
+
+            $xmlString = "<root>" . $wbw . "</root>";
+            try{
+                $xmlWord = simplexml_load_string($xmlString);
+            }catch(Exception $e){
+                continue;
+            }
+            $wordsList = $xmlWord->xpath('//word');
+            foreach ($wordsList as $word) {
+                $case = \str_replace(['#','.'],['$',''],$word->case->__toString());
+                $case = \str_replace('$$','$',$case);
+                $case = trim($case);
+                $case = trim($case,"$");
+                $wbwId = explode('-',$word->id->__toString());
+
+                $wbwData = [
+                    'uid'=>$wbwrow->uid,
+                    'book'=>$wbwrow->book_id,
+                    'para'=>$wbwrow->paragraph,
+                    'sn'=> array_slice($wbwId,2),
+                    'word'=>['value'=>$word->pali->__toString(),'status'=>0],
+                    'real'=> ['value'=>$word->real->__toString(),'status'=>0],
+                    'meaning'=> ['value'=>$word->mean->__toString() ,'status'=>0],
+                    'type'=> ['value'=>$word->type->__toString(),'status'=>0],
+                    'grammar'=> ['value'=>$word->gramma->__toString(),'status'=>0],
+                    'case'=> ['value'=>$word->case->__toString(),'status'=>0],
+                    'parent'=> ['value'=>$word->parent->__toString(),'status'=>0],
+                    'style'=> ['value'=>$word->style->__toString(),'status'=>0],
+                    'factors'=> ['value'=>$word->org->__toString(),'status'=>0],
+                    'factorMeaning'=> ['value'=>$word->om->__toString(),'status'=>0],
+                    'confidence'=> $word->cf->__toString(),
+                    'hasComment'=>Discussion::where('res_id',$wbwrow->uid)->exists(),
+                ];
+                if(isset($word->parent2)){
+                    $wbwData['parent2']['value'] = $word->parent2->__toString();
+                    if(isset($word->parent2['status'])){
+                        $wbwData['parent2']['status'] = (int)$word->parent2['status'];
+                    }else{
+                        $wbwData['parent2']['status'] = 0;
+                    }
+                }
+                if(isset($word->pg)){
+                    $wbwData['grammar2']['value'] = $word->pg->__toString();
+                    if(isset($word->pg['status'])){
+                        $wbwData['grammar2']['status'] = (int)$word->pg['status'];
+                    }else{
+                        $wbwData['grammar2']['status'] = 0;
+                    }
+                }
+                if(isset($word->rela)){
+                    $wbwData['relation']['value'] = $word->rela->__toString();
+                    if(isset($word->rela['status'])){
+                        $wbwData['relation']['status'] = (int)$word->rela['status'];
+                    }else{
+                        $wbwData['relation']['status'] = 7;
+                    }
+                }
+                if(isset($word->bmt)){
+                    $wbwData['bookMarkText']['value'] = $word->bmt->__toString();
+                    if(isset($word->bmt['status'])){
+                        $wbwData['bookMarkText']['status'] = (int)$word->bmt['status'];
+                    }else{
+                        $wbwData['bookMarkText']['status'] = 7;
+                    }
+                }
+                if(isset($word->bmc)){
+                    $wbwData['bookMarkColor']['value'] = $word->bmc->__toString();
+                    if(isset($word->bmc['status'])){
+                        $wbwData['bookMarkColor']['status'] = (int)$word->bmc['status'];
+                    }else{
+                        $wbwData['bookMarkColor']['status'] = 7;
+                    }
+                }
+                if(isset($word->note)){
+                    $wbwData['note']['value'] = $word->note->__toString();
+                    if(isset($word->note['status'])){
+                        $wbwData['note']['status'] = (int)$word->note['status'];
+                    }else{
+                        $wbwData['note']['status'] = 7;
+                    }
+                }
+                if(isset($word->cf)){
+                    $wbwData['confidence'] = (float)$word->cf->__toString();
+                }
+                if(isset($word->pali['status'])){
+                    $wbwData['word']['status'] = (int)$word->pali['status'];
+                }
+                if(isset($word->real['status'])){
+                    $wbwData['real']['status'] = (int)$word->real['status'];
+                }
+                if(isset($word->mean['status'])){
+                    $wbwData['meaning']['status'] = (int)$word->mean['status'];
+                }
+                if(isset($word->type['status'])){
+                    $wbwData['type']['status'] = (int)$word->type['status'];
+                }
+                if(isset($word->gramma['status'])){
+                    $wbwData['grammar']['status'] = (int)$word->gramma['status'];
+                }
+                if(isset($word->case['status'])){
+                    $wbwData['case']['status'] = (int)$word->case['status'];
+                }
+                if(isset($word->parent['status'])){
+                    $wbwData['parent']['status'] = (int)$word->parent['status'];
+                }
+                if(isset($word->org['status'])){
+                    $wbwData['factors']['status'] = (int)$word->org['status'];
+                }
+                if(isset($word->om['status'])){
+                    $wbwData['factorMeaning']['status'] = (int)$word->om['status'];
+                }
+
+                $wbwContent[] = $wbwData;
+            }
+        }
+        if(count($wbwContent)===0){
+            return false;
+        }
+        return \json_encode($wbwContent,JSON_UNESCAPED_UNICODE);
+
+    }
+    /**
+     * 将句子放进结果列表
+     */
+	private function pushSent($result,$sent,$level=0,$mode='read'){
+
+		$sentProps = base64_encode(\json_encode($sent)) ;
+        if($mode === 'read'){
+            $sentWidget = "<MdTpl tpl='sentread' props='{$sentProps}' />";
+        }else{
+            $sentWidget = "<MdTpl tpl='sentedit' props='{$sentProps}' />";
+        }
+		//增加标题的html标记
+		if($level>0){
+			$sentWidget = "<h{$level}>".$sentWidget."</h{$level}>";
+		}
+		array_push($result,$sentWidget);
+        return $result;
+	}
+	private function newSent($book,$para,$word_start,$word_end){
+		$sent = [
+            "id"=>"{$book}-{$para}-{$word_start}-{$word_end}",
+            "book"=>$book,
+            "para"=>$para,
+            "wordStart"=>$word_start,
+            "wordEnd"=>$word_end,
+			"origin"=>[],
+			"translation"=>[],
+		];
+
+		#生成channel 数量列表
+		$sentId = "{$book}-{$para}-{$word_start}-{$word_end}";
+        $channelCount = CorpusController::sentResCount($book,$para,$word_start,$word_end);
+        $path = json_decode(PaliText::where('book',$book)->where('paragraph',$para)->value("path"),true);
+        $sent["path"] = [];
+        foreach ($path as $key => $value) {
+            # code...
+            $value['paliTitle'] = $value['title'];
+            $sent["path"][] = $value;
+        }
+		$sent["tranNum"] = $channelCount['tranNum'];
+		$sent["nissayaNum"] = $channelCount['nissayaNum'];
+		$sent["commNum"] = $channelCount['commNum'];
+		$sent["originNum"] = $channelCount['originNum'];
+		$sent["simNum"] = $channelCount['simNum'];
+		return $sent;
+	}
+
+    /**
+     * 获取某个句子的相关资源的句子数量
+     */
+    public static function sentResCount($book,$para,$start,$end){
+		$sentId = "{$book}-{$para}-{$start}-{$end}";
+		$channelCount = Cache::remember("/sentence/{$sentId}/channels/count",
+                          60,
+                          function() use($book,$para,$start,$end){
+			$channels =  Sentence::where('book_id',$book)
+							->where('paragraph',$para)
+							->where('word_start',$start)
+							->where('word_end',$end)
+							->select('channel_uid')
+                            ->groupBy('channel_uid')
+							->get();
+            $channelList = [];
+            foreach ($channels as $key => $value) {
+                # code...
+                if(Str::isUuid($value->channel_uid)){
+                    $channelList[] = $value->channel_uid;
+                }
+            }
+            $simId = PaliSentence::where('book',$book)
+                                 ->where('paragraph',$para)
+                                 ->where('word_begin',$start)
+                                 ->where('word_end',$end)
+                                 ->value('id');
+            if($simId){
+                $output["simNum"]=SentSimIndex::where('sent_id',$simId)->value('count');
+            }else{
+                $output["simNum"]=0;
+            }
+            $channelInfo = Channel::whereIn("uid",$channelList)->select('type')->get();
+            $output["tranNum"]=0;
+            $output["nissayaNum"]=0;
+            $output["commNum"]=0;
+            $output["originNum"]=0;
+
+            foreach ($channelInfo as $key => $value) {
+                # code...
+                switch($value->type){
+                    case "translation":
+                        $output["tranNum"]++;
+                        break;
+                    case "nissaya":
+                        $output["nissayaNum"]++;
+                        break;
+                    case "commentary":
+                        $output["commNum"]++;
+                        break;
+                    case "original":
+                        $output["originNum"]++;
+                        break;
+                }
+            }
+			return $output;
+
+		});
+        return $channelCount;
+    }
+    private function markdownRender($input){
+
+    }
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Sentence  $sentence
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Sentence $sentence)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\Sentence  $sentence
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Sentence $sentence)
+    {
+        //
+    }
+}

+ 255 - 0
app/Http/Controllers/CourseController.php

@@ -0,0 +1,255 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Course;
+use App\Models\CourseMember;
+use Illuminate\Http\Request;
+use App\Http\Api\AuthApi;
+use App\Http\Api\StudioApi;
+use App\Http\Resources\CourseResource;
+use Illuminate\Support\Facades\DB;
+
+class CourseController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+		$result=false;
+		$indexCol = ['id','title','subtitle','cover','content','content_type','teacher','start_at','end_at','publicity','updated_at','created_at'];
+		switch ($request->get('view')) {
+            case 'new':
+                //最新公开课程列表
+                $table = Course::where('publicity', 30);
+                break;
+            case 'open':
+                /**
+                 * 开放课程列表
+                 * 开放规则:
+                 * 1. 公开
+                 * 2. 课程开始时间比现在时间晚
+                 */
+                $table = Course::where('publicity', 30)
+                            ->whereDate('start_at',">",date("Y-m-d",strtotime("today")));
+                break;
+            case 'close':
+                /**
+                 * 已经关闭课程列表
+                 * 判定规则:
+                 * 1. 公开
+                 * 2. 课程开始时间比现在时间早
+                 */
+                $table = Course::where('publicity', 30)
+                        ->whereDate('start_at',"<=",date("Y-m-d",strtotime("today")));
+                break;
+            case 'create':
+	            # 获取 studio 建立的所有 course
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //判断当前用户是否有指定的studio的权限
+                if($user['user_uid'] !== StudioApi::getIdByName($request->get('studio'))){
+                    return $this->error(__('auth.failed'));
+                }
+
+                $table = Course::where('studio_id', $user["user_uid"]);
+				break;
+            case 'study':
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //我学习的课程
+                $course = CourseMember::where('user_id',$user["user_uid"])
+                                      ->where('role','student')
+                                      ->select('course_id')
+                                      ->get();
+                $courseId = [];
+                foreach ($course as $key => $value) {
+                    # code...
+                    $courseId[] = $value->course_id;
+                }
+                $table = Course::whereIn('id', $courseId);
+                break;
+            case 'teach':
+                //我任教的课程
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                $course = CourseMember::where('user_id',$user["user_uid"])
+                ->where('role','assistant')
+                ->select('course_id')
+                ->get();
+                $courseId = [];
+                foreach ($course as $key => $value) {
+                    # code...
+                    $courseId[] = $value->course_id;
+                }
+                $table = Course::whereIn('id', $courseId);
+                break;
+        }
+        $table = $table->select($indexCol);
+        if(isset($_GET["search"])){
+            $table = $table->where('title', 'like', $_GET["search"]."%");
+        }
+        $count = $table->count();
+        if(isset($_GET["order"]) && isset($_GET["dir"])){
+            $table = $table->orderBy($_GET["order"],$_GET["dir"]);
+        }else{
+            $table = $table->orderBy('updated_at','desc');
+        }
+
+        if(isset($_GET["limit"])){
+            $offset = 0;
+            if(isset($_GET["offset"])){
+                $offset = $_GET["offset"];
+            }
+            $table = $table->skip($offset)->take($_GET["limit"]);
+        }
+        $result = $table->get();
+		if($result){
+			return $this->ok(["rows"=>CourseResource::collection($result),"count"=>$count]);
+		}else{
+			return $this->error("没有查询到数据");
+		}
+    }
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function showMyCourseNumber(Request $request){
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //我建立的课程
+        $create = Course::where('studio_id', $user["user_uid"])->count();
+        //我学习的课程
+        $study = CourseMember::where('user_id',$user["user_uid"])
+        ->where('role','student')
+        ->count();
+        //我任教的课程
+        $teach = CourseMember::where('user_id',$user["user_uid"])
+        ->where('role','assistant')
+        ->count();
+        return $this->ok(['create'=>$create,'teach'=>$teach,'study'=>$study]);
+    }
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的studio的权限
+        $studio_id = StudioApi::getIdByName($request->get('studio'));
+        if($user['user_uid'] !== $studio_id){
+            return $this->error(__('auth.failed'));
+        }
+        //查询是否重复
+        if(Course::where('title',$request->get('title'))->where('studio_id',$user['user_uid'])->exists()){
+            return $this->error(__('validation.exists',['name']));
+        }
+
+        $course = new Course;
+        $course->title = $request->get('title');
+        $course->studio_id = $studio_id;
+        $course->save();
+        return $this->ok(new CourseResource($course));
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Course  $course
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Course $course)
+    {
+        //
+        return $this->ok(new CourseResource($course));
+
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Course  $course
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Course $course)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的studio的权限
+        if($user['user_uid'] !== $course->studio_id){
+            return $this->error(__('auth.failed'));
+        }
+        //查询标题是否重复
+        if(Course::where('title',$request->get('title'))->where('studio_id',$user['user_uid'])->exists()){
+            if($course->title !== $request->get('title')){
+                return $this->error(__('validation.exists',['name']));
+            }
+        }
+        $course->title = $request->get('title');
+        $course->subtitle = $request->get('subtitle');
+        $course->summary = $request->get('summary');
+        if($request->has('cover')) {$course->cover = $request->get('cover');}
+        $course->content = $request->get('content');
+        if($request->has('teacher_id')) {$course->teacher = $request->get('teacher_id');}
+        if($request->has('anthology_id')) {$course->anthology_id = $request->get('anthology_id');}
+        $course->channel_id = $request->get('channel_id');
+        if($request->has('publicity')) {$course->publicity = $request->get('publicity');}
+        $course->start_at = $request->get('start_at');
+        $course->end_at = $request->get('end_at');
+        $course->join = $request->get('join');
+        $course->request_exp = $request->get('request_exp');
+        $course->save();
+        return $this->ok($course);
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Course  $course
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Request $request,Course $course)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的studio的权限
+        if($user['user_uid'] !== $course->studio_id){
+            return $this->error(__('auth.failed'));
+        }
+        $delete = 0;
+        DB::transaction(function() use($delete,$course){
+            //删除group member
+            $memberDelete = CourseMember::where('course_id',$course->id)->delete();
+            $delete = $course->delete();
+        });
+
+        return $this->ok($delete);
+    }
+}

+ 282 - 0
app/Http/Controllers/CourseMemberController.php

@@ -0,0 +1,282 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\CourseMember;
+use App\Models\Course;
+use Illuminate\Http\Request;
+use App\Http\Resources\CourseMemberResource;
+use App\Http\Api\AuthApi;
+use Illuminate\Support\Facades\Log;
+
+class CourseMemberController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $result=false;
+		$indexCol = ['id','user_id','course_id','role','updated_at','created_at'];
+		switch ($request->get('view')) {
+            case 'course':
+	            # 获取 course 内所有 成员
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //TODO 判断当前用户是否有指定的 course 的权限
+                $table = CourseMember::where('course_id', $request->get('id'));
+				break;
+            case 'user':
+                //获取某个用户的角色
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //TODO 判断当前用户是否有指定的 course 的权限
+                $table = CourseMember::where('course_id', $request->get('course'))
+                                    ->where('user_id', $user['user_uid']);
+                break;
+        }
+        if(isset($_GET["search"])){
+            $table = $table->where('title', 'like', $_GET["search"]."%");
+        }
+        $count = $table->count();
+        if(isset($_GET["order"]) && isset($_GET["dir"])){
+            $table = $table->orderBy($_GET["order"],$_GET["dir"]);
+        }else{
+            $table = $table->orderBy('updated_at','desc');
+        }
+
+        if(isset($_GET["limit"])){
+            $offset = 0;
+            if(isset($_GET["offset"])){
+                $offset = $_GET["offset"];
+            }
+            $table = $table->skip($offset)->take($_GET["limit"]);
+        }
+        $result = $table->get();
+
+        //获取当前用户角色
+        $isOwner = Course::where('id',$request->get('id'))->where('studio_id',$user["user_uid"])->exists();
+        $role = 'unknown';
+        if($isOwner){
+            $role = 'owner';
+        }else{
+            foreach ($result as $key => $value) {
+            # 找到当前用户
+            if($user["user_uid"]===$value->user_id){
+                switch ($value->role) {
+                    case 'assistant':
+                        $role = 'manager';
+                        break;
+                    default:
+                        # code...
+                        break;
+                }
+                break;
+            }
+        }
+        }
+
+		if($result){
+			return $this->ok(["rows"=>CourseMemberResource::collection($result),'role'=>$role,"count"=>$count]);
+		}else{
+			return $this->error("没有查询到数据");
+		}
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        $validated = $request->validate([
+            'user_id' => 'required',
+            'course_id' => 'required',
+            'role' => 'required',
+            'operating' => 'required',
+        ]);
+        //查找重复的项目
+        if(CourseMember::where('course_id', $validated['course_id'])
+                      ->where('user_id',$validated['user_id'])
+                      ->exists()){
+            return $this->error('member exists');
+        }
+        $newMember = new CourseMember();
+        $newMember->user_id = $validated['user_id'];
+        $newMember->course_id = $validated['course_id'];
+        $newMember->role = $validated['role'];
+        /**
+         * 查找course 信息,根据加入方式设置状态
+         * open : accepted
+         * manual: progressing
+         */
+        $course  = Course::find($validated['course_id']);
+        if($course){
+            switch ($course->join) {
+                case 'open': //开放学习课程
+                    switch ($validated['operating']) {
+                        case 'invite':
+                            $newMember->status = 'invited';
+                            break;
+                        case 'sign_up':
+                            $newMember->status = 'normal';
+                            break;
+                    }
+                    break;
+                case 'manual': //人工审核课程
+                    switch ($validated['operating']) {
+                        case 'invite':
+                            $newMember->status = 'invited';
+                            break;
+                        case 'sign_up':
+                            $newMember->status = 'sign_up';
+                            break;
+                    }
+                    break;
+                case 'invite': //仅限邀请
+                    $newMember->status = 'invited';
+                    break;
+
+                default:
+                    # code...
+                    break;
+            }
+        }else{
+            return $this->error('invalid course');
+        }
+        $newMember->save();
+        return $this->ok(new CourseMemberResource($newMember));
+
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\CourseMember  $courseMember
+     * @return \Illuminate\Http\Response
+     */
+    public function show(CourseMember $courseMember)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\CourseMember  $courseMember
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, CourseMember $courseMember)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+
+        if($request->has('channel_id')) {
+            if($courseMember->user_id !== $user['user_uid']){
+                return $this->error(__('auth.failed'));
+            }
+            $courseMember->channel_id = $request->get('channel_id');
+        }
+        if($request->has('status')) {
+            $courseMember->status = $request->get('status');
+        }
+        $courseMember->save();
+        return $this->ok(new CourseMemberResource($courseMember));
+
+    }
+    public function set_channel(Request $request)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+
+        if($request->has('channel_id')) {
+            $courseMember = CourseMember::where('course_id',$request->get('course_id'))
+                                        ->where('user_id',$user['user_uid'])
+                                        ->first();
+            if($courseMember){
+                $courseMember->channel_id = $request->get('channel_id');
+                $courseMember->save();
+                return $this->ok(new CourseMemberResource($courseMember));
+            }else{
+                return $this->error(__('auth.failed'));
+            }
+        }
+
+
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\CourseMember  $courseMember
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Request $request,CourseMember $courseMember)
+    {
+        //查看删除者有没有删除权限
+        //查询删除者的权限
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+
+        $isOwner = Course::where('id',$courseMember->course_id)->where('studio_id',$user["user_uid"])->exists();
+        if(!$isOwner){
+            $courseUser = CourseMember::where('course_id',$courseMember->course_id)
+                ->where('user_id',$user["user_uid"])
+                ->select('role')->first();
+            //open 课程 可以删除自己
+
+            if(!$courseUser){
+                //被删除的不是自己
+                if($courseUser->role ==="student"){
+                    //普通成员没有删除权限
+                    return $this->error(__('auth.failed'));
+                }
+            }
+        }
+
+        $delete = $courseMember->delete();
+        return $this->ok($delete);
+    }
+
+    /**
+     * 获取当前用户权限
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function curr(Request $request)
+    {
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        $courseUser = CourseMember::where('course_id',$request->get("course_id"))
+                ->where('user_id',$user["user_uid"])
+                ->select(['role','channel_id'])->first();
+        if($courseUser){
+            return $this->ok($courseUser);
+        }else{
+            return $this->error("not member");
+        }
+    }
+}

+ 456 - 85
app/Http/Controllers/DhammaTermController.php

@@ -3,10 +3,21 @@
 namespace App\Http\Controllers;
 
 use App\Models\DhammaTerm;
+use App\Models\Channel;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Str;
+use App\Http\Api\AuthApi;
+use App\Http\Api\StudioApi;
+use App\Http\Api\ChannelApi;
+use App\Http\Api\ShareApi;
+use App\Tools\Tools;
+use App\Http\Resources\TermResource;
+use Illuminate\Support\Facades\App;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+use Illuminate\Support\Facades\Log;
 
 class DhammaTermController extends Controller
 {
@@ -18,9 +29,88 @@ class DhammaTermController extends Controller
     public function index(Request $request)
     {
         $result=false;
-		$indexCol = ['id','guid','word','word_en','meaning','other_meaning','note','language','channal','updated_at'];
+		$indexCol = ['id','guid','word','meaning','other_meaning','note','tag','language','channal','owner','editor_id','created_at','updated_at'];
 
 		switch ($request->get('view')) {
+            case 'create-by-channel':
+                # 新建术语时。根据术语所在channel 给出新建术语所需数据。如语言,备选意思等。
+                #获取channel信息
+                $currChannel = Channel::where('uid',$request->get('channel'))->first();
+                if(!$currChannel){
+                    return $this->error(__('auth.failed'));
+                }
+                #TODO 查询studio信息
+                #获取同studio的channel列表
+                $studioChannels = Channel::where('owner_uid',$currChannel->owner_uid)
+                                        ->select(['name','uid'])
+                                        ->get();
+                #获取全网意思列表
+                $meanings = DhammaTerm::where('word',$request->get('word'))
+                                        ->where('language',$currChannel->lang)
+                                        ->select(['meaning','other_meaning'])
+                                        ->get();
+                $meaningList=[];
+                foreach ($meanings as $key => $value) {
+                    # code...
+                    $meaning1 = [$value->meaning];
+
+                    if(!empty($value->other_meaning)){
+                        $meaning2 = \explode(',',$value->other_meaning);
+                        $meaning1 = array_merge($meaning1,$meaning2);
+                    }
+                    foreach ($meaning1 as $key => $value) {
+                        # code...
+                        if(isset($meaningList[$value])){
+                            $meaningList[$value]++;
+                        }else{
+                            $meaningList[$value] = 1;
+                        }
+                    }
+                }
+                $meaningCount = [];
+                foreach ($meaningList as $key => $value) {
+                    # code...
+                    $meaningCount[] = ['meaning'=>$key,'count'=>$value];
+                }
+                return $this->ok([
+                    "word"=>$request->get('word'),
+                    "meaningCount"=>$meaningCount,
+                    "studioChannels"=>$studioChannels,
+                    "language"=>$currChannel->lang,
+                    'studio'=>StudioApi::getById($currChannel->owner_uid),
+                ]);
+                break;
+            case 'studio':
+				# 获取 studio 内所有 term
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'),[],401);
+                }
+                //判断当前用户是否有指定的studio的权限
+                if($user['user_uid'] !== StudioApi::getIdByName($request->get('name'))){
+                    return $this->error(__('auth.failed'),[],403);
+                }
+                $table = DhammaTerm::select($indexCol)
+                                   ->where('owner', $user["user_uid"]);
+				break;
+            case 'channel':
+                # 获取 studio 内所有 term
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //判断当前用户是否有指定的 channel 的权限
+                $channel = Channel::find($request->get('id'));
+                if($user['user_uid'] !== $channel->owner_uid ){
+                    //看是否为协作
+                    $power = ShareApi::getResPower($user['user_uid'],$request->get('id'));
+                    if($power === 0){
+                        return $this->error(__('auth.failed'),[],403);
+                    }
+                }
+                $table = DhammaTerm::select($indexCol)
+                                    ->where('channal', $request->get('id'));
+                break;
             case 'show':
                 return $this->ok(DhammaTerm::find($request->get('id')));
                 break;
@@ -30,32 +120,10 @@ class DhammaTermController extends Controller
                 $search = $request->get('search');
 				$table = DhammaTerm::select($indexCol)
 									->where('owner', $userUid);
-				if(!empty($search)){
-					$table->where('word', 'like', $search."%")
-                          ->orWhere('word_en', 'like', $search."%")
-                          ->orWhere('meaning', 'like', "%".$search."%");
-				}
-				if(!empty($request->get('order')) && !empty($request->get('dir'))){
-					$table->orderBy($request->get('order'),$request->get('dir'));
-				}else{
-					$table->orderBy('updated_at','desc');
-				}
-				$count = $table->count();
-				if(!empty($request->get('limit'))){
-					$offset = 0;
-					if(!empty($request->get("offset"))){
-						$offset = $request->get("offset");
-					}
-					$table->skip($offset)->take($request->get('limit'));
-				}
-				$result = $table->get();
 				break;
 			case 'word':
-				$result = DhammaTerm::select($indexCol)
-									->where('word', $request->get("word"))
-									->orderBy('created_at','desc')
-									->get();
-                $count = count($result);
+				$table = DhammaTerm::select($indexCol)
+									->where('word', $request->get("word"));
 				break;
             case 'hot-meaning':
                 $key='term/hot_meaning';
@@ -65,7 +133,7 @@ class DhammaTermController extends Controller
                                 ->where('language',$request->get("language"))
                                 ->groupby('word')
                                 ->get();
-                    
+
                     foreach ($words as $key => $word) {
                         # code...
                         $result = DhammaTerm::select(DB::raw('count(*) as word_count, meaning'))
@@ -92,8 +160,23 @@ class DhammaTermController extends Controller
 				# code...
 				break;
 		}
+
+        $search = $request->get('search');
+        if(!empty($search)){
+            $table = $table->where(function($query) use($search){
+                $query->where('word', 'like', $search."%")
+                  ->orWhere('word_en', 'like', $search."%")
+                  ->orWhere('meaning', 'like', "%".$search."%");
+            });
+        }
+        $count = $table->count();
+        $table = $table->orderBy($request->get('order','updated_at'),$request->get('dir','desc'));
+        $table = $table->skip($request->get("offset",0))
+                       ->take($request->get('limit',1000));
+        $result = $table->get();
+
 		if($result){
-			return $this->ok(["rows"=>$result,"count"=>$count]);
+			return $this->ok(["rows"=>TermResource::collection($result),"count"=>$count]);
 		}else{
 			return $this->error("没有查询到数据");
 		}
@@ -107,69 +190,81 @@ class DhammaTermController extends Controller
      */
     public function store(Request $request)
     {
-                // validate
-        // read more on validation at http://laravel.com/docs/validation
-        $rules = array(
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        $validated = $request->validate([
             'word' => 'required',
             'meaning' => 'required',
             'language' => 'required'
-        );
-        $validator = Validator::make($request->all(), $rules);
-
-        // process the login
-        if ($validator->fails()) {
-            return $this->error($validator);
-        } else {
-            #查询重复的
-            /*
-            重复判定:
-            一个channel下面word+tag+language 唯一
-            */
-            $table = DhammaTerm::where('owner', $_COOKIE["user_uid"])
-                    ->where('word',$request->get("word"))
-                    ->where('tag',$request->get("tag"));
-            if($request->get("channel")){
-                $isDoesntExist = $table->where('channel',$request->get("channel"))
-                      ->doesntExist();
-            }else{
-                $isDoesntExist = $table->where('language',$request->get("language"))
+        ]);
+        #查询重复的
+        /*
+        重复判定:
+        一个channel下面word+tag+language 唯一
+        */
+        $table = DhammaTerm::where('owner', $user["user_uid"])
+                ->where('word',$request->get("word"))
+                ->where('tag',$request->get("tag"));
+        if($request->get("channel")){
+            $isDoesntExist = $table->where('channel',$request->get("channel"))
                     ->doesntExist();
-            }
-	
-            if($isDoesntExist){
-                #不存在插入数据
-                $term = new DhammaTerm;
-                $term->id=app('snowflake')->id();
-                $term->guid=Str::uuid();
-                $term->word=$request->get("word");
-                $term->meaning=$request->get("meaning");
-                $term->save();
-                return $this->ok($data);
+        }else{
+            $isDoesntExist = $table->where('language',$request->get("language"))
+                ->doesntExist();
+        }
 
+        if($isDoesntExist){
+            #不存在插入数据
+            $term = new DhammaTerm;
+            $term->id = app('snowflake')->id();
+            $term->guid = Str::uuid();
+            $term->word = $request->get("word");
+            $term->word_en = Tools::getWordEn($request->get("word"));
+            $term->meaning = $request->get("meaning");
+            $term->other_meaning = $request->get("other_meaning");
+            $term->note = $request->get("note");
+            $term->tag = $request->get("tag");
+            $term->channal = $request->get("channal");
+            $term->language = $request->get("language");
+            if($request->has("channal")){
+                $channelInfo = ChannelApi::getById($request->get("channal"));
+                if(!$channelInfo){
+                    return $this->error("channel id failed");
+                }else{
+                    $term->owner = $channelInfo['studio_id'];
+                }
             }else{
-                return $this->error("word existed");
+                $term->owner = StudioApi::getIdByName($request->get("studioName"));
             }
-            // store
-            /*
-            $data = $request->all();
-            $data['id'] = app('snowflake')->id();
-            $data['guid'] = Str::uuid();
-            DhammaTerm::create($data);
-            */
-            
+            $term->editor_id = $user["user_id"];
+            $term->create_time = time()*1000;
+            $term->modify_time = time()*1000;
+            $term->save();
+            return $this->ok($term);
+        }else{
+            return $this->error("word existed",[],200);
         }
+
     }
 
     /**
      * Display the specified resource.
      *
-     * @param  \App\Models\DhammaTerm  $dhammaTerm
+     * @param  string  $id
      * @return \Illuminate\Http\Response
      */
-    public function show(DhammaTerm $dhammaTerm)
+    public function show(Request  $request,$id)
     {
         //
-        return $dhammaTerm;
+		$result  = DhammaTerm::where('guid', $id)->first();
+		if($result){
+			return $this->ok(new TermResource($result));
+		}else{
+			return $this->error("没有查询到数据");
+		}
+
     }
 
     /**
@@ -179,9 +274,44 @@ class DhammaTermController extends Controller
      * @param  \App\Models\DhammaTerm  $dhammaTerm
      * @return \Illuminate\Http\Response
      */
-    public function update(Request $request, DhammaTerm $dhammaTerm)
+    public function update(Request $request, string $id)
     {
         //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        $dhammaTerm = DhammaTerm::find($id);
+        $dhammaTerm->word = $request->get("word");
+        $dhammaTerm->word_en = Tools::getWordEn($request->get("word"));
+        $dhammaTerm->meaning = $request->get("meaning");
+        $dhammaTerm->other_meaning = $request->get("other_meaning");
+        $dhammaTerm->note = $request->get("note");
+        $dhammaTerm->tag = $request->get("tag");
+        $dhammaTerm->channal = $request->get("channal");
+        $dhammaTerm->language = $request->get("language");
+        if($request->has("channal") && Str::isUuid($request->has("channal"))){
+            $channelInfo = ChannelApi::getById($request->get("channal"));
+            if(!$channelInfo){
+                return $this->error("channel id failed");
+            }else{
+                $dhammaTerm->owner = $channelInfo['studio_id'];
+            }
+        }
+        if($request->has("studioName")){
+            $dhammaTerm->owner = StudioApi::getIdByName($request->get("studioName"));
+        }else if($request->has("studioId")){
+            $dhammaTerm->owner = $request->get("studioId");
+        }else{
+            $dhammaTerm->owner = null;
+        }
+
+        $dhammaTerm->editor_id = $user["user_id"];
+        $dhammaTerm->create_time = time()*1000;
+        $dhammaTerm->modify_time = time()*1000;
+        $dhammaTerm->save();
+		return $this->ok($dhammaTerm);
+
     }
 
     /**
@@ -192,18 +322,259 @@ class DhammaTermController extends Controller
      */
     public function destroy(DhammaTerm $dhammaTerm,Request $request)
     {
-        //
-        $arrId = json_decode($request->get("id"),true) ;
-		$count = 0;
-		foreach ($arrId as $key => $id) {
-			# code...
-			$result = DhammaTerm::where('id', $id)
-							->where('owner', $_COOKIE["user_uid"])
-							->delete();
-            if($result){
-                $count++;
+        /**
+         * 一次删除多个单词
+         */
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        $count = 0;
+        if($request->has("uuid")){
+            //查看是否有删除权限
+            foreach ($request->get("id") as $key => $uuid) {
+                $term = DhammaTerm::find($uuid);
+                if($term->owner !== $user['user_uid']){
+                    if(!empty($term->channal)){
+                        //看是否为协作
+                        $power = ShareApi::getResPower($user['user_uid'],$term->channal);
+                        if($power < 20){
+                            continue;
+                        }
+                    }else{
+                        continue;
+                    }
+                }
+                $count += $term->delete();
             }
-		}
+        }else{
+            $arrId = json_decode($request->get("id"),true) ;
+            foreach ($arrId as $key => $id) {
+                # code...
+                $result = DhammaTerm::where('id', $id)
+                                ->where('owner', $user['user_uid'])
+                                ->delete();
+                if($result){
+                    $count++;
+                }
+            }
+        }
+
 		return $this->ok($count);
     }
+
+    public function export(Request $request){
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+//TODO 判断是否有导出权限
+        switch ($request->get("view")) {
+            case 'channel':
+                # code...
+                $rows = DhammaTerm::where('channal',$request->get("id"))->cursor();
+                break;
+            case 'studio':
+                # code...
+                $studioId = StudioApi::getIdByName($request->get("name"));
+                $rows = DhammaTerm::where('owner',$studioId)->cursor();
+                break;
+            default:
+                $this->error('no view');
+                break;
+        }
+
+        $spreadsheet = new Spreadsheet();
+        $activeWorksheet = $spreadsheet->getActiveSheet();
+        $activeWorksheet->setCellValue('A1', 'id');
+        $activeWorksheet->setCellValue('B1', 'word');
+        $activeWorksheet->setCellValue('C1', 'meaning');
+        $activeWorksheet->setCellValue('D1', 'other_meaning');
+        $activeWorksheet->setCellValue('E1', 'note');
+        $activeWorksheet->setCellValue('F1', 'tag');
+        $activeWorksheet->setCellValue('G1', 'language');
+        $activeWorksheet->setCellValue('H1', 'channel_id');
+
+        $currLine = 2;
+        foreach ($rows as $key => $row) {
+            # code...
+            $activeWorksheet->setCellValue("A{$currLine}", $row->guid);
+            $activeWorksheet->setCellValue("B{$currLine}", $row->word);
+            $activeWorksheet->setCellValue("C{$currLine}", $row->meaning);
+            $activeWorksheet->setCellValue("D{$currLine}", $row->other_meaning);
+            $activeWorksheet->setCellValue("E{$currLine}", $row->note);
+            $activeWorksheet->setCellValue("F{$currLine}", $row->tag);
+            $activeWorksheet->setCellValue("G{$currLine}", $row->language);
+            $activeWorksheet->setCellValue("H{$currLine}", $row->channal);
+            $currLine++;
+        }
+        $writer = new Xlsx($spreadsheet);
+        $fId = Str::uuid();
+        $filename = storage_path("app/tmp/{$fId}");
+        $writer->save($filename);
+        Cache::put("download/tmp/{$fId}",file_get_contents($filename),300);
+        unlink($filename);
+        return $this->ok(['uuid'=>$fId,'filename'=>"term.xlsx",'type'=>"application/vnd.ms-excel"]);
+    }
+
+    public function import(Request $request){
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        /**
+         * 判断是否有权限
+         */
+        switch ($request->get('view')) {
+            case 'channel':
+                # 向channel里面导入,忽略源数据的channel id 和 owner 都设置为这个channel 的
+                $channel = ChannelApi::getById($request->get('id'));
+                $owner_id = $channel['studio_id'];
+                if($owner_id !== $user["user_uid"]){
+                    //判断是否为协作
+                    $power = ShareApi::getResPower($user["user_uid"],$request->get('id'));
+                    if($power<30){
+                        return $this->error(__('auth.failed'),[],403);
+                    }
+                }
+                $language = $channel['lang'];
+                break;
+            case 'studio':
+                # 向 studio 里面导入,忽略源数据的 owner 但是要检测 channel id 是否有权限
+                $owner_id = StudioApi::getIdByName($request->get('name'));
+                if(!$owner_id){
+                    return $this->error('no studio name',[],403);
+                }
+
+                break;
+        }
+
+        $message = "";
+        $filename = $request->get('filename');
+        $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
+        $reader->setReadDataOnly(true);
+        $spreadsheet = $reader->load($filename);
+        $activeWorksheet = $spreadsheet->getActiveSheet();
+        $currLine = 2;
+        $countFail = 0;
+
+        do {
+            # code...
+            $id = $activeWorksheet->getCell("A{$currLine}")->getValue();
+            $word = $activeWorksheet->getCell("B{$currLine}")->getValue();
+            $meaning = $activeWorksheet->getCell("C{$currLine}")->getValue();
+            $other_meaning = $activeWorksheet->getCell("D{$currLine}")->getValue();
+            $note = $activeWorksheet->getCell("E{$currLine}")->getValue();
+            $tag = $activeWorksheet->getCell("F{$currLine}")->getValue();
+            $language = $activeWorksheet->getCell("G{$currLine}")->getValue();
+            $channel_id = $activeWorksheet->getCell("H{$currLine}")->getValue();
+            $query = ['word'=>$word,'tag'=>$tag];
+            $channelId = null;
+            switch ($request->get('view')) {
+                case 'channel':
+                    # 向channel里面导入,忽略源数据的channel id 和 owner 都设置为这个channel 的
+                    $query['channal'] = $request->get('id');
+                    $channelId = $request->get('id');
+                    break;
+                case 'studio':
+                    # 向 studio 里面导入,忽略源数据的owner 但是要检测 channel id 是否有权限
+                    $query['owner'] = $owner_id;
+                    if(!empty($channel_id)){
+
+                        //有channel 数据,查看是否在studio中
+                        $channel = ChannelApi::getById($channel_id);
+                        if($channel === false){
+                            $message .= "没有查到版本信息:{$channel_id} - {$word}\n";
+                            $currLine++;
+                            $countFail++;
+                            continue 2;
+                        }
+                        if($owner_id != $channel['studio_id']){
+                            $message .= "版本不在studio中:{$channel_id} - {$word}\n";
+                            $currLine++;
+                            $countFail++;
+                            continue 2;
+                        }
+                        $query['channal'] = $channel_id;
+                        $channelId = $channel_id;
+                    }
+                    # code...
+                    break;
+            }
+
+            if(empty($id) && empty($word)){
+                break;
+            }
+
+            //查询此id是否有旧数据
+            if(!empty($id)){
+                $oldRow = DhammaTerm::find($id);
+                //TODO 有 id 无 word 删除数据
+                if(empty($word)){
+                    //查看权限
+                    if($oldRow->owner !== $user['user_uid']){
+                        if(!empty($oldRow->channal)){
+                            //看是否为协作
+                            $power = ShareApi::getResPower($user['user_uid'],$oldRow->channal);
+                            if($power < 20){
+                                $message .= "无删除权限:{$id} - {$word}\n";
+                                $currLine++;
+                                $countFail++;
+                                continue;
+                            }
+                        }else{
+                            $message .= "无删除权限:{$id} - {$word}\n";
+                            $currLine++;
+                            $countFail++;
+                            continue;
+                        }
+                    }
+                    //删除
+                    $oldRow->delete();
+                    $currLine++;
+                    continue;
+                }
+            }else{
+                $oldRow = null;
+            }
+            //查询是否跟已有数据重复
+            $row = DhammaTerm::where($query)->first();
+            if(!$row){
+                //不重复
+                if(isset($oldRow) && $oldRow){
+                    //找到旧的记录-修改旧数据
+                    $row = $oldRow;
+                }else{
+                    //没找到旧的记录-新建
+                    $row = new DhammaTerm();
+                    $row->id = app('snowflake')->id();
+                    $row->guid = Str::uuid();
+                    $row->word = $word;
+                    $row->create_time = time()*1000;
+                }
+            }else{
+                //重复-如果与旧的id不同,报错
+                if(isset($oldRow) && $oldRow && $row->guid !== $id){
+                    $message .= "重复的数据:{$id} - {$word}\n";
+                    $currLine++;
+                    $countFail++;
+                    continue;
+                }
+            }
+            $row->word_en = Tools::getWordEn($word);
+            $row->meaning = $meaning;
+            $row->other_meaning = $other_meaning;
+            $row->note = $note;
+            $row->tag = $tag;
+            $row->language = $language;
+            $row->channal = $channelId;
+            $row->editor_id = $user['user_id'];
+            $row->owner = $owner_id;
+            $row->modify_time = time()*1000;
+            $row->save();
+
+            $currLine++;
+        } while (true);
+        return $this->ok(["success"=>$currLine-2-$countFail,'fail'=>($countFail)],$message);
+    }
 }

+ 225 - 0
app/Http/Controllers/DictController.php

@@ -0,0 +1,225 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\UserDict;
+use App\Models\DictInfo;
+use Illuminate\Http\Request;
+use App\Tools\CaseMan;
+use Illuminate\Support\Facades\Log;
+use App\Http\Api\DictApi;
+
+require_once __DIR__."/../../../public/app/dict/grm_abbr.php";
+
+
+class DictController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $output = [];
+        $wordDataOutput = [];
+        $dictListOutput = [];
+        $caseListOutput = [];
+		$indexCol = ['word','note','dict_id'];
+        $words = [];
+        $word_base = [];
+        $searched = [];
+        $words[$request->get('word')] = [];
+        $userLang = $request->get('lang',"zh");
+
+        for ($i=0; $i < 2; $i++) {
+            # code...
+            $word_base = [];
+            foreach ($words as $word => $case) {
+                # code...
+                $searched[] = $word;
+                $result = UserDict::select($indexCol)->where('word',$word)->where('source','_PAPER_')->get();
+                $anchor = $word;
+                $wordData=[
+                    'word'=> $word,
+                    'factors'=> "",
+                    'parents'=> "",
+                    'case'=> [],
+                    'grammar'=>$case,
+                    'anchor'=> $anchor,
+                    'dict' => [],
+                ];
+                /**
+                 * 按照语言调整词典顺序
+                 * 算法:准备理想的词典顺序容器。
+                 * 将查询的结果放置在对应的容器中。
+                 * 最后将结果扁平化
+                 * 准备字典容器
+                * $wordDict = [
+                *    "zh"=>[
+                *        "0d79e8e8-1430-4c99-a0f1-b74f2b4b26d8"=>[];
+                *    ]
+                * ]
+                 */
+
+                foreach (DictApi::langOrder($userLang) as  $langId) {
+                    # code...
+                    $dictContainer = [];
+                    foreach (DictApi::dictOrder($langId) as $dictId) {
+                        $dictContainer[$dictId] = [];
+                    }
+                    $wordDict[$langId] = $dictContainer;
+                }
+                $dictList=[
+                    'href'=> '#'.$anchor,
+                    'title'=> "{$word}",
+                    'children' => [],
+                ];
+                foreach ($result as $key => $value) {
+                    # code...
+                    $dictInfo= DictInfo::find($value->dict_id);
+                    $dict_lang = explode('-',$dictInfo->dest_lang);
+                    $anchor = "{$word}-{$dictInfo->shortname}";
+                    $currData = [
+                            'dictname'=> $dictInfo->name,
+                            'shortname'=> $dictInfo->shortname,
+                            'dict_id' => $value->dict_id,
+                            'lang' => $dict_lang[0],
+                            'word'=> $word,
+                            'note'=> $this->GrmAbbr($value->note,0),
+                            'anchor'=> $anchor,
+                    ];
+                    if(isset($wordDict[$dict_lang[0]])){
+                        if(isset($wordDict[$dict_lang[0]][$value->dict_id])){
+                            array_push($wordDict[$dict_lang[0]][$value->dict_id],$currData);
+                        }else{
+                            array_push($wordDict[$dict_lang[0]]["others"],$currData);
+                        }
+                    }else{
+                        array_push($wordDict['others']['others'],$currData);
+                    }
+                }
+                /**
+                 * 把树状数据变为扁平数据
+                 */
+                foreach ($wordDict as $oneLang) {
+                    # code...
+                    foreach ($oneLang as $langId => $dictId) {
+                        # code...
+                        foreach ($dictId as $oneData) {
+                            # code...
+                            $wordData['dict'][] = $oneData;
+                            if(isset($dictList['children']) && count($dictList['children'])>0){
+                                $lastHref = end($dictList['children'])['href'];
+                            }else{
+                                $lastHref = '';
+                            }
+                            $currHref = '#'.$oneData['anchor'];
+                            if($lastHref !== $currHref){
+                                $dictList['children'][] = [
+                                    'href'=> $currHref,
+                                    'title'=> $oneData['shortname'],
+                                ];
+                            }
+                        }
+                    }
+                }
+
+                $wordDataOutput[]=$wordData;
+                $dictListOutput[]=$dictList;
+
+                //TODO 加变格查询
+                $case = new CaseMan();
+                $parent = $case->WordToBase($word);
+                foreach ($parent as $base => $case) {
+                    # code...
+                    if(!in_array($base,$searched)){
+                        $word_base[$base] = $case;
+                    }
+                }
+            }
+            if(count($word_base)===0){
+                break;
+            }else{
+                $words = $word_base;
+            }
+        }
+
+
+
+
+        $output['words'] = $wordDataOutput;
+        $output['dictlist'] = $dictListOutput;
+        $output['caselist'] = $caseListOutput;
+
+        //$result = UserDict::select('word')->where('word','like',"{$word}%")->groupBy('word')->get();
+        //$output['like'] = $result;
+
+        return $this->ok($output);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\UserDict  $userDict
+     * @return \Illuminate\Http\Response
+     */
+    public function show(UserDict $userDict)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\UserDict  $userDict
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, UserDict $userDict)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\UserDict  $userDict
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(UserDict $userDict)
+    {
+        //
+    }
+
+    private function GrmAbbr($input,$dictid){
+        $mean = $input;
+
+        foreach (GRM_ABBR as $key => $value) {
+            # code...
+            if($dictid !== 0){
+                if($value["dictid"]=== $dictid && strpos($input,$value["abbr"]."|") == false){
+                    $mean = str_ireplace($value["abbr"],"|@{$value["abbr"]}-{$value["replace"]}",$mean);
+                }
+            }else{
+                if( strpos($mean,"|@".$value["abbr"]) == false){
+                    $mean = str_ireplace($value["abbr"],"|@{$value["abbr"]}-{$value["replace"]}|",$mean);
+                }
+            }
+
+        }
+        return $mean;
+    }
+}

+ 127 - 0
app/Http/Controllers/DictMeaningController.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\UserDict;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
+
+class DictMeaningController extends Controller
+{
+    protected $langOrder = [
+        "zh-Hans"=>[
+            "zh-Hans","zh-Hant","jp","en","my","vi"
+        ],
+        "zh-Hant"=>[
+            "zh-Hant","zh-Hans","jp","en","my","vi"
+        ],
+        "en"=>[
+            "en","my","zh-Hant","zh-Hans","jp","vi"
+        ],
+        "jp"=>[
+            "jp","en","my","zh-Hant","zh-Hans","vi"
+        ],
+    ];
+
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $words = explode("-",$request->get('word'));
+        $lang = $request->get('lang');
+        $key = "dict_first_mean/";
+        $meaning = [];
+        foreach ($words as $key => $word) {
+            # code...
+            $meaning[] = ['word'=>$word,'meaning'=>$this->get($word,$lang)];
+        }
+
+        return $this->ok($meaning);
+    }
+
+    public function get(string $word,string $lang){
+        $currMeaning = "";
+        if(isset($this->langOrder[$lang])){
+            foreach ($this->langOrder[$lang] as $key => $value) {
+                # 遍历每种语言。找到返回
+                $cacheKey = "dict_first_mean/{$value}/{$word}";
+                $meaning = Cache::get($cacheKey);
+                if(!empty($meaning)){
+                    $currMeaning = $meaning;
+                    break;
+                }
+            }
+        }
+        return $currMeaning;
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function create()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\UserDict  $userDict
+     * @return \Illuminate\Http\Response
+     */
+    public function show(UserDict $userDict)
+    {
+        //
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     *
+     * @param  \App\Models\UserDict  $userDict
+     * @return \Illuminate\Http\Response
+     */
+    public function edit(UserDict $userDict)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\UserDict  $userDict
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, UserDict $userDict)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\UserDict  $userDict
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(UserDict $userDict)
+    {
+        //
+    }
+}

+ 255 - 0
app/Http/Controllers/DiscussionController.php

@@ -0,0 +1,255 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Discussion;
+use App\Models\Wbw;
+use App\Models\WbwBlock;
+use App\Models\PaliSentence;
+use App\Models\Sentence;
+use Illuminate\Http\Request;
+use App\Http\Resources\DiscussionResource;
+use App\Http\Api\MdRender;
+
+class DiscussionController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+		switch ($request->get('view')) {
+            case 'question-by-topic':
+                $topic = Discussion::where('id',$request->get('id'))->select('res_id')->first();
+                if(!$topic){
+			        return $this->error("无效的id");
+                }
+                $table = Discussion::where('res_id',$topic->res_id)->where('parent',null);
+                break;
+            case 'question':
+                $table = Discussion::where('res_id',$request->get('id'))->where('parent',null);
+                break;
+            case 'answer':
+                $table = Discussion::where('parent',$request->get('id'));
+                break;
+            case 'all':
+                $table = Discussion::where('parent',null);
+                break;
+        }
+        if(!empty($search)){
+            $table->where('title', 'like', $search."%");
+        }
+        if(!empty($request->get('order')) && !empty($request->get('dir'))){
+            $table->orderBy($request->get('order'),$request->get('dir'));
+        }else{
+            $table->orderBy('updated_at','desc');
+        }
+        $count = $table->count();
+        if(!empty($request->get('limit'))){
+            $offset = 0;
+            if(!empty($request->get("offset"))){
+                $offset = $request->get("offset");
+            }
+            $table->skip($offset)->take($request->get('limit'));
+        }
+        $result = $table->get();
+        if($result){
+			return $this->ok(["rows"=>DiscussionResource::collection($result),"count"=>$count]);
+		}else{
+			return $this->error("没有查询到数据");
+		}
+    }
+
+    public function discussion_tree(Request $request){
+        $output = [];
+        $sentences = $request->get("data");
+        foreach ($sentences as $key => $sentence) {
+            # 先查句子信息
+            $sentInfo = Sentence::where('book_id',$sentence['book'])
+                                ->where('paragraph',$sentence['paragraph'])
+                                ->where('word_start',$sentence['word_start'])
+                                ->where('word_end',$sentence['word_end'])
+                                ->where('channel_uid',$sentence['channel_id'])
+                                ->first();
+            if($sentInfo){
+                $sentPr = Discussion::where('res_id',$sentInfo['uid'])
+                                ->whereNull('parent')
+                                ->select('title','children_count','editor_uid')
+                                ->orderBy('created_at','desc')->get();
+                $output[] = [
+                    'sentence' => [
+                        'book' => $sentInfo->book_id,
+                        'paragraph' => $sentInfo->paragraph,
+                        'word_start' => $sentInfo->word_start,
+                        'word_end' => $sentInfo->word_end,
+                        'channel_id' => $sentInfo->channel_uid,
+                        'content' => $sentInfo->content,
+                        'pr_count' => count($sentPr),
+                    ],
+                    'pr' => $sentPr,
+                ];
+            }
+
+        }
+        return $this->ok(['rows'=>$output,'count'=>count($output)]);
+    }
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        $user = \App\Http\Api\AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //
+        // validate
+        // read more on validation at http://laravel.com/docs/validation
+
+        if($request->has('parent')){
+            $rules = [];
+            $parentInfo = Discussion::find($request->get('parent'));
+            if(!$parentInfo){
+                return $this->error('no record');
+            }
+        }else{
+            $rules = array(
+            'res_id' => 'required',
+            'res_type' => 'required',
+            'title' => 'required',
+        );
+        }
+
+        $validated = $request->validate($rules);
+
+        $discussion = new Discussion;
+        if($request->has('parent')){
+            $discussion->res_id = $parentInfo->res_id;
+            $discussion->res_type = $parentInfo->res_type;
+        }else{
+            $discussion->res_id = $request->get('res_id');
+            $discussion->res_type = $request->get('res_type');
+        }
+        $discussion->title = $request->get('title',null);
+        $discussion->content = $request->get('content',null);
+        $discussion->content_type = $request->get('content_type',"markdown");
+        $discussion->parent = $request->get('parent',null);
+        $discussion->editor_uid = $user['user_uid'];
+        $discussion->save();
+        //更新parent children_count
+        if($request->has('parent')){
+            $parentInfo->increment('children_count',1);
+            $parentInfo->save();
+        }
+
+        return $this->ok(new DiscussionResource($discussion));
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Discussion  $discussion
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Discussion $discussion)
+    {
+        //
+        return $this->ok(new DiscussionResource($discussion));
+
+    }
+
+        /**
+     * 获取discussion 锚点的数据。以句子为最小单位,逐词解析也要显示单词所在的句子
+     *
+     * @param  string  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function anchor($id)
+    {
+        //
+        $discussion = Discussion::find($id);
+        switch ($discussion->res_type) {
+            case 'wbw':
+                # 从逐词解析表获取逐词解析数据
+                $wbw = Wbw::where('uid',$discussion->res_id)->first();
+                if(!$wbw){
+                    return $this->error('no wbw data');
+                }
+                $wbwBlock = WbwBlock::where('uid',$wbw->block_uid)->first();
+                if(!$wbwBlock){
+                    return $this->error('no wbwBlock data');
+                }
+                $sent = PaliSentence::where('book',$wbw->book_id)
+                                    ->where('paragraph',$wbw->paragraph)
+                                    ->where('word_begin','<=',$wbw->wid)
+                                    ->where('word_end','>=',$wbw->wid)
+                                    ->first();
+                if(!$sent){
+                    return $this->error('no sent data');
+                }
+                $sentId = "{$sent['book']}-{$sent['paragraph']}-{$sent['word_begin']}-{$sent['word_end']}";
+                $channel = $wbwBlock->channel_uid;
+                $content = MdRender::render("{{".$sentId."}}",$channel);
+                return $this->ok($content);
+                break;
+
+            default:
+                # code...
+                break;
+        }
+        return $this->ok();
+
+    }
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Discussion  $discussion
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Discussion $discussion)
+    {
+        //
+        $user = \App\Http\Api\AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //
+        if($discussion->editor !== $user['user_uid']){
+            return $this->error(__('auth.failed'));
+        }
+        $discussion->title = $request->get('title',null);
+        $discussion->content = $request->get('content',null);
+        $discussion->editor_uid = $user['user_uid'];
+        $discussion->save();
+        return $this->ok($discussion);
+
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\Discussion  $discussion
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Discussion $discussion)
+    {
+        //
+        $user = \App\Http\Api\AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //TODO 其他有权限的人也可以删除
+        if($discussion->editor !== $user['user_uid']){
+            return $this->error(__('auth.failed'));
+        }
+        $delete = $discussion->delete();
+        return $this->ok($delete);
+    }
+}

+ 171 - 0
app/Http/Controllers/ExerciseController.php

@@ -0,0 +1,171 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Course;
+use App\Models\CourseMember;
+use App\Models\Article;
+use App\Models\WbwBlock;
+use App\Models\Wbw;
+use App\Models\Discussion;
+use App\Models\Sentence;
+use Illuminate\Http\Request;
+use App\Http\Api\MDRender;
+use App\Http\Api\UserApi;
+
+class ExerciseController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        /**
+         * 列出某个练习所有人的提交情况
+         * 情况包括
+         * 1.作业填充百分比
+         * 2.问题数量
+         */
+        $validated = $request->validate([
+            'course_id' => 'required',
+            'article_id' => 'required',
+            'exercise_id' => 'required',
+        ]);
+        $output = [];
+        //课程信息
+        $course = Course::findOrFail($validated['course_id']);
+
+        //查询练习句子编号
+        $article = Article::where('uid',$validated['article_id'])->value('content');
+
+        $wiki = MdRender::markdown2wiki($article);
+        $xml = MdRender::wiki2xml($wiki);
+        $html = MdRender::xmlQueryId($xml, $validated['exercise_id']);
+        $sentences = MdRender::take_sentence($html);
+
+        //获取课程答案逐词解析列表
+        $answerWbw = [];
+        foreach ($sentences as  $sent) {
+            # code...wbw
+            $sentId = explode('-',$sent);
+            if(count($sentId)<4){
+                break;
+            }
+            $courseWb = WbwBlock::where('book_id',$sentId[0])
+                            ->where('paragraph',$sentId[1])
+                            ->where('channel_uid',$course->channel_id)
+                            ->value('uid');
+            if($courseWb){
+                $wbwId = Wbw::where('block_uid',$courseWb)
+                    ->whereBetween('wid',[$sentId[2],$sentId[3]])
+                    ->select('uid')->get();
+                foreach ($wbwId as $id) {
+                    # code...
+                    $answerWbw[] = $id->uid;
+                }
+            }
+        }
+        $members = CourseMember::where('course_id',$validated['course_id'])
+                            ->where('role','student')
+                            ->select(['user_id','channel_id'])
+                            ->get();
+        foreach ($members as  $member) {
+            # code...
+            $data = [
+                'user' => UserApi::getById($member->user_id),
+                'wbw' => 0,
+                'translation' => 0,
+                'question' => 0,
+                'html' => ""
+            ];
+            if(!empty($member->channel_id)){
+                //
+                foreach ($sentences as  $sent) {
+                    # code...wbw
+                    $sentId = explode('-',$sent);
+                    if(count($sentId)<4){
+                        break;
+                    }
+                    $wb = WbwBlock::where('book_id',$sentId[0])
+                            ->where('paragraph',$sentId[1])
+                            ->where('channel_uid',$member->channel_id)
+                            ->value('uid');
+                    if($wb){
+                        $wbwCount = Wbw::where('block_uid',$wb)
+                            ->whereBetween('wid',[$sentId[2],$sentId[3]])
+                            ->where('status','>',4)
+                            ->count();
+                        $data['wbw'] += $wbwCount;
+                    }
+                    //translation
+                    $sentCount = Sentence::where('book_id',$sentId[0])
+                            ->where('paragraph',$sentId[1])
+                            ->where('word_start',$sentId[2])
+                            ->where('word_end',$sentId[3])
+                            ->where('channel_uid',$member->channel_id)
+                            ->count();
+                    $data['translation'] += $sentCount;
+                    //discussion
+                    //查找答案的wbw 对应的discussion
+                    $discussionCount = Discussion::whereIn('res_id',$answerWbw)
+                            ->where('editor_uid',$member->user_id)
+                            ->whereNull('parent')
+                            ->count();
+                    $data['question'] += $discussionCount;
+
+                    $tpl = MdRender::xml2tpl($html,$member->channel_id);
+                    $data['html'] .= $tpl;
+                }
+            }
+            $output[] = $data;
+        }
+        return $this->ok(["rows"=>$output,"count"=>count($output)]);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Course  $course
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Course $course)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Course  $course
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Course $course)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\Course  $course
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Course $course)
+    {
+        //
+    }
+}

+ 143 - 0
app/Http/Controllers/ExportWbwController.php

@@ -0,0 +1,143 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Wbw;
+use App\Models\WbwBlock;
+use App\Models\PaliSentence;
+use Illuminate\Http\Request;
+
+class ExportWbwController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $sent = explode("\n",$request->get("sent"));
+        $output = [];
+        foreach ($sent as $key => $value) {
+            # code...
+            $sent = [];
+            $value = trim($value);
+            $sentId = explode("-",$value);
+            //先查wbw block 拿到block id
+            $block = WbwBlock::where('book_id',$sentId[0])
+                        ->where('paragraph',$sentId[1])
+                        ->select('uid')
+                        ->where('channel_uid',$request->get("channel"))->first();
+            if(!$block){
+                continue;
+            }
+            $wbwdata = Wbw::where('book_id',$sentId[0])
+                        ->where('paragraph',$sentId[1])
+                        ->where('wid','>=',$sentId[2])
+                        ->where('wid','<=',$sentId[3])
+                        ->where('block_uid',$block->uid)
+                        ->get();
+            $sent['sid']=$value;
+            $sent['text'] = PaliSentence::where('book',$sentId[0])
+                                        ->where('paragraph',$sentId[1])
+                                        ->where('word_begin',$sentId[2])
+                                        ->where('word_end','<=',$sentId[3])
+                                        ->value('html');
+            $sent['data']=[];
+            foreach ($wbwdata as  $wbw) {
+                # code...
+                $data = str_replace("&nbsp;",' ',$wbw->data);
+                $data = str_replace("<br>",' ',$data);
+
+                $xmlString = "<root>" . $data . "</root>";
+                try{
+                    $xmlWord = simplexml_load_string($xmlString);
+                }catch(Exception $e){
+                    continue;
+                }
+
+                $wordsList = $xmlWord->xpath('//word');
+                foreach ($wordsList as $word) {
+                    $pali = $word->real->__toString();
+                    $case = explode("#",$word->case->__toString()) ;
+                    if(isset($case[0])){
+                        $type = $case[0];
+                    }else{
+                        $type = "";
+                    }
+
+                    if(isset($case[1])){
+                        $grammar = $case[1];
+                        $grammar = str_replace("null","",$grammar);
+                    }else{
+                        $grammar = "";
+                    }
+
+                    $style = $word->style->__toString();
+                    $factormeaning = str_replace("
","",$word->om->__toString());
+                    $factormeaning = str_replace("↓↓","",$factormeaning);
+                    if($type !== '.ctl.' && $style !== 'note' && !empty($pali)){
+                        $sent['data'][]=[
+                            'pali'=>$word->real->__toString(),
+                            'mean' => str_replace("
","",$word->mean->__toString()),
+                            'type' => ltrim($type,'.'),
+                            'grammar' => ltrim(str_replace('$.',',',$grammar),'.') ,
+                            'parent' => $word->parent->__toString(),
+                            'factors' => $word->org->__toString(),
+                            'factormeaning' => $factormeaning
+                        ];
+                    }
+
+                }
+            }
+            $output[]=$sent;
+        }
+        return view('export_wbw',['sentences' => $output] );
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Wbw  $wbw
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Wbw $wbw)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Wbw  $wbw
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Wbw $wbw)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\Wbw  $wbw
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Wbw $wbw)
+    {
+        //
+    }
+}

+ 83 - 0
app/Http/Controllers/GrammarGuideController.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\DhammaTerm;
+use App\Http\Api\ChannelApi;
+use Illuminate\Http\Request;
+
+class GrammarGuideController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\DhammaTerm  $dhammaTerm
+     * @return \Illuminate\Http\Response
+     */
+    public function show(string $id)
+    {
+        //
+        $param = explode('_',$id);
+
+        $localTermChannel = ChannelApi::getSysChannel(
+            "_System_Grammar_Term_".strtolower($param[1])."_",
+            "_System_Grammar_Term_en_"
+        );
+        if(!$localTermChannel){
+            return $this->error('no term channel');
+        }
+        $result = DhammaTerm::where('word',$param[0])
+                    ->where('channal',$localTermChannel)->first();
+
+        if($result){
+            return $this->ok("# {$result->meaning}\n {$result->note}");
+        }else{
+            return $this->ok("# {$id}\n no record");
+        }
+
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\DhammaTerm  $dhammaTerm
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, DhammaTerm $dhammaTerm)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\DhammaTerm  $dhammaTerm
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(DhammaTerm $dhammaTerm)
+    {
+        //
+    }
+}

+ 237 - 0
app/Http/Controllers/GroupController.php

@@ -0,0 +1,237 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\GroupInfo;
+use App\Models\GroupMember;
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\DB;
+use App\Http\Api\AuthApi;
+use App\Http\Api\StudioApi;
+use App\Http\Resources\GroupResource;
+
+
+class GroupController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+		$result=false;
+		$indexCol = ['uid','name','description','owner','updated_at','created_at'];
+		switch ($request->get('view')) {
+            case 'studio':
+	            # 获取studio内所有group
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //判断当前用户是否有指定的studio的权限
+                $studioId = StudioApi::getIdByName($request->get('name'));
+                if($user['user_uid'] !== $studioId){
+                    return $this->error(__('auth.failed'));
+                }
+
+                $table = GroupInfo::select($indexCol);
+                if($request->get('view2','my')==='my'){
+                    $table = $table->where('owner', $studioId);
+                }else{
+                    //我参加的group
+                    $groupId = GroupMember::where('user_id',$studioId)
+                                          ->groupBy('group_id')
+                                          ->select('group_id')
+                                          ->get();
+                    $table = $table->whereIn('uid', $groupId);
+                    $table = $table->where('owner','<>', $studioId);
+                }
+				break;
+            case 'key':
+                $table = GroupInfo::select($indexCol)->where('name','like', $request->get('key')."%");
+                break;
+        }
+        if($request->has("search")){
+            $table = $table->where('name', 'like', "%" . $request->get("search")."%");
+        }
+        $count = $table->count();
+        if(isset($_GET["order"]) && isset($_GET["dir"])){
+            $table = $table->orderBy($_GET["order"],$_GET["dir"]);
+        }else{
+            if($request->get('view') === 'studio_list'){
+                $table = $table->orderBy('count','desc');
+            }else{
+                $table = $table->orderBy('updated_at','desc');
+            }
+        }
+
+        if(isset($_GET["limit"])){
+            $offset = 0;
+            if(isset($_GET["offset"])){
+                $offset = $_GET["offset"];
+            }
+            $table = $table->skip($offset)->take($_GET["limit"]);
+        }
+        $result = $table->get();
+		if($result){
+			return $this->ok(["rows"=>GroupResource::collection($result),"count"=>$count]);
+		}else{
+			return $this->error("没有查询到数据");
+		}
+
+    }
+    /**
+     * 获取我的,和协作channel数量
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function showMyNumber(Request $request){
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的studio的权限
+        $studioId = StudioApi::getIdByName($request->get('studio'));
+        if($user['user_uid'] !== $studioId){
+            return $this->error(__('auth.failed'));
+        }
+        //我的
+        $my = GroupMember::where('user_id', $studioId)->where('power',0)->count();
+        //协作
+        $collaboration = GroupMember::where('user_id', $studioId)->where('power','<>',0)->count();
+
+        return $this->ok(['my'=>$my,'collaboration'=>$collaboration]);
+    }
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的studio的权限
+        if($user['user_uid'] !== StudioApi::getIdByName($request->get('studio_name'))){
+            return $this->error(__('auth.failed'));
+        }
+        //查询是否重复
+        if(GroupInfo::where('name',$request->get('name'))->where('owner',$user['user_uid'])->exists()){
+            return $this->error(__('validation.exists',['name']));
+        }
+        $studioId = StudioApi::getIdByName($request->get('studio_name'));
+        $group = new GroupInfo;
+        DB::transaction(function() use($group,$request,$user,$studioId){
+            $group->id = app('snowflake')->id();
+            $group->uid = Str::uuid();
+            $group->name = $request->get('name');
+            $group->owner = $studioId;
+            $group->create_time = time()*1000;
+            $group->modify_time = time()*1000;
+            $group->save();
+
+            $newMember = new GroupMember();
+            $newMember->id=app('snowflake')->id();
+            $newMember->user_id = $studioId;
+            $newMember->group_id = $group->uid;
+            $newMember->power = 0;
+            $newMember->group_name = $request->get('name');
+            $newMember->save();
+        });
+
+        return $this->ok($group);
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  string  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Request  $request,$id)
+    {
+        //
+		$indexCol = ['uid','name','description','owner','updated_at','created_at'];
+
+		$result  = GroupInfo::select($indexCol)->where('uid', $id)->first();
+		if(!$result){
+            return $this->error("没有查询到数据");
+		}
+        if($result->status<30){
+            //私有,判断权限
+            $user = AuthApi::current($request);
+            if(!$user){
+                return $this->error(__('auth.failed'));
+            }
+            //判断当前用户是否有指定的studio的权限
+            if($user['user_uid'] !== $result->owner){
+                //非所有者
+                //TODO 判断是否协作
+                return $this->error(__('auth.failed'));
+            }
+        }
+        return $this->ok(new GroupResource($result));
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\GroupInfo  $group
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, GroupInfo $group)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有修改权限
+        if($user['user_uid'] !== $group->owner){
+            return $this->error(__('auth.failed'));
+        }
+        $group->name = $request->get('name');
+        $group->description = $request->get('description');
+        if($request->has('status')) { $group->status = $request->get('status'); }
+        $group->create_time = time()*1000;
+        $group->modify_time = time()*1000;
+        $group->save();
+        return $this->ok($group);
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\GroupInfo  $group
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Request $request,GroupInfo $group)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //判断当前用户是否有指定的 group 的删除权限
+        if($user['user_uid'] !== $group->owner){
+            return $this->error(__('auth.failed'));
+        }
+        $delete = 0;
+        DB::transaction(function() use($group,$delete){
+            //删除group member
+            $memberDelete = GroupMember::where('group_id',$group->uid)->delete();
+            $delete = $group->delete();
+        });
+
+        return $this->ok($delete);
+    }
+}

+ 164 - 0
app/Http/Controllers/GroupMemberController.php

@@ -0,0 +1,164 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\GroupMember;
+use App\Models\GroupInfo;
+use Illuminate\Http\Request;
+use App\Http\Resources\GroupMemberResource;
+use App\Http\Api\AuthApi;
+
+class GroupMemberController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+		$result=false;
+		$indexCol = ['id','user_id','group_id','power','level','status','updated_at','created_at'];
+		switch ($request->get('view')) {
+            case 'group':
+	            # 获取 group 内所有 成员
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                    //判断当前用户是否有指定的 group 的权限
+                    if(GroupMember::where('group_id', $request->get('id'))
+                            ->where('user_id',$user['user_uid'])
+                            ->exists()){
+                                $table = GroupMember::where('group_id', $request->get('id'));
+                    }else{
+                        return $this->error(__('auth.failed'));
+                    }
+				break;
+        }
+        if(isset($_GET["search"])){
+            $table = $table->where('title', 'like', $_GET["search"]."%");
+        }
+        $count = $table->count();
+        if(isset($_GET["order"]) && isset($_GET["dir"])){
+            $table = $table->orderBy($_GET["order"],$_GET["dir"]);
+        }else{
+            $table = $table->orderBy('updated_at','desc');
+        }
+
+        if(isset($_GET["limit"])){
+            $offset = 0;
+            if(isset($_GET["offset"])){
+                $offset = $_GET["offset"];
+            }
+            $table = $table->skip($offset)->take($_GET["limit"]);
+        }
+        $result = $table->get();
+        foreach ($result as $key => $value) {
+            # 找到当前用户
+            if($user["user_uid"]===$value->user_id){
+                switch ($value->power) {
+                    case 0:
+                        $role = "owner";
+                        break;
+                    case 1:
+                        $role = "manager";
+                        break;
+                    case 2:
+                        $role = "member";
+                        break;
+                    default:
+                        $role="unknown";
+                        break;
+                }
+            }
+        }
+
+		if($result){
+			return $this->ok(["rows"=>GroupMemberResource::collection($result),"count"=>$count,'role'=>$role]);
+		}else{
+			return $this->error("没有查询到数据",[],200);
+		}
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        $validated = $request->validate([
+            'user_id' => 'required',
+            'group_id' => 'required',
+        ]);
+        //查找重复的项目
+        if(GroupMember::where('group_id', $validated['group_id'])->where('user_id',$validated['user_id'])->exists()){
+            return $this->error('member exists');
+        }
+        $newMember = new GroupMember();
+        $newMember->id=app('snowflake')->id();
+        $newMember->user_id = $validated['user_id'];
+        $newMember->group_id = $validated['group_id'];
+        $newMember->power = 2;
+        $newMember->group_name = GroupInfo::find($validated['group_id'])->name;
+        $newMember->save();
+        return $this->ok(new GroupMemberResource($newMember));
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\GroupMember  $groupMember
+     * @return \Illuminate\Http\Response
+     */
+    public function show(GroupMember $groupMember)
+    {
+        //
+
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\GroupMember  $groupMember
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, GroupMember $groupMember)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *@param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\GroupMember  $groupMember
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Request $request, GroupMember $groupMember)
+    {
+        //
+        //查看删除者有没有删除权限
+        //查询删除者的权限
+        $currUser = AuthApi::current($request);
+        if(!$currUser){
+            return $this->error(__('auth.failed'));
+        }
+
+        $power = GroupMember::where('group_id',$groupMember->group_id)
+                        ->where('user_id',$currUser["user_uid"])
+                        ->select('power')->first();
+        if(!$power || $power->power>=2){
+            //普通成员没有删除权限
+            return $this->error(__('auth.failed'));
+        }
+
+        $delete = $groupMember->delete();
+        return $this->ok($delete);
+
+    }
+}

+ 340 - 0
app/Http/Controllers/NissayaEndingController.php

@@ -0,0 +1,340 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\NissayaEnding;
+use App\Models\Relation;
+use App\Models\DhammaTerm;
+use Illuminate\Http\Request;
+use App\Http\Resources\NissayaEndingResource;
+use App\Http\Api\AuthApi;
+use App\Http\Api\ChannelApi;
+use Illuminate\Support\Facades\App;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+use mustache\mustache;
+
+class NissayaEndingController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $table = NissayaEnding::select(['id','ending','lang','relation','case','count','editor_id','updated_at']);
+
+        if(($request->has('case'))){
+            $table->whereIn('case', explode(",",$request->get('case')) );
+        }
+
+        if(($request->has('lang'))){
+            $table->whereIn('lang', explode(",",$request->get('lang')) );
+        }
+
+        if(($request->has('relation'))){
+            $table->where('relation', $request->get('relation'));
+        }
+
+        if(($request->has('search'))){
+            $table->where('ending', 'like', $request->get('search')."%");
+        }
+        if(!empty($request->get('order')) && !empty($request->get('dir'))){
+            $table->orderBy($request->get('order'),$request->get('dir'));
+        }else{
+            $table->orderBy('updated_at','desc');
+        }
+        $count = $table->count();
+        if(!empty($request->get('limit'))){
+            $offset = 0;
+            if(!empty($request->get("offset"))){
+                $offset = $request->get("offset");
+            }
+            $table->skip($offset)->take($request->get('limit'));
+        }
+        $result = $table->get();
+
+		if($result){
+			return $this->ok(["rows"=>NissayaEndingResource::collection($result),"count"=>$count]);
+		}else{
+			return $this->error("没有查询到数据");
+		}
+    }
+
+    public function vocabulary(Request $request){
+        $result = NissayaEnding::select(['ending'])
+                              ->where('lang', $request->get('lang') )
+                              ->groupBy('ending')
+                              ->get();
+        return $this->ok(["rows"=>$result,"count"=>count($result)]);
+    }
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //TODO 判断权限
+        $validated = $request->validate([
+            'ending' => 'required',
+            'lang' => 'required',
+        ]);
+        $new = new NissayaEnding;
+        $new->ending = $validated['ending'];
+        $new->strlen = mb_strlen($validated['ending'],"UTF-8") ;
+        $new->lang = $validated['lang'];
+        $new->relation = $request->get('relation');
+        $new->case = $request->get('case');
+        $new->editor_id = $user['user_uid'];
+        $new->save();
+        return $this->ok(new NissayaEndingResource($new));
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\NissayaEnding  $nissayaEnding
+     * @return \Illuminate\Http\Response
+     */
+    public function show(NissayaEnding $nissayaEnding)
+    {
+        //
+        return $this->ok(new NissayaEndingResource($nissayaEnding));
+
+    }
+
+    public function nissaya_card(Request $request)
+    {
+        //
+        $cardData = [];
+        App::setLocale($request->get('lang'));
+        $localTerm = ChannelApi::getSysChannel(
+                                "_System_Grammar_Term_".strtolower($request->get('lang'))."_",
+                                "_System_Grammar_Term_en_"
+                            );
+        if(!$localTerm){
+            return $this->error('no term channel');
+        }
+        $termTable = DhammaTerm::where('channal',$localTerm);
+        $cardData['ending'] = $request->get('ending');
+        $endingTerm = $termTable->where('word',$request->get('ending'))->first();
+        if($endingTerm){
+            $cardData['ending_tag'] = $endingTerm->tag;
+            $cardData['ending_meaning'] = $endingTerm->meaning;
+            $cardData['ending_note'] = $endingTerm->note;
+        }
+
+        $myEnding = NissayaEnding::where('ending',$request->get('ending'))
+                                 ->groupBy('relation')
+                                 ->select('relation')->get();
+        if(count($myEnding) === 0){
+            if(!isset($cardData['ending_note'])){
+                $cardData['ending_note'] = "no record\n";
+            }
+        }
+
+        $relations = Relation::whereIn('name',$myEnding)->get();
+        if(count($relations) > 0){
+            $cardData['title_case'] = "格位";
+            $cardData['title_content'] = "含义";
+            $cardData['title_local_ending'] = "翻译建议";
+            $cardData['title_local_relation'] = "关系";
+            $cardData['title_relation'] = "关系";
+            foreach ($relations as $key => $relation) {
+                $relationInTerm = DhammaTerm::where('channal',$localTerm)->where('word',$relation['name'])->first();
+                if(empty($relation->case)){
+                    $cardData['row'][] = ["relation"=>$relation->name];
+                    continue;
+                }
+                $case = $relation->case;
+                # 格位
+                $newLine['case'] = __("grammar.".$case);
+                //含义
+                if($relationInTerm){
+                    $newLine['other_meaning'] = $relationInTerm->other_meaning;
+                    $newLine['note'] = $relationInTerm->note;
+                    if(!empty($relationInTerm->note)){
+                        $newLine['summary'] = explode("\n",$relationInTerm->note)[0];
+                    }
+                }
+                //翻译建议
+                $localEnding = '';
+                $localEndingRecord = NissayaEnding::where('relation',$relation['name'])
+                                                ->where('lang',$request->get('lang'));
+                if(!empty($case)){
+                    $localEndingRecord = $localEndingRecord->where('case',$case);
+                }
+                $localLangs = $localEndingRecord->get();
+                foreach ($localLangs as $localLang) {
+                    # code...
+                    $localEnding .= $localLang->ending.",";
+                }
+                $newLine['local_ending'] = $localEnding;
+
+                //本地语言 关系名称
+                if($relationInTerm){
+                    $newLine['local_relation'] =  $relationInTerm->meaning;
+                }
+                //关系名称
+                $newLine['relation'] =  strtoupper($relation['name']);
+                $cardData['row'][] = $newLine;
+            }
+        }
+
+
+        $m = new \Mustache_Engine(array('entity_flags'=>ENT_QUOTES));
+        $tpl = file_get_contents(resource_path("mustache/nissaya_ending_card.tpl"));
+        $md = $m->render($tpl,$cardData);
+        return $this->ok($md);
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\NissayaEnding  $nissayaEnding
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, NissayaEnding $nissayaEnding)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //查询是否重复
+        if(NissayaEnding::where('ending',$request->get('ending'))
+                 ->where('lang',$request->get('lang'))
+                 ->where('relation',$request->get('relation'))
+                 ->where('case',$request->get('case'))
+                 ->exists()){
+            return $this->error(__('validation.exists',['name']));
+        }
+        $nissayaEnding->ending = $request->get('ending');
+        $nissayaEnding->strlen = mb_strlen($request->get('ending'),"UTF-8") ;
+        $nissayaEnding->lang = $request->get('lang');
+        $nissayaEnding->relation = $request->get('relation');
+        $nissayaEnding->case = $request->get('case');
+        $nissayaEnding->editor_id = $user['user_uid'];
+        $nissayaEnding->save();
+        return $this->ok(new NissayaEndingResource($nissayaEnding));
+
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\NissayaEnding  $nissayaEnding
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Request $request,NissayaEnding $nissayaEnding)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //TODO 判断当前用户是否有权限
+        $delete = 0;
+        $delete = $nissayaEnding->delete();
+
+        return $this->ok($delete);
+    }
+
+    public function export(){
+        $spreadsheet = new Spreadsheet();
+        $activeWorksheet = $spreadsheet->getActiveSheet();
+        $activeWorksheet->setCellValue('A1', 'id');
+        $activeWorksheet->setCellValue('B1', 'ending');
+        $activeWorksheet->setCellValue('C1', 'lang');
+        $activeWorksheet->setCellValue('D1', 'relation');
+
+        $nissaya = NissayaEnding::cursor();
+        $currLine = 2;
+        foreach ($nissaya as $key => $row) {
+            # code...
+            $activeWorksheet->setCellValue("A{$currLine}", $row->id);
+            $activeWorksheet->setCellValue("B{$currLine}", $row->ending);
+            $activeWorksheet->setCellValue("C{$currLine}", $row->lang);
+            $activeWorksheet->setCellValue("D{$currLine}", $row->relation);
+            $activeWorksheet->setCellValue("E{$currLine}", $row->case);
+            $currLine++;
+        }
+        $writer = new Xlsx($spreadsheet);
+        header('Content-Type: application/vnd.ms-excel');
+        header('Content-Disposition: attachment; filename="nissaya-ending.xlsx"');
+        $writer->save("php://output");
+    }
+
+    public function import(Request $request){
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+
+        $filename = $request->get('filename');
+        $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
+        $reader->setReadDataOnly(true);
+        $spreadsheet = $reader->load($filename);
+        $activeWorksheet = $spreadsheet->getActiveSheet();
+        $currLine = 2;
+        $countFail = 0;
+        $error = "";
+        do {
+            # code...
+            $id = $activeWorksheet->getCell("A{$currLine}")->getValue();
+            $ending = $activeWorksheet->getCell("B{$currLine}")->getValue();
+            $lang = $activeWorksheet->getCell("C{$currLine}")->getValue();
+            $relation = $activeWorksheet->getCell("D{$currLine}")->getValue();
+            $case = $activeWorksheet->getCell("E{$currLine}")->getValue();
+            if(!empty($ending)){
+                //查询是否有冲突数据
+                //查询此id是否有旧数据
+                if(!empty($id)){
+                    $oldRow = NissayaEnding::find($id);
+                }
+                //查询是否跟已有数据重复
+                $row = NissayaEnding::where(['ending'=>$ending,'relation'=>$relation,'case'=>$case])->first();
+                if(!$row){
+                    //不重复
+                    if(isset($oldRow) && $oldRow){
+                        //有旧的记录-修改旧数据
+                        $row = $oldRow;
+                    }else{
+                        //没找到旧的记录-新建
+                        $row = new NissayaEnding();
+                    }
+                }else{
+                    //重复-如果与旧的id不同旧报错
+                    if(isset($oldRow) && $oldRow && $row->id !== $id){
+                        $error .= "重复的数据:{$id} - {$word}\n";
+                        $currLine++;
+                        $countFail++;
+                        continue;
+                    }
+                }
+                $row->ending = $ending;
+                $row->strlen = mb_strlen($ending,"UTF-8") ;
+                $row->lang = $lang;
+                $row->relation = $relation;
+                $row->case = $case;
+                $row->editor_id = $user['user_uid'];
+                $row->save();
+            }else{
+                break;
+            }
+            $currLine++;
+        } while (true);
+        return $this->ok(["success"=>$currLine-2-$countFail,'fail'=>($countFail)],$error);
+    }
+}

+ 105 - 21
app/Http/Controllers/PaliTextController.php

@@ -4,9 +4,12 @@ namespace App\Http\Controllers;
 
 use Illuminate\Support\Facades\DB;
 use App\Models\PaliText;
+use App\Models\BookTitle;
 use App\Models\Tag;
 use App\Models\TagMap;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
 
 class PaliTextController extends Controller
 {
@@ -18,11 +21,12 @@ class PaliTextController extends Controller
     public function index(Request $request)
     {
         //
+        $all_count = 0;
         switch ($request->get('view')) {
             case 'chapter-tag':
                 $tm = (new TagMap)->getTable();
-                $tg = (new Tag)->getTable();     
-                $pt = (new PaliText)->getTable();  
+                $tg = (new Tag)->getTable();
+                $pt = (new PaliText)->getTable();
                 if($request->get('tags') && $request->get('tags')!==''){
                     $tags = explode(',',$request->get('tags'));
                     foreach ($tags as $tag) {
@@ -32,7 +36,7 @@ class PaliTextController extends Controller
                         }
                     }
                 }
-                
+
                 if(isset($tagNames)){
                     $where1 = " where co = ".count($tagNames);
                     $a = implode(",",array_fill(0, count($tagNames), '?')) ;
@@ -43,7 +47,7 @@ class PaliTextController extends Controller
                     $in1 = " ";
                 }
                 $query = "
-                    select tags.id,tags.name,co as count 
+                    select tags.id,tags.name,co as count
                         from (
                             select tm.tag_id,count(*) as co from (
                                 select anchor_id as id from (
@@ -51,13 +55,13 @@ class PaliTextController extends Controller
                                         from $tm as  tm
                                         left join $tg as t on tm.tag_id = t.id
                                         left join $pt as pc on tm.anchor_id = pc.uid
-                                        where tm.table_name  = 'pali_texts' 
+                                        where tm.table_name  = 'pali_texts'
                                         $in1
                                         group by tm.anchor_id
-                                ) T 
+                                ) T
                                     $where1
-                            ) CID 
-                            left join $tm as tm on tm.anchor_id = CID.id 
+                            ) CID
+                            left join $tm as tm on tm.anchor_id = CID.id
                             group by tm.tag_id
                         ) tid
                         left join $tg on $tg.id = tid.tag_id
@@ -73,8 +77,8 @@ class PaliTextController extends Controller
 
             case 'chapter':
                 $tm = (new TagMap)->getTable();
-                $tg = (new Tag)->getTable();     
-                $pt = (new PaliText)->getTable();  
+                $tg = (new Tag)->getTable();
+                $pt = (new PaliText)->getTable();
                 if($request->get('tags') && $request->get('tags')!==''){
                     $tags = explode(',',$request->get('tags'));
                     foreach ($tags as $tag) {
@@ -84,8 +88,8 @@ class PaliTextController extends Controller
                         }
                     }
                 }
-                
-                
+
+
                 if(isset($tagNames)){
                     $where1 = " where co = ".count($tagNames);
                     $a = implode(",",array_fill(0, count($tagNames), '?')) ;
@@ -100,19 +104,19 @@ class PaliTextController extends Controller
                 $query = "
                         select uid as id,book,paragraph,level,toc as title,chapter_strlen,parent,path from (
                             select anchor_id as cid from (
-                                select tm.anchor_id , count(*) as co 
+                                select tm.anchor_id , count(*) as co
                                     from $tm as  tm
                                     left join $tg as t on tm.tag_id = t.id
                                     where tm.table_name  = 'pali_texts'
                                     $in1
                                     group by tm.anchor_id
-                            ) T 
-                                $where1 
-                        ) CID 
-                        left join $pt as pt on CID.cid = pt.uid 
+                            ) T
+                                $where1
+                        ) CID
+                        left join $pt as pt on CID.cid = pt.uid
                         $where2
                         order by book,paragraph";
-                        
+
                     if(isset($param)){
                         $chapters = DB::select($query,$param);
                     }else{
@@ -121,8 +125,87 @@ class PaliTextController extends Controller
 
                 $all_count = count($chapters);
                 break;
-        }
+            case 'chapter_children':
+                $table = PaliText::where('book',$request->get('book'))
+                                ->where('parent',$request->get('para'))
+                                ->where('level','<',8);
+                $all_count = $table->count();
+                $chapters = $table->orderBy('paragraph')->get();
+                break;
+            case 'paragraph':
+                $result = PaliText::where('book',$request->get('book'))
+                                  ->where('paragraph',$request->get('para'))
+                                  ->first();
+                if($result){
+                    return $this->ok($result);
+                }else{
+                    return $this->error("no data");
+                }
+                break;
+
+            case 'book-toc':
+                /**
+                 * 获取全书目录
+                 * 2023-1-25 改进算法
+                 * 需求:目录显示丛书以及此丛书下面的所有书。比如,选择清净道论的一个章节。显示清净道论两本书的目录
+                 * 算法:
+                 * 1. 查询这个目录的顶级目录
+                 * 2. 查询book-title 获取丛书名
+                 * 3. 根据从书名找到全部的书
+                 * 4. 获取全部书的目录
+                 */
+
+                $path = PaliText::where('book',$request->get('book'))
+                                ->where('paragraph',$request->get('para'))
+                                ->select('path')->first();
+                if(!$path){
+                    return $this->error("no data");
+                }
+                $json = \json_decode($path->path);
+                $root = null;
+                foreach ($json as $key => $value) {
+                    # code...
+                    if( $value->level == 1 ){
+                        $root = $value;
+                        break;
+                    }
+                }
+                if($root===null){
+                    return $this->error("no data");
+                }
+                //查询书起始段落
+                $rootPara = PaliText::where('book',$root->book)
+                                ->where('paragraph',$root->paragraph)
+                                ->first();
+                $book_title = BookTitle::where('book',$rootPara->book)->where('paragraph',$rootPara->paragraph)->value('title');
+                $books = BookTitle::where('title',$book_title)->get();
+                $chapters = [];
+                $chapters[] = ['book'=>0,'paragraph'=>0,'toc'=>$book_title,'level'=>1];
+                foreach ($books as  $book) {
+                    # code...
+                    $rootPara = PaliText::where('book',$book->book)
+                                ->where('paragraph',$book->paragraph)
+                                ->first();
+                    $table = PaliText::where('book',$rootPara->book)
+                                    ->whereBetween('paragraph',[$rootPara->paragraph,($rootPara->paragraph+$rootPara->chapter_len-1)])
+                                    ->where('level','<',8);
+                    $all_count = $table->count();
+                    $curr_chapters = $table->select(['book','paragraph','toc','level'])->orderBy('paragraph')->get();
+                    foreach ($curr_chapters as  $chapter) {
+                        # code...
+                        $chapters[] = ['book'=>$chapter->book,'paragraph'=>$chapter->paragraph,'toc'=>$chapter->toc,'level'=>($chapter->level+1)];
+                    }
+                }
+
+                break;
+            }
         if($chapters){
+            if($request->get('view') !== 'book-toc'){
+                foreach ($chapters as $key => $value) {
+                    $progress_key="/chapter_dynamic/{$value->book}/{$value->paragraph}/global";
+                    $chapters[$key]->progress_line = Cache::get($progress_key);
+                }
+            }
             return $this->ok(["rows"=>$chapters,"count"=>$all_count]);
         }else{
             return $this->error("no data");
@@ -143,12 +226,13 @@ class PaliTextController extends Controller
     /**
      * Display the specified resource.
      *
-     * @param  \App\Models\PaliText  $paliText
+     * @param  \Illuminate\Http\Request  $request
      * @return \Illuminate\Http\Response
      */
-    public function show(PaliText $paliText)
+    public function show(Request $request)
     {
         //
+
     }
 
     /**

+ 136 - 73
app/Http/Controllers/ProgressChapterController.php

@@ -2,6 +2,8 @@
 
 namespace App\Http\Controllers;
 
+require_once __DIR__.'/../../../public/app/ucenter/function.php';
+
 use Illuminate\Support\Str;
 use Illuminate\Support\Facades\DB;
 use App\Models\ProgressChapter;
@@ -12,6 +14,8 @@ use App\Models\PaliText;
 use App\Models\View;
 use App\Models\Like;
 use Illuminate\Http\Request;
+use App\Http\Api\StudioApi;
+use Illuminate\Support\Facades\Cache;
 
 class ProgressChapterController extends Controller
 {
@@ -22,17 +26,12 @@ class ProgressChapterController extends Controller
      */
     public function index(Request $request)
     {
-        
-        if($request->get('progress')){
-            $minProgress = (float)$request->get('progress');
-        }else{
-            $minProgress = 0.8;
-        }
-        if($request->get('offset')){
-            $offset = (int)$request->get('offset');
-        }else{
-            $offset = 0;
-        }
+
+        $minProgress = (float)$request->get('progress',0.8);
+
+        $offset = (int)$request->get('offset',0);
+
+        $limit = (int)$request->get('limit',20);
 
         $channel_id = $request->get('channel');
 
@@ -53,7 +52,13 @@ class ProgressChapterController extends Controller
                 break;
 			case 'studio':
                 #查询该studio的channel
-                $channels = Channel::where('owner_uid',$request->get('id'))->select('uid')->get();
+				$name = $request->get('name');
+				$userinfo = new \UserInfo();
+				$studio = $userinfo->getUserByName($name);
+				if($studio == false){
+					return $this->error('no user');
+				}
+                $channels = Channel::where('owner_uid',$studio['userid'])->select('uid')->get();
                 $aChannel = [];
                 foreach ($channels as $channel) {
                     # code...
@@ -74,11 +79,11 @@ class ProgressChapterController extends Controller
             case 'tag':
                 $tm = (new TagMap)->getTable();
                 $pc =(new ProgressChapter)->getTable();
-                $t = (new Tag)->getTable();            
+                $t = (new Tag)->getTable();
                 $query = "select t.name,count(*) from $tm  tm
                             join tags as t on tm.tag_id = t.id
                             join progress_chapters as pc on tm.anchor_id = pc.uid
-                            where tm.table_name  = 'progress_chapters' and 
+                            where tm.table_name  = 'progress_chapters' and
                             pc.progress > ?
                             group by t.name;";
                 $chapters = DB::select($query, [$minProgress]);
@@ -91,8 +96,8 @@ class ProgressChapterController extends Controller
             case 'chapter-tag':
                 $tm = (new TagMap)->getTable();
                 $pc =(new ProgressChapter)->getTable();
-                $tg = (new Tag)->getTable();     
-                $pt = (new PaliText)->getTable();  
+                $tg = (new Tag)->getTable();
+                $pt = (new PaliText)->getTable();
                 if($request->get('tags') && $request->get('tags')!==''){
                     $tags = explode(',',$request->get('tags'));
                     foreach ($tags as $tag) {
@@ -102,7 +107,7 @@ class ProgressChapterController extends Controller
                         }
                     }
                 }
-                
+
                 $param[] = $minProgress;
                 if(isset($tagNames)){
                     $where1 = " where co = ".count($tagNames);
@@ -114,13 +119,13 @@ class ProgressChapterController extends Controller
                     $in1 = " ";
                 }
                 if(Str::isUuid($channel_id)){
-                    $channel = "and channel_id = '{$channel_id}' "; 
+                    $channel = "and channel_id = '{$channel_id}' ";
                 }else{
                     $channel = "";
                 }
 
                 $query = "
-                    select tags.id,tags.name,co as count 
+                    select tags.id,tags.name,co as count
                         from (
                             select tm.tag_id,count(*) as co from (
                                 select anchor_id as id from (
@@ -128,15 +133,15 @@ class ProgressChapterController extends Controller
                                         from $tm as  tm
                                         left join $tg as t on tm.tag_id = t.id
                                         left join $pc as pc on tm.anchor_id = pc.uid
-                                        where tm.table_name  = 'progress_chapters' and 
-                                              pc.progress  > ? 
+                                        where tm.table_name  = 'progress_chapters' and
+                                              pc.progress  > ?
                                         $in1
                                         $channel
                                         group by tm.anchor_id
-                                ) T 
+                                ) T
                                     $where1
-                            ) CID 
-                            left join $tm as tm on tm.anchor_id = CID.id 
+                            ) CID
+                            left join $tm as tm on tm.anchor_id = CID.id
                             group by tm.tag_id
                         ) tid
                         left join $tg on $tg.id = tid.tag_id
@@ -150,7 +155,7 @@ class ProgressChapterController extends Controller
                     $all_count = count($chapters);
                 break;
             case 'lang':
-                
+
                 $chapters = ProgressChapter::select('lang')
                                             ->selectRaw('count(*) as count')
                                             ->where("progress",">",$minProgress)
@@ -161,12 +166,12 @@ class ProgressChapterController extends Controller
             case 'channel-type':
                 break;
             case 'channel':
-            /*
-            总共有多少channel
-            */
+            /**
+             * 总共有多少channel
+             */
                 $chapters = ProgressChapter::select('channel_id')
                                            ->selectRaw('count(*) as count')
-                                           ->with(['channel' => function($query) {  
+                                           ->with(['channel' => function($query) {
                                                 return $query->select('*');
                                             }])
                                            ->leftJoin('channels','progress_chapters.channel_id', '=', 'channels.uid')
@@ -181,12 +186,16 @@ class ProgressChapterController extends Controller
                 $chapters =  $chapters->groupBy('channel_id')
                                             ->orderBy('count','desc')
                                             ->get();
+                foreach ($chapters as $key => $chapter) {
+                    $chapter->studio = StudioApi::getById($chapter->channel->owner_uid);
+                }
                 $all_count = count($chapters);
                 break;
             case 'chapter_channels':
-            /*
-                某个章节 有多少channel
-            */
+            /**
+             * 某个章节 有多少channel
+             */
+
                 $chapters = ProgressChapter::select('book','para','progress_chapters.uid','progress_chapters.channel_id','progress','updated_at')
                                             ->with(['channel' => function($query) {
                                                 return $query->select('*');
@@ -198,7 +207,7 @@ class ProgressChapterController extends Controller
                 foreach ($chapters as $key => $value) {
                     # code...
                     $chapters[$key]->views = View::where("target_id",$value->uid)->count();
-                    
+
                     $likes = Like::where("target_id",$value->uid)
                                 ->groupBy("type")
                                 ->select("type")
@@ -216,16 +225,20 @@ class ProgressChapterController extends Controller
                         }
                     }
                     $chapters[$key]->likes = $likes;
-                    
+                    $chapters[$key]->studio = StudioApi::getById($value->channel->owner_uid);
+                    $progress_key="/chapter_dynamic/{$value->book}/{$value->para}/ch_{$value->channel_id}";
+                    $chapters[$key]->progress_line = Cache::get($progress_key);
                 }
-                
+
                 $all_count = count($chapters);
                 break;
             case 'chapter':
                 $tm = (new TagMap)->getTable();
                 $pc =(new ProgressChapter)->getTable();
-                $tg = (new Tag)->getTable();     
-                $pt = (new PaliText)->getTable();  
+                $tg = (new Tag)->getTable();
+                $pt = (new PaliText)->getTable();
+
+                //标签过滤
                 if($request->get('tags') && $request->get('tags')!==''){
                     $tags = explode(',',$request->get('tags'));
                     foreach ($tags as $tag) {
@@ -235,8 +248,6 @@ class ProgressChapterController extends Controller
                         }
                     }
                 }
-                
-                
                 if(isset($tagNames)){
                     $where1 = " where co = ".count($tagNames);
                     $a = implode(",",array_fill(0, count($tagNames), '?')) ;
@@ -247,69 +258,68 @@ class ProgressChapterController extends Controller
                     $in1 = " ";
                 }
                 if(Str::isUuid($channel_id)){
-                    $channel = "and channel_id = '{$channel_id}' "; 
+                    $channel = "and channel_id = '{$channel_id}' ";
                 }else{
                     $channel = "";
                 }
-
-				
-
-
+                //完成度过滤
                 $param[] = $minProgress;
 
+                //语言过滤
                 if(!empty($request->get('lang'))){
                     $whereLang = " and pc.lang = ? ";
                     $param[] = $request->get('lang');
                 }else{
                     $whereLang = "   ";
-                }                
-
+                }
+                //channel type过滤
 				if($request->has('channel_type') && !empty($request->get('channel_type'))){
-					$channel_type = "and ch.type = ? "; 
+					$channel_type = "and ch.type = ? ";
 					$param[] = $request->get('channel_type');
 				}else{
 					$channel_type = "";
-				}	
+				}
 
                 $param_count = $param;
                 $param[] = $offset;
 
 
                 $query = "
-                select tpc.pc_uid as uid, tpc.book ,tpc.para,tpc.channel_id,tpc.title,pt.toc,pt.path,tpc.progress,tpc.summary,tpc.created_at,tpc.updated_at 
+                select tpc.pc_uid as uid, tpc.book ,tpc.para,tpc.channel_id,tpc.title,pt.toc,pt.path,tpc.progress,tpc.summary,tpc.created_at,tpc.updated_at
                     from (
 						select pcd.uid as pc_uid, ch.uid as ch_uid, book , para, channel_id,progress, title ,pcd.summary , pcd.created_at,pcd.updated_at
 							from (
 								select uid, book,para,lang,progress,channel_id,title,summary ,created_at ,updated_at
 									from (
-										select anchor_id as cid 
+										select anchor_id as cid
 											from (
-												select tm.anchor_id , count(*) as co 
+												select tm.anchor_id , count(*) as co
 													from $tm as  tm
 													left join $tg as t on tm.tag_id = t.id
-													where tm.table_name  = 'progress_chapters'  
+													where tm.table_name  = 'progress_chapters'
 													$in1
 													group by tm.anchor_id
-											) T 
-											$where1 
-									) CID 
-								left join $pc as pc on CID.cid = pc.uid 
-								where pc.progress > ? 
+											) T
+											$where1
+									) CID
+								left join $pc as pc on CID.cid = pc.uid
+								where pc.progress > ?
 								$channel  $whereLang
 							) pcd
 						left join channels as ch on pcd.channel_id = ch.uid
 						where ch.status >= 30 $channel_type
                         order by pcd.created_at desc
-                        limit 20 offset ?
-                    ) tpc 
+                        limit {$limit} offset ?
+                    ) tpc
                     left join $pt as pt on tpc.book = pt.book and tpc.para = pt.paragraph;";
                 $chapters = DB::select($query,$param);
-                foreach ($chapters as $key => $value) {
+                foreach ($chapters as $key => $chapter) {
                     # code...
-                    $chapters[$key]->channel = Channel::where('uid',$value->channel_id)->select(['name','owner_uid'])->first();
-                    $chapters[$key]->views = View::where("target_id",$value->uid)->count();
-                    $chapters[$key]->likes = Like::where(["type"=>"like","target_id"=>$value->uid])->count();
-                    $chapters[$key]->tags = TagMap::where("anchor_id",$value->uid)
+                    $chapter->channel = Channel::where('uid',$chapter->channel_id)->select(['name','owner_uid'])->first();
+                    $chapter->studio = StudioApi::getById($chapter->channel["owner_uid"]);
+                    $chapter->views = View::where("target_id",$chapter->uid)->count();
+                    $chapter->likes = Like::where(["type"=>"like","target_id"=>$chapter->uid])->count();
+                    $chapter->tags = TagMap::where("anchor_id",$chapter->uid)
                                                 ->leftJoin('tags','tag_maps.tag_id', '=', 'tags.id')
                                                 ->select(['tags.id','tags.name','tags.description'])
                                                 ->get();
@@ -317,23 +327,23 @@ class ProgressChapterController extends Controller
 
                 //计算按照这个条件搜索到的总数
                 $query  = "
-                         select count(*) as count 
+                         select count(*) as count
 							from (
 								select *
 								from (
-									select anchor_id as cid 
+									select anchor_id as cid
 										from (
-											select tm.anchor_id , count(*) as co 
+											select tm.anchor_id , count(*) as co
 												from $tm as  tm
 												left join $tg as t on tm.tag_id = t.id
-												where tm.table_name  = 'progress_chapters'  
+												where tm.table_name  = 'progress_chapters'
 												$in1
 												group by tm.anchor_id
-										) T 
-										$where1 
-								) CID 
-								left join $pc as pc on CID.cid = pc.uid 
-								where pc.progress > ? 
+										) T
+										$where1
+								) CID
+								left join $pc as pc on CID.cid = pc.uid
+								where pc.progress > ?
 								$channel   $whereLang
 							) pcd
 							left join channels as ch on pcd.channel_id = ch.uid
@@ -345,7 +355,60 @@ class ProgressChapterController extends Controller
                 break;
             case 'top':
             break;
+            case 'search':
+                $key = $request->get('key');
+                $table = ProgressChapter::where('title','like',"%{$key}%");
+                //获取记录总条数
+                $all_count = $table->count();
+                //处理排序
+                if($request->has("order") && $request->has("dir")){
+                    $table = $table->orderBy($request->get("order"),$request->get("dir"));
+                }else{
+                    //默认排序
+                    $table = $table->orderBy('updated_at','desc');
+                }
+                //处理分页
+                if($request->has("limit")){
+                    if($request->has("offset")){
+                        $offset = $request->get("offset");
+                    }else{
+                        $offset = 0;
+                    }
+                    $table = $table->skip($offset)->take($request->get("limit"));
+                }
+                //获取数据
+                $chapters = $table->get();
+                //TODO 移到resource
+                foreach ($chapters as $key => $chapter) {
+                    # code...
+                    $chapter->toc = PaliText::where('book',$chapter->book)->where('paragraph',$chapter->para)->value('toc');
+                    $chapter->path = PaliText::where('book',$chapter->book)->where('paragraph',$chapter->para)->value('path');
+                    $chapter->channel = Channel::where('uid',$chapter->channel_id)->select(['name','owner_uid'])->first();
+                    if($chapter->channel){
+                        $chapter->studio = StudioApi::getById($chapter->channel["owner_uid"]);
+                    }else{
+                        $chapter->channel = [
+                            'name'=>"unknown",
+                            'owner_uid'=>"unknown",
+                        ];
+                        $chapter->studio = [
+                            'id'=>"",
+                            'nickName'=>"unknown",
+                            'realName'=>"unknown",
+                            'avatar'=>'',
+                        ];
+                    }
+
+                    $chapter->views = View::where("target_id",$chapter->uid)->count();
+                    $chapter->likes = Like::where(["type"=>"like","target_id"=>$chapter->uid])->count();
+                    $chapter->tags = TagMap::where("anchor_id",$chapter->uid)
+                                                ->leftJoin('tags','tag_maps.tag_id', '=', 'tags.id')
+                                                ->select(['tags.id','tags.name','tags.description'])
+                                                ->get();
+                }
+                break;
         }
+
         if($chapters){
             return $this->ok(["rows"=>$chapters,"count"=>$all_count]);
         }else{

+ 113 - 0
app/Http/Controllers/RelatedParagraphController.php

@@ -0,0 +1,113 @@
+<?php
+/*
+ *查询相关联的书
+ *mula->attakhata->tika
+ *算法:
+ *在原始的html 文件里 如 s0404m1.mul.htm 有 <a name="para2_an8"></a>
+ * 在 so404a.att.htm 里也有 </a><a name="para2_an8"></a>
+ * 这说明这两个段落是关联段落,para2是段落编号 an8是书名只要书名一样,段落编号一样。
+ * 两个就是关联段落
+ *
+ * 表名:cs6_para
+ * 所以数据库结构是
+ * book 书号 1-217
+ * para 段落号
+ * bookid
+ * cspara 上述段落号
+ * book_name 上述书名
+ *
+ * 输入 book para
+ * 查询书名和段落号
+ * 输入这个书名和段落号
+ * 查询有多少段落有一样的书名和段落号
+ * 有些book 里面有两本书。所以又加了一个bookid
+ * 每个bookid代表一本真正的书。所以bookid 要比 book 多
+ * bookid 是为了输出书名用的。不是为了查询相关段落
+ *
+ * 数据要求:
+ * 制作时包含全部段落。做好后把没有相关段落的段落删掉??
+ *
+ */
+namespace App\Http\Controllers;
+
+use App\Models\RelatedParagraph;
+use Illuminate\Http\Request;
+use App\Http\Resources\RelatedParagraphResource;
+
+class RelatedParagraphController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $first = RelatedParagraph::where('book',$request->get('book'))
+                                    ->where('para',$request->get('para'))
+                                    ->where('cs_para','>',0)
+                                    ->first();
+        $result = RelatedParagraph::where('book_name',$first->book_name)
+                                    ->where('cs_para',$first->cs_para)
+                                    ->orderBy('book_id')
+                                    ->orderBy('para')
+                                    ->get();
+        $books=[];
+        foreach ($result as $value) {
+            # 把段落整合成书。有几本书就有几条输出纪录
+            if(!isset($books[$value->book_id])){
+                $books[$value->book_id]['book'] = $value->book;
+                $books[$value->book_id]['book_id'] = $value->book_id;
+                $books[$value->book_id]['cs6_para'] = $value->cs_para;
+            }
+            $books[$value->book_id]['para'][]=$value->para;
+        }
+        return $this->ok(["rows"=>RelatedParagraphResource::collection($books),"count"=>count($books)]);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\RelatedParagraph  $relatedParagraph
+     * @return \Illuminate\Http\Response
+     */
+    public function show(RelatedParagraph $relatedParagraph)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\RelatedParagraph  $relatedParagraph
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, RelatedParagraph $relatedParagraph)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\RelatedParagraph  $relatedParagraph
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(RelatedParagraph $relatedParagraph)
+    {
+        //
+    }
+}

+ 237 - 0
app/Http/Controllers/RelationController.php

@@ -0,0 +1,237 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Relation;
+use Illuminate\Http\Request;
+use App\Http\Resources\RelationResource;
+use App\Http\Api\AuthApi;
+use Illuminate\Support\Facades\App;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+class RelationController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $table = Relation::select(['id','name','case','to','editor_id','updated_at','created_at']);
+        if(($request->has('case'))){
+            $table->whereIn('case', explode(",",$request->get('case')) );
+        }
+        if(($request->has('search'))){
+            $table->where('name', 'like', $request->get('search')."%");
+        }
+        if(!empty($request->get('order')) && !empty($request->get('dir'))){
+            $table->orderBy($request->get('order'),$request->get('dir'));
+        }else{
+            $table->orderBy('updated_at','desc');
+        }
+        $count = $table->count();
+        if(!empty($request->get('limit'))){
+            $offset = 0;
+            if(!empty($request->get("offset"))){
+                $offset = $request->get("offset");
+            }
+            $table->skip($offset)->take($request->get('limit'));
+        }
+        $result = $table->get();
+
+		if($result){
+			return $this->ok(["rows"=>RelationResource::collection($result),"count"=>$count]);
+		}else{
+			return $this->error("没有查询到数据");
+		}
+    }
+
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //TODO 判断权限
+        $validated = $request->validate([
+            'name' => 'required',
+        ]);
+        $case = $request->get('case','');
+        $new = new Relation;
+        $new->name = $validated['name'];
+        if($request->has('case')){
+            $new->case = $request->get('case');
+        }else{
+            $new->case = null;
+        }
+        if($request->has('to')){
+            $new->to = json_encode($request->get('to'),JSON_UNESCAPED_UNICODE);
+        }else{
+            $new->to = null;
+        }
+        $new->editor_id = $user['user_uid'];
+        $new->save();
+        return $this->ok(new RelationResource($new));
+
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Relation  $relation
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Relation $relation)
+    {
+        //
+        return $this->ok(new RelationResource($relation));
+
+    }
+
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Relation  $relation
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Relation $relation)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+
+        $relation->name = $request->get('name');
+        if($request->has('case')){
+            $relation->case = $request->get('case');
+        }else{
+            $relation->case = null;
+        }
+        if($request->has('to')){
+            $relation->to = json_encode($request->get('to'),JSON_UNESCAPED_UNICODE);
+        }else{
+            $relation->to = null;
+        }
+        $relation->editor_id = $user['user_uid'];
+        $relation->save();
+        return $this->ok(new RelationResource($relation));
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Relation  $relation
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Request $request,Relation $relation)
+    {
+        //
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        //TODO 判断当前用户是否有权限
+        $delete = 0;
+        $delete = $relation->delete();
+
+        return $this->ok($delete);
+    }
+
+    public function export(){
+        $spreadsheet = new Spreadsheet();
+        $activeWorksheet = $spreadsheet->getActiveSheet();
+        $activeWorksheet->setCellValue('A1', 'id');
+        $activeWorksheet->setCellValue('B1', 'name');
+        $activeWorksheet->setCellValue('C1', 'case');
+        $activeWorksheet->setCellValue('D1', 'to');
+
+        $nissaya = Relation::cursor();
+        $currLine = 2;
+        foreach ($nissaya as $key => $row) {
+            # code...
+            $activeWorksheet->setCellValue("A{$currLine}", $row->id);
+            $activeWorksheet->setCellValue("B{$currLine}", $row->name);
+            $activeWorksheet->setCellValue("C{$currLine}", $row->case);
+            $activeWorksheet->setCellValue("D{$currLine}", $row->to);
+            $currLine++;
+        }
+        $writer = new Xlsx($spreadsheet);
+        header('Content-Type: application/vnd.ms-excel');
+        header('Content-Disposition: attachment; filename="relation.xlsx"');
+        $writer->save("php://output");
+    }
+
+    public function import(Request $request){
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+
+        $filename = $request->get('filename');
+        $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
+        $reader->setReadDataOnly(true);
+        $spreadsheet = $reader->load($filename);
+        $activeWorksheet = $spreadsheet->getActiveSheet();
+        $currLine = 2;
+        $countFail = 0;
+        $error = "";
+        do {
+            # code...
+            $id = $activeWorksheet->getCell("A{$currLine}")->getValue();
+            $name = $activeWorksheet->getCell("B{$currLine}")->getValue();
+            $case = $activeWorksheet->getCell("C{$currLine}")->getValue();
+            $to = $activeWorksheet->getCell("D{$currLine}")->getValue();
+            if(!empty($name)){
+                                //查询是否有冲突数据
+                //查询此id是否有旧数据
+                if(!empty($id)){
+                    $oldRow = Relation::find($id);
+                }
+                //查询是否跟已有数据重复
+                $row = Relation::where(['name'=>$name,'case'=>$case])->first();
+                if(!$row){
+                    //不重复
+                    if(isset($oldRow) && $oldRow){
+                        //有旧的记录-修改旧数据
+                        $row = $oldRow;
+                    }else{
+                        //没找到旧的记录-新建
+                        $row = new Relation();
+                    }
+                }else{
+                    //重复-如果与旧的id不同旧报错
+                    if(isset($oldRow) && $oldRow && $row->id !== $id){
+                        $error .= "重复的数据:{$id} - {$word}\n";
+                        $currLine++;
+                        $countFail++;
+                        continue;
+                    }
+                }
+                $row->name = $name;
+                $row->case = $case;
+                $row->to = $to;
+                $row->editor_id = $user['user_uid'];
+                $row->save();
+            }else{
+                break;
+            }
+            $currLine++;
+        } while (true);
+        return $this->ok(["success"=>$currLine-2-$countFail,'fail'=>($countFail)],$error);
+    }
+}

+ 341 - 0
app/Http/Controllers/SearchController.php

@@ -0,0 +1,341 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Models\BookTitle;
+use App\Models\FtsText;
+use App\Models\Tag;
+use App\Models\TagMap;
+use App\Models\PaliText;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\DB;
+use App\Http\Resources\SearchResource;
+use App\Http\Resources\SearchBookResource;
+use Illuminate\Support\Facades\Log;
+use App\Tools\Tools;
+use App\Models\WbwTemplate;
+
+
+class SearchController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request){
+        switch ($request->get('view','pali')) {
+            case 'pali':
+                $pageHead = ['M','P','T','V','O'];
+                $key = $request->get('key');
+                if(substr($key,0,4) === 'para' || in_array(substr($key,0,1),$pageHead)){
+                    return $this->page($request);
+                }else{
+                    return $this->pali($request);
+                }
+                break;
+            case 'page':
+                return $this->page($request);
+                break;
+            default:
+                # code...
+                break;
+        }
+    }
+    public function pali(Request $request)
+    {
+        //
+        $searchChapters = [];
+        $searchBooks = [];
+        $searchBookId = [];
+        $queryBookId = '';
+
+        if($request->has('book')){
+            $queryBookId = ' AND pcd_book_id = ' . (int)$request->get('book');
+        }else if($request->has('tags')){
+            //查询搜索范围
+            //查询搜索范围
+            $tagItems = explode(';',$request->get('tags'));
+            $bookId = [];
+            foreach ($tagItems as $tagItem) {
+                # code...
+                $bookId = array_merge($bookId,$this->getBookIdByTags(explode(',',$tagItem)));
+            }
+            $queryBookId = ' AND pcd_book_id in ('.implode(',',$bookId).') ';
+        }
+
+        $key = explode(';',$request->get('key')) ;
+        $param = [];
+        $countParam = [];
+        switch ($request->get('match','case')) {
+            case 'complete':
+            case 'case':
+                # code...
+                $querySelect_rank_base = " ts_rank('{0.1, 0.2, 0.4, 1}',
+                                                full_text_search_weighted,
+                                                websearch_to_tsquery('pali', ?)) ";
+                $querySelect_rank_head = implode('+', array_fill(0, count($key), $querySelect_rank_base));
+                $param = array_merge($param,$key);
+                $querySelect_rank = " {$querySelect_rank_head} AS rank, ";
+                $querySelect_highlight = " ts_headline('pali', content,
+                                            websearch_to_tsquery('pali', ?),
+                                            'StartSel = ~~, StopSel = ~~,MaxWords=3500, MinWords=3500,HighlightAll=TRUE')
+                                            AS highlight,";
+                array_push($param,implode(' ',$key));
+                break;
+            case 'similar':
+                # 形似,去掉变音符号
+                $key = Tools::getWordEn($key[0]);
+                $querySelect_rank = "
+                    ts_rank('{0.1, 0.2, 0.4, 1}',
+                        full_text_search_weighted_unaccent,
+                        websearch_to_tsquery('pali_unaccent', ?))
+                    AS rank, ";
+                    $param[] = $key;
+                $querySelect_highlight = " ts_headline('pali_unaccent', content,
+                        websearch_to_tsquery('pali_unaccent', ?),
+                        'StartSel = ~~, StopSel = ~~,MaxWords=3500, MinWords=3500,HighlightAll=TRUE')
+                        AS highlight,";
+                $param[] = $key;
+                break;
+        }
+        $_queryWhere = $this->getQueryWhere($request->get('key'),$request->get('match','case'));
+        $queryWhere = $_queryWhere['query'];
+        $param = array_merge($param,$_queryWhere['param']);
+
+        $querySelect_2 = "  book,paragraph,content ";
+
+        $queryCount = "SELECT count(*) as co FROM fts_texts WHERE {$queryWhere} {$queryBookId};";
+        $resultCount = DB::select($queryCount, $_queryWhere['param']);
+
+        $limit = $request->get('limit',10);
+        $offset = $request->get('offset',0);
+        switch ( $request->get('orderby',"rank")) {
+            case 'rank':
+                $orderby = " ORDER BY rank DESC ";
+                break;
+            case 'paragraph':
+                $orderby = " ORDER BY book,paragraph ";
+                break;
+            default:
+                $orderby = "";
+                break;
+        };
+        $query = "SELECT
+            {$querySelect_rank}
+            {$querySelect_highlight}
+            {$querySelect_2}
+            FROM fts_texts
+            WHERE
+                {$queryWhere}
+                {$queryBookId}
+                {$orderby}
+            LIMIT ? OFFSET ? ;";
+        $param[] = $limit;
+        $param[] = $offset;
+
+        $result = DB::select($query, $param);
+
+        //待查询单词列表
+        //$caseMan = new CaseMan();
+        //$wordSpell = $caseMan->BaseToWord($key);
+
+        return $this->ok(["rows"=>SearchResource::collection($result),"count"=>$resultCount[0]->co]);
+    }
+    public function page(Request $request)
+    {
+        //
+        $searchChapters = [];
+        $searchBooks = [];
+        $searchBookId = [];
+        $queryBookId = '';
+        $bookId = [];
+        if($request->has('book')){
+            $bookId[] = $request->get('book');
+        }else if($request->has('tags')){
+            //查询搜索范围
+            //查询搜索范围
+            $tagItems = explode(';',$request->get('tags'));
+            foreach ($tagItems as $tagItem) {
+                # code...
+                $bookId = array_merge($bookId,$this->getBookIdByTags(explode(',',$tagItem)));
+            }
+        }
+
+//type='.ctl.' and word like 'P%038'
+        $key = $request->get('key');
+        $searchKey = '';
+        $table = WbwTemplate::where('type','.ctl.');
+        if(is_numeric($key)){
+            $table = $table->where('word','like',$request->get('type')."%0".$key);
+        }else{
+            $table = $table->where('word',$key);
+        }
+
+        if(count($bookId)>0){
+            $table = $table->whereIn('pcd_book_id',$bookId);
+        }
+        $count = $table->count();
+        $table = $table->select(['book','paragraph']);
+        $table->skip($request->get("offset",0))->take($request->get('limit',10));
+        $result = $table->get();
+
+        return $this->ok(["rows"=>SearchResource::collection($result),"count"=>$count]);
+    }
+
+    public function book_list(Request $request){
+        $searchChapters = [];
+        $searchBooks = [];
+        $queryBookId = '';
+
+        if($request->has('tags')){
+            //查询搜索范围
+            $tagItems = explode(';',$request->get('tags'));
+            $bookId = [];
+            foreach ($tagItems as $tagItem) {
+                # code...
+                $bookId = array_merge($bookId,$this->getBookIdByTags(explode(',',$tagItem)));
+            }
+            $queryBookId = ' AND pcd_book_id in ('.implode(',',$bookId).') ';
+        }
+        $key = $request->get('key');
+        switch ($request->get('view','pali')) {
+            case 'pali':
+                # code...
+                $pageHead = ['M','P','T','V','O'];
+                if(substr($key,0,4) === 'para' || in_array(substr($key,0,1),$pageHead)){
+                    $queryWhere = "type='.ctl.' AND word = ?";
+                    $query = "SELECT pcd_book_id, count(*) as co FROM wbw_templates WHERE {$queryWhere} {$queryBookId} GROUP BY pcd_book_id ORDER BY co DESC;";
+                    $result = DB::select($query, [$key]);
+                }else{
+                    $queryWhere = $this->getQueryWhere($key,$request->get('match','case'));
+                    $query = "SELECT pcd_book_id, count(*) as co FROM fts_texts WHERE {$queryWhere['query']} {$queryBookId} GROUP BY pcd_book_id ORDER BY co DESC;";
+                    $result = DB::select($query, $queryWhere['param']);
+                }
+                break;
+            case 'page';
+                $type = $request->get('type','P');
+                $word = "{$type}%0{$key}";
+                $queryWhere = "type='.ctl.' AND word like ?";
+                $query = "SELECT pcd_book_id, count(*) as co FROM wbw_templates WHERE {$queryWhere} {$queryBookId} GROUP BY pcd_book_id ORDER BY co DESC;";
+                $result = DB::select($query, [$word]);
+                break;
+            default:
+                # code...
+                return $this->error('unknown view');
+                break;
+        }
+
+
+        return $this->ok(["rows"=>SearchBookResource::collection($result),"count"=>count($result)]);
+    }
+
+    private function getQueryWhere($key,$match){
+        $key = explode(';',$key) ;
+        $param = [];
+        $queryWhere = '';
+        switch ($match) {
+            case 'complete':
+            case 'case':
+                # code...
+                $queryWhereBase = " full_text_search_weighted @@ websearch_to_tsquery('pali', ?) ";
+                $queryWhereBody = implode(' or ', array_fill(0, count($key), $queryWhereBase));
+                $queryWhere = " ({$queryWhereBody}) ";
+                $param = array_merge($param,$key);
+                break;
+            case 'similar':
+                # 形似,去掉变音符号
+                $queryWhere = " full_text_search_weighted_unaccent @@ websearch_to_tsquery('pali_unaccent', ?) ";
+                $key = Tools::getWordEn($key[0]);
+                $param = [$key];
+                break;
+        };
+        return (['query'=>$queryWhere,'param'=>$param]);
+    }
+
+    private function getBookIdByTags($tags){
+        $searchBookId = [];
+        if(empty($tags)){
+            return $searchBookId;
+        }
+
+        //查询搜索范围
+        $tagIds = Tag::whereIn('name',$tags)->select('id')->get();
+        $paliTextIds = TagMap::where('table_name','pali_texts')->whereIn('tag_id',$tagIds)->select('anchor_id')->get();
+        $paliPara=[];
+        foreach ($paliTextIds as $key => $value) {
+            # code...
+            if(isset($paliPara[$value->anchor_id])){
+                $paliPara[$value->anchor_id]++;
+            }else{
+                $paliPara[$value->anchor_id]=1;
+            }
+        }
+        $paliId=[];
+        foreach ($paliPara as $key => $value) {
+            # code...
+            if($value===count($tags)){
+                $paliId[] = $key;
+            }
+        }
+        $para = PaliText::where('level',1)->whereIn('uid',$paliId)->get();
+
+        if(count($para)>0){
+            foreach ($para as $key => $value) {
+                # code...
+                $book_id = BookTitle::where('book',$value['book'])->where('paragraph',$value['paragraph'])->value('id');
+                if(!empty($book_id)){
+                    $searchBookId[] = $book_id;
+                }
+            }
+        }
+        return $searchBookId;
+
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function show($id)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, $id)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        //
+    }
+}

+ 80 - 25
app/Http/Controllers/SentPrController.php

@@ -6,6 +6,8 @@ namespace App\Http\Controllers;
 use App\Models\SentPr;
 use App\Models\Channel;
 use App\Models\PaliSentence;
+use App\Models\Sentence;
+use App\Http\Resources\SentPrResource;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Log;
@@ -19,11 +21,69 @@ class SentPrController extends Controller
      *
      * @return \Illuminate\Http\Response
      */
-    public function index()
+    public function index(Request $request)
     {
         //
+        switch ($request->get('view')) {
+            case 'sent-info':
+                $table = SentPr::where('book_id',$request->get('book'))
+                                ->where('paragraph',$request->get('para'))
+                                ->where('word_start',$request->get('start'))
+                                ->where('word_end',$request->get('end'))
+                                ->where('channel_uid',$request->get('channel'));
+                $all_count = $table->count();
+                $chapters = $table->orderBy('created_at','desc')->get();
+
+                break;
+        }
+        if($chapters){
+            return $this->ok(["rows"=>SentPrResource::collection($chapters),"count"=>$all_count]);
+        }else{
+            return $this->error("no data");
+        }
     }
 
+    public function pr_tree(Request $request){
+        $output = [];
+        $sentences = $request->get("data");
+        foreach ($sentences as $key => $sentence) {
+            # 先查句子信息
+            $sentInfo = Sentence::where('book_id',$sentence['book'])
+                                ->where('paragraph',$sentence['paragraph'])
+                                ->where('word_start',$sentence['word_start'])
+                                ->where('word_end',$sentence['word_end'])
+                                ->where('channel_uid',$sentence['channel_id'])
+                                ->first();
+            $sentPr = SentPr::where('book_id',$sentence['book'])
+                            ->where('paragraph',$sentence['paragraph'])
+                            ->where('word_start',$sentence['word_start'])
+                            ->where('word_end',$sentence['word_end'])
+                            ->where('channel_uid',$sentence['channel_id'])
+                            ->select('content','editor_uid')
+                            ->orderBy('created_at','desc')->get();
+            if(count($sentPr)>0){
+                if($sentInfo){
+                    $content = $sentInfo->content;
+                }else{
+                    $content = "null";
+                }
+                $output[] = [
+                    'sentence' => [
+                        'book' => $sentence['book'],
+                        'paragraph' => $sentence['paragraph'],
+                        'word_start' => $sentence['word_start'],
+                        'word_end' => $sentence['word_end'],
+                        'channel_id' => $sentence['channel_id'],
+                        'content' => $content,
+                        'pr_count' => count($sentPr),
+                    ],
+                    'pr' => $sentPr,
+                ];
+            }
+
+        }
+        return $this->ok(['rows'=>$output,'count'=>count($output)]);
+    }
     /**
      * Store a newly created resource in storage.
      *
@@ -33,16 +93,16 @@ class SentPrController extends Controller
     public function store(Request $request)
     {
         //
-        if(!isset($_COOKIE['user_uid'])){
-            return $this->error('not login');
-        }else{
-			$user_uid = $_COOKIE['user_uid'];
-		}
+        $user = \App\Http\Api\AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'));
+        }
+        $user_uid = $user['user_uid'];
 
         $data = $request->all();
 
-		
-		#查询是否存在 
+
+		#查询是否存在
 		#同样的内容只能提交一次
 		$exists = SentPr::where('book_id',$data['book'])
 						->where('paragraph',$data['para'])
@@ -67,7 +127,7 @@ class SentPrController extends Controller
 			$new->strlen = mb_strlen($data['text'],"UTF-8");
 			$new->create_time = time()*1000;
 			$new->modify_time = time()*1000;
-			$new->save();			
+			$new->save();
 		}
 
 		$robotMessageOk=false;
@@ -91,7 +151,7 @@ class SentPrController extends Controller
 			book67 par:759-1152
 			*/
 
-			if(($data['book']==65 && $data['para']>=829 && $data['para']<=1306) || ($data['book']== 67 && $data['para'] >= 759 && $data['para'] <= 1152)){
+			//if(($data['book']==65 && $data['para']>=829 && $data['para']<=1306) || ($data['book']== 67 && $data['para'] >= 759 && $data['para'] <= 1152)){
 				$userinfo = new \UserInfo();
 
 				$username = $userinfo->getName($user_uid)['nickname'];
@@ -102,9 +162,8 @@ class SentPrController extends Controller
 										->value('text');
 				$sent_num = "{$data['book']}-{$data['para']}-{$data['begin']}-{$data['end']}";
 				$palitext = mb_substr($palitext,0,20,"UTF-8");
-				$prtext = mb_substr($data['text'],0,20,"UTF-8");
+				$prtext = mb_substr($data['text'],0,140,"UTF-8");
 				$link = "https://www-hk.wikipali.org/app/article/index.php?view=para&book={$data['book']}&par={$data['para']}&begin={$data['begin']}&end={$data['end']}&channel={$data['channel']}&mode=edit";
-				Log::info("palitext:{$palitext} prtext = {$prtext} link={$link}");
 				switch ($data['channel']) {
 					//测试
 					//case '3b0cb0aa-ea88-4ce5-b67d-00a3e76220cc':
@@ -130,15 +189,14 @@ class SentPrController extends Controller
 					default:
 						$strMessage = "";
 						break;
-				}		
+				}
 				$url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=25dbd74f-c89c-40e5-8cbc-48b1ef7710b8";
 				$param = [
 						"msgtype"=>"markdown",
 						"markdown"=> [
-							"content"=> $strMessage, 
-						], 
+							"content"=> $strMessage,
+						],
 					];
-				Log::info("message:{$strMessage}");
 				if(!empty($strMessage)){
 					$response = Http::post($url, $param);
 					if($response->successful()){
@@ -147,14 +205,14 @@ class SentPrController extends Controller
 					}else{
 						$webHookMessage = "消息发送失败";
 						$robotMessageOk = false;
-					}         					
+					}
 				}else{
 					$webHookMessage = "channel不符";
 					$robotMessageOk = false;
 				}
-			}else{
-				$webHookMessage = "不在段落范围内";
-			}
+			//}else{
+			//	$webHookMessage = "不在段落范围内";
+			//}
 		}
 
 		#同时返回此句子pr数量
@@ -169,9 +227,8 @@ class SentPrController extends Controller
 						->where('word_end' , $data['end'])
 						->where('channel_uid' , $data['channel'])
 						->count();
-		Log::info("count:{$count} webhook-ok={$robotMessageOk}");
 		return $this->ok(["new"=>$info,"count"=>$count,"webhook"=>["message"=>$webHookMessage,"ok"=>$robotMessageOk]]);
-        
+
     }
 
     /**
@@ -213,7 +270,7 @@ class SentPrController extends Controller
 			}else{
 				return $this->error('没有更新');
 			}
-			
+
 		}else{
 			return $this->error('not power');
 		}
@@ -229,12 +286,10 @@ class SentPrController extends Controller
     public function destroy($id)
     {
         //
-		Log::info("user_uid=" .$_COOKIE['user_uid']);
 		$old = SentPr::where('id', $id)->first();
 		$result = SentPr::where('id', $id)
 							->where('editor_uid', $_COOKIE["user_uid"])
 							->delete();
-		Log::info("delete=" .$result);
 		if($result>0){
 					#同时返回此句子pr数量
 		$count = SentPr::where('book_id' , $old->book_id)

+ 116 - 0
app/Http/Controllers/SentSimController.php

@@ -0,0 +1,116 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\SentSim;
+use App\Models\PaliSentence;
+use Illuminate\Http\Request;
+use App\Http\Resources\SentSimResource;
+
+class SentSimController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        switch ($request->get('view')) {
+            case 'sentence':
+                $sentId = PaliSentence::where('book',$request->get('book'))
+                                ->where('paragraph',$request->get('paragraph'))
+                                ->where('word_begin',$request->get('start'))
+                                ->where('word_end',$request->get('end'))
+                                ->value('id');
+                if(!$sentId){
+                    return $this->error("no sent");
+                }
+                $table = SentSim::where('sent1',$sentId)
+                                ->where('sim',">",0.7)
+                                ->orderBy('sim','desc');
+                break;
+        }
+        $count = $table->count();
+        if(!empty($request->get('limit'))){
+            $offset = 0;
+            if(!empty($request->get("offset"))){
+                $offset = $request->get("offset");
+            }
+            $table->skip($offset)->take($request->get('limit'));
+        }
+        $result = $table->get();
+        if($result){
+            return $this->ok(["rows"=>SentSimResource::collection($result),"count"=>$count]);
+        }else{
+            return $this->error("no data");
+        }
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function create()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\SentSim  $sentSim
+     * @return \Illuminate\Http\Response
+     */
+    public function show(SentSim $sentSim)
+    {
+        //
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     *
+     * @param  \App\Models\SentSim  $sentSim
+     * @return \Illuminate\Http\Response
+     */
+    public function edit(SentSim $sentSim)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\SentSim  $sentSim
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, SentSim $sentSim)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\SentSim  $sentSim
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(SentSim $sentSim)
+    {
+        //
+    }
+}

+ 228 - 94
app/Http/Controllers/SentenceController.php

@@ -3,7 +3,13 @@
 namespace App\Http\Controllers;
 
 use App\Models\Sentence;
+use App\Models\Channel;
 use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+use App\Http\Resources\SentResource;
+use App\Http\Api\AuthApi;
+use App\Http\Api\ShareApi;
+use App\Http\Api\ChannelApi;
 
 class SentenceController extends Controller
 {
@@ -15,9 +21,32 @@ class SentenceController extends Controller
     public function index(Request $request)
     {
         $result=false;
-		$indexCol = ['id','book_id','paragraph','word_start','word_end','content','channel_uid','updated_at'];
+		$indexCol = ['id','book_id','paragraph','word_start','word_end','content','content_type','channel_uid','editor_uid','acceptor_uid','pr_edit_at','updated_at'];
 
 		switch ($request->get('view')) {
+            case 'public':
+                //获取全部公开的译文
+                //首先获取某个类型的 channel 列表
+                $channels = [];
+                $channel_type = $request->get('channel_type','translation');
+                if($channel_type === "original"){
+                    $pali_channel = ChannelApi::getSysChannel("_System_Pali_VRI_");
+                    if($pali_channel !== false){
+                        $channels[] = $pali_channel;
+                    }
+                }else{
+                    $channelList = Channel::where('type',$channel_type)
+                                              ->where('status',30)
+                                              ->select('uid')->get();
+                    foreach ($channelList as $channel) {
+                        # code...
+                        $channels[] = $channel->uid;
+                    }
+                }
+                $table = Sentence::select($indexCol)
+                                  ->whereIn('channel_uid',$channels)
+                                  ->where('updated_at','>',$request->get('updated_after','1970-1-1'));
+                break;
             case 'fulltext':
                 if(isset($_COOKIE['user_uid'])){
                     $userUid = $_COOKIE['user_uid'];
@@ -29,95 +58,127 @@ class SentenceController extends Controller
                 $table = Sentence::select($indexCol)
 								  ->where('content','like', '%'.$key.'%')
                                   ->where('editor_uid',$userUid);
-				if(!empty($request->get('order')) && !empty($request->get('dir'))){
-					$table->orderBy($request->get('order'),$request->get('dir'));
-				}else{
-					$table->orderBy('updated_at','desc');
-				}
-				$count = $table->count();
-				if(!empty($request->get('limit'))){
-					$offset = 0;
-					if(!empty($request->get("offset"))){
-						$offset = $request->get("offset");
-					}
-					$table->skip($offset)->take($request->get('limit'));
-				}
-				$result = $table->get();
+
                 break;
-			case 'user':
-				# code...
-                $userUid = $_COOKIE['user_uid'];
-                $search = $request->get('search');
-				$table = Sentence::select($indexCol)
-									->where('owner', $userUid);
-				if(!empty($search)){
-					$table->where('word', 'like', $search."%")
-                          ->orWhere('word_en', 'like', $search."%")
-                          ->orWhere('meaning', 'like', "%".$search."%");
-				}
-				if(!empty($request->get('order')) && !empty($request->get('dir'))){
-					$table->orderBy($request->get('order'),$request->get('dir'));
-				}else{
-					$table->orderBy('updated_at','desc');
-				}
-				$count = $table->count();
-				if(!empty($request->get('limit'))){
-					$offset = 0;
-					if(!empty($request->get("offset"))){
-						$offset = $request->get("offset");
-					}
-					$table->skip($offset)->take($request->get('limit'));
-				}
-				$result = $table->get();
-				break;
-			case 'word':
-				$result = Sentence::select($indexCol)
-									->where('word', $request->get("word"))
-									->orderBy('created_at','desc')
-									->get();
-				break;
-            case 'hot-meaning':
-                $key='term/hot_meaning';
-                $value = Cache::get($key, function()use($request) {
-                    $hotMeaning=[];
-                    $words = Sentence::select('word')
-                                ->where('language',$request->get("language"))
-                                ->groupby('word')
-                                ->get();
-                    
-                    foreach ($words as $key => $word) {
-                        # code...
-                        $result = Sentence::select(DB::raw('count(*) as word_count, meaning'))
-                                ->where('language',$request->get("language"))
-                                ->where('word',$word['word'])
-                                ->groupby('meaning')
-                                ->orderby('word_count','desc')
-                                ->first();
-                        if($result){
-                            $hotMeaning[]=[
-                                'word'=>$word['word'],
-                                'meaning'=>$result['meaning'],
-                                'language'=>$request->get("language"),
-                                'owner'=>'',
-                            ];
+            case 'channel':
+                $sent = explode(',',$request->get('sentence')) ;
+                $query = [];
+                foreach ($sent as $value) {
+                    # code...
+                    $ids = explode('-',$value);
+                    $query[] = $ids;
+                }
+                $table = Sentence::select($indexCol)
+                                ->where('channel_uid', $request->get('channel'))
+                                ->whereIns(['book_id','paragraph','word_start','word_end'],$query);
+                break;
+            case 'sent-can-read':
+                /**
+                 * 某句的全部译文
+                 */
+                //获取用户有阅读权限的所有channel
+                //全网公开
+                $type = $request->get('type','translation');
+                $channelTable = Channel::where("type",$type)->select(['uid','name']);
+                $channelPub = $channelTable->where('status',30)->get();
+
+                $user = AuthApi::current($request);
+                if($user){
+                    //自己的
+                    $channelMy = $channelTable->where('owner_uid',$user['user_uid'])->get();
+                    //协作
+                    $channelShare = ShareApi::getResList($user['user_uid'],2);
+                }
+                $channelCanRead = [];
+                foreach ($channelPub as $key => $value) {
+                    $channelCanRead[$value->uid] = [
+                        'id' => $value->uid,
+                        'role' => 'member',
+                        'name' => $value->name,
+                    ];
+                }
+                foreach ($channelShare as $key => $value) {
+                    if($value['type'] === $type){
+                        $channelCanRead[$value['res_id']] = [
+                            'id' => $value['res_id'],
+                            'role' => 'member',
+                            'name' => $value['res_title'],
+                        ];
+                        if($value['power']>=20){
+                            $channelCanRead[$value['res_id']]['role'] = "editor";
                         }
                     }
-                    Cache::put($key, $hotMeaning, 3600);
-                    return $hotMeaning;
-                });
-                return $this->ok(["rows"=>$value,"count"=>count($value)]);
-                break;
+                }
+                foreach ($channelMy as $key => $value) {
+                    $channelCanRead[$value->uid] = [
+                        'id' => $value->uid,
+                        'role' => 'owner',
+                        'name' => $value->name,
+                    ];
+                }
+                $channels = [];
+                foreach ($channelCanRead as $key => $value) {
+                    # code...
+                    $channels[] = $key;
+                }
+                $sent = explode('-',$request->get('sentence')) ;
+                $table = Sentence::select($indexCol)
+                                ->whereIn('channel_uid', $channels)
+                                ->where('book_id',$sent[0])
+                                ->where('paragraph',$sent[1])
+                                ->where('word_start',$sent[2])
+                                ->where('word_end',$sent[3]);
 			default:
 				# code...
 				break;
 		}
+        $count = $table->count();
+        if($request->get('strlen',false)){
+            $totalStrLen = $table->sum('strlen');
+        }
+        $table = $table->orderBy($request->get('order','updated_at'),$request->get('dir','desc'));
+        $table = $table->skip($request->get("offset",0))
+                       ->take($request->get('limit',1000));
+        $result = $table->get();
+
 		if($result){
-			return $this->ok(["rows"=>$result,"count"=>$count]);
+            if($request->get('view') === 'sent-can-read'){
+                $output = ["rows"=>SentResource::collection($result),"count"=>$count];
+            }else{
+                $output = ["rows"=>$result,"count"=>$count];
+            }
+            if(isset($totalStrLen)){
+                $output['total_strlen'] = $totalStrLen;
+            }
+            return $this->ok($output);
+
 		}else{
 			return $this->error("没有查询到数据");
 		}
     }
-
+    /**
+     * 用channel 和句子编号列表查询句子
+     */
+    public function sent_in_channel(Request $request){
+        $sent = $request->get('sentences') ;
+        $query = [];
+        foreach ($sent as $value) {
+            # code...
+            $ids = explode('-',$value);
+            if(count($ids)===4){
+                $query[] = $ids;
+            }
+        }
+        $table = Sentence::select(['id','book_id','paragraph','word_start','word_end','content','channel_uid','updated_at'])
+                        ->where('channel_uid', $request->get('channel'))
+                        ->whereIns(['book_id','paragraph','word_start','word_end'],$query);
+        $result = $table->get();
+        if($result){
+            return $this->ok(["rows"=>$result,"count"=>count($result)]);
+        }else{
+            return $this->error("没有查询到数据");
+        }
+    }
     /**
      * Show the form for creating a new resource.
      *
@@ -129,14 +190,52 @@ class SentenceController extends Controller
     }
 
     /**
-     * Store a newly created resource in storage.
-     *
+     * 新建多个句子
+     * 如果句子存在,修改
      * @param  \Illuminate\Http\Request  $request
      * @return \Illuminate\Http\Response
      */
     public function store(Request $request)
     {
-        //
+        //鉴权
+        $user = AuthApi::current($request);
+        if(!$user ){
+            //未登录用户
+            return $this->error(__('auth.failed'),[],401);
+        }
+        $channel = Channel::where('uid',$request->get('channel'))->first();
+        if(!$channel){
+            return $this->error(__('auth.failed'));
+        }
+        if($channel->owner_uid !== $user["user_uid"]){
+            //判断是否为协作
+            $power = ShareApi::getResPower($user["user_uid"],$channel->uid);
+            if($power<30){
+                return $this->error(__('auth.failed'));
+            }
+        }
+        foreach ($request->get('sentences') as $key => $sent) {
+            # code...
+            $row = Sentence::firstOrNew([
+                "book_id"=>$sent['book_id'],
+                "paragraph"=>$sent['paragraph'],
+                "word_start"=>$sent['word_start'],
+                "word_end"=>$sent['word_end'],
+                "channel_uid"=>$channel->uid,
+            ],[
+                "id"=>app('snowflake')->id(),
+                "uid"=>Str::orderedUuid(),
+            ]);
+            $row->content = $sent['content'];
+            $row->strlen = mb_strlen($sent['content'],"UTF-8");
+            $row->language = $channel->lang;
+            $row->status = $channel->status;
+            $row->editor_uid = $user["user_uid"];
+            $row->create_time = time()*1000;
+            $row->modify_time = time()*1000;
+            $row->save();
+        }
+        return $this->ok(count($request->get('sentences')));
     }
 
     /**
@@ -150,27 +249,62 @@ class SentenceController extends Controller
         //
     }
 
-    /**
-     * Show the form for editing the specified resource.
-     *
-     * @param  \App\Models\Sentence  $sentence
-     * @return \Illuminate\Http\Response
-     */
-    public function edit(Sentence $sentence)
-    {
-        //
-    }
 
     /**
-     * Update the specified resource in storage.
+     * 修改单个句子
      *
      * @param  \Illuminate\Http\Request  $request
-     * @param  \App\Models\Sentence  $sentence
+     * @param  string  $id book_para_start_end_channel
      * @return \Illuminate\Http\Response
      */
-    public function update(Request $request, Sentence $sentence)
+    public function update(Request $request,  $id)
     {
         //
+        $param = \explode('_',$id);
+
+        //鉴权
+        $user = AuthApi::current($request);
+        if(!$user){
+            //未登录鉴权失败
+            return $this->error(__('auth.failed'),[],403);
+        }
+        $channel = Channel::where('uid',$param[4])->first();
+        if(!$channel){
+            return $this->error("not found channel");
+        }
+        if($channel->owner_uid !== $user["user_uid"]){
+            //TODO 判断是否为协作
+            return $this->error(__('auth.failed'),[],403);
+        }
+
+        $sent = Sentence::firstOrNew([
+            "book_id"=>$param[0],
+            "paragraph"=>$param[1],
+            "word_start"=>$param[2],
+            "word_end"=>$param[3],
+            "channel_uid"=>$param[4],
+        ],[
+            "id"=>app('snowflake')->id(),
+            "uid"=>Str::orderedUuid(),
+            "create_time"=>time()*1000,
+        ]);
+        $sent->content = $request->get('content');
+        if($request->has('content_type')){
+            $sent->content_type = $request->get('content_type');
+        }
+        $sent->language = $channel->lang;
+        $sent->status = $channel->status;
+        $sent->editor_uid = $user["user_uid"];
+        $sent->strlen = mb_strlen($request->get('content'),"UTF-8");
+        $sent->modify_time = time()*1000;
+        if($request->has('prEditor')){
+            $sent->acceptor_uid = $user["user_uid"];
+            $sent->pr_edit_at = $request->get('prEditAt');
+            $sent->editor_uid = $request->get('prEditor');
+            $sent->pr_id = $request->get('prId');
+        }
+        $sent->save();
+        return $this->ok(new SentResource($sent));
     }
 
     /**

+ 196 - 0
app/Http/Controllers/ShareController.php

@@ -0,0 +1,196 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Share;
+use App\Models\GroupInfo;
+use App\Models\Article;
+use App\Models\Collection;
+use Illuminate\Http\Request;
+use App\Http\Resources\ShareResource;
+use App\Http\Api\AuthApi;
+use App\Http\Api\ShareApi;
+
+class ShareController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $user = AuthApi::current($request);
+        $result=false;
+        $role = "member";
+		$indexCol = ['id','res_id','res_type','power','updated_at','created_at'];
+		switch ($request->get('view')) {
+            case 'res':
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                $table = Share::where('res_id',$request->get('id'));
+                $power = ShareApi::getResPower($user['user_uid'],$request->get('id'),$table->value('res_type'));
+                switch ($power) {
+                    case 10:
+                        $role = "member";
+                        break;
+                    case 20:
+                        $role = "editor";
+                        break;
+                    case 30:
+                        $role = "owner";
+                        break;
+                }
+                break;
+            case 'group':
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //TODO 判断当前用户是否有指定的 group 的权限
+                if(GroupInfo::where('uid',$request->get('id'))->where('owner',$user['user_uid'])->exists()){
+                    $role = "owner";
+                }
+                $table = Share::where('cooperator_id', $request->get('id'));
+				break;
+        }
+        if(isset($_GET["search"])){
+            //TODO 搜索资源标题
+            $table = $table->where('title', 'like', $_GET["search"]."%");
+        }
+        $count = $table->count();
+        if(isset($_GET["order"]) && isset($_GET["dir"])){
+            $table = $table->orderBy($_GET["order"],$_GET["dir"]);
+        }else{
+            $table = $table->orderBy('updated_at','desc');
+        }
+
+        if(isset($_GET["limit"])){
+            $offset = 0;
+            if(isset($_GET["offset"])){
+                $offset = $_GET["offset"];
+            }
+            $table = $table->skip($offset)->take($_GET["limit"]);
+        }
+        $result = $table->get();
+        //TODO 获取当前用户的身份
+
+
+		if($result){
+			return $this->ok(["rows"=>ShareResource::collection($result),"count"=>$count,'role'=>$role]);
+		}else{
+			return $this->error("没有查询到数据");
+		}
+
+
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        foreach ($request->get('user_id') as $key => $value) {
+            # code...
+            $row = Share::where('cooperator_id',$value)
+                        ->where('res_id',$request->get('res_id'))->first();
+            if(!$row){
+                $row = new Share();
+                $row->id = app('snowflake')->id();
+                $row->cooperator_id = $value;
+                $row->res_id = $request->get('res_id');
+                $row->res_type = $request->get('res_type');
+                $row->create_time = time()*1000;
+            }
+            $c_type=['user'=>0,'group'=>1];
+            $row->cooperator_type = $c_type[$request->get('user_type')];
+            switch ($request->get('role')) {
+                case 'manager':
+                case 'editor':
+                    $row->power = 20;
+                    break;
+                case 'reader':
+                    $row->power = 10;
+                    break;
+            }
+            $row->modify_time = time()*1000;
+            $row->save();
+        }
+        return $this->ok(count($request->get('user_id')));
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Share  $share
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Share $share)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Share  $share
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Share $share)
+    {
+        //查询权限
+        $currUser = AuthApi::current($request);
+        if(!$currUser){
+            return $this->error(__('auth.failed'));
+        }
+
+        $power = ShareApi::getResPower($currUser['user_uid'],$share->res_id,$share->res_type);
+        if(!$power || $power <= 20){
+            //普通成员没有删除权限
+            return $this->error(__('auth.failed'));
+        }
+        switch ($request->get('role')) {
+            case 'manager':
+            case 'editor':
+                $share->power = 20;
+                break;
+            case 'reader':
+                $share->power = 10;
+                break;
+        }
+        $share->modify_time = time()*1000;
+        $share->save();
+        return $this->ok($share);
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Share  $share
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Request $request, Share $share)
+    {
+        //查询权限
+        $currUser = AuthApi::current($request);
+        if(!$currUser){
+            return $this->error(__('auth.failed'));
+        }
+
+        $power = ShareApi::getResPower($currUser['user_uid'],$share->res_id,$share->res_type);
+        if(!$power || $power <= 20){
+            //普通成员没有删除权限
+            return $this->error(__('auth.failed'));
+        }
+
+        $delete = $share->delete();
+        return $this->ok($delete);
+    }
+}

+ 88 - 0
app/Http/Controllers/StudioController.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Http\Api\AuthApi;
+use App\Http\Api\StudioApi;
+use App\Http\Api\ShareApi;
+use App\Models\Channel;
+
+class StudioController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        switch ($request->get('view')) {
+            case 'collaboration-channel':
+                //协作channel 拥有者列表
+                $studioId = StudioApi::getIdByName($request->get('studio_name'));
+                $resList = ShareApi::getResList($studioId,2);
+                $resId=[];
+                foreach ($resList as $res) {
+                    $resId[] = $res['res_id'];
+                }
+                $owners = Channel::whereIn('uid', $resId)
+                                ->where('owner_uid','<>', $studioId)
+                                ->select('owner_uid')
+                                ->groupBy('owner_uid')->get();
+                $output = [];
+                foreach ($owners as $key => $owner) {
+                    # code...
+                    $output[] = StudioApi::getById($owner->owner_uid);
+                }
+                return $this->ok(['rows'=>$output,'count'=>count($output)]);
+                break;
+        }
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function show($id)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, $id)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        //
+    }
+}

+ 74 - 0
app/Http/Controllers/SuggestionController.php

@@ -0,0 +1,74 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\SentPr;
+use App\Http\Api\PaliTextApi;
+use Illuminate\Http\Request;
+
+class SuggestionController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        //
+        switch ($request->get('view')) {
+            case 'chapter':
+                $chapter = PaliTextApi::getChapterStartEnd($request->get('book'),$request->get('para'));
+                if(!$chapter){
+                    return $this->error("no data");
+                }
+
+                break;
+        }
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Article  $article
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Article $article)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Article  $article
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Article $article)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\Article  $article
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Article $article)
+    {
+        //
+    }
+}

+ 94 - 1
app/Http/Controllers/TagController.php

@@ -2,7 +2,11 @@
 
 namespace App\Http\Controllers;
 
+use Illuminate\Support\Str;
+use Illuminate\Support\Facades\DB;
 use App\Models\Tag;
+use App\Models\TagMap;
+use App\Models\ProgressChapter;
 use Illuminate\Http\Request;
 
 class TagController extends Controller
@@ -12,9 +16,98 @@ class TagController extends Controller
      *
      * @return \Illuminate\Http\Response
      */
-    public function index()
+    public function index(Request $request)
     {
         //
+        switch ($request->get('view')) {
+            case "chapter":
+                $progress = $request->get('progress',0.8);
+                $lang = $request->get('lang');
+                $channelType = $request->get('type','translation');
+
+                $tm = (new TagMap)->getTable();
+                $pc =(new ProgressChapter)->getTable();
+                $tg = (new Tag)->getTable();
+
+                //标签过滤
+                if($request->get('tags') && $request->get('tags')!==''){
+                    $tags = explode(',',$request->get('tags'));
+                    foreach ($tags as $tag) {
+                        # code...
+                        if(!empty($tag)){
+                            $tagNames[] = $tag;
+                        }
+                    }
+                }
+                if(isset($tagNames)){
+                    $where1 = " where co = ".count($tagNames);
+                    $a = implode(",",array_fill(0, count($tagNames), '?')) ;
+                    $in1 = "and t.name in ({$a})";
+                    $param = $tagNames;
+                }else{
+                    $where1 = " ";
+                    $in1 = " ";
+                }
+                if(Str::isUuid($request->get('channel'))){
+                    $channel = "and channel_id = '".$request->get('channel')."' ";
+                }else{
+                    $channel = "";
+                }
+                //完成度过滤
+                $param[] = $progress;
+
+                //语言过滤
+                if(!empty($request->get('lang'))){
+                    $whereLang = " and pc.lang = ? ";
+                    $param[] = $request->get('lang');
+                }else{
+                    $whereLang = "   ";
+                }
+                //channel type过滤
+				if($request->has('channel_type') && !empty($request->get('channel_type'))){
+					$channel_type = "and ch.type = ? ";
+					$param[] = $request->get('channel_type');
+				}else{
+					$channel_type = "";
+				}
+
+                $param_count = $param;
+
+                $query = "
+                select TID.tag_id as id,name, TID.count from(
+                    select tm2.tag_id, count(*)      from(
+						select pcd.uid as pc_uid
+							from (
+								select uid, book,para,lang,progress,channel_id,title,summary ,created_at ,updated_at
+									from (
+										select anchor_id as cid
+											from (
+												select tm.anchor_id , count(*) as co
+													from $tm as  tm
+													left join $tg as t on tm.tag_id = t.id
+													where tm.table_name  = 'progress_chapters'
+													$in1
+													group by tm.anchor_id
+											) T
+											$where1
+									) CID
+								left join $pc as pc on CID.cid = pc.uid
+								where pc.progress > ?
+								$channel  $whereLang
+							) pcd
+						left join channels as ch on pcd.channel_id = ch.uid
+						where ch.status >= 30 $channel_type
+                    ) CUID
+                    left join tag_maps tm2 on CUID.pc_uid = tm2.anchor_id
+				group by tm2.tag_id
+				) TID
+				left join tags t2 on t2.id = TID.tag_id
+				order by count desc";
+                $result = DB::select($query,$param);
+                return $this->ok(['rows'=>$result,'count'=>count($result)]);
+                break;
+        }
+
     }
 
     /**

+ 95 - 0
app/Http/Controllers/TermVocabularyController.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\DhammaTerm;
+use Illuminate\Http\Request;
+use App\Http\Resources\TermVocabularyResource;
+use App\Http\Api\ChannelApi;
+
+class TermVocabularyController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $table = DhammaTerm::select(['word','meaning']);
+        switch ($request->get('view')) {
+            case "grammar":
+                $localTerm = ChannelApi::getSysChannel(
+                    "_System_Grammar_Term_".strtolower($request->get('lang'))."_",
+                    "_System_Grammar_Term_en_"
+                );
+                if(!$localTerm){
+                    return $this->error('no term channel');
+                }
+                $table = $table->where('channal',$localTerm);
+                break;
+            case "studio":
+                break;
+            case "user":
+                break;
+            case "community":
+                $localTerm = ChannelApi::getSysChannel(
+                    "_community_term_".strtolower($request->get('lang'))."_",
+                    "_community_term_en_"
+                );
+                if(!$localTerm){
+                    return $this->error('no term channel');
+                }
+                $table = $table->where('channal',$localTerm);
+                break;
+        }
+        $result = $table->get();
+        return $this->ok(["rows"=>TermVocabularyResource::collection($result),'count'=>count($result)]);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\DhammaTerm  $dhammaTerm
+     * @return \Illuminate\Http\Response
+     */
+    public function show(DhammaTerm $dhammaTerm)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\DhammaTerm  $dhammaTerm
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, DhammaTerm $dhammaTerm)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\DhammaTerm  $dhammaTerm
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(DhammaTerm $dhammaTerm)
+    {
+        //
+    }
+}

+ 81 - 0
app/Http/Controllers/UploadController.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+
+
+class UploadController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        $request->validate([
+            'file' => 'required',
+        ]);
+        $file = $request->file('file');
+
+       //Move Uploaded File
+        $filename = $file->store('public/upload');
+
+        $json = array(
+            'name' => $filename,
+            'size' => $file->getSize(),
+            'type' => $file->getMimeType(),
+            'url' => str_replace('public','storage',$filename),
+            'uid' => Str::uuid(),
+            );
+        return $this->ok($json);
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function show($id)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, $id)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        //
+    }
+}

+ 76 - 0
app/Http/Controllers/UserController.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace App\Http\Controllers;
+require_once __DIR__.'/../../../public/app/ucenter/function.php';
+
+use Illuminate\Http\Request;
+use App\Http\Resources\UserResource;
+
+class UserController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        switch ($request->get("view")) {
+            case 'key':
+                $userInfo = new \UserInfo();
+                $users = $userInfo->getUserList($request->get("key"));
+                if($users){
+                    return $this->ok(['rows'=>UserResource::collection($users),'count'=>count($users)]);
+                }else{
+                    return $this->error();
+                }
+                break;
+        }
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function show($id)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, $id)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  int  $id
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy($id)
+    {
+        //
+    }
+}

+ 131 - 82
app/Http/Controllers/UserDictController.php

@@ -6,6 +6,10 @@ use App\Models\UserDict;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Redis;
 use Illuminate\Support\Facades\Log;
+use App\Http\Api;
+use App\Http\Api\AuthApi;
+use App\Http\Api\DictApi;
+use App\Http\Resources\UserDictResource;
 
 class UserDictController extends Controller
 {
@@ -18,43 +22,71 @@ class UserDictController extends Controller
     {
         //
 		$result=false;
-		$indexCol = ['id','word','type','grammar','mean','factors','confidence','updated_at','creator_id'];
+		$indexCol = ['id','word','type','grammar','mean','parent','note','factors','confidence','updated_at','creator_id'];
 		switch ($request->get('view')) {
+            case 'studio':
+				# 获取studio内所有channel
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //判断当前用户是否有指定的studio的权限
+                if($user['user_uid'] !== \App\Http\Api\StudioApi::getIdByName($request->get('name'))){
+                    return $this->error(__('auth.failed'));
+                }
+                $table = UserDict::select($indexCol)
+                            ->where('creator_id', $user["user_id"])
+                            ->where('source', "_USER_WBW_");
+				break;
 			case 'user':
 				# code...
 				$table = UserDict::select($indexCol)
 									->where('creator_id', $_COOKIE["user_id"])
 									->where('source', '<>', "_SYS_USER_WBW_");
-				if(isset($_GET["search"])){
-					$table->where('word', 'like', $_GET["search"]."%");
-				}
-				if(isset($_GET["order"]) && isset($_GET["dir"])){
-					$table->orderBy($_GET["order"],$_GET["dir"]);
-				}else{
-					$table->orderBy('updated_at','desc');
-				}
-				$count = $table->count();
-				if(isset($_GET["limit"])){
-					$offset = 0;
-					if(isset($_GET["offset"])){
-						$offset = $_GET["offset"];
-					}
-					$table->skip($offset)->take($_GET["limit"]);
-				}			
-				$result = $table->get();
 				break;
 			case 'word':
-				$result = UserDict::select($indexCol)
-									->where('word', $_GET["word"])
-									->orderBy('created_at','desc')
-									->get();				
+				$table = UserDict::select($indexCol)
+								 ->where('word', $request->get("word"));
 				break;
+            case 'community':
+                $table = UserDict::select($indexCol)
+                                ->where('word', $request->get("word"))
+                                ->where('source', "_USER_WBW_");;
+                break;
+            case 'compound':
+                $dict_id = DictApi::getSysDict('robot_compound');
+                if($dict_id===false){
+                    $this->error('no robot_compound');
+                }
+                $table = UserDict::where("dict_id",$dict_id)->where("word",$request->get('word'));
+                break;
 			default:
 				# code...
 				break;
 		}
+        if(isset($_GET["search"])){
+            $table->where('word', 'like', $_GET["search"]."%");
+        }
+        if(isset($_GET["order"]) && isset($_GET["dir"])){
+            $table->orderBy($_GET["order"],$_GET["dir"]);
+        }else{
+            if($request->get('view') === "compound"){
+                $table->orderBy('confidence','desc');
+            }else{
+                $table->orderBy('updated_at','desc');
+            }
+        }
+        $count = $table->count();
+        if(isset($_GET["limit"])){
+            $offset = 0;
+            if(isset($_GET["offset"])){
+                $offset = $_GET["offset"];
+            }
+            $table->skip($offset)->take($_GET["limit"]);
+        }
+        $result = $table->get();
 		if($result){
-			return $this->ok(["rows"=>$result,"count"=>$count]);
+			return $this->ok(["rows"=>UserDictResource::collection($result),"count"=>$count]);
 		}else{
 			return $this->error("没有查询到数据");
 		}
@@ -69,47 +101,47 @@ class UserDictController extends Controller
     public function store(Request $request)
     {
         //
-		if(!isset($_COOKIE["user_id"])){
+        $user  = AuthApi::current($request);
+		if(!$user){
 			$this->error("not login");
 		}
-        $snowflake = new SnowFlakeId();
 
-		$_data = json_decode($_POST["data"],true);
+		$_data = json_decode($request->get("data"),true);
 		switch($request->get('view')){
+            case "dict":
+                $src = "_USER_DICT_";
 			case "wbw":
+                $src = "_USER_WBW_";
 				#查询用户重复的数据
 				$iOk = 0;
 				$updateOk=0;
 				foreach ($_data as $key => $word) {
 					# code...
-					$isDoesntExist = UserDict::where('creator_id', $_COOKIE["user_id"])
-										->where('word',$word["word"])
-										->where('type',$word["type"])
-										->where('grammar',$word["grammar"])
-										->where('parent',$word["parent"])
-										->where('mean',$word["mean"])
-										->where('factors',$word["factors"])
-										->where('factormean',$word["factormean"])
-										->where('source','_USER_WBW_')
-										->doesntExist();
-					
+					$table = UserDict::where('creator_id', $user["user_id"])
+										->where('word',$word["word"]);
+                    if(isset($word["type"])){$table = $table->where('type',$word["type"]);}
+                    if(isset($word["grammar"])){$table = $table->where('grammar',$word["grammar"]);}
+                    if(isset($word["parent"])){$table = $table->where('parent',$word["parent"]);}
+                    if(isset($word["mean"])){$table = $table->where('mean',$word["mean"]);}
+                    if(isset($word["factors"])){$table = $table->where('factors',$word["factors"]);}
+					$isDoesntExist = $table->doesntExist();
+
 					if($isDoesntExist){
 						#不存在插入数据
 						$word["id"]=app('snowflake')->id();
-						$word["source"]='_USER_WBW_';
-						$word["create_time"]=mTime();
-						$word["creator_id"]=$_COOKIE["user_id"];
+						$word["source"] = $src;
+						$word["create_time"] = time()*1000;
+						$word["creator_id"]=$user["user_id"];
 						$id = UserDict::insert($word);
 						$updateOk = $this->update_sys_wbw($word);
 						$this->update_redis($word);
 						$iOk++;
 					}
 				}
-				$this->ok([$iOk,$updateOk]);
-				break;
-			case "dict":
+
 				break;
 		}
+        return $this->ok([$iOk,$updateOk]);
     }
 
     /**
@@ -123,9 +155,9 @@ class UserDictController extends Controller
         //
 		$result = UserDict::find($id);
 		if($result){
-			$this->ok($result);
+			return $this->ok($result);
 		}else{
-			$this->error("没有查询到数据");
+			return $this->error("没有查询到数据");
 		}
     }
 
@@ -145,9 +177,9 @@ class UserDictController extends Controller
 		if($result){
 			$updateOk = $this->update_sys_wbw($newData);
 			$this->update_redis($newData);
-			$this->ok([$result,$updateOk]);
+			return $this->ok([$result,$updateOk]);
 		}else{
-		$this->error("没有查询到数据");
+		    return $this->error("没有查询到数据");
 		}
     }
 
@@ -160,47 +192,59 @@ class UserDictController extends Controller
     public function destroy(Request $request,$id)
     {
         //
-		Log::info("userDictController->destroy start");
-		Log::info("userDictController->destroy id= {$id}");
-		$arrId = json_decode($request->get("id"),true) ;
-		$count = 0;
-		foreach ($arrId as $key => $id) {
-			# 找到对应数据
-			$data = UserDict::find($id);
-			//查看是否有权限删除
-			if($data->creator_id == $_COOKIE["user_id"]){
-				$result = UserDict::where('id', $id)
-								->delete();
-				$count += $result;
-				$updateOk = $this->update_sys_wbw($data);
-				$this->update_redis($data);
-			}
-		}
-		return $this->ok([$count,$updateOk]);
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $this->error(__('auth.failed'),[],403);
+        }
+        $user_id = $user['user_id'];
+
+        if($request->has("id")){
+            $arrId = json_decode($request->get("id"),true) ;
+            $count = 0;
+            $updateOk = false;
+            foreach ($arrId as $key => $id) {
+                # 找到对应数据
+                $data = UserDict::find($id);
+                //查看是否有权限删除
+                if($data->creator_id == $user_id){
+                    $result = UserDict::where('id', $id)
+                                    ->delete();
+                    $count += $result;
+                    $updateOk = $this->update_sys_wbw($data);
+                    $this->update_redis($data);
+                }
+            }
+            return $this->ok([$count,$updateOk]);
+        }else{
+            //删除单个单词
+            $userDict = UserDict::find($id);
+            //判断当前用户是否有指定的studio的权限
+            if((int)$user_id !== $userDict->creator_id){
+                return $this->error(__('auth.failed'));
+            }
+            $delete = $userDict->delete();
+            return $this->ok($delete);
+        }
+
     }
 	public function delete(Request $request){
-		Log::info("userDictController->delete start");
 		$arrId = json_decode($request->get("id"),true) ;
-		Log::info("id=".$request->get("id"));
 		$count = 0;
 		$updateOk = false;
 		foreach ($arrId as $key => $id) {
 			$data = UserDict::where('id',$id)->first();
 			if($data){
-				# 找到对应数据 
-				Log::info('creator_id:'.$data->creator_id);
+				# 找到对应数据
 				$param = [
 					"id"=>$id,
 					'creator_id'=>$_COOKIE["user_id"]
 				];
-				Log::info($param);
 				$del = UserDict::where($param)->delete();
 				$count += $del;
 				$updateOk = $this->update_sys_wbw($data);
-				$this->update_redis($data);				
+				$this->update_redis($data);
 			}
 		}
-		Log::info("delete:".$count);
 		return $this->ok(['deleted'=>$count]);
 	}
 
@@ -210,15 +254,22 @@ class UserDictController extends Controller
 	private function update_sys_wbw($data){
 
 		#查询用户重复的数据
+        if(!isset($data["type"])){$data["type"]='';}
+        if(!isset($data["grammar"])){$data["grammar"]='';}
+        if(!isset($data["parent"])){$data["parent"]='';}
+        if(!isset($data["mean"])){$data["mean"]='';}
+        if(!isset($data["factors"])){$data["factors"]='';}
+        if(!isset($data["factormean"])){$data["factormean"]='';}
+
 		$count = UserDict::where('word',$data["word"])
-		->where('type',$data["type"])
-		->where('grammar',$data["grammar"])
-		->where('parent',$data["parent"])
-		->where('mean',$data["mean"])
-		->where('factors',$data["factors"])
-		->where('factormean',$data["factormean"])
-		->where('source','_USER_WBW_')
-		->count();
+                        ->where('type',$data["type"])
+                        ->where('grammar',$data["grammar"])
+                        ->where('parent',$data["parent"])
+                        ->where('mean',$data["mean"])
+                        ->where('factors',$data["factors"])
+                        ->where('factormean',$data["factormean"])
+                        ->where('source','_USER_WBW_')
+                        ->count();
 
 		if($count==0){
             # 没有任何用户有这个数据
@@ -261,7 +312,7 @@ class UserDictController extends Controller
                 #系统字典没有 新增
                 $result = UserDict::insert(
 				[
-                    'id' =>$snowflake->id(),
+                    'id' =>app('snowflake')->id(),
 					'word'=>$data["word"],
 					'type'=>$data["type"],
 					'grammar'=>$data["grammar"],
@@ -318,10 +369,8 @@ class UserDictController extends Controller
 							);
 		}
 		$redisData = json_encode($redisWord,JSON_UNESCAPED_UNICODE);
-		Log::info("word={$word['word']} redis-data={$redisData}");
 		Redis::hSet("dict/user",$word['word'],$redisData);
 		$redisData1 = Redis::hGet("dict/user",$word['word']);
-		Log::info("word={$word['word']} redis-data1={$redisData1}");
 
 		#更新redis结束
 	}

+ 113 - 0
app/Http/Controllers/UserOperationDailyController.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\UserOperationDaily;
+use Illuminate\Http\Request;
+use App\Http\Api\AuthApi;
+use App\Http\Api\UserApi;
+
+class UserOperationDailyController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        switch ($request->get('view')) {
+            case "user-all":
+                $queryUserUuid = UserApi::getIdByName($request->get('studio_name'));
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //TODO 判断是否有查看权限
+                if($queryUserUuid !== $user["user_uid"]){
+                    return $this->error(__('auth.failed'));
+                }
+                $result = UserOperationDaily::where('user_id',$user["user_id"])
+                                  ->select(['date_int','duration','hit'])
+                                  ->orderBy("date_int")
+                                  ->get();
+                break;
+            case "user-year":
+                $queryUserId = UserApi::getIntIdByName($request->get('studio_name'));
+                //TODO 判断是否有查看权限
+                $result = UserOperationDaily::where('user_id',$queryUserId)
+                                  ->select(['date_int','duration'])
+                                  ->orderBy("date_int")
+                                  ->get();
+                break;
+        }
+        return $this->ok(["rows"=>$result,"count"=>count($result)]);
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function create()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\UserOperationDaily  $userOperationDaily
+     * @return \Illuminate\Http\Response
+     */
+    public function show(UserOperationDaily $userOperationDaily)
+    {
+        //
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     *
+     * @param  \App\Models\UserOperationDaily  $userOperationDaily
+     * @return \Illuminate\Http\Response
+     */
+    public function edit(UserOperationDaily $userOperationDaily)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\UserOperationDaily  $userOperationDaily
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, UserOperationDaily $userOperationDaily)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\UserOperationDaily  $userOperationDaily
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(UserOperationDaily $userOperationDaily)
+    {
+        //
+    }
+}

+ 150 - 0
app/Http/Controllers/UserStatisticController.php

@@ -0,0 +1,150 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\UserOperationDaily;
+use App\Models\UserOperationLog;
+use App\Models\Wbw;
+use App\Models\Sentence;
+use App\Models\DhammaTerm;
+use App\Models\UserDict;
+use Illuminate\Http\Request;
+use App\Http\Api\AuthApi;
+use App\Http\Api\UserApi;
+use Illuminate\Support\Facades\Cache;
+
+class UserStatisticController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * Show the form for creating a new resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function create()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\UserOperationDaily  $userOperationDaily
+     * @return \Illuminate\Http\Response
+     */
+    public function show(string $userName)
+    {
+        //
+        $queryUserId = UserApi::getIntIdByName($userName);
+        $queryUserUuid = UserApi::getIdByName($userName);
+        $cacheExpiry = 600;
+        //总经验值
+        $expSum = Cache::remember("user/{$userName}/exp/sum",$cacheExpiry,function() use($queryUserId){
+			return UserOperationDaily::where('user_id',$queryUserId)
+                                     ->sum('duration');
+		});
+
+        //逐词解析
+        $wbwCount = Cache::remember("user/{$userName}/wbw/count",$cacheExpiry,function() use($queryUserId){
+                    return Wbw::where('editor_id',$queryUserId)
+                        ->count();
+                        });
+        //查字典次数
+        $lookupCount = Cache::remember("user/{$userName}/lookup/count",$cacheExpiry,function() use($queryUserId){
+                            return UserOperationLog::where('user_id',$queryUserId)
+                                                    ->where('op_type','dict_lookup')
+                                                    ->count();
+                                });
+        //译文
+        //TODO 判断是否是译文channel
+        $translationCount = Cache::remember("user/{$userName}/translation/count",$cacheExpiry,function() use($queryUserUuid){
+                            return Sentence::where('editor_uid',$queryUserUuid)
+                                           ->count();
+                            });
+        $translationCountPub = Cache::remember("user/{$userName}/translation/count-pub",$cacheExpiry,function() use($queryUserUuid){
+                                    return Sentence::where('editor_uid',$queryUserUuid)
+                                    ->where('status',30)
+                                    ->count();
+                                });
+        //术语
+        $termCount = Cache::remember("user/{$userName}/term/count",$cacheExpiry,function() use($queryUserId){
+                        return DhammaTerm::where('editor_id',$queryUserId)
+                                    ->count();
+                    });
+        $termCountWithNote = Cache::remember("user/{$userName}/term/count-note",$cacheExpiry,function() use($queryUserId){
+                                return DhammaTerm::where('editor_id',$queryUserId)
+                                                    ->where('note',"<>","")
+                                                    ->count();
+                                });
+        //单词本
+        $myDictCount = Cache::remember("user/{$userName}/dict/count",$cacheExpiry,function() use($queryUserId){
+                            return UserDict::where('creator_id',$queryUserId)
+                                        ->count();
+                        });
+
+        return $this->ok([
+            "exp" => ["sum"=>(int)$expSum],
+            "wbw" => ["count"=>(int)$wbwCount],
+            "lookup" => ["count"=>(int)$lookupCount],
+            "translation" =>["count"=>(int)$translationCount,
+                             "count_pub"=>(int)$translationCountPub],
+            "term" => ["count"=>(int)$termCount,
+                      "count_with_note"=>(int)$termCountWithNote],
+            "dict" => ["count"=>(int)$myDictCount],
+        ]);
+    }
+
+    /**
+     * Show the form for editing the specified resource.
+     *
+     * @param  \App\Models\UserOperationDaily  $userOperationDaily
+     * @return \Illuminate\Http\Response
+     */
+    public function edit(UserOperationDaily $userOperationDaily)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\UserOperationDaily  $userOperationDaily
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, UserOperationDaily $userOperationDaily)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\UserOperationDaily  $userOperationDaily
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(UserOperationDaily $userOperationDaily)
+    {
+        //
+    }
+}

+ 57 - 30
app/Http/Controllers/ViewController.php

@@ -8,6 +8,9 @@ use App\Models\PaliText;
 use Illuminate\Http\Request;
 use Illuminate\Support\Str;
 use Illuminate\Support\Facades\Log;
+use App\Http\Resources\ViewResource;
+use App\Http\Api\AuthApi;
+use App\Http\Api\StudioApi;
 
 class ViewController extends Controller
 {
@@ -23,9 +26,6 @@ class ViewController extends Controller
                 break;
             case 'chapter':
                 # code...
-                $channel = $request->get("channel");
-                $book = $request->get("book");
-                $para = $request->get("para");
                 $target_id = ProgressChapter::where("channel_id",$request->get("channel"))
                                             ->where("book",$request->get("book"))
                                             ->where("para",$request->get("para"))
@@ -47,7 +47,7 @@ class ViewController extends Controller
         }else{
             return false;
         }
-        
+
     }
     /**
      * Display a listing of the resource.
@@ -69,25 +69,55 @@ class ViewController extends Controller
                 return $this->ok($count);
                 break;
             case 'user-recent':
-                if(!isset($_COOKIE["user_uid"])){
-                    return $this->error("no login");
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
                 }
-                $user_id = $_COOKIE["user_uid"];
+                $user_id = $user["user_uid"];
 				$views =  View::where("user_id",$user_id)->orderBy('created_at','desc');
-				if($request->has("take")){
-					$views = $views->take($request->get("take"));
-				}else{
-					$views = $views->take(10);
-				}
+				$views = $views->take($request->get("take",10));
                 $items = $views->get();
-                
                 return $this->ok($items);
                 break;
+            case 'user':
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                $user_id = $user["user_uid"];
+                $table =  View::where("user_id",$user_id);
+                break;
+            case 'studio':
+                # 获取studio内所有 数据
+                $user = AuthApi::current($request);
+                if(!$user){
+                    return $this->error(__('auth.failed'));
+                }
+                //判断当前用户是否有指定的studio的权限
+                $studioId = StudioApi::getIdByName($request->get('name'));
+                if($user['user_uid'] !== $studioId){
+                    return $this->error(__('auth.failed'));
+                }
+                $table = View::where('user_id',$studioId);
+                break;
             default:
                 # code...
                 break;
         }
-        
+        //处理搜索
+        if($request->has("search")){
+            $table = $table->where('name', 'like', "%".$request->get("search")."%");
+        }
+        //获取记录总条数
+        $count = $table->count();
+        //处理排序
+        $table = $table->orderBy($request->get("order",'updated_at'),$request->get("dir",'desc'));
+        //处理分页
+        $table = $table->skip($request->get("offset",0))
+                       ->take($request->get("limit",20));
+        //获取数据
+        $result = $table->get();
+        return $this->ok(["rows"=>ViewResource::collection($result),"count"=>$count]);
     }
 
     /**
@@ -111,27 +141,23 @@ class ViewController extends Controller
 */
         //根据target type 获取 target id
         $target_id = $this->getTargetId($request);
+        if(!$target_id){
+            return $this->error('no id');
+        }
         $clientIp = request()->ip();
         $param = [
             'target_id' => $target_id,
             'target_type' => $request->get("target_type"),
         ];
-        if(isset($_COOKIE['user_uid'])){
+        $user = AuthApi::current($request);
+        if($user){
             //已经登陆
-			Log::info('已经登陆');
-            $user_id = $_COOKIE['user_uid'];
+            $user_id = $user['user_uid'];
             $param['user_id'] = $user_id;
-        }else{
-			Log::info('没有登陆');
-            $param['user_ip'] = $clientIp;
         }
-		
+        $param['user_ip'] = $clientIp;
         $new = View::firstOrNew($param);
-		Log::info('获取记录或新建');
-		Log::info(print_r($new, true));
         $new->user_ip = $clientIp;
-		//获取标题 和 meta数据
-		Log::info('获取标题 和 meta数据');
 
 		switch($request->get("target_type")){
 			case "chapter":
@@ -142,20 +168,21 @@ class ViewController extends Controller
 				$new->org_title = PaliText::where("book",$request->get("book"))
 										->where("paragraph",$request->get("para"))
 										->value("toc");
-				Log::info('获取标题 成功');
-
+				//获取标题 成功
 				$new->meta = \json_encode([
 					"book"=>$request->get("book"),
 					"para"=>$request->get("para"),
 					"channel"=>$request->get("channel"),
+                    "mode"=>$request->get("mode","read"),
 				]);
-				Log::info('获取meta数据成功');
-
 				break;
+            default:
+                return $this->error('未知的数据类型');
+                break;
 		}
 		$new->count = $new->count+1;
         $new->save();
-		Log::info('保存成功');
+		//保存成功
 
         $count = View::where("target_id",$new->target_id)->count();
         return $this->ok($count);

+ 80 - 0
app/Http/Controllers/VocabularyController.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Vocabulary;
+use Illuminate\Http\Request;
+use App\Http\Resources\VocabularyResource;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
+
+class VocabularyController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        switch ($request->get("view")) {
+            case 'key':
+                $key = $request->get("key");
+                $result = Cache::remember("/dict_vocabulary/{$key}",10,function() use($key){
+                        return Vocabulary::whereRaw('word like ? or word_en like ?',[$key."%",$key."%"])
+                                    ->whereOr('word_en','like',$key."%")
+                                    ->orderBy('strlen')
+                                    ->orderBy('word')
+                                    ->take(10)->get();
+                });
+                return $this->ok(['rows'=>VocabularyResource::collection($result),'count'=>count($result)]);
+                break;
+        }
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Vocabulary  $vocabulary
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Vocabulary $vocabulary)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Vocabulary  $vocabulary
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Vocabulary $vocabulary)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\Vocabulary  $vocabulary
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Vocabulary $vocabulary)
+    {
+        //
+    }
+}

+ 189 - 0
app/Http/Controllers/WbwController.php

@@ -0,0 +1,189 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Wbw;
+use App\Models\WbwBlock;
+use App\Models\Channel;
+use App\Models\PaliSentence;
+use App\Models\Sentence;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+use App\Tools\Tools;
+use App\Http\Api\AuthApi;
+use App\Http\Api\ShareApi;
+use App\Http\Api\ChannelApi;
+
+class WbwController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     * 新建多个
+     * 如果存在,修改
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        //鉴权
+        $user = AuthApi::current($request);
+        if(!$user ){
+            //未登录用户
+            return $this->error(__('auth.failed'),[],401);
+        }
+        $channel = Channel::where('uid',$request->get('channel_id'))->first();
+        if(!$channel){
+            return $this->error(__('auth.failed'));
+        }
+        if($channel->owner_uid !== $user["user_uid"]){
+            //判断是否为协作
+            $power = ShareApi::getResPower($user["user_uid"],$channel->uid);
+            if($power<30){
+                return $this->error(__('auth.failed'));
+            }
+        }
+        //查看WbwBlock是否已经建立
+        $wbwBlockId = WbwBlock::where('book_id',$request->get('book'))
+                            ->where('paragraph',$request->get('para'))
+                            ->where('channel_uid',$request->get('channel_id'))
+                            ->value('uid');
+        if(!Str::isUuid($wbwBlockId)){
+            $wbwBlock = new WbwBlock();
+            $wbwBlockId = Str::uuid();
+            $wbwBlock->id = app('snowflake')->id();
+			$wbwBlock->uid = $wbwBlockId;
+            $wbwBlock->creator_uid = $user["user_uid"];
+            $wbwBlock->editor_id = $user["user_id"];
+            $wbwBlock->book_id = $request->get('book');
+            $wbwBlock->paragraph = $request->get('para');
+            $wbwBlock->channel_uid = $request->get('channel_id');
+            $wbwBlock->lang = $channel->lang;
+            $wbwBlock->status = $channel->status;
+            $wbwBlock->create_time = time()*1000;
+            $wbwBlock->modify_time = time()*1000;
+            $wbwBlock->save();
+        }
+        $wbw = Wbw::where('block_uid',$wbwBlockId)
+                        ->where('wid',$request->get('sn'))
+                        ->first();
+        if(!$wbw){
+            //建立一个句子的逐词解析数据
+            //找到句子
+            $sent = PaliSentence::where('book',$request->get('book'))
+                                 ->where('paragraph',$request->get('para'))
+                                 ->where('word_begin',"<=",$request->get('sn'))
+                                 ->where('word_end',">=",$request->get('sn'))
+                                 ->first();
+            $channelId = ChannelApi::getSysChannel('_System_Wbw_VRI_');
+            $wbwContent = Sentence::where('book_id',$sent->book)
+							->where('paragraph',$sent->paragraph)
+							->where('word_start',$sent->word_begin)
+							->where('word_end',$sent->word_end)
+							->where('channel_uid',$channelId)
+							->value('content');
+            $words = json_decode($wbwContent);
+            foreach ($words as $word) {
+                # code...
+                $xmlObj = simplexml_load_string("<word></word>");
+                $xmlObj->addChild('id',"{$sent->book}-{$sent->paragraph}-{$word->sn[0]}");
+                $xmlObj->addChild('pali',$word->word->value)->addAttribute('status',0);
+                $xmlObj->addChild('real',$word->real->value)->addAttribute('status',0);
+                $xmlObj->addChild('type',$word->type->value)->addAttribute('status',0);
+                $xmlObj->addChild('gramma',$word->grammar->value)->addAttribute('status',0);
+                $xmlObj->addChild('case',$word->case->value)->addAttribute('status',0);
+                $xmlObj->addChild('style',$word->style->value)->addAttribute('status',0);
+                $xmlObj->addChild('org',$word->factors->value)->addAttribute('status',0);
+                $xmlObj->addChild('om',$word->factorMeaning->value)->addAttribute('status',0);
+                $xmlObj->addChild('status',1);
+                $xml = $xmlObj->asXml();
+                $xml = str_replace('<?xml version="1.0"?>','',$xml);
+
+                $newWbw = new Wbw();
+                $newWbw->id = app('snowflake')->id();
+                $newWbw->uid = Str::uuid();
+                $newWbw->creator_uid = $channel->owner_uid;
+                $newWbw->editor_id = $user["user_id"];
+                $newWbw->book_id = $request->get('book');
+                $newWbw->paragraph = $request->get('para');
+                $newWbw->wid = $word->sn[0];
+                $newWbw->block_uid = $wbwBlockId;
+                $newWbw->data = $xml;
+                $newWbw->word = $word->real->value;
+                $newWbw->status = 0;
+                $newWbw->create_time = time()*1000;
+                $newWbw->modify_time = time()*1000;
+                $newWbw->save();
+                if($word->sn[0] === $request->get('sn')){
+                    $wbw = $newWbw;
+                }
+            }
+        }
+
+        $count=0;
+        foreach ($request->get('data') as $row) {
+            $wbw = Wbw::where('block_uid',$wbwBlockId)
+                        ->where('wid',$row['sn'])
+                        ->first();
+            if($wbw){
+                $wbwData = "";
+                foreach ($row['words'] as $word) {
+                    $xml = Tools::JsonToXml($word);
+                    $xml = str_replace('<?xml version="1.0"?>','',$xml);
+                    $wbwData .= $xml;
+                }
+                $wbw->data = $wbwData;
+                $wbw->status = 5;
+                $wbw->save();
+                $count++;
+            }
+        }
+
+        return $this->ok(['rows'=>[],"count"=>$count]);
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Wbw  $wbw
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Wbw $wbw)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\Wbw  $wbw
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, Wbw $wbw)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\Wbw  $wbw
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(Wbw $wbw)
+    {
+        //
+    }
+}

+ 463 - 0
app/Http/Controllers/WbwLookupController.php

@@ -0,0 +1,463 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\UserDict;
+use App\Models\DictInfo;
+use App\Models\WbwTemplate;
+use App\Models\Channel;
+use App\Models\WbwAnalysis;
+use Illuminate\Http\Request;
+use App\Tools\CaseMan;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Cache;
+use App\Http\Api\DictApi;
+
+
+
+class WbwLookupController extends Controller
+{
+	private $dictList = [
+		'85dcc61c-c9e1-4ae0-9b44-cd6d9d9f0d01',//社区汇总
+		'4d3a0d92-0adc-4052-80f5-512a2603d0e8',// system irregular
+		'8359757e-9575-455b-a772-cc6f036caea0',// system sandhi
+		'61f23efb-b526-4a8e-999e-076965034e60',// pali myanmar grammar
+		'eae9fd6f-7bac-4940-b80d-ad6cd6f433bf',// Concise P-E Dict
+		'2f93d0fe-3d68-46ee-a80b-11fa445a29c6',// unity
+		'beb45062-7c20-4047-bcd4-1f636ba443d1',// U Hau Sein
+		'8833de18-0978-434c-b281-a2e7387f69be',// 巴汉增订
+		'3acf0c0f-59a7-4d25-a3d9-bf394a266ebd',// 汉译パーリ语辞典-黃秉榮
+        '9ce6a53b-e28f-4fb7-b69d-b35fd5d76a24',//缅英字典
+	];
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    private function initSysDict()
+    {
+        // system regular
+        $this->dictList[] = DictApi::getSysDict('system_regular');
+        $this->dictList[] = DictApi::getSysDict('robot_compound');
+        $this->dictList[] = DictApi::getSysDict('community');
+        $this->dictList[] = DictApi::getSysDict('community_extract');
+    }
+
+    /**
+     * Display a listing of the resource.
+     * @param  \Illuminate\Http\Request  $request
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+
+        //
+		$startAt = microtime(true)*1000;
+
+        $this->initSysDict();
+
+		$words = \explode(',',$request->get("word"));
+        $bases = \explode(',',$request->get("base"));
+        # 查询深度
+		$deep = $request->get("deep",2);
+        $result = $this->lookup($words,$bases,$deep);
+        $endAt = microtime(true)*1000;
+
+
+		return $this->ok(["rows"=>$result,
+                          "count"=>count($result),
+                          "time"=>(int)($endAt-$startAt)]);
+    }
+
+    public function lookup($words,$bases,$deep){
+		$wordPool = array();
+		$output  = array();
+        foreach ($words as $word) {
+			$wordPool[$word] = ['base' => false,'done' => false,'apply' => false];
+		}
+		foreach ($bases as $base) {
+			$wordPool[$base] = ['base' => true,'done' => false,'apply' => false];
+		}
+        /**
+         * 先查询字典名称
+         */
+        $dict_info = DictInfo::whereIn('id',$this->dictList)->select('id','shortname')->get();
+        $dict_name = [];
+        foreach ($dict_info as $key => $value) {
+            # code...
+            $dict_name[$value->id] = $value->shortname;
+        }
+        $caseman = new CaseMan();
+		for ($i=0; $i < $deep; $i++) {
+            $newBase = array();
+
+            $newWords = [];
+            foreach ($wordPool as $word => $info) {
+                # code...
+                if($info['done'] === false){
+                    $newWords[] = $word;
+                    $wordPool[$word]['done'] = true;
+                }
+            }
+            $data = UserDict::whereIn('word',$newWords)
+                            ->whereIn('dict_id',$this->dictList)
+                            ->leftJoin('dict_infos', 'user_dicts.dict_id', '=', 'dict_infos.id')
+                            ->orderBy('confidence','desc')
+                            ->get();
+            foreach ($data as $row) {
+                # code...
+                array_push($output,$row);
+                if(!empty($row->parent) && !isset($wordPool[$row->parent]) ){
+                    //将parent 插入待查询列表
+                    $wordPool[$row->parent] = ['base' => true,'done' => false,'apply' => false];
+                }
+            }
+
+			//处理查询结果中的拆分信息
+			$newWordPart = array();
+			foreach ($wordPool as $word => $info) {
+				if(!empty($info['factors'])){
+					$factors = \explode('+',$info['factors']);
+					foreach ($factors as $factor) {
+						# 将没有的拆分放入单词查询列表
+						if(!isset($wordPool[$factor])){
+							$wordPool[$factor] = ['base' => true,'done' => false,'apply' => false];
+						}
+					}
+				}
+			}
+		}
+
+        return $output;
+    }
+    private function langCheck($query,$lang){
+        if($query===[]){
+            return true;
+        }else{
+            return in_array($lang,$query);
+        }
+    }
+    /**
+     * 自动查词
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+        $startAt = microtime(true)*1000;
+
+        // system regular
+        $this->initSysDict();
+
+        $channel = Channel::find($request->get('channel_id'));
+        $orgData = $request->get('data');
+        $lang = $request->get('lang',[]);
+        //句子中的单词
+        $words = [];
+        foreach ($orgData as  $word) {
+            # code...
+            if( isset($word['type']) && $word['type']['value'] === '.ctl.'){
+                continue;
+            }
+            if(!empty($word['real']['value'])){
+                $words[] = $word['real']['value'];
+            }
+        }
+
+        $result = $this->lookup($words,[],2);
+        $indexed = $this->toIndexed($result);
+
+        foreach ($orgData as  $key => $word) {
+            if( isset($word['type']) && $word['type']['value'] === '.ctl.'){
+                continue;
+            }
+            if(empty($word['real']['value'])){
+                continue;
+            }
+            {
+                $data = $word;
+                if(isset($indexed[$word['real']['value']])){
+                    //parent
+                    $case = [];
+                    $parent = [];
+                    $factors = [];
+                    $factorMeaning = [];
+                    $meaning = [];
+                    $parent2 = [];
+                    $case2 = [];
+                    foreach ($indexed[$word['real']['value']] as $value) {
+                        //非base优先
+                        if(strstr($value->type,'base') === FALSE){
+                            $increment = 10;
+                        }else{
+                            $increment = 1;
+                        }
+                        //将全部结果加上得分放入数组
+                        $parent = $this->insertValue([$value->parent],$parent,$increment);
+                        if(!empty($value->type) && $value->type !== ".cp."){
+                            $case = $this->insertValue([$value->type."#".$value->grammar],$case,$increment);
+                        }
+                        $factors = $this->insertValue([$value->factors],$factors,$increment);
+                        $factorMeaning = $this->insertValue([$value->factormean],$factorMeaning,$increment);
+                        if($this->langCheck($lang,$value->language)){
+                            $meaning = $this->insertValue(explode('$',$value->mean),$meaning,$increment,false);
+                        }
+                    }
+                    if(count($case)>0){
+                        arsort($case);
+                        $first = array_keys($case)[0];
+                        $data['case'] = ['value'=>$first==="_null"?"":$first,'status'=>3];
+                    }
+                    if(count($parent)>0){
+                        arsort($parent);
+                        $first = array_keys($parent)[0];
+                        $data['parent'] = ['value'=>$first==="_null"?"":$first,'status'=>3];
+                    }
+                    if(count($factors)>0){
+                        arsort($factors);
+                        $first = array_keys($factors)[0];
+                        $data['factors'] = ['value'=>$first==="_null"?"":$first,'status'=>3];
+                    }
+                    //拆分意思
+                    if(count($factorMeaning)>0){
+                        arsort($factorMeaning);
+                        $first = array_keys($factorMeaning)[0];
+                        $data['factorMeaning'] = ['value'=>$first==="_null"?"":$first,'status'=>3];
+                    }
+                    $wbwFactorMeaning = [];
+                    if(!empty($data['factors']['value'])){
+                        foreach (explode("+",$data['factors']['value']) as  $factor) {
+                            # code...
+                            $wbwAnalyses = WbwAnalysis::where('wbw_word',$factor)
+                                                      ->where('type',7)
+                                                      ->selectRaw('data,count(*)')
+                                                      ->groupBy("data")
+                                                      ->orderBy("count", "desc")
+                                                      ->first();
+                            if($wbwAnalyses){
+                                $wbwFactorMeaning[]=$wbwAnalyses->data;
+                            }else{
+                                $wbwFactorMeaning[]="";
+                            }
+                        }
+                    }
+                    $data['factorMeaning'] = ['value'=>implode('+',$wbwFactorMeaning),'status'=>3];
+
+                    if(!empty($data['parent'])){
+                        if(isset($indexed[$data['parent']['value']])){
+                            foreach ($indexed[$data['parent']['value']] as $value) {
+                                //根据base 查找词意
+                                //非base优先
+                                $increment = 10;
+                                if($this->langCheck($lang,$value->language)){
+                                    $meaning = $this->insertValue(explode('$',$value->mean),$meaning,$increment,false);
+                                }
+                                //查找词源
+                                if(!empty($value->parent) && $value->parent !== $value->word && strstr($value->type,"base") !== FALSE ){
+                                    $parent2 = $this->insertValue([$value->grammar."$".$value->parent],$parent2,1,false);
+                                }
+                            }
+                        }
+                    }
+                    if(count($meaning)>0){
+                        arsort($meaning);
+                        $first = array_keys($meaning)[0];
+                        $data['meaning'] = ['value'=>$first==="_null"?"":$first,'status'=>3];
+                    }
+                    if(count($parent2)>0){
+                        arsort($parent2);
+                        $first = explode("$",array_keys($parent2)[0]);
+                        $data['parent2'] = ['value'=>$first[1],'status'=>3];
+                        $data['grammar2'] = ['value'=>$first[0],'status'=>3];
+                    }
+                }
+                $orgData[$key] = $data;
+            }
+        }
+        return $this->ok($orgData);
+    }
+
+    /**
+     * 自动查词
+     *
+     * @param  string  $sentId
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Request $request,string $sentId)
+    {
+        $startAt = microtime(true)*1000;
+
+        $channel = Channel::find($request->get('channel_id'));
+
+        //查询句子中的单词
+        $sent = \explode('-',$sentId);
+        $wbw = WbwTemplate::where('book',$sent[0])
+                ->where('paragraph',$sent[1])
+                ->whereBetween('wid',[$sent[2],$sent[3]])
+                ->orderBy('wid')
+                ->get();
+        $words = [];
+        foreach ($wbw as  $row) {
+            if($row->type !== '.ctl.' && !empty($row->real)){
+                $words[] = $row->real;
+            }
+        }
+        $result = $this->lookup($words,[],2);
+        $indexed = $this->toIndexed($result);
+
+        //生成自动填充结果
+        $wbwContent = [];
+        foreach ($wbw as  $row) {
+            $type = $row->type=='?'? '':$row->type;
+            $grammar = $row->gramma=='?'? '':$row->gramma;
+            $part = $row->part=='?'? '':$row->part;
+            if(!empty($type) || !empty($grammar)){
+                $case = "{$type}#$grammar";
+            }else{
+                $case = "";
+            }
+            $data = [
+                    'sn'=>[$row->wid],
+                    'word'=>['value'=>$row->word,'status'=>3],
+                    'real'=> ['value'=>$row->real,'status'=>3],
+                    'meaning'=> ['value'=>[],'status'=>3],
+                    'type'=> ['value'=>$type,'status'=>3],
+                    'grammar'=> ['value'=>$grammar,'status'=>3],
+                    'case'=> ['value'=>$case,'status'=>3],
+                    'style'=> ['value'=>$row->style,'status'=>3],
+                    'factors'=> ['value'=>$part,'status'=>3],
+                    'factorMeaning'=> ['value'=>'','status'=>3],
+                    'confidence'=> 0.5
+                ];
+            if($row->type !== '.ctl.' && !empty($row->real)){
+                if(isset($indexed[$row->real])){
+                    //parent
+                    $case = [];
+                    $parent = [];
+                    $factors = [];
+                    $factorMeaning = [];
+                    $meaning = [];
+                    $parent2 = [];
+                    $case2 = [];
+                    foreach ($indexed[$row->real] as $value) {
+                        //非base优先
+                        if(strstr($value->type,'base') === FALSE){
+                            $increment = 10;
+                        }else{
+                            $increment = 1;
+                        }
+                        //将全部结果加上得分放入数组
+                        $parent = $this->insertValue([$value->parent],$parent,$increment);
+                        $case = $this->insertValue([$value->type."#".$value->grammar],$case,$increment);
+                        $factors = $this->insertValue([$value->factors],$factors,$increment);
+                        $factorMeaning = $this->insertValue([$value->factormean],$factorMeaning,$increment);
+                        $meaning = $this->insertValue(explode('$',$value->mean),$meaning,$increment,false);
+                    }
+                    if(count($case)>0){
+                        arsort($case);
+                        $first = array_keys($case)[0];
+                        $data['case'] = ['value'=>$first==="_null"?"":$first,'status'=>3];
+                    }
+                    if(count($parent)>0){
+                        arsort($parent);
+                        $first = array_keys($parent)[0];
+                        $data['parent'] = ['value'=>$first==="_null"?"":$first,'status'=>3];
+                    }
+                    if(count($factors)>0){
+                        arsort($factors);
+                        $first = array_keys($factors)[0];
+                        $data['factors'] = ['value'=>$first==="_null"?"":$first,'status'=>3];
+                    }
+                    if(count($factorMeaning)>0){
+                        arsort($factorMeaning);
+                        $first = array_keys($factorMeaning)[0];
+                        $data['factorMeaning'] = ['value'=>$first==="_null"?"":$first,'status'=>3];
+                    }
+
+                    //根据base 查找词意
+                    if(!empty($data['parent'])){
+                        if(isset($indexed[$data['parent']['value']])){
+                            Log::info($data['parent']['value']."=".count($indexed[$data['parent']['value']]));
+                            foreach ($indexed[$data['parent']['value']] as $value) {
+                                //非base优先
+                                $increment = 10;
+                                $meaning = $this->insertValue(explode('$',$value->mean),$meaning,$increment,false);
+                            }
+                        }else{
+                            Log::error("no set parent".$data['parent']['value']);
+                        }
+                    }
+                    if(count($meaning)>0){
+                        arsort($meaning);
+                        Log::info('meanings=');
+                        Log::info(array_keys($meaning));
+                        $first = array_keys($meaning)[0];
+                        $data['meaning'] = ['value'=>$first==="_null"?"":$first,'status'=>3];
+                    }
+
+                }
+            }
+            $wbwContent[]  = $data;
+        }
+        $endAt = microtime(true)*1000;
+        return $this->ok(["rows"=>$wbwContent,
+                        "count"=>count($wbwContent),
+                        "time"=>(int)($endAt-$startAt)]);
+    }
+
+    private function toIndexed($words){
+        //转成索引数组
+        $indexed = [];
+        foreach ($words as $key => $value) {
+            # code...
+            $indexed[$value->word][] = $value;
+        }
+        return $indexed;
+    }
+
+    private function insertValue($value,$container,$increment,$empty=true){
+        foreach ($value as $one) {
+            if($empty === false){
+                if(empty($one)){
+                    break;
+                }
+            }
+            $one=trim($one);
+            $key = $one;
+            if(empty($key)){
+                $key = '_null';
+            }
+            if(isset($container[$key])){
+                $container[$key] += $increment;
+            }else{
+                $container[$key] = $increment;
+            }
+        }
+        return $container;
+    }
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\UserDict  $userDict
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, UserDict $userDict)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\UserDict  $userDict
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(UserDict $userDict)
+    {
+        //
+    }
+}

+ 94 - 0
app/Http/Controllers/WordIndexController.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\WordIndex;
+use Illuminate\Http\Request;
+use App\Http\Resources\WordIndexResource;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\DB;
+
+class WordIndexController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        switch ($request->get("view")) {
+            case 'key':
+                $key = $request->get("key");
+                /*
+                $result = Cache::remember("/word_index/{$key}",10,function() use($key){
+                    return WordIndex::where('word','like',$key."%")
+                                    ->whereOr('word_en','like',$key."%")
+                                    ->orderBy('word_en')
+                                    ->take(10)->get();
+                });
+                $table = WordIndex::where('word','like',$key."%")
+                                   ->whereOr('word_en','like',$key."%")
+                                   ->orderBy('len')
+                                   ->orderBy('word_en')
+                                   ->take(10);
+                Log::info($table->toSql());
+                $result = $table->get();
+*/
+                $result = DB::select("SELECT * from  word_indices where word like ? or word_en like ? order by len, word_en limit 10",[$key."%",$key."%"]);
+
+                return $this->ok(['rows'=>$result,'count'=>count($result)]);
+                break;
+            default:
+                return $this->error('view error');
+                break;
+        }
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\WordIndex  $wordIndex
+     * @return \Illuminate\Http\Response
+     */
+    public function show(WordIndex $wordIndex)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\Models\WordIndex  $wordIndex
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, WordIndex $wordIndex)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\WordIndex  $wordIndex
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(WordIndex $wordIndex)
+    {
+        //
+    }
+}

+ 3 - 2
app/Http/Kernel.php

@@ -15,13 +15,14 @@ class Kernel extends HttpKernel
      */
     protected $middleware = [
         // \App\Http\Middleware\TrustHosts::class,
-        \App\Http\Middleware\TrustProxies::class,
         \Fruitcake\Cors\HandleCors::class,
+        \App\Http\Middleware\TrustProxies::class,
+        \App\Http\Middleware\ApiLog::class,
         \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
         \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
         \App\Http\Middleware\TrimStrings::class,
         \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
-        \App\Http\Middleware\EnableCrossRequestMiddleware::class,
+        \App\Http\Middleware\UserOperation::class,
     ];
 
     /**

+ 32 - 0
app/Http/Middleware/ApiLog.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Storage;
+
+class ApiLog
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse)  $next
+     * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        $response = $next($request);
+        if (defined('LARAVEL_START'))
+        {
+            $api = [];
+            $api[] = date("h:i:sa",LARAVEL_START);
+            $api[] = round((microtime(true) - LARAVEL_START)*1000,2);
+            $api[] = $request->method();
+            $api[] = $request->path();
+            Storage::disk('local')->append("logs/api/".date("Y-m-d").".log",\implode(',',$api) );
+        }
+        return $response;
+    }
+}

+ 0 - 40
app/Http/Middleware/EnableCrossRequestMiddleware.php

@@ -1,40 +0,0 @@
-<?php
-
-namespace App\Http\Middleware;
-
-use Closure;
-use Illuminate\Http\Request;
-
-class EnableCrossRequestMiddleware
-{
-    /**
-     * Handle an incoming request.
-     *
-     * @param  \Illuminate\Http\Request  $request
-     * @param  \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse)  $next
-     * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
-     */
-    public function handle(Request $request, Closure $next)
-    {
-        $response = $next($request);
-        $origin = $request->server('HTTP_ORIGIN') ? $request->server('HTTP_ORIGIN') : '';
-        $allow_origin = [
-            env("CROSS_REQUEST_ALLOW_ORIGIN",'http://localhost:8001'),
-        ];
-        if (in_array($origin, $allow_origin)) {
-            $response->header('Access-Control-Allow-Origin', $origin);
-            $response->header('Access-Control-Allow-Headers', 'Origin, Content-Type, Cookie, X-CSRF-TOKEN, Accept, Authorization, X-XSRF-TOKEN');
-            $response->header('Access-Control-Expose-Headers', 'Authorization, authenticated');
-            $response->header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, OPTIONS');
-            $response->header('Access-Control-Allow-Credentials', 'true');
-        }
-        /*
-        ————————————————
-        原文作者:qbhy
-        转自链接:https://learnku.com/articles/6504/laravel-cross-domain-solution
-        版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请保留以上作者信息和原文链接。
-        */
-        //return $next($request);
-		return $response;
-    }
-}

+ 256 - 0
app/Http/Middleware/UserOperation.php

@@ -0,0 +1,256 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Storage;
+use App\Models\UserOperationLog;
+use App\Models\UserOperationFrame;
+use App\Models\UserOperationDaily;
+use App\Http\Api\AuthApi;
+
+define("MAX_INTERVAL", 600000);
+define("MIN_INTERVAL", 60000);
+
+/**
+ * 	$active_type[10] = "channel_update";
+ * 	$active_type[11] = "channel_create";
+ * 	$active_type[20] = "article_update";
+ * 	$active_type[21] = "article_create";
+ * 	$active_type[30] = "dict_lookup";
+ * 	$active_type[40] = "term_update";
+ * 	$active_type[42] = "term_create";
+ * 	$active_type[41] = "term_lookup";
+ * 	$active_type[60] = "wbw_update";
+ * 	$active_type[61] = "wbw_create";
+ * 	$active_type[70] = "sent_update";
+ * 	$active_type[71] = "sent_create";
+ * 	$active_type[80] = "collection_update";
+ * 	$active_type[81] = "collection_create";
+ * 	$active_type[90] = "nissaya_open";
+ */
+class UserOperation
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse)  $next
+     * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        $response = $next($request);
+        $user = AuthApi::current($request);
+        if(!$user){
+            return $response;
+        }
+
+
+        $api = explode('/',$request->path());
+        if(count($api)<3){
+            return $response;
+        }if($api[0] !== 'api' || $api[1] !=='v2'){
+            return $response;
+        }
+        $method = $request->method();
+        switch ($api[2]) {
+            case 'channel':
+                switch ($method) {
+                    case 'POST':
+                        $newLog = [
+                            "op_type_id"=>11,
+                            "op_type"=>"channel_create",
+                            "content"=>$request->get('studio').'/'.$request->get('name'),
+                        ];
+                        break;
+                    case 'PUT':
+                        $newLog = [
+                            "op_type_id"=>10,
+                            "op_type"=>"channel_update",
+                            "content"=>$request->get('name'),
+                        ];
+                        break;
+                }
+                break;
+            case 'article':
+                switch ($method) {
+                    case 'POST':
+                        $newLog = [
+                            "op_type_id"=>21,
+                            "op_type"=>"article_create",
+                            "content"=>$request->get('studio').'/'.$request->get('title'),
+                        ];
+                        break;
+                    case 'PUT':
+                        $newLog = [
+                            "op_type_id"=>20,
+                            "op_type"=>"article_update",
+                            "content"=>$request->get('title'),
+                        ];
+                        break;
+                }
+                break;
+            case 'dict':
+                $newLog = [
+                    "op_type_id"=>30,
+                    "op_type"=>"dict_lookup",
+                    "content"=>$request->get("word")
+                ];
+                break;
+            case 'terms':
+                switch ($method) {
+                    case 'POST':
+                        $newLog = [
+                            "op_type_id"=>42,
+                            "op_type"=>"term_create",
+                            "content"=>$request->get('word'),
+                        ];
+                        break;
+                    case 'PUT':
+                        $newLog = [
+                            "op_type_id"=>40,
+                            "op_type"=>"term_update",
+                            "content"=>$request->get('word'),
+                        ];
+                        break;
+                }
+                break;
+            case 'sentence':
+                switch ($method) {
+                    case 'POST':
+                        $newLog = [
+                            "op_type_id"=>71,
+                            "op_type"=>"sent_create",
+                            "content"=>$request->get('channel'),
+                        ];
+                        break;
+                    case 'PUT':
+                        $newLog = [
+                            "op_type_id"=>70,
+                            "op_type"=>"sent_update",
+                            "content"=>$request->get('channel'),
+                        ];
+                        break;
+                }
+                break;
+            case 'anthology':
+                switch ($method) {
+                    case 'POST':
+                        $newLog = [
+                            "op_type_id"=>81,
+                            "op_type"=>"collection_create",
+                            "content"=>$request->get('title'),
+                        ];
+                        break;
+                    case 'PUT':
+                        $newLog = [
+                            "op_type_id"=>80,
+                            "op_type"=>"collection_update",
+                            "content"=>$request->get('title'),
+                        ];
+                        break;
+                }
+                break;
+            case 'wbw':
+                switch ($method) {
+                    case 'POST':
+                        $newLog = [
+                            "op_type_id"=>60,
+                            "op_type"=>"wbw_update",
+                            "content"=>$request->get('book')."_".$request->get('para')."_".$request->get('channel_id'),
+                        ];
+                        break;
+                }
+                break;
+        }
+        if(isset($newLog)){
+            $currTime = round((microtime(true))*1000,0);
+            #获取客户端时区偏移 beijing = +8
+            if (isset($_COOKIE["timezone"])) {
+                $client_timezone = (0 - (int) $_COOKIE["timezone"]) * 60 * 1000;
+            } else {
+                $client_timezone = 0;
+            }
+
+            $log = new UserOperationLog();
+            $log->id = app('snowflake')->id();
+            $log->user_id = $user['user_id'];
+            $log->op_type_id = $newLog["op_type_id"];
+            $log->op_type = $newLog["op_type"];
+            $log->content = $newLog["content"];
+            $log->timezone = $client_timezone;
+            $log->create_time = $currTime;
+            $log->save();
+
+            //frame
+            // 查询上次编辑活跃结束时间
+            $last = UserOperationFrame::where("user_id",$user['user_id'])->orderBy("updated_at","desc")->first();
+            if($last){
+                //找到,判断是否超时,超时新建,未超时修改
+                $id = (int) $last["id"];
+                $start_time = (int) $last["op_start"];
+                $endtime = (int) $last["op_end"];
+                $hit = (int) $last["hit"];
+                if ($currTime - $endtime > MAX_INTERVAL) {
+                    //超时新建
+                    $new_record = true;
+                } else {
+                    //未超时修改
+                    $new_record = false;
+                }
+            }else{
+                //没找到,新建
+                $new_record = true;
+            }
+            $this_active_time = 0; //时间增量
+            if ($new_record) {
+                #新建
+                $newFrame = new UserOperationFrame();
+                #最小思考时间 MIN_INTERVAL
+                $newFrame->id = app('snowflake')->id();
+                $newFrame->user_id = $user['user_id'];
+                $newFrame->op_start = $currTime - MIN_INTERVAL;
+                $newFrame->op_end = $currTime;
+                $newFrame->duration = MIN_INTERVAL;
+                $newFrame->hit = 1;
+                $newFrame->timezone = $client_timezone;
+                $newFrame->save();
+                $this_active_time = MIN_INTERVAL;
+            } else {
+                #修改
+                $last->op_end = $currTime;
+                $last->duration = $currTime - $start_time;
+                $last->hit = $last->hit + 1;
+                $last->save();
+            }
+
+            #更新经验总量表
+            #计算客户端日期 unix时间戳 以毫秒计
+            $client_currtime = $currTime + $client_timezone;
+            $client_date = strtotime(gmdate("Y-m-d", $client_currtime / 1000)) * 1000;
+
+            #查询是否存在
+            $daily = UserOperationDaily::where("user_id",$user['user_id'])->where("date_int",$client_date)->first();
+            if ($daily) {
+                #更新
+                $daily->duration = $daily->duration + $this_active_time;
+                $daily->hit = $daily->hit + 1;
+                $daily->save();
+            } else {
+                #新建
+                $daily = new UserOperationDaily();
+                $daily->id = app('snowflake')->id();
+                $daily->user_id = $user['user_id'];
+                $daily->date_int = $client_date;
+                $daily->duration = MIN_INTERVAL;
+                $daily->hit = 1;
+                $daily->save();
+            }
+            #更新经验总量表结束
+        }
+
+        return $response;
+    }
+}

+ 44 - 0
app/Http/Requests/CollectionRequest.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class CollectionRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            /*
+            string Id        = 1;
+            string Title     = 2;
+            string Subtitle  = 3;
+            string Summary   = 4;
+            string ArticleList   = 5;
+            repeated Tag Tags = 6;
+
+            string Lang = 51;
+            string EditorId = 52;
+            EnumPublicity Publicity = 53;
+             */
+            'id' => 'required|unique:posts',
+            'title' => 'required|max:512',
+            'subtitle' => 'nullable|max:512',
+        ];
+    }
+}

Vissa filer visades inte eftersom för många filer har ändrats