[游戏管理]玩家钱包流水

This commit is contained in:
2026-04-15 11:50:14 +08:00
parent 9d06c7a226
commit ba80e7c392
5 changed files with 418 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
<?php
namespace app\admin\controller\game;
use app\common\controller\Backend;
use support\think\Db;
use support\Response;
use Webman\Http\Request as WebmanRequest;
/**
* 玩家钱包流水(只读列表,数据由业务入账写入本表)
*/
class UserWalletRecord extends Backend
{
protected ?object $model = null;
protected bool $modelValidate = false;
protected string|array $quickSearchField = ['id', 'biz_type', 'ref_type', 'remark', 'idempotency_key'];
protected string|array $defaultSortField = ['id' => 'desc'];
protected string|array $orderGuarantee = ['id' => 'desc'];
protected array $withJoinTable = ['gameUser', 'channel', 'operatorAdmin'];
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\GameUserWalletRecord();
return null;
}
public function add(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
return $this->error('钱包流水仅允许业务入账,禁止后台手工新增');
}
public function edit(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
return $this->error('钱包流水不可编辑');
}
public function del(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
return $this->error('钱包流水不可删除');
}
public function sortable(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
return $this->error('不支持排序');
}
/**
* 列表渠道管理员仅看本渠道流水channel_id 快照)
*/
protected function _index(): Response
{
if ($this->request && $this->request->get('select')) {
return $this->select($this->request);
}
list($where, $alias, $limit, $order) = $this->queryBuilder();
$table = strtolower($this->model->getTable());
$mainShort = $alias[$table] ?? '';
if ($mainShort !== '' && $this->auth && !$this->auth->isSuperAdmin()) {
$channelIds = $this->getScopedChannelIdsForFilter();
$where[] = [$mainShort . '.channel_id', 'in', $channelIds !== [] ? $channelIds : [0]];
}
$res = $this->model
->withJoin($this->withJoinTable, $this->withJoinType)
->with($this->withJoinTable)
->visible([
'gameUser' => ['username', 'phone'],
'channel' => ['name'],
'operatorAdmin' => ['username'],
])
->alias($alias)
->where($where)
->order($order)
->paginate($limit);
return $this->success('', [
'list' => $res->items(),
'total' => $res->total(),
'remark' => get_route_remark(),
]);
}
/**
* 非超管:与渠道管理一致,仅本账号相关渠道
*
* @return int[]
*/
private function getScopedChannelIdsForFilter(): array
{
if (!$this->auth) {
return [0];
}
if ($this->auth->isSuperAdmin()) {
return [];
}
$admin = Db::name('admin')
->field(['id', 'channel_id'])
->where('id', $this->auth->id)
->find();
$ids = [];
if ($admin && !empty($admin['channel_id'])) {
$ids[] = $admin['channel_id'];
}
$owned = Db::name('channel')->where('top_admin_id', $this->auth->id)->column('id');
$created = Db::name('channel')->where('admin_id', $this->auth->id)->column('id');
return array_values(array_unique(array_merge($ids, $owned, $created)));
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace app\common\model;
use support\think\Model;
/**
* 玩家游戏币钱包流水(只增不改;余额变更须与 game_user.coin 条件更新同事务)
*/
class GameUserWalletRecord extends Model
{
protected $name = 'game_user_wallet_record';
protected $autoWriteTimestamp = false;
protected $createTime = 'create_time';
protected $updateTime = false;
protected $type = [
'create_time' => 'integer',
'amount' => 'string',
'balance_before' => 'string',
'balance_after' => 'string',
];
public function gameUser(): \think\model\relation\BelongsTo
{
return $this->belongsTo(GameUser::class, 'user_id', 'id');
}
public function channel(): \think\model\relation\BelongsTo
{
return $this->belongsTo(Channel::class, 'channel_id', 'id');
}
public function operatorAdmin(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\admin\model\Admin::class, 'operator_admin_id', 'id');
}
}

View File

@@ -0,0 +1,33 @@
export default {
id: 'Record ID',
user_id: 'User ID',
'game_user__username': 'Username',
'channel__name': 'Channel',
biz_type: 'Biz type',
direction: 'Direction',
amount: 'Amount',
balance_before: 'Balance before',
balance_after: 'Balance after',
ref_type: 'Ref type',
ref_id: 'Ref ID',
idempotency_key: 'Idempotency key',
'operator_admin__username': 'Operator',
remark: 'Remark',
create_time: 'Created at',
'quick Search Fields': 'ID, biz type, ref type, remark, idempotency key',
'direction in': 'Credit',
'direction out': 'Debit',
'biz deposit': 'Deposit',
'biz withdraw': 'Withdraw',
'biz withdraw_freeze': 'Withdraw freeze',
'biz withdraw_unfreeze': 'Withdraw unfreeze',
'biz platform_in': 'Platform credit',
'biz platform_out': 'Platform debit',
'biz admin_credit': 'Admin credit',
'biz admin_deduct': 'Admin debit',
'biz bet': 'Bet',
'biz payout': 'Payout',
'biz fee': 'Fee',
'biz void_refund': 'Void refund',
'biz adjust': 'Adjustment',
}

View File

@@ -0,0 +1,33 @@
export default {
id: '流水ID',
user_id: '用户ID',
'game_user__username': '用户名',
'channel__name': '渠道',
biz_type: '业务类型',
direction: '方向',
amount: '变动额',
balance_before: '变动前余额',
balance_after: '变动后余额',
ref_type: '关联类型',
ref_id: '关联ID',
idempotency_key: '幂等键',
'operator_admin__username': '操作管理员',
remark: '备注',
create_time: '创建时间',
'quick Search Fields': 'ID、业务类型、关联类型、备注、幂等键',
'direction in': '入金',
'direction out': '出金',
'biz deposit': '充值入账',
'biz withdraw': '提现出账',
'biz withdraw_freeze': '提现冻结',
'biz withdraw_unfreeze': '提现解冻',
'biz platform_in': '平台划入',
'biz platform_out': '平台划出',
'biz admin_credit': '管理员加币',
'biz admin_deduct': '管理员扣币',
'biz bet': '下注扣款',
'biz payout': '派彩',
'biz fee': '手续费',
'biz void_refund': '作废退款',
'biz adjust': '其他调账',
}

View File

@@ -0,0 +1,180 @@
<template>
<div class="default-main ba-table-box">
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
:buttons="['refresh', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('game.walletRecord.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
defineOptions({
name: 'game/walletRecord',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
function formatAmount(_row: anyObj, _column: any, cellValue: unknown) {
if (cellValue === null || cellValue === undefined || cellValue === '') {
return '-'
}
const s = String(cellValue).trim().replace(',', '.')
const n = parseFloat(s)
if (!Number.isFinite(n)) {
return String(cellValue)
}
return n.toFixed(4)
}
const bizReplace = {
deposit: t('game.walletRecord.biz deposit'),
withdraw: t('game.walletRecord.biz withdraw'),
withdraw_freeze: t('game.walletRecord.biz withdraw_freeze'),
withdraw_unfreeze: t('game.walletRecord.biz withdraw_unfreeze'),
platform_in: t('game.walletRecord.biz platform_in'),
platform_out: t('game.walletRecord.biz platform_out'),
admin_credit: t('game.walletRecord.biz admin_credit'),
admin_deduct: t('game.walletRecord.biz admin_deduct'),
bet: t('game.walletRecord.biz bet'),
payout: t('game.walletRecord.biz payout'),
fee: t('game.walletRecord.biz fee'),
void_refund: t('game.walletRecord.biz void_refund'),
adjust: t('game.walletRecord.biz adjust'),
}
const dirReplace = {
'1': t('game.walletRecord.direction in'),
'2': t('game.walletRecord.direction out'),
}
const baTable = new baTableClass(
new baTableApi('/admin/game.UserWalletRecord/'),
{
pk: 'id',
column: [
{ label: t('game.walletRecord.id'), prop: 'id', align: 'center', width: 100, operator: 'RANGE', sortable: 'custom' },
{
label: t('game.walletRecord.user_id'),
prop: 'user_id',
align: 'center',
width: 90,
operator: 'RANGE',
sortable: false,
},
{
label: t('game.walletRecord.game_user__username'),
prop: 'gameUser.username',
align: 'center',
minWidth: 110,
operatorPlaceholder: t('Fuzzy query'),
operator: 'LIKE',
render: 'tags',
comSearchRender: 'string',
},
{
label: t('game.walletRecord.channel__name'),
prop: 'channel.name',
align: 'center',
minWidth: 100,
operatorPlaceholder: t('Fuzzy query'),
operator: 'LIKE',
render: 'tags',
comSearchRender: 'string',
},
{
label: t('game.walletRecord.biz_type'),
prop: 'biz_type',
align: 'center',
minWidth: 120,
operator: 'eq',
render: 'tag',
replaceValue: bizReplace,
},
{
label: t('game.walletRecord.direction'),
prop: 'direction',
align: 'center',
width: 90,
operator: 'eq',
render: 'tag',
replaceValue: dirReplace,
},
{ label: t('game.walletRecord.amount'), prop: 'amount', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount },
{ label: t('game.walletRecord.balance_before'), prop: 'balance_before', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount },
{ label: t('game.walletRecord.balance_after'), prop: 'balance_after', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount },
{
label: t('game.walletRecord.ref_type'),
prop: 'ref_type',
align: 'center',
minWidth: 100,
showOverflowTooltip: true,
operator: 'LIKE',
},
{ label: t('game.walletRecord.ref_id'), prop: 'ref_id', align: 'center', width: 100, operator: 'RANGE' },
{
label: t('game.walletRecord.idempotency_key'),
prop: 'idempotency_key',
align: 'center',
minWidth: 120,
showOverflowTooltip: true,
operator: 'LIKE',
},
{
label: t('game.walletRecord.operator_admin__username'),
prop: 'operatorAdmin.username',
align: 'center',
minWidth: 100,
operatorPlaceholder: t('Fuzzy query'),
operator: 'LIKE',
render: 'tags',
comSearchRender: 'string',
},
{
label: t('game.walletRecord.remark'),
prop: 'remark',
align: 'center',
minWidth: 140,
showOverflowTooltip: true,
operator: 'LIKE',
},
{
label: t('game.walletRecord.create_time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 160,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
],
},
{}
)
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
})
</script>
<style scoped lang="scss"></style>