diff --git a/app/Enums/Permission/PermissionEnum.php b/app/Enums/Permission/PermissionEnum.php index 342815fb..07647c32 100644 --- a/app/Enums/Permission/PermissionEnum.php +++ b/app/Enums/Permission/PermissionEnum.php @@ -16,5 +16,6 @@ enum PermissionEnum: string { case MANAGE_USER_BASIC_INFO = "prfmanage"; case MANAGE_USER_CONFIDENTIAL_INFO = "cruprfmanage"; case VIEW_USER_CONFIDENTIAL_INFO = "userprofile"; + case VIEW_USER_HISTORY = "viewhistory"; } diff --git a/app/Filament/Resources/User/BonusLogResource.php b/app/Filament/Resources/User/BonusLogResource.php index 9a58bc98..35cab68d 100644 --- a/app/Filament/Resources/User/BonusLogResource.php +++ b/app/Filament/Resources/User/BonusLogResource.php @@ -92,7 +92,7 @@ class BonusLogResource extends Resource }) , SelectFilter::make('business_type') - ->options(BonusLogs::listStaticProps(Arr::except(BonusLogs::$businessTypes, BonusLogs::$businessTypeBonus), 'bonus-log.business_types', true)) + ->options(BonusLogs::listBusinessTypeOptions(BonusLogs::CATEGORY_COMMON)) ->label(__('bonus-log.fields.business_type')) ->searchable(true) , diff --git a/app/Models/BonusLogs.php b/app/Models/BonusLogs.php index d7e916cb..17d55355 100644 --- a/app/Models/BonusLogs.php +++ b/app/Models/BonusLogs.php @@ -4,14 +4,17 @@ namespace App\Models; use Carbon\Carbon; +use Illuminate\Support\Arr; class BonusLogs extends NexusModel { protected $table = 'bonus_logs'; - protected $fillable = ['uid', 'business_type', 'old_total_value', 'value', 'new_total_value', 'comment']; + protected $fillable = ['uid', 'business_type', 'old_total_value', 'value', 'new_total_value', 'comment', 'created_at', 'updated_at']; public $timestamps = true; + const CATEGORY_COMMON = 'common'; + const CATEGORY_SEEDING = 'seeding'; const DEFAULT_BONUS_CANCEL_ONE_HIT_AND_RUN = 10000; const DEFAULT_BONUS_BUY_ATTENDANCE_CARD = 1000; @@ -100,7 +103,7 @@ class BonusLogs extends NexusModel self::BUSINESS_TYPE_SEEDING_MEDAL_ADDITION => ['text' => 'Seeding medal addition'], ]; - public static array $businessTypeBonus = [ + public static array $businessTypeSeeding = [ self::BUSINESS_TYPE_SEEDING_BASIC, self::BUSINESS_TYPE_SEEDING_DONOR_ADDITION, self::BUSINESS_TYPE_SEEDING_OFFICIAL_ADDITION, @@ -108,6 +111,28 @@ class BonusLogs extends NexusModel self::BUSINESS_TYPE_SEEDING_MEDAL_ADDITION ]; + public static function listBusinessTypeOptions($category = ''): array + { + $source = BonusLogs::$businessTypes; + if ($category == self::CATEGORY_COMMON) { + $source = Arr::except(BonusLogs::$businessTypes, BonusLogs::$businessTypeSeeding); + } else if ($category == self::CATEGORY_SEEDING) { + $source = Arr::only(BonusLogs::$businessTypes, BonusLogs::$businessTypeSeeding); + } + return self::listStaticProps($source, 'bonus-log.business_types', true); + } + + public static function listCategoryOptions(bool $includeSeeding): array + { + $result = [ + self::CATEGORY_COMMON => nexus_trans('bonus-log.category_common') + ]; + if ($includeSeeding) { + $result[self::CATEGORY_SEEDING] = nexus_trans('bonus-log.category_seeding'); + } + return $result; + } + public function getBusinessTypeTextAttribute() { return nexus_trans('bonus-log.business_types.' . $this->business_type); diff --git a/app/Repositories/BonusRepository.php b/app/Repositories/BonusRepository.php index 5be08792..0eeb9cb2 100644 --- a/app/Repositories/BonusRepository.php +++ b/app/Repositories/BonusRepository.php @@ -15,6 +15,7 @@ use App\Models\UserMedal; use App\Models\UserMeta; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; +use Nexus\Database\ClickHouse; use Nexus\Database\NexusDB; class BonusRepository extends BaseRepository @@ -362,5 +363,51 @@ class BonusRepository extends BaseRepository }); } + public function getCount(int $userId, string $category, int $businessType = 0): int + { + if ($category == BonusLogs::CATEGORY_COMMON) { + $query = BonusLogs::query()->where('uid', $userId); + if ($businessType > 0) { + $query->where('business_type', $businessType); + } + return $query->count(); + } else if ($category == BonusLogs::CATEGORY_SEEDING) { + $whereStr = "uid = :uid"; + $binds = ["uid" => $userId]; + if ($businessType > 0) { + $whereStr .= " AND business_type = :business_type"; + $binds["business_type"] = $businessType; + } + return ClickHouse::count("bonus_logs", $whereStr, $binds); + } + throw new \InvalidArgumentException("Invalid category: $category"); + } + + public function getList(int $userId, string $category, int $businessType = 0, int $page = 1, int $perPage = 50) + { + if ($category == BonusLogs::CATEGORY_COMMON) { + $query = BonusLogs::query()->where('uid', $userId); + if ($businessType > 0) { + $query->where('business_type', $businessType); + } + return $query->orderBy("id", "desc")->forPage($page, $perPage)->get(); + } else if ($category == BonusLogs::CATEGORY_SEEDING) { + $sql = "select * from bonus_logs where uid = :uid"; + $binds = ["uid" => $userId]; + if ($businessType > 0) { + $sql .= " AND business_type = :business_type"; + $binds["business_type"] = $businessType; + } + $offset = ($page - 1) * $perPage; + $rows = ClickHouse::list("$sql order by created_at desc limit $offset, $perPage", $binds); + $result = []; + foreach ($rows as $row) { + $result[] = new BonusLogs($row); + } + return $result; + } + throw new \InvalidArgumentException("Invalid category: $category"); + } + } diff --git a/config/clickhouse.php b/config/clickhouse.php index 0ec71cca..78f0c3a6 100644 --- a/config/clickhouse.php +++ b/config/clickhouse.php @@ -24,12 +24,12 @@ return [ */ 'connection' => [ - 'host' => env('CLICKHOUSE_HOST', 'localhost'), - 'port' => env('CLICKHOUSE_HTTP_PORT', 8123), - 'username' => env('CLICKHOUSE_USER', 'default'), - 'password' => env('CLICKHOUSE_PASSWORD', ''), + 'host' => nexus_env('CLICKHOUSE_HOST', 'localhost'), + 'port' => nexus_env('CLICKHOUSE_HTTP_PORT', 8123), + 'username' => nexus_env('CLICKHOUSE_USER', 'default'), + 'password' => nexus_env('CLICKHOUSE_PASSWORD', ''), 'options' => [ - 'database' => env('CLICKHOUSE_DATABASE', 'default'), + 'database' => nexus_env('CLICKHOUSE_DATABASE', 'default'), 'timeout' => 1, 'connectTimeOut' => 2, ], @@ -42,7 +42,7 @@ return [ */ 'migrations' => [ - 'table' => env('CLICKHOUSE_MIGRATION_TABLE', 'migrations'), - 'path' => database_path('clickhouse-migrations'), + 'table' => nexus_env('CLICKHOUSE_MIGRATION_TABLE', 'migrations'), + 'path' => __DIR__ . '/../database/clickhouse-migrations', ], ]; diff --git a/include/globalfunctions.php b/include/globalfunctions.php index 269a15b8..6add1f01 100644 --- a/include/globalfunctions.php +++ b/include/globalfunctions.php @@ -301,6 +301,7 @@ function nexus_config($key, $default = null) ROOT_PATH . 'config/nexus.php', ROOT_PATH . 'config/emoji.php', ROOT_PATH . 'config/captcha.php', + ROOT_PATH . 'config/clickhouse.php', ]; foreach ($files as $file) { $basename = basename($file); diff --git a/nexus/Database/ClickHouse.php b/nexus/Database/ClickHouse.php new file mode 100644 index 00000000..32fac00b --- /dev/null +++ b/nexus/Database/ClickHouse.php @@ -0,0 +1,54 @@ +database($options['database']); +// $client->setTimeout($options['timeout']); + $client->setConnectTimeOut($options['connectTimeOut']); + self::$client = $client; + } + return self::$client; + } + + public static function count(string $table, string $whereStr = '', array $binds = []): int + { + $start = microtime(true); + $countAlias = "count"; + $sql = "select count(*) as $countAlias from $table"; + $whereStr = strtolower(trim($whereStr)); + if ($whereStr) { + if (!str_starts_with($whereStr, 'where')) { + $whereStr = "where $whereStr"; + } + $sql .= " $whereStr"; + } + $stat = self::getClient()->select($sql, $binds); + $result = $stat->fetchOne($countAlias) ?? 0; + $costTime = number_format(microtime(true) - $start, 3); + do_log("table: $table, whereStr: $whereStr, binds: " . json_encode($binds) . ", result: $result, cost time: $costTime sec."); + return $result; + } + + public static function list(string $sql, array $binds = []): array + { + $start = microtime(true); + $stat = self::getClient()->select($sql, $binds); + $result = $stat->rows(); + $costTime = number_format(microtime(true) - $start, 3); + do_log("sql: $sql, binds: " . json_encode($binds) . ", result count: " . count($result) . ", cost time: $costTime sec."); + return $result; + } + + +} diff --git a/public/bonus-log.php b/public/bonus-log.php new file mode 100644 index 00000000..d29d946e --- /dev/null +++ b/public/bonus-log.php @@ -0,0 +1,110 @@ +where('id', $uid)->first(\App\Models\User::$commonFields); +if (!$user) { + stderr("Error", "Invalid uid: $uid"); +} +if ($uid != $CURUSER['id']) { + user_can(\App\Enums\Permission\PermissionEnum::VIEW_USER_HISTORY->value, true, $CURUSER['id']); +} +$isRecordSeedingBonusLog = \App\Models\Setting::getIsRecordSeedingBonusLog(); +$defaultCategory = \App\Models\BonusLogs::CATEGORY_COMMON; +$category = $_REQUEST['category'] ?? $defaultCategory; +$categoryOptions = \App\Models\BonusLogs::listCategoryOptions($isRecordSeedingBonusLog); +if (!isset($categoryOptions[$category])) { + stderr("Error", "Invalid category: $category"); +} +$businessType = $_REQUEST['business_type'] ?? 0; +$businessTypeOptions = \App\Models\BonusLogs::listBusinessTypeOptions($isRecordSeedingBonusLog ? '' : $defaultCategory); +if ($businessType && !isset($businessTypeOptions[$businessType])) { + stderr("Error", "Invalid business_type: $businessType"); +} + +stdhead(nexus_trans('bonus-log.title_for_user')); +$pagerParam = "?uid=$uid&category=$category&business_type=$businessType"; +print("

".nexus_trans('bonus-log.title_for_user') . " ".htmlspecialchars($user->username)."

"); + +$textSelectOnePlease = nexus_trans('nexus.select_one_please'); +$categoryOptionsText = $businessTypeOptionsText = ''; +foreach ($categoryOptions as $name => $text) { + $categoryOptionsText .= sprintf( + '', + $name, isset($_REQUEST['category']) && $_REQUEST['category'] == $name ? ' selected' : '', $text + ); +} +foreach ($businessTypeOptions as $name => $text) { + $businessTypeOptionsText .= sprintf( + '', + $name, isset($_REQUEST['business_type']) && $_REQUEST['business_type'] == $name ? ' selected' : '', $text + ); +} + +$resetText = nexus_trans('label.reset'); +$submitText = nexus_trans('label.submit'); +$categoryText = nexus_trans('bonus-log.category'); +$businessTypeText = nexus_trans('bonus-log.fields.business_type'); +$filterForm = <<
+ + + {$categoryText}: + +    + {$businessTypeText}: + +    + + +
+ +FORM; +$resetJs = <<getCount($uid, $category, $businessType); +list($pagertop, $pagerbottom, $limit, $offset, $pageSize, $page) = pager(50, $total, "$pagerParam&"); +$list = $rep->getList($uid, $category, $businessType, $page + 1, $pageSize); +begin_main_frame(); +print($filterForm); +print(""); +print(" + + + + + + +"); +foreach ($list as $row) { + print(" + + + + + + + "); +} + +print("
".nexus_trans('bonus-log.fields.business_type')."".nexus_trans('bonus-log.fields.old_total_value')."".nexus_trans('bonus-log.fields.value')."".nexus_trans('bonus-log.fields.new_total_value')."".nexus_trans('label.comment')."".nexus_trans('label.created_at')."
" . $row->businessTypeText . "" . ($row->old_total_value > 0 ? number_format($row->old_total_value, 1) : '-') . "" . ($row->old_total_value < $row->new_total_value ? "+" . number_format($row->value, 1) : "-" . number_format($row->value, 1)) . "" . ($row->new_total_value > 0 ? number_format($row->new_total_value, 1) : '-') . "" . $row->comment . "" . $row->created_at . "
"); +print($pagerbottom); +end_main_frame(); +stdfoot(); + + diff --git a/public/claim.php b/public/claim.php index 4ea8e9ad..be8c239f 100644 --- a/public/claim.php +++ b/public/claim.php @@ -115,7 +115,7 @@ if ($sort == 'seed_time') { } $list = $query->selectRaw("claims.*")->get(); print($filterForm); -print(""); +print("
"); print(" diff --git a/public/userdetails.php b/public/userdetails.php index edf021c8..8ebde2c3 100644 --- a/public/userdetails.php +++ b/public/userdetails.php @@ -400,7 +400,8 @@ if ($user["id"] == $CURUSER["id"] || user_can('viewhistory')) { $states = (new \App\Repositories\ClaimRepository())->getStats($user['id']); tr_small($lang_functions['menu_claim'], sprintf('%s', $user['id'], $states), 1); } - tr_small($lang_userdetails['row_karma_points'], number_format($user['seedbonus'], 1), 1); + $bonusLogText = sprintf('  [%s]', $user['id'], nexus_trans("bonus-log.view_detail")); + tr_small($lang_userdetails['row_karma_points'], number_format($user['seedbonus'], 1) . $bonusLogText, 1); tr_small($lang_functions['text_seed_points'], number_format($user['seed_points'], 1) . "  (" . nexus_trans('label.updated_at') . ": " . $user['seed_points_updated_at'] . ")", 1); } @@ -511,7 +512,7 @@ if (user_can('prfmanage') && $user["class"] < get_user_class()) tr($lang_userdetails['row_comment'], "", 1); $bonuscomment = \App\Models\BonusLogs::query() ->where("uid", $user["id"]) - ->whereNotIn("business_type", \App\Models\BonusLogs::$businessTypeBonus) + ->whereNotIn("business_type", \App\Models\BonusLogs::$businessTypeSeeding) ->orderBy("id", "desc") ->limit(20) ->get() diff --git a/resources/lang/en/bonus-log.php b/resources/lang/en/bonus-log.php index 7b0ff6ce..add8d52c 100644 --- a/resources/lang/en/bonus-log.php +++ b/resources/lang/en/bonus-log.php @@ -45,4 +45,9 @@ return [ 'new_total_value' => 'Post-trade value', ], 'exclude_seeding_bonus' => 'Exclude seeding bonus', + 'title_for_user' => 'User bonus details', + 'category' => 'Category', + 'category_common' => 'Common', + 'category_seeding' => 'Seeding', + 'view_detail' => 'Details', ]; diff --git a/resources/lang/zh_CN/bonus-log.php b/resources/lang/zh_CN/bonus-log.php index e82d3875..783f51b5 100644 --- a/resources/lang/zh_CN/bonus-log.php +++ b/resources/lang/zh_CN/bonus-log.php @@ -47,4 +47,9 @@ return [ 'new_total_value' => '交易后值', ], 'exclude_seeding_bonus' => '不包含做种魔力', + 'title_for_user' => '用户魔力明细', + 'category' => '分类', + 'category_common' => '普通', + 'category_seeding' => '做种', + 'view_detail' => '明细', ]; diff --git a/resources/lang/zh_TW/bonus-log.php b/resources/lang/zh_TW/bonus-log.php index 6c8bd0f4..b6d12bb7 100644 --- a/resources/lang/zh_TW/bonus-log.php +++ b/resources/lang/zh_TW/bonus-log.php @@ -45,4 +45,9 @@ return [ 'new_total_value' => '交易後值', ], 'exclude_seeding_bonus' => '不包含做種魔力', + 'title_for_user' => '用戶魔力明細', + 'category' => '分類', + 'category_common' => '普通', + 'category_seeding' => '做種', + 'view_detail' => '明細', ];
".nexus_trans('claim.th_id')." ".nexus_trans('claim.th_username')."