From 9c0f458920b899c925796097e9d8c156d961d022 Mon Sep 17 00:00:00 2001 From: xiaomlove Date: Tue, 31 Jan 2023 16:38:21 +0800 Subject: [PATCH] login notify + bonus log --- app/Console/Commands/UserLoginNotify.php | 38 +++++++ .../Resources/User/BonusLogResource.php | 104 ++++++++++++++++++ .../Pages/ManageBonusLogs.php | 20 ++++ .../Resources/User/LoginLogResource.php | 87 +++++++++++++++ .../Pages/ManageLoginLogs.php | 20 ++++ app/Jobs/SendLoginNotify.php | 60 ++++++++++ app/Models/BonusLogs.php | 5 + app/Models/LoginLog.php | 14 +++ app/Models/User.php | 2 +- ...3_01_30_154106_create_login_logs_table.php | 36 ++++++ include/constants.php | 2 +- include/functions.php | 8 ++ include/globalfunctions.php | 10 +- public/takelogin.php | 24 +++- resources/lang/en/admin.php | 2 + resources/lang/en/bonus-log.php | 30 +++++ resources/lang/en/label.php | 3 + resources/lang/en/message.php | 8 ++ resources/lang/zh_CN/admin.php | 2 + resources/lang/zh_CN/bonus-log.php | 30 +++++ resources/lang/zh_CN/label.php | 3 + resources/lang/zh_CN/message.php | 8 ++ resources/lang/zh_TW/admin.php | 2 + resources/lang/zh_TW/bonus-log.php | 30 +++++ resources/lang/zh_TW/label.php | 3 + resources/lang/zh_TW/message.php | 8 ++ 26 files changed, 552 insertions(+), 7 deletions(-) create mode 100644 app/Console/Commands/UserLoginNotify.php create mode 100644 app/Filament/Resources/User/BonusLogResource.php create mode 100644 app/Filament/Resources/User/BonusLogResource/Pages/ManageBonusLogs.php create mode 100644 app/Filament/Resources/User/LoginLogResource.php create mode 100644 app/Filament/Resources/User/LoginLogResource/Pages/ManageLoginLogs.php create mode 100644 app/Jobs/SendLoginNotify.php create mode 100644 app/Models/LoginLog.php create mode 100644 database/migrations/2023_01_30_154106_create_login_logs_table.php create mode 100644 resources/lang/en/bonus-log.php create mode 100644 resources/lang/zh_CN/bonus-log.php create mode 100644 resources/lang/zh_TW/bonus-log.php diff --git a/app/Console/Commands/UserLoginNotify.php b/app/Console/Commands/UserLoginNotify.php new file mode 100644 index 00000000..5a8d7e96 --- /dev/null +++ b/app/Console/Commands/UserLoginNotify.php @@ -0,0 +1,38 @@ +option('this_id'); + $lastId = $this->option('last_id'); + $this->info("thisId: $thisId, lastId: $lastId"); + SendLoginNotify::dispatch($thisId, $lastId); + return Command::SUCCESS; + } +} diff --git a/app/Filament/Resources/User/BonusLogResource.php b/app/Filament/Resources/User/BonusLogResource.php new file mode 100644 index 00000000..ed81691a --- /dev/null +++ b/app/Filament/Resources/User/BonusLogResource.php @@ -0,0 +1,104 @@ +schema([ + // + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('id')->sortable(), + Tables\Columns\TextColumn::make('uid') + ->formatStateUsing(fn ($state) => username_for_admin($state)) + ->label(__('label.username')) + , + Tables\Columns\TextColumn::make('business_type_text') + ->label(__('bonus-log.fields.business_type')) + , + Tables\Columns\TextColumn::make('old_total_value') + ->label(__('bonus-log.fields.old_total_value')) + , + Tables\Columns\TextColumn::make('value') + ->label(__('bonus-log.fields.value')) + , + Tables\Columns\TextColumn::make('new_total_value') + ->label(__('bonus-log.fields.new_total_value')) + , + Tables\Columns\TextColumn::make('comment') + ->label(__('label.comment')) + , + Tables\Columns\TextColumn::make('created_at') + ->label(__('label.created_at')) + , + ]) + ->defaultSort('id', 'desc') + ->filters([ + Tables\Filters\Filter::make('uid') + ->form([ + Forms\Components\TextInput::make('uid') + ->label(__('label.username')) + ->placeholder('UID') + , + ])->query(function (Builder $query, array $data) { + return $query->when($data['uid'], fn (Builder $query, $value) => $query->where("uid", $value)); + }) + , + Tables\Filters\SelectFilter::make('business_type') + ->options(BonusLogs::listStaticProps(BonusLogs::$businessTypes, 'bonus-log.business_types', true)) + ->label(__('bonus-log.fields.business_type')) + , + ]) + ->actions([ +// Tables\Actions\EditAction::make(), +// Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ +// Tables\Actions\DeleteBulkAction::make(), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ManageBonusLogs::route('/'), + ]; + } +} diff --git a/app/Filament/Resources/User/BonusLogResource/Pages/ManageBonusLogs.php b/app/Filament/Resources/User/BonusLogResource/Pages/ManageBonusLogs.php new file mode 100644 index 00000000..d57f7580 --- /dev/null +++ b/app/Filament/Resources/User/BonusLogResource/Pages/ManageBonusLogs.php @@ -0,0 +1,20 @@ +schema([ + // + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('id')->sortable(), + Tables\Columns\TextColumn::make('uid') + ->formatStateUsing(fn ($state) => username_for_admin($state)) + ->label(__('label.username')) + , + Tables\Columns\TextColumn::make('ip')->searchable(), + Tables\Columns\TextColumn::make('country')->label(__('label.country'))->searchable(), + Tables\Columns\TextColumn::make('city')->label(__('label.city'))->searchable(), + Tables\Columns\TextColumn::make('client')->label(__('label.client')), + Tables\Columns\TextColumn::make('created_at')->label(__('label.created_at')), + ]) + ->defaultSort('id', 'desc') + ->filters([ + Tables\Filters\Filter::make('uid') + ->form([ + Forms\Components\TextInput::make('uid') + ->label(__('label.username')) + ->placeholder('UID') + , + ])->query(function (Builder $query, array $data) { + return $query->when($data['uid'], fn (Builder $query, $value) => $query->where("uid", $value)); + }) + , + ]) + ->actions([ +// Tables\Actions\EditAction::make(), +// Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ +// Tables\Actions\DeleteBulkAction::make(), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ManageLoginLogs::route('/'), + ]; + } +} diff --git a/app/Filament/Resources/User/LoginLogResource/Pages/ManageLoginLogs.php b/app/Filament/Resources/User/LoginLogResource/Pages/ManageLoginLogs.php new file mode 100644 index 00000000..1c3ac98a --- /dev/null +++ b/app/Filament/Resources/User/LoginLogResource/Pages/ManageLoginLogs.php @@ -0,0 +1,20 @@ +thisLoginLogId = $thisLoginLogId; + + $this->lastLoginLogId = $lastLoginLogId; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $thisLoginLog = LoginLog::query()->findOrFail($this->thisLoginLogId); + $lastLoginLog = LoginLog::query()->findOrFail($this->lastLoginLogId); + $user = User::query()->findOrFail($thisLoginLog->uid, User::$commonFields); + $locale = $user->locale; + $toolRep = new ToolRepository(); + $subject = nexus_trans('message.login_notify.subject', ['site_name' => Setting::get('basic.SITENAME')], $locale); + $body = nexus_trans('message.login_notify.body', [ + 'this_login_time' => $thisLoginLog->created_at, + 'this_ip' => $thisLoginLog->ip, + 'this_location' => sprintf('%s·%s', $thisLoginLog->city, $thisLoginLog->country), + 'last_login_time' => $lastLoginLog->created_at, + 'last_ip' => $lastLoginLog->ip, + 'last_location' => sprintf('%s·%s', $lastLoginLog->city, $lastLoginLog->country), + ], $locale); + $result = $toolRep->sendMail($user->email, $subject, $body); + do_log(sprintf('user: %s login notify result: %s', $user->username, var_export($result, true))); + } +} diff --git a/app/Models/BonusLogs.php b/app/Models/BonusLogs.php index 37109ce4..cfa49e1b 100644 --- a/app/Models/BonusLogs.php +++ b/app/Models/BonusLogs.php @@ -55,6 +55,11 @@ class BonusLogs extends NexusModel self::BUSINESS_TYPE_GIFT_MEDAL => ['text' => 'Gift medal to someone'], ]; + public function getBusinessTypeTextAttribute() + { + return nexus_trans('bonus-log.business_types.' . $this->business_type); + } + public static function getBonusForCancelHitAndRun() { $result = Setting::get('bonus.cancel_hr'); diff --git a/app/Models/LoginLog.php b/app/Models/LoginLog.php new file mode 100644 index 00000000..46bf087b --- /dev/null +++ b/app/Models/LoginLog.php @@ -0,0 +1,14 @@ +id(); + $table->integer('uid')->index(); + $table->string('ip', 128)->index(); + $table->string('country')->nullable(true)->index(); + $table->string('city')->nullable(true)->index(); + $table->string('client')->nullable(true)->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('login_logs'); + } +}; diff --git a/include/constants.php b/include/constants.php index 1c598408..628371e2 100644 --- a/include/constants.php +++ b/include/constants.php @@ -1,6 +1,6 @@ '', 'country' => '', 'city' => '', + 'country_en' => '', + 'city_en' => '', ]; try { $record = $reader->city($ip); @@ -5842,7 +5844,10 @@ function get_ip_location_from_geoip($ip): bool|array $info['version'] = 6; } $info['country'] = $countryName; + $info['country_en'] = $record->country->names['en'] ?? ''; $info['city'] = $cityName; + $info['city_en'] = $record->city->names['en'] ?? ''; + } catch (\Exception $exception) { do_log($exception->getMessage() . $exception->getTraceAsString(), 'error'); } @@ -5857,6 +5862,9 @@ function get_ip_location_from_geoip($ip): bool|array 'flagpic' => '', 'start_ip' => $ip, 'end_ip' => $ip, + 'ip_version' => $locationInfo['version'], + 'country_en' => $locationInfo['country_en'], + 'city_en' => $locationInfo['city_en'], ]; } diff --git a/include/globalfunctions.php b/include/globalfunctions.php index 9e273ee8..069d7ca9 100644 --- a/include/globalfunctions.php +++ b/include/globalfunctions.php @@ -1113,18 +1113,24 @@ function get_passkey_by_authkey($authkey) }); } -function executeCommand($command, $format = 'string'): string|array +function executeCommand($command, $format = 'string', $artisan = false, $exception = true): string|array { $append = " 2>&1"; if (!str_ends_with($command, $append)) { $command .= $append; } + if ($artisan) { + $phpPath = nexus_env('PHP_PATH', 'php'); + $webRoot = rtrim(ROOT_PATH, '/'); + $command = "$phpPath $webRoot/artisan $command"; + } do_log("command: $command"); $result = exec($command, $output, $result_code); $outputString = implode("\n", $output); do_log(sprintf('result_code: %s, result: %s, output: %s', $result_code, $result, $outputString)); - if ($result_code != 0) { + if ($exception && $result_code != 0) { throw new \RuntimeException($outputString); } return $format == 'string' ? $outputString : $output; } + diff --git a/public/takelogin.php b/public/takelogin.php index 711352b0..ec7d1fde 100644 --- a/public/takelogin.php +++ b/public/takelogin.php @@ -34,9 +34,27 @@ if (!empty($row['two_step_secret'])) { } } $log = "user: {$row['id']}, ip: $ip"; -if ($row["passhash"] != md5($row["secret"] . $password . $row["secret"])) - login_failedlogins(); - +if ($row["passhash"] != md5($row["secret"] . $password . $row["secret"])) { + login_failedlogins(); +} +$locationInfo = get_ip_location_from_geoip($ip); +$thisLoginLog = \App\Models\LoginLog::query()->create([ + 'ip' => $ip, + 'uid' => $row['id'], + 'country' => $locationInfo['country_en'], + 'city' => $locationInfo['city_en'], + 'client' => 'Web', +]); +$lastLoginLog = \App\Models\LoginLog::query()->where('uid', $row['id'])->orderBy('id', 'desc')->first(); +if ( + $lastLoginLog && $lastLoginLog->country && $lastLoginLog->city + && $locationInfo['country_en'] && $locationInfo['city_en'] + && ($lastLoginLog->country != $locationInfo['country_en'] || $lastLoginLog->city != $locationInfo['city_en']) +) { + $command = sprintf("user:login_notify --this_id=%s --last_id=%s", $thisLoginLog->id, $lastLoginLog->id); + do_log("[LOGIN_NOTIFY], user: {$row['id']}, $command"); +// executeCommand($command, "string", true, false); +} if ($row["enabled"] == "no") bark($lang_takelogin['std_account_disabled']); diff --git a/resources/lang/en/admin.php b/resources/lang/en/admin.php index a0de25d9..a139a6d4 100644 --- a/resources/lang/en/admin.php +++ b/resources/lang/en/admin.php @@ -34,6 +34,8 @@ return [ 'torrent_operation_log' => 'Torrent operation logs', 'invite' => 'Invites', 'user_props' => 'User props', + 'login_log' => 'Login logs', + 'bonus_log' => 'Bonus logs', ], 'resources' => [ 'agent_allow' => [ diff --git a/resources/lang/en/bonus-log.php b/resources/lang/en/bonus-log.php new file mode 100644 index 00000000..b5530052 --- /dev/null +++ b/resources/lang/en/bonus-log.php @@ -0,0 +1,30 @@ + [ + \App\Models\BonusLogs::BUSINESS_TYPE_CANCEL_HIT_AND_RUN => 'Cancel H&R', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_MEDAL => 'Buy medal', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_ATTENDANCE_CARD => 'Buy attendance card', + \App\Models\BonusLogs::BUSINESS_TYPE_STICKY_PROMOTION => 'Sticky promotion', + \App\Models\BonusLogs::BUSINESS_TYPE_POST_REWARD => 'Post reward', + \App\Models\BonusLogs::BUSINESS_TYPE_EXCHANGE_UPLOAD => 'Exchange uploaded', + \App\Models\BonusLogs::BUSINESS_TYPE_EXCHANGE_INVITE => 'Buy invite', + \App\Models\BonusLogs::BUSINESS_TYPE_CUSTOM_TITLE => 'Custom title', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_VIP => 'Buy VIP', + \App\Models\BonusLogs::BUSINESS_TYPE_GIFT_TO_SOMEONE => 'Gift to someone', + \App\Models\BonusLogs::BUSINESS_TYPE_NO_AD => 'Cancel ad', + \App\Models\BonusLogs::BUSINESS_TYPE_GIFT_TO_LOW_SHARE_RATIO => 'Gift to low share ratio', + \App\Models\BonusLogs::BUSINESS_TYPE_LUCKY_DRAW => 'Lucky draw', + \App\Models\BonusLogs::BUSINESS_TYPE_EXCHANGE_DOWNLOAD => 'Exchange downloaded', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_TEMPORARY_INVITE => 'Buy temporary invite', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_RAINBOW_ID => 'Buy rainbow ID', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_CHANGE_USERNAME_CARD => 'Buy change username card', + \App\Models\BonusLogs::BUSINESS_TYPE_GIFT_MEDAL => 'Gift medal', + ], + 'fields' => [ + 'business_type' => 'Business type', + 'old_total_value' => 'Pre-trade value', + 'value' => 'Trade value', + 'new_total_value' => 'Post-trade value', + ], +]; diff --git a/resources/lang/en/label.php b/resources/lang/en/label.php index c1413aad..36f90368 100644 --- a/resources/lang/en/label.php +++ b/resources/lang/en/label.php @@ -36,6 +36,9 @@ return [ 'anonymous' => 'Anonymous', 'infinite' => 'Infinite', 'save' => 'Save', + 'country' => 'Country', + 'city' => 'City', + 'client' => 'Client', 'setting' => [ 'nav_text' => 'Setting', 'backup' => [ diff --git a/resources/lang/en/message.php b/resources/lang/en/message.php index 6ad20a50..b42777a1 100644 --- a/resources/lang/en/message.php +++ b/resources/lang/en/message.php @@ -31,4 +31,12 @@ return [ 'subject' => 'Receive gift medal', 'body' => "User :username purchased a medal [:medal_name] at a cost of :cost_bonus and gave it to you. The medal is worth :price, the fee is :gift_fee_total(factor: :gift_fee_factor), you will have this medal until: :expire_at, and the medal's bonus addition factor is: :bonus_addition_factor.", ], + 'login_notify' => [ + 'subject' => ':site_name Offsite login alert', + 'body' => << '种子操作记录', 'invite' => '用户邀请', 'user_props' => '用户道具', + 'login_log' => '登录记录', + 'bonus_log' => '魔力记录', ], 'resources' => [ 'agent_allow' => [ diff --git a/resources/lang/zh_CN/bonus-log.php b/resources/lang/zh_CN/bonus-log.php new file mode 100644 index 00000000..f8897b40 --- /dev/null +++ b/resources/lang/zh_CN/bonus-log.php @@ -0,0 +1,30 @@ + [ + \App\Models\BonusLogs::BUSINESS_TYPE_CANCEL_HIT_AND_RUN => '消除 H&R', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_MEDAL => '购买勋章', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_ATTENDANCE_CARD => '购买补签卡', + \App\Models\BonusLogs::BUSINESS_TYPE_STICKY_PROMOTION => '置顶促销', + \App\Models\BonusLogs::BUSINESS_TYPE_POST_REWARD => '帖子奖励', + \App\Models\BonusLogs::BUSINESS_TYPE_EXCHANGE_UPLOAD => '兑换上传量', + \App\Models\BonusLogs::BUSINESS_TYPE_EXCHANGE_INVITE => '购买邀请', + \App\Models\BonusLogs::BUSINESS_TYPE_CUSTOM_TITLE => '自定义头衔', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_VIP => '购买 VIP', + \App\Models\BonusLogs::BUSINESS_TYPE_GIFT_TO_SOMEONE => '捐赠给某人', + \App\Models\BonusLogs::BUSINESS_TYPE_NO_AD => '消除广告', + \App\Models\BonusLogs::BUSINESS_TYPE_GIFT_TO_LOW_SHARE_RATIO => '捐赠给低分享率者', + \App\Models\BonusLogs::BUSINESS_TYPE_LUCKY_DRAW => '幸运大转盘', + \App\Models\BonusLogs::BUSINESS_TYPE_EXCHANGE_DOWNLOAD => '兑换下载量', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_TEMPORARY_INVITE => '购买临时邀请', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_RAINBOW_ID => '购买彩虹 ID', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_CHANGE_USERNAME_CARD => '购买改名卡', + \App\Models\BonusLogs::BUSINESS_TYPE_GIFT_MEDAL => '赠送勋章', + ], + 'fields' => [ + 'business_type' => '业务类型', + 'old_total_value' => '交易前值', + 'value' => '交易值', + 'new_total_value' => '交易后值', + ], +]; diff --git a/resources/lang/zh_CN/label.php b/resources/lang/zh_CN/label.php index 696ce3d5..d47e6bc5 100644 --- a/resources/lang/zh_CN/label.php +++ b/resources/lang/zh_CN/label.php @@ -36,6 +36,9 @@ return [ 'anonymous' => '匿名', 'infinite' => '无限', 'save' => '保存', + 'country' => '国家', + 'city' => '城市', + 'client' => '客户端', 'setting' => [ 'nav_text' => '设置', 'backup' => [ diff --git a/resources/lang/zh_CN/message.php b/resources/lang/zh_CN/message.php index 63629fdf..1c83cc64 100644 --- a/resources/lang/zh_CN/message.php +++ b/resources/lang/zh_CN/message.php @@ -31,4 +31,12 @@ return [ 'subject' => '收到赠送勋章', 'body' => '用户 :username 花费魔力 :cost_bonus 购买了勋章[:medal_name]并赠送与你。此勋章价值 :price,手续费 :gift_fee_total(系数::gift_fee_factor),你将拥有此勋章有效期至: :expire_at,勋章的魔力加成系数为: :bonus_addition_factor。', ], + 'login_notify' => [ + 'subject' => ':site_name 异地登录提醒', + 'body' => << '種子操作記錄', 'invite' => '用戶邀請', 'user_props' => '用戶道具', + 'login_log' => '登錄記錄', + 'bonus_log' => '魔力記錄', ], 'resources' => [ 'agent_allow' => [ diff --git a/resources/lang/zh_TW/bonus-log.php b/resources/lang/zh_TW/bonus-log.php new file mode 100644 index 00000000..5572c1e5 --- /dev/null +++ b/resources/lang/zh_TW/bonus-log.php @@ -0,0 +1,30 @@ + [ + \App\Models\BonusLogs::BUSINESS_TYPE_CANCEL_HIT_AND_RUN => '消除 H&R', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_MEDAL => '購買勛章', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_ATTENDANCE_CARD => '購買補簽卡', + \App\Models\BonusLogs::BUSINESS_TYPE_STICKY_PROMOTION => '置頂促銷', + \App\Models\BonusLogs::BUSINESS_TYPE_POST_REWARD => '帖子獎勵', + \App\Models\BonusLogs::BUSINESS_TYPE_EXCHANGE_UPLOAD => '兌換上傳量', + \App\Models\BonusLogs::BUSINESS_TYPE_EXCHANGE_INVITE => '購買邀請', + \App\Models\BonusLogs::BUSINESS_TYPE_CUSTOM_TITLE => '自定義頭銜', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_VIP => '購買 VIP', + \App\Models\BonusLogs::BUSINESS_TYPE_GIFT_TO_SOMEONE => '捐贈給某人', + \App\Models\BonusLogs::BUSINESS_TYPE_NO_AD => '消除廣告', + \App\Models\BonusLogs::BUSINESS_TYPE_GIFT_TO_LOW_SHARE_RATIO => '捐贈給低分享率者', + \App\Models\BonusLogs::BUSINESS_TYPE_LUCKY_DRAW => '幸運大轉盤', + \App\Models\BonusLogs::BUSINESS_TYPE_EXCHANGE_DOWNLOAD => '兌換下載量', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_TEMPORARY_INVITE => '購買臨時邀請', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_RAINBOW_ID => '購買彩虹 ID', + \App\Models\BonusLogs::BUSINESS_TYPE_BUY_CHANGE_USERNAME_CARD => '購買改名卡', + \App\Models\BonusLogs::BUSINESS_TYPE_GIFT_MEDAL => '贈送勛章', + ], + 'fields' => [ + 'business_type' => '業務類型', + 'old_total_value' => '交易前值', + 'value' => '交易值', + 'new_total_value' => '交易後值', + ], +]; diff --git a/resources/lang/zh_TW/label.php b/resources/lang/zh_TW/label.php index e0097e79..63fe67ae 100644 --- a/resources/lang/zh_TW/label.php +++ b/resources/lang/zh_TW/label.php @@ -36,6 +36,9 @@ return [ 'anonymous' => '匿名', 'infinite' => '無限', 'save' => '保存', + 'country' => '國家', + 'city' => '城市', + 'client' => '客戶端', 'setting' => [ 'nav_text' => '設置', 'backup' => [ diff --git a/resources/lang/zh_TW/message.php b/resources/lang/zh_TW/message.php index 5e681cf3..e7ecd59b 100644 --- a/resources/lang/zh_TW/message.php +++ b/resources/lang/zh_TW/message.php @@ -30,4 +30,12 @@ return [ 'subject' => '收到贈送勛章', 'body' => '用戶 :username 花費魔力 :cost_bonus 購買了勛章[:medal_name]並贈送與你。此勛章價值 :price,手續費 :gift_fee_total(系數::gift_fee_factor),你將擁有此勛章有效期至: :expire_at,勛章的魔力加成系數為: :bonus_addition_factor。', ], + 'login_notify' => [ + 'subject' => ':site_name 異地登錄提醒', + 'body' => <<