bonus logs

This commit is contained in:
xiaomlove
2026-01-29 20:24:36 +07:00
parent 695d5f071f
commit ea176b1615
13 changed files with 267 additions and 13 deletions

View File

@@ -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";
}

View File

@@ -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)
,

View File

@@ -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);

View File

@@ -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");
}
}

View File

@@ -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',
],
];

View File

@@ -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);

View File

@@ -0,0 +1,54 @@
<?php
namespace Nexus\Database;
use ClickHouseDB\Client;
class ClickHouse
{
private static ?Client $client = null;
public static function getClient(): Client
{
if (is_null(self::$client)) {
$config = nexus_config('clickhouse.connection');
$client = new Client($config);
$options = $config['options'];
$client->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;
}
}

110
public/bonus-log.php Normal file
View File

@@ -0,0 +1,110 @@
<?php
require "../include/bittorrent.php";
dbconn();
loggedinorreturn();
$uid = $_REQUEST['uid'] ?? $CURUSER['id'] ?? 0;
int_check($uid,true);
$user = \App\Models\User::query()->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("<h1 align=center>".nexus_trans('bonus-log.title_for_user') . "<a href=userdetails.php?id=" . htmlspecialchars($uid) . "><b>&nbsp;".htmlspecialchars($user->username)."</b></a></h1>");
$textSelectOnePlease = nexus_trans('nexus.select_one_please');
$categoryOptionsText = $businessTypeOptionsText = '';
foreach ($categoryOptions as $name => $text) {
$categoryOptionsText .= sprintf(
'<option value="%s"%s>%s</option>',
$name, isset($_REQUEST['category']) && $_REQUEST['category'] == $name ? ' selected' : '', $text
);
}
foreach ($businessTypeOptions as $name => $text) {
$businessTypeOptionsText .= sprintf(
'<option value="%s"%s>%s</option>',
$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 = <<<FORM
<div>
<form id="filterForm" action="{$_SERVER['REQUEST_URI']}" method="get">
<input type="hidden" name="uid" value="{$uid}" />
<span>{$categoryText}:</span>
<select name="category">
{$categoryOptionsText}
</select>
&nbsp;&nbsp;
<span>{$businessTypeText}:</span>
<select name="business_type">
<option value="0">-{$textSelectOnePlease}-</option>
{$businessTypeOptionsText}
</select>
&nbsp;&nbsp;
<input type="submit" value="{$submitText}">
<input type="button" id="reset" value="{$resetText}">
</form>
</div>
FORM;
$resetJs = <<<JS
jQuery("#reset").on('click', function () {
jQuery("select[name=category]").val('')
jQuery("select[name=business_type]").val('')
})
JS;
\Nexus\Nexus::js($resetJs, 'footer', false);
$rep = new \App\Repositories\BonusRepository();
$total = $rep->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("<table id='bonus-log-table' width='100%' cellpadding='5'>");
print("<tr>
<td class='colhead' align='left'>".nexus_trans('bonus-log.fields.business_type')."</td>
<td class='colhead' align='left'>".nexus_trans('bonus-log.fields.old_total_value')."</td>
<td class='colhead' align='left'>".nexus_trans('bonus-log.fields.value')."</td>
<td class='colhead' align='left'>".nexus_trans('bonus-log.fields.new_total_value')."</td>
<td class='colhead' align='left'>".nexus_trans('label.comment')."</td>
<td class='colhead' align='left'>".nexus_trans('label.created_at')."</td>
</tr>");
foreach ($list as $row) {
print("<tr>
<td class='rowfollow nowrap' align='left'>" . $row->businessTypeText . "</td>
<td class='rowfollow nowrap' align='left'>" . ($row->old_total_value > 0 ? number_format($row->old_total_value, 1) : '-') . "</td>
<td class='rowfollow nowrap' align='left'>" . ($row->old_total_value < $row->new_total_value ? "+" . number_format($row->value, 1) : "-" . number_format($row->value, 1)) . "</td>
<td class='rowfollow nowrap' align='left'>" . ($row->new_total_value > 0 ? number_format($row->new_total_value, 1) : '-') . "</td>
<td class='rowfollow nowrap' align='left'>" . $row->comment . "</td>
<td class='rowfollow nowrap' align='left'>" . $row->created_at . "</td>
</tr>");
}
print("</table>");
print($pagerbottom);
end_main_frame();
stdfoot();

View File

@@ -115,7 +115,7 @@ if ($sort == 'seed_time') {
}
$list = $query->selectRaw("claims.*")->get();
print($filterForm);
print("<table id='claim-table' width='100%'>");
print("<table id='claim-table' width='100%' cellpadding='5'>");
print("<tr>
<td class='colhead' align='center'>".nexus_trans('claim.th_id')."</td>
<td class='colhead' align='center'>".nexus_trans('claim.th_username')."</td>

View File

@@ -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('<a href="claim.php?uid=%s" target="_blank">%s</a>', $user['id'], $states), 1);
}
tr_small($lang_userdetails['row_karma_points'], number_format($user['seedbonus'], 1), 1);
$bonusLogText = sprintf('&nbsp;&nbsp;<a href="bonus-log.php?uid=%s" target="_blank" class="altlink">[%s]</a>', $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) . "&nbsp;&nbsp;<span class='text-muted'>(" . nexus_trans('label.updated_at') . ": " . $user['seed_points_updated_at'] . ")</span>", 1);
}
@@ -511,7 +512,7 @@ if (user_can('prfmanage') && $user["class"] < get_user_class())
tr($lang_userdetails['row_comment'], "<textarea cols=\"60\" rows=\"6\" name=\"modcomment\">".$modcomment."</textarea>", 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()

View File

@@ -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',
];

View File

@@ -47,4 +47,9 @@ return [
'new_total_value' => '交易后值',
],
'exclude_seeding_bonus' => '不包含做种魔力',
'title_for_user' => '用户魔力明细',
'category' => '分类',
'category_common' => '普通',
'category_seeding' => '做种',
'view_detail' => '明细',
];

View File

@@ -45,4 +45,9 @@ return [
'new_total_value' => '交易後值',
],
'exclude_seeding_bonus' => '不包含做種魔力',
'title_for_user' => '用戶魔力明細',
'category' => '分類',
'category_common' => '普通',
'category_seeding' => '做種',
'view_detail' => '明細',
];