Initial commit
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
require dirname(__DIR__) . '/includes/game_engine.php';
|
||||
$admin = require_admin();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') redirect('/admin/index.php');
|
||||
$return = (string)($_POST['return'] ?? '/admin/index.php');
|
||||
if (!str_starts_with($return, '/admin/')) $return = '/admin/index.php';
|
||||
|
||||
try {
|
||||
verify_csrf();
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
if ($action === 'logout') {
|
||||
admin_audit(db(), 'admin_logout', 'admin', (int)$admin['id']);
|
||||
unset($_SESSION['admin_id']); session_regenerate_id(true); redirect('/admin/login.php');
|
||||
}
|
||||
if ($action === 'review_recharge') {
|
||||
$id = (int)($_POST['id'] ?? 0); $decision = (string)($_POST['decision'] ?? '');
|
||||
if (!in_array($decision, ['approved','rejected'], true)) throw new RuntimeException('审核操作无效。');
|
||||
$pdo = db(); $pdo->beginTransaction();
|
||||
try {
|
||||
$lock = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? ' FOR UPDATE' : '';
|
||||
$stmt=$pdo->prepare('SELECT * FROM recharge_orders WHERE id=?'.$lock);$stmt->execute([$id]);$order=$stmt->fetch();
|
||||
if(!$order || $order['status']!=='pending') throw new RuntimeException('订单已处理或不存在。');
|
||||
if($decision==='approved'){
|
||||
$stmt=$pdo->prepare('SELECT balance FROM users WHERE id=?'.$lock);$stmt->execute([$order['user_id']]);$before=(float)$stmt->fetchColumn();$after=$before+(float)$order['amount'];
|
||||
$pdo->prepare('UPDATE users SET balance=?, updated_at=? WHERE id=?')->execute([$after,now(),$order['user_id']]);
|
||||
create_ledger($pdo,(int)$order['user_id'],'recharge',(float)$order['amount'],$before,$after,'充值审核到账',$order['order_no']);
|
||||
credit_recharge_commissions($pdo,(int)$order['user_id'],(float)$order['amount'],'order',(string)$order['order_no']);
|
||||
}
|
||||
$pdo->prepare('UPDATE recharge_orders SET status=?, admin_note=?, reviewed_by=?, reviewed_at=? WHERE id=?')->execute([$decision,mb_substr(trim((string)($_POST['note']??'')),0,200),$_SESSION['admin_id'],now(),$id]);
|
||||
admin_audit($pdo, 'recharge_review', 'recharge_order', $id, ['status'=>'pending'], ['status'=>$decision,'amount'=>(float)$order['amount']]);
|
||||
$pdo->commit();
|
||||
} catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
|
||||
flash('success',$decision==='approved'?'订单已通过并到账。':'订单已拒绝。');
|
||||
} elseif ($action === 'reverse_recharge') {
|
||||
reverse_recharge_order(db(), (int)($_POST['id']??0), (string)($_POST['note']??''));
|
||||
flash('success','充值及关联佣金已完成冲正。');
|
||||
} elseif ($action === 'review_withdrawal') {
|
||||
$id=(int)($_POST['id']??0);$decision=(string)($_POST['decision']??'');if(!in_array($decision,['approved','rejected'],true))throw new RuntimeException('审核操作无效。');$pdo=db();$pdo->beginTransaction();try{$lock=$pdo->getAttribute(PDO::ATTR_DRIVER_NAME)==='mysql'?' FOR UPDATE':'';$stmt=$pdo->prepare('SELECT * FROM withdrawal_orders WHERE id=?'.$lock);$stmt->execute([$id]);$order=$stmt->fetch();if(!$order||$order['status']!=='pending')throw new RuntimeException('订单已处理或不存在。');if($decision==='rejected'){$stmt=$pdo->prepare('SELECT balance FROM users WHERE id=?'.$lock);$stmt->execute([$order['user_id']]);$before=(float)$stmt->fetchColumn();$after=$before+(float)$order['amount'];$pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after,now(),$order['user_id']]);create_ledger($pdo,(int)$order['user_id'],'withdrawal_refund',(float)$order['amount'],$before,$after,'提现拒绝退回','WD'.$id);}$pdo->prepare('UPDATE withdrawal_orders SET status=? WHERE id=?')->execute([$decision,$id]);admin_audit($pdo,'withdrawal_review','withdrawal_order',$id,['status'=>'pending'],['status'=>$decision,'amount'=>(float)$order['amount']]);$pdo->commit();}catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}flash('success',$decision==='approved'?'提现已通过。':'提现已拒绝并退回余额。');
|
||||
} elseif ($action === 'adjust_balance') {
|
||||
$userId=(int)($_POST['user_id']??0);$amount=round((float)($_POST['amount']??0),2);
|
||||
if($amount===0.0 || abs($amount)>1000000) throw new RuntimeException('调整金额无效。');
|
||||
$pdo=db();$pdo->beginTransaction();
|
||||
try{$lock=$pdo->getAttribute(PDO::ATTR_DRIVER_NAME)==='mysql'?' FOR UPDATE':'';$stmt=$pdo->prepare('SELECT balance FROM users WHERE id=?'.$lock);$stmt->execute([$userId]);$value=$stmt->fetchColumn();if($value===false)throw new RuntimeException('用户不存在。');$before=(float)$value;$after=$before+$amount;if($after<0)throw new RuntimeException('调整后余额不能小于 0。');$pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after,now(),$userId]);create_ledger($pdo,$userId,'admin_adjust',$amount,$before,$after,'后台手动调整');admin_audit($pdo,'balance_adjust','user',$userId,['balance'=>$before],['balance'=>$after,'amount'=>$amount]);$pdo->commit();}catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
|
||||
flash('success','余额已调整。');
|
||||
} elseif ($action === 'toggle_user') {
|
||||
$userId=(int)($_POST['user_id']??0);$status=(int)($_POST['status']??0)===1?1:0;$pdo=db();$pdo->prepare('UPDATE users SET status=?,updated_at=? WHERE id=?')->execute([$status,now(),$userId]);admin_audit($pdo,'user_status','user',$userId,null,['status'=>$status]);flash('success',$status?'账号已启用。':'账号已停用。');
|
||||
} elseif ($action === 'create_cards') {
|
||||
$amount = round((float)($_POST['amount'] ?? 0), 2);
|
||||
$cardType = (string)($_POST['card_type'] ?? 'single');
|
||||
if (!in_array($cardType, ['single', 'shared'], true)) throw new RuntimeException('充值卡类型无效。');
|
||||
$count = $cardType === 'shared' ? 1 : min(100, max(1, (int)($_POST['count'] ?? 1)));
|
||||
if ($amount <= 0 || $amount > 100000) throw new RuntimeException('卡面额无效。');
|
||||
$pdo = db();
|
||||
$codes = [];
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$stmt = $pdo->prepare('INSERT INTO recharge_cards(code,amount,card_type,status,created_at) VALUES(?,?,?,?,?)');
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$code = ($cardType === 'shared' ? 'GX' : 'JG').strtoupper(bin2hex(random_bytes(6)));
|
||||
$stmt->execute([$code, $amount, $cardType, $cardType === 'shared' ? 'active' : 'unused', now()]);
|
||||
$codes[] = $code;
|
||||
}
|
||||
admin_audit($pdo, 'cards_create', 'recharge_card', '', ['amount'=>$amount, 'count'=>$count, 'card_type'=>$cardType]);
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
$_SESSION['generated_card_codes'] = $codes;
|
||||
flash('success', $cardType === 'shared' ? '已生成公共充值卡,每个账号可兑换一次。' : "已生成 {$count} 张充值卡,并自动选中本批卡号。");
|
||||
} elseif ($action === 'toggle_shared_card') {
|
||||
$cardId = (int)($_POST['id'] ?? 0);
|
||||
$status = (string)($_POST['status'] ?? '');
|
||||
if (!in_array($status, ['active', 'disabled'], true)) throw new RuntimeException('公共卡状态无效。');
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("UPDATE recharge_cards SET status=? WHERE id=? AND card_type='shared'");
|
||||
$stmt->execute([$status, $cardId]);
|
||||
if ($stmt->rowCount() !== 1) throw new RuntimeException('公共充值卡不存在。');
|
||||
admin_audit($pdo, 'shared_card_status', 'recharge_card', $cardId, null, ['status'=>$status]);
|
||||
flash('success', $status === 'active' ? '公共充值卡已启用。' : '公共充值卡已停用。');
|
||||
} elseif ($action === 'game_settings') {
|
||||
$target=max(1.0,min(200.0,(float)($_POST['target_rtp']??92)));$window=max(100,min(100000,(int)($_POST['rtp_window']??1000)));$sizeRate=max(0.0,min(100.0,(float)($_POST['size_win_rate']??46)));$luckyEatRate=round(max(0.0,min(100.0,(float)($_POST['lucky_eat_rate']??2))),4);
|
||||
$values=['target_rtp'=>(string)$target,'rtp_window'=>(string)$window,'size_win_rate'=>(string)$sizeRate,'lucky_eat_rate'=>(string)$luckyEatRate];
|
||||
foreach(['bar','77','xx','xg','ld','nm','jz','pg'] as $key){foreach(['low','high'] as $level){$field='odds_'.$key.'_'.$level;$value=round((float)($_POST[$field]??0),4);if($value<0||$value>1000)throw new RuntimeException('普通赔率需在 0-1000 之间。');$values[$field]=(string)$value;}}
|
||||
for($position=1;$position<=24;$position++){$field='normal_weight_'.$position;$value=round((float)($_POST[$field]??1),4);if($value<0.01||$value>1000)throw new RuntimeException('普通格子权重需在 0.01-1000 之间。');$values[$field]=(string)$value;}
|
||||
$totalRate=0.0;$specialRtp=0.0;foreach(['xsy','dsy','dsx','tnsh','tlbb','shuaidao','huoche','dmg'] as $key){$rate=round((float)($_POST['special_'.$key.'_rate']??0),4);$odds=round((float)($_POST['special_'.$key.'_odds']??0),4);if($rate<0||$rate>100)throw new RuntimeException('特殊玩法触发率需在 0%-100% 之间。');if($odds<0||$odds>1000)throw new RuntimeException('特殊玩法倍数需在 0-1000 之间。');$values['special_'.$key.'_rate']=(string)$rate;$values['special_'.$key.'_odds']=(string)$odds;$totalRate+=$rate;$specialRtp+=$rate*$odds;}
|
||||
if($totalRate+$luckyEatRate>=100)throw new RuntimeException('LUCKY 通吃率与特殊玩法总触发率之和必须小于 100%。');if($specialRtp>$target+0.0001)throw new RuntimeException('特殊玩法理论 RTP '.number_format($specialRtp,2).'% 已超过目标 RTP,请降低触发率或倍数。');
|
||||
$pdo=db();$before=settings_map();game_probability_model(array_replace($before,$values));$driver=$pdo->getAttribute(PDO::ATTR_DRIVER_NAME);$sql=$driver==='mysql'?'INSERT INTO settings(`key`,`value`) VALUES(?,?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)':'INSERT INTO settings(`key`,`value`) VALUES(?,?) ON CONFLICT(`key`) DO UPDATE SET `value`=excluded.`value`';$pdo->beginTransaction();try{$stmt=$pdo->prepare($sql);foreach($values as $key=>$value)$stmt->execute([$key,$value]);admin_audit($pdo,'game_settings','settings','game',array_intersect_key($before,$values),$values);$pdo->commit();}catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}flash('success','玩法概率、格子权重与 RTP 设置已生效。');
|
||||
} elseif ($action === 'settings') {
|
||||
$inviteReward=round((float)($_POST['invite_reward']??0),2);if($inviteReward<0||$inviteReward>1000000)throw new RuntimeException('单人邀请奖励需在 0-1000000 之间。');$_POST['invite_reward']=(string)$inviteReward;
|
||||
foreach(['direct_commission_rate','second_commission_rate'] as $rateKey){$rate=round((float)($_POST[$rateKey]??0),4);if($rate<0||$rate>100)throw new RuntimeException('充值佣金比例需在 0%-100% 之间。');$_POST[$rateKey]=(string)$rate;}
|
||||
$keys=['site_name','announcement','customer_service','recharge_instructions','register_bonus','invite_reward','direct_commission_rate','second_commission_rate','game_enabled'];$pdo=db();$before=settings_map();$driver=$pdo->getAttribute(PDO::ATTR_DRIVER_NAME);$sql=$driver==='mysql'?'INSERT INTO settings(`key`,`value`) VALUES(?,?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)':'INSERT INTO settings(`key`,`value`) VALUES(?,?) ON CONFLICT(`key`) DO UPDATE SET `value`=excluded.`value`';$stmt=$pdo->prepare($sql);$after=[];foreach($keys as $key){$after[$key]=mb_substr(trim((string)($_POST[$key]??'')),0,1000);$stmt->execute([$key,$after[$key]]);}admin_audit($pdo,'site_settings','settings','site',array_intersect_key($before,$after),$after);flash('success','站点设置已保存。');
|
||||
} elseif ($action === 'admin_password') {
|
||||
$admin=current_admin();$pdo=db();$stmt=$pdo->prepare('SELECT password_hash FROM admins WHERE id=?');$stmt->execute([$admin['id']]);$hash=(string)$stmt->fetchColumn();$old=(string)($_POST['old_password']??'');$new=(string)($_POST['new_password']??'');if(!password_verify($old,$hash))throw new RuntimeException('原密码不正确。');if(strlen($new)<12||strlen($new)>72)throw new RuntimeException('新密码需为 12-72 位。');$pdo->prepare('UPDATE admins SET password_hash=? WHERE id=?')->execute([password_hash($new,PASSWORD_DEFAULT),$admin['id']]);admin_audit($pdo,'admin_password','admin',(int)$admin['id']);flash('success','管理员密码已更新。');
|
||||
} else throw new RuntimeException('无效的操作。');
|
||||
} catch(Throwable $e){flash('error',$e instanceof RuntimeException?$e->getMessage():'操作失败,请稍后重试。');}
|
||||
redirect($return);
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
$admin = require_admin();
|
||||
$logs = db()->query('SELECT l.*,a.username AS admin_name FROM admin_audit_logs l JOIN admins a ON a.id=l.admin_id ORDER BY l.id DESC LIMIT 300')->fetchAll();
|
||||
?><!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>操作审计 - 管理后台</title><link rel="stylesheet" href="/assets/app.css?v=20260721"><link rel="stylesheet" href="/assets/admin.css?v=20260721"></head><body><div class="admin-layout"><aside class="admin-side"><a class="admin-brand" href="/admin/index.php"><span>◆</span><?=e(setting('site_name'))?></a><nav><a href="/admin/index.php">数据概览</a><a href="/admin/index.php?view=users">用户管理</a><a href="/admin/index.php?view=recharges">充值审核</a><a href="/admin/index.php?view=withdrawals">提现审核</a><a href="/admin/index.php?view=cards">充值卡</a><a href="/admin/index.php?view=referrals">邀请记录</a><a href="/admin/index.php?view=game">玩法与RTP</a><a class="active" href="/admin/audits.php">操作审计</a><a href="/admin/index.php?view=settings">系统设置</a></nav><footer><span>当前管理员:<?=e($admin['username'])?></span><a href="/index.php" target="_blank">查看用户端 ↗</a></footer></aside><div class="admin-main"><header class="admin-top"><h1>操作审计</h1><span><?=date('Y-m-d H:i')?></span></header><main class="admin-body"><section class="panel"><div class="panel-head"><div><h2>管理员操作记录</h2><p>最近 300 条关键操作,仅记录必要变更信息,不记录密码</p></div></div><div class="table-wrap"><table><thead><tr><th>时间</th><th>管理员</th><th>操作</th><th>对象</th><th>来源 IP</th><th>变更结果</th></tr></thead><tbody><?php foreach($logs as $log):?><tr><td><?=e($log['created_at'])?></td><td><?=e($log['admin_name'])?></td><td class="mono"><?=e($log['action'])?></td><td><?=e(trim($log['target_type'].' '.$log['target_id']))?></td><td class="mono"><?=e($log['ip_address']?:'-')?></td><td class="admin-note" title="<?=e($log['after_json']??'')?>"><?=e(mb_substr((string)($log['after_json']??'-'),0,100))?></td></tr><?php endforeach;?><?php if(!$logs):?><tr><td colspan="6" class="empty-cell">暂无审计记录</td></tr><?php endif;?></tbody></table></div></section></main></div></div></body></html>
|
||||
@@ -0,0 +1,7 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>玩法与RTP - 管理后台</title><link rel="stylesheet" href="/assets/app.css?v=20260721"><link rel="stylesheet" href="/assets/admin.css?v=20260721"><link rel="stylesheet" href="/assets/game-admin.css?v=20260722-weights"></head><body><div class="admin-layout"><aside class="admin-side"><a class="admin-brand" href="/admin/index.php"><span>◆</span><?=e(setting('site_name'))?></a><nav><?php foreach(['dashboard'=>'数据概览','users'=>'用户管理','recharges'=>'充值审核','withdrawals'=>'提现审核','cards'=>'充值卡','referrals'=>'邀请记录','game'=>'玩法与RTP','settings'=>'系统设置'] as $key=>$name):?><a class="<?=$key==='game'?'active':''?>" href="/admin/index.php?view=<?=$key?>"><?=e($name)?></a><?php endforeach;?></nav><footer><span>当前管理员:<?=e($admin['username'])?></span><a href="/index.php" target="_blank">查看用户端 ↗</a><form method="post" action="/admin/action.php"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="logout"><button class="mini-btn">退出登录</button></form></footer></aside><div class="admin-main"><header class="admin-top"><h1>玩法与 RTP</h1><span><?=date('Y-m-d H:i')?></span></header><main class="admin-body"><?php if($flash):?><div class="alert <?=e($flash['type'])?>"><?=e($flash['message'])?></div><?php endif;?>
|
||||
<section class="rtp-metrics"><article><span>实际 RTP</span><strong><?=$gameStats['rtp']===null?'--':number_format($gameStats['rtp']*100,2).'%'?></strong><small><?=$gameStats['rounds']?> 局</small></article><article><span>目标 RTP</span><strong><?=number_format((float)($gameSettings['target_rtp']??92),2)?>%</strong><small>窗口 <?=number_format((int)($gameSettings['rtp_window']??1000))?> 局</small></article><article><span>窗口投入</span><strong>¥<?=number_format($gameStats['bets'],2)?></strong><small>派彩 ¥<?=number_format($gameStats['wins'],2)?></small></article><article><span>特殊理论 RTP</span><strong><?=number_format(game_theoretical_special_rtp($gameSettings),2)?>%</strong><small>触发率 <?=number_format(array_sum(array_column($specialDefinitions,'rate')),2)?>%</small></article></section>
|
||||
<form method="post" action="/admin/action.php" class="game-config"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="game_settings"><input type="hidden" name="return" value="/admin/index.php?view=game">
|
||||
<section class="panel"><div class="panel-head"><div><h2>RTP 控制</h2><p>普通开奖根据窗口实际 RTP 动态校准</p></div></div><div class="config-row"><label><span>目标 RTP (%)</span><input type="number" name="target_rtp" min="1" max="200" step="0.01" value="<?=e($gameSettings['target_rtp']??92)?>" required></label><label><span>统计窗口 (局)</span><input type="number" name="rtp_window" min="100" max="100000" step="1" value="<?=e($gameSettings['rtp_window']??1000)?>" required></label><label><span>LUCKY 通吃率 (%)</span><input type="number" name="lucky_eat_rate" min="0" max="100" step="0.0001" value="<?=e($gameSettings['lucky_eat_rate']??2)?>" required></label><label><span>比大小胜率 (%)</span><input type="number" name="size_win_rate" min="0" max="100" step="0.01" value="<?=e($gameSettings['size_win_rate']??46)?>" required></label></div></section>
|
||||
<section class="panel"><div class="panel-head"><div><h2>普通图案赔率</h2><p>大图案与小图案格</p></div></div><div class="table-wrap"><table class="odds-table"><thead><tr><th>图案</th><th>小图案倍数</th><th>大图案倍数</th></tr></thead><tbody><?php foreach(['bar'=>'BAR','77'=>'77','xx'=>'双星','xg'=>'西瓜','ld'=>'铃铛','nm'=>'柠檬','jz'=>'橘子','pg'=>'苹果'] as $key=>$label):?><tr><td><b><?=$label?></b></td><td><input type="number" name="odds_<?=$key?>_low" min="0" max="1000" step="0.01" value="<?=e($gameSettings['odds_'.$key.'_low']??0)?>" required></td><td><input type="number" name="odds_<?=$key?>_high" min="0" max="1000" step="0.01" value="<?=e($gameSettings['odds_'.$key.'_high']??0)?>" required></td></tr><?php endforeach;?></tbody></table></div></section>
|
||||
<section class="panel"><div class="panel-head"><div><h2>普通格子出现权重</h2><p>开奖结果与本局押注无关;抽中 LUCKY 时进入通吃或特殊派彩</p></div><span class="probability-summary">当前校准 RTP <?=number_format($gameProbabilityModel['target_rtp']*100,2)?>% · LUCKY通吃 <?=number_format($gameProbabilityModel['lucky_probability']*100,2)?>% · LUCKY派彩 <?=number_format($gameProbabilityModel['special_rate']*100,2)?>%</span></div><div class="table-wrap"><table class="odds-table position-table"><thead><tr><th>位置</th><th>图案</th><th>赔率</th><th>相对权重</th><th>当前单局概率</th><th>平均出现</th></tr></thead><tbody><?php foreach($gameBoard as $position=>$cell): $probability=(float)$gameProbabilityModel['positions'][$position]['probability']; ?><tr><td class="mono">#<?=$position?></td><td><b><?=e($cell['name'])?></b></td><td><?=$cell['multiplier']>0?number_format((float)$cell['multiplier'],2).'x':'-'?></td><td><input type="number" name="normal_weight_<?=$position?>" min="0.01" max="1000" step="0.01" value="<?=e($gameSettings['normal_weight_'.$position]??'1')?>" required></td><td><?=number_format($probability*100,4)?>%</td><td><?=$probability>0?'约 '.number_format(1/$probability,1).' 局':'-'?></td></tr><?php endforeach;?></tbody></table></div></section>
|
||||
<section class="panel"><div class="panel-head"><div><h2>特殊玩法</h2><p>触发率为每局独立概率,倍数按本局总投入计算</p></div></div><div class="table-wrap"><table class="odds-table"><thead><tr><th>玩法</th><th>触发率 (%)</th><th>奖励倍数</th><th>理论 RTP 贡献</th></tr></thead><tbody><?php foreach($specialDefinitions as $special):?><tr><td><b><?=e($special['name'])?></b></td><td><input type="number" name="special_<?=e($special['key'])?>_rate" min="0" max="100" step="0.0001" value="<?=e($special['rate'])?>" required></td><td><input type="number" name="special_<?=e($special['key'])?>_odds" min="0" max="1000" step="0.01" value="<?=e($special['odds'])?>" required></td><td><?=number_format($special['rate']*$special['odds'],2)?>%</td></tr><?php endforeach;?></tbody></table></div><div class="config-submit"><button class="btn primary" type="submit">保存并立即生效</button></div></section></form></main></div></div><script src="/assets/app.js?v=20260722-cards2"></script></body></html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
require dirname(__DIR__) . '/includes/game_engine.php';
|
||||
$admin=require_admin();$view=(string)($_GET['view']??'dashboard');$allowed=['dashboard','users','recharges','withdrawals','cards','referrals','game','settings'];if(!in_array($view,$allowed,true))$view='dashboard';$flash=take_flash();$pdo=db();
|
||||
$stats=['users'=>(int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(),'pending'=>(int)$pdo->query("SELECT COUNT(*) FROM recharge_orders WHERE status='pending'")->fetchColumn(),'balance'=>(float)$pdo->query('SELECT COALESCE(SUM(balance),0) FROM users')->fetchColumn(),'today'=>(float)$pdo->query("SELECT COALESCE(SUM(amount),0) FROM recharge_orders WHERE status='approved' AND reviewed_at >= '".date('Y-m-d 00:00:00')."'")->fetchColumn()];
|
||||
$users=$orders=$cards=$withdrawals=$referrals=[];
|
||||
$generatedCardCodes = [];
|
||||
if ($view === 'cards' && isset($_SESSION['generated_card_codes']) && is_array($_SESSION['generated_card_codes'])) {
|
||||
$generatedCardCodes = array_values(array_filter($_SESSION['generated_card_codes'], 'is_string'));
|
||||
unset($_SESSION['generated_card_codes']);
|
||||
}
|
||||
if($view==='users'){$q=trim((string)($_GET['q']??''));if($q!==''){$stmt=$pdo->prepare('SELECT * FROM users WHERE username LIKE ? OR id = ? ORDER BY id DESC LIMIT 100');$stmt->execute(['%'.$q.'%',(int)$q]);$users=$stmt->fetchAll();}else $users=$pdo->query('SELECT * FROM users ORDER BY id DESC LIMIT 100')->fetchAll();}
|
||||
if(in_array($view,['dashboard','recharges'],true)){$orders=$pdo->query('SELECT r.*,u.username FROM recharge_orders r JOIN users u ON u.id=r.user_id ORDER BY r.id DESC LIMIT 100')->fetchAll();}
|
||||
if($view==='cards')$cards=$pdo->query('SELECT c.*,u.username,(SELECT COUNT(*) FROM recharge_card_redemptions r WHERE r.card_id=c.id) AS redemption_count FROM recharge_cards c LEFT JOIN users u ON u.id=c.used_by ORDER BY c.id DESC LIMIT 100')->fetchAll();
|
||||
if($view==='withdrawals')$withdrawals=$pdo->query('SELECT w.*,u.username FROM withdrawal_orders w JOIN users u ON u.id=w.user_id ORDER BY w.id DESC LIMIT 100')->fetchAll();
|
||||
if($view==='referrals')$referrals=$pdo->query('SELECT r.*,i.username AS inviter_name,e.username AS invitee_name FROM referral_rewards r JOIN users i ON i.id=r.inviter_id JOIN users e ON e.id=r.invitee_id ORDER BY r.id DESC LIMIT 100')->fetchAll();
|
||||
$referralStats=['rewarded'=>(int)$pdo->query("SELECT COUNT(*) FROM referral_rewards WHERE status='rewarded'")->fetchColumn(),'rejected'=>(int)$pdo->query("SELECT COUNT(*) FROM referral_rewards WHERE status='rejected'")->fetchColumn(),'amount'=>(float)$pdo->query("SELECT COALESCE(SUM(reward_amount),0) FROM referral_rewards WHERE status='rewarded'")->fetchColumn()];
|
||||
$titles=['dashboard'=>'数据概览','users'=>'用户管理','recharges'=>'充值审核','withdrawals'=>'提现审核','cards'=>'充值卡','referrals'=>'邀请记录','game'=>'玩法与RTP','settings'=>'系统设置'];$statusNames=['pending'=>'待审核','approved'=>'已到账','rejected'=>'已拒绝','reversed'=>'已冲正','rewarded'=>'已奖励','qualified'=>'关系有效'];
|
||||
$gameSettings=settings_map();$gameStats=game_window_stats($pdo,(int)($gameSettings['rtp_window']??1000));$specialDefinitions=game_special_definitions($gameSettings);$gameBoard=game_board($gameSettings);$gameProbabilityModel=game_effective_probability_model($gameSettings,$gameStats);
|
||||
if($view==='game'){require __DIR__.'/game.php';exit;}
|
||||
?><!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title><?=e($titles[$view])?> - 管理后台</title><link rel="stylesheet" href="/assets/app.css?v=20260721"><link rel="stylesheet" href="/assets/admin.css?v=20260721"></head><body><div class="admin-layout"><aside class="admin-side"><a class="admin-brand" href="/admin/index.php"><span>◆</span><?=e(setting('site_name'))?></a><nav><?php foreach(['dashboard'=>'数据概览','users'=>'用户管理','recharges'=>'充值审核','withdrawals'=>'提现审核','cards'=>'充值卡','referrals'=>'邀请记录','game'=>'玩法与RTP','settings'=>'系统设置'] as $k=>$name):?><a class="<?=$view===$k?'active':''?>" href="/admin/index.php?view=<?=$k?>"><?=e($name)?></a><?php endforeach;?></nav><footer><span>当前管理员:<?=e($admin['username'])?></span><a href="/index.php" target="_blank">查看用户端 ↗</a><form method="post" action="/admin/action.php"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="logout"><button class="mini-btn">退出登录</button></form></footer></aside><div class="admin-main"><header class="admin-top"><h1><?=e($titles[$view])?></h1><span><?=date('Y-m-d H:i')?></span></header><main class="admin-body"><?php if($flash):?><div class="alert <?=e($flash['type'])?>"><?=e($flash['message'])?></div><?php endif;?>
|
||||
<?php if($view==='dashboard'):?><section class="admin-stats"><article><span>注册用户</span><strong><?=$stats['users']?></strong></article><article><span>待审核充值</span><strong><?=$stats['pending']?></strong></article><article><span>用户余额合计</span><strong>¥<?=number_format($stats['balance'],2)?></strong></article><article><span>今日到账</span><strong>¥<?=number_format($stats['today'],2)?></strong></article></section><section class="panel"><div class="panel-head"><div><h2>最新充值订单</h2><p>优先处理待审核订单</p></div><a href="?view=recharges">全部订单</a></div><?=render_orders($orders,$statusNames)?></section>
|
||||
<?php elseif($view==='users'):?><section class="panel"><div class="panel-head"><div><h2>用户列表</h2><p>最近 100 个账号</p></div><form class="searchbar"><input type="hidden" name="view" value="users"><input name="q" value="<?=e($_GET['q']??'')?>" placeholder="搜索账号或 ID"><button class="mini-btn">搜索</button></form></div><div class="table-wrap"><table><thead><tr><th>ID</th><th>账号</th><th>手机</th><th>余额</th><th>注册时间</th><th>状态</th><th>余额调整</th><th>操作</th></tr></thead><tbody><?php foreach($users as $u):?><tr><td><?=(int)$u['id']?></td><td><b><?=e($u['username'])?></b></td><td><?=e($u['phone']?:'-')?></td><td>¥<?=number_format((float)$u['balance'],2)?></td><td><?=e($u['created_at'])?></td><td><span class="badge <?= (int)$u['status']===1?'approved':'rejected' ?>"><?= (int)$u['status']===1?'正常':'停用' ?></span></td><td><form class="inline-form" method="post" action="/admin/action.php"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="adjust_balance"><input type="hidden" name="return" value="/admin/index.php?view=users"><input type="hidden" name="user_id" value="<?=(int)$u['id']?>"><input type="number" step="0.01" name="amount" placeholder="+/- 金额" required><button class="mini-btn">确定</button></form></td><td><form method="post" action="/admin/action.php"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="toggle_user"><input type="hidden" name="return" value="/admin/index.php?view=users"><input type="hidden" name="user_id" value="<?=(int)$u['id']?>"><input type="hidden" name="status" value="<?= (int)$u['status']===1?0:1 ?>"><button class="mini-btn <?= (int)$u['status']===1?'reject':'approve' ?>"><?= (int)$u['status']===1?'停用':'启用' ?></button></form></td></tr><?php endforeach;?></tbody></table></div></section>
|
||||
<?php elseif($view==='recharges'):?><section class="panel"><div class="panel-head"><div><h2>充值订单</h2><p>审核通过后系统自动增加用户余额</p></div></div><?=render_orders($orders,$statusNames)?></section>
|
||||
<?php elseif($view==='withdrawals'):?><section class="panel"><div class="panel-head"><div><h2>提现订单</h2><p>用户提交时已冻结余额,拒绝后自动退回</p></div></div><div class="table-wrap"><table><thead><tr><th>ID</th><th>用户</th><th>金额</th><th>收款账号</th><th>姓名</th><th>时间</th><th>状态</th><th>操作</th></tr></thead><tbody><?php foreach($withdrawals as $w):?><tr><td>#<?=(int)$w['id']?></td><td><?=e($w['username'])?></td><td><b>¥<?=number_format((float)$w['amount'],2)?></b></td><td><?=e($w['account'])?></td><td><?=e($w['real_name'])?></td><td><?=e($w['created_at'])?></td><td><span class="badge <?=e($w['status'])?>"><?=e($statusNames[$w['status']]??$w['status'])?></span></td><td><?php if($w['status']==='pending'):?><div class="admin-actions"><?php foreach(['approved'=>'通过','rejected'=>'拒绝'] as $decision=>$label):?><form method="post" action="/admin/action.php"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="review_withdrawal"><input type="hidden" name="return" value="/admin/index.php?view=withdrawals"><input type="hidden" name="id" value="<?=(int)$w['id']?>"><input type="hidden" name="decision" value="<?=$decision?>"><button class="mini-btn <?=$decision==='approved'?'approve':'reject'?>"><?=$label?></button></form><?php endforeach;?></div><?php else:?>-<?php endif;?></td></tr><?php endforeach;?><?php if(!$withdrawals):?><tr><td colspan="8" class="empty-cell">暂无提现订单</td></tr><?php endif;?></tbody></table></div></section>
|
||||
<?php elseif($view==='cards'): ?>
|
||||
<div class="settings-grid">
|
||||
<section class="panel"><h2>生成充值卡</h2><form method="post" action="/admin/action.php" class="form-stack" data-card-create-form><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="create_cards"><input type="hidden" name="return" value="/admin/index.php?view=cards"><label><span>卡类型</span><select name="card_type" data-card-type><option value="single">普通一次性卡</option><option value="shared">公共卡(每账号一次)</option></select></label><label><span>卡面额</span><input type="number" name="amount" min="1" max="100000" step="0.01" required></label><label><span>生成数量</span><input type="number" name="count" min="1" max="100" value="10" required data-card-count></label><button class="btn primary">生成充值卡</button></form></section>
|
||||
<section class="panel"><h2>使用说明</h2><p class="card-note">普通卡仅能被一个用户使用一次。公共卡可由所有用户各兑换一次,并可随时停用。请通过安全渠道发放卡号。</p></section>
|
||||
</div>
|
||||
<section class="panel card-manager" data-card-manager>
|
||||
<div class="panel-head"><div><h2>充值卡列表</h2><p>最近 100 张,可复制或导出当前可用卡</p></div><div class="card-batch-actions"><button type="button" class="mini-btn" data-card-select-all>全选可用卡</button><button type="button" class="mini-btn approve" data-card-copy disabled>复制所选</button><button type="button" class="mini-btn" data-card-export disabled>导出 CSV</button><span data-card-selected-count>已选 0 张</span></div></div>
|
||||
<div class="table-wrap"><table><thead><tr><th class="card-check"><input type="checkbox" data-card-toggle-all aria-label="全选可用充值卡" title="全选可用充值卡"></th><th>卡号</th><th>类型</th><th>面额</th><th>状态</th><th>使用情况</th><th>生成时间</th><th>使用时间</th><th>操作</th></tr></thead><tbody>
|
||||
<?php foreach($cards as $c): $isShared=($c['card_type']??'single')==='shared'; $selectable=$isShared?$c['status']==='active':$c['status']==='unused'; $selected=$selectable&&in_array((string)$c['code'],$generatedCardCodes,true); $statusLabel=$isShared?($c['status']==='active'?'启用':'停用'):($c['status']==='unused'?'未使用':'已使用'); ?>
|
||||
<tr class="<?=$selected?'card-new-batch':''?>"><td class="card-check"><input type="checkbox" class="card-select" <?=$selectable?'':'disabled'?> <?=$selected?'checked':''?> data-card-code="<?=e($c['code'])?>" data-card-type="<?=$isShared?'公共卡':'普通卡'?>" data-card-amount="<?=e(number_format((float)$c['amount'],2,'.',''))?>" data-card-status="<?=e($statusLabel)?>" data-card-created="<?=e($c['created_at'])?>" aria-label="选择卡号 <?=e($c['code'])?>"></td><td class="mono"><b><?=e($c['code'])?></b></td><td><span class="badge <?=$isShared?'shared':'pending'?>"><?=$isShared?'公共卡':'普通卡'?></span></td><td>¥<?=number_format((float)$c['amount'],2)?></td><td><span class="badge <?=$selectable?'approved':'rejected'?>"><?=e($statusLabel)?></span></td><td><?=$isShared?(int)$c['redemption_count'].' 人':e($c['username']??'-')?></td><td><?=e($c['created_at'])?></td><td><?=$isShared?'-':e($c['used_at']??'-')?></td><td><?php if($isShared): ?><form method="post" action="/admin/action.php"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="toggle_shared_card"><input type="hidden" name="return" value="/admin/index.php?view=cards"><input type="hidden" name="id" value="<?=(int)$c['id']?>"><input type="hidden" name="status" value="<?=$c['status']==='active'?'disabled':'active'?>"><button class="mini-btn <?=$c['status']==='active'?'reject':'approve'?>"><?=$c['status']==='active'?'停用':'启用'?></button></form><?php else: ?>-<?php endif; ?></td></tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if(!$cards): ?><tr><td colspan="9" class="empty-cell">暂无充值卡</td></tr><?php endif; ?>
|
||||
</tbody></table></div>
|
||||
</section>
|
||||
<?php elseif($view==='referrals'):?><section class="admin-stats"><article><span>成功奖励</span><strong><?=$referralStats['rewarded']?></strong></article><article><span>IP 拦截</span><strong><?=$referralStats['rejected']?></strong></article><article><span>累计发放</span><strong>¥<?=number_format($referralStats['amount'],2)?></strong></article><article><span>单人奖励</span><strong>¥<?=number_format((float)setting('invite_reward','0'),2)?></strong></article></section><section class="panel"><div class="panel-head"><div><h2>邀请奖励记录</h2><p>同 IP 自邀或重复领取会自动拦截</p></div></div><div class="table-wrap"><table><thead><tr><th>邀请人</th><th>新用户</th><th>注册 IP</th><th>奖励</th><th>状态</th><th>原因</th><th>时间</th></tr></thead><tbody><?php foreach($referrals as $r):?><tr><td><b><?=e($r['inviter_name'])?></b> (#<?=(int)$r['inviter_id']?>)</td><td><?=e($r['invitee_name'])?> (#<?=(int)$r['invitee_id']?>)</td><td class="mono"><?=e($r['invitee_ip']?:'-')?></td><td>¥<?=number_format((float)$r['reward_amount'],2)?></td><td><span class="badge <?=e($r['status'])?>"><?=e($statusNames[$r['status']]??$r['status'])?></span></td><td><?=e($r['reason']?:'-')?></td><td><?=e($r['created_at'])?></td></tr><?php endforeach;?><?php if(!$referrals):?><tr><td colspan="7" class="empty-cell">暂无邀请记录</td></tr><?php endif;?></tbody></table></div></section>
|
||||
<?php else:?><div class="settings-grid"><section class="panel"><h2>站点参数</h2><form method="post" action="/admin/action.php" class="form-stack"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="settings"><input type="hidden" name="return" value="/admin/index.php?view=settings"><label><span>站点名称</span><input name="site_name" value="<?=e(setting('site_name'))?>" required></label><label><span>站点公告</span><input name="announcement" value="<?=e(setting('announcement'))?>"></label><label><span>客服微信</span><input name="customer_service" value="<?=e(setting('customer_service'))?>"></label><label><span>充值说明</span><input name="recharge_instructions" value="<?=e(setting('recharge_instructions'))?>"></label><label><span>注册赠送余额</span><input type="number" min="0" step="0.01" name="register_bonus" value="<?=e(setting('register_bonus'))?>"></label><label><span>单人邀请奖励</span><input type="number" min="0" max="1000000" step="0.01" name="invite_reward" value="<?=e(setting('invite_reward','100'))?>" required></label><label><span>一级下线充值佣金 (%)</span><input type="number" min="0" max="100" step="0.0001" name="direct_commission_rate" value="<?=e(setting('direct_commission_rate','10'))?>" required></label><label><span>二级下线充值佣金 (%)</span><input type="number" min="0" max="100" step="0.0001" name="second_commission_rate" value="<?=e(setting('second_commission_rate','1'))?>" required></label><label><span>游戏开关</span><select name="game_enabled"><option value="1" <?=setting('game_enabled')==='1'?'selected':''?>>开启</option><option value="0" <?=setting('game_enabled')==='0'?'selected':''?>>关闭</option></select></label><button class="btn primary">保存站点设置</button></form></section><section class="panel"><h2>管理员密码</h2><form method="post" action="/admin/action.php" class="form-stack"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="admin_password"><input type="hidden" name="return" value="/admin/index.php?view=settings"><label><span>原密码</span><input type="password" name="old_password" required></label><label><span>新密码</span><input type="password" name="new_password" minlength="10" required></label><button class="btn primary">更新管理员密码</button></form></section></div><?php endif;?></main></div></div><script src="/assets/app.js?v=20260722-cards2"></script></body></html>
|
||||
<?php
|
||||
function render_orders(array $orders, array $statusNames): string
|
||||
{
|
||||
ob_start(); ?>
|
||||
<div class="table-wrap"><table><thead><tr><th>订单号</th><th>用户</th><th>金额</th><th>方式</th><th>付款备注</th><th>时间</th><th>状态</th><th>操作</th></tr></thead><tbody>
|
||||
<?php foreach($orders as $o): ?><tr>
|
||||
<td class="mono"><?=e($o['order_no'])?></td><td><?=e($o['username'])?> (#<?=(int)$o['user_id']?>)</td><td><b>¥<?=number_format((float)$o['amount'],2)?></b></td><td><?=e($o['method'])?></td><td class="admin-note" title="<?=e($o['payer_note'])?>"><?=e($o['payer_note']?:'-')?></td><td><?=e($o['created_at'])?></td><td><span class="badge <?=e($o['status'])?>"><?=e($statusNames[$o['status']]??$o['status'])?></span></td>
|
||||
<td><?php if($o['status']==='pending'): ?><div class="admin-actions">
|
||||
<?php foreach(['approved'=>'通过','rejected'=>'拒绝'] as $decision=>$label): ?><form method="post" action="/admin/action.php"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="review_recharge"><input type="hidden" name="return" value="/admin/index.php?view=recharges"><input type="hidden" name="id" value="<?=(int)$o['id']?>"><input type="hidden" name="decision" value="<?=$decision?>"><button class="mini-btn <?=$decision==='approved'?'approve':'reject'?>"><?=$label?></button></form><?php endforeach; ?>
|
||||
</div><?php elseif($o['status']==='approved'): ?><form class="inline-form" method="post" action="/admin/action.php" onsubmit="return confirm('确认冲正该笔充值及其推广佣金?')"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="reverse_recharge"><input type="hidden" name="return" value="/admin/index.php?view=recharges"><input type="hidden" name="id" value="<?=(int)$o['id']?>"><input name="note" maxlength="200" placeholder="冲正原因" required><button class="mini-btn reject">冲正</button></form><?php else: ?>-<?php endif; ?></td>
|
||||
</tr><?php endforeach; ?>
|
||||
<?php if(!$orders): ?><tr><td colspan="8" class="empty-cell">暂无订单</td></tr><?php endif; ?></tbody></table></div>
|
||||
<?php return (string) ob_get_clean();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
if (current_admin()) redirect('/admin/index.php');
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
verify_csrf();
|
||||
$username = trim((string)($_POST['username'] ?? ''));
|
||||
assert_auth_not_limited('admin_login', $username, 5, 900);
|
||||
$stmt = db()->prepare('SELECT * FROM admins WHERE username = ?');
|
||||
$stmt->execute([$username]);
|
||||
$admin = $stmt->fetch();
|
||||
if (!$admin || !password_verify((string)($_POST['password'] ?? ''), $admin['password_hash'])) {
|
||||
record_auth_attempt('admin_login', $username, false);
|
||||
throw new RuntimeException('管理员账号或密码错误。');
|
||||
}
|
||||
session_regenerate_id(true); $_SESSION['admin_id'] = (int)$admin['id'];
|
||||
record_auth_attempt('admin_login', $username, true);
|
||||
db()->prepare('UPDATE admins SET last_login_at = ? WHERE id = ?')->execute([now(), $admin['id']]);
|
||||
admin_audit(db(), 'admin_login', 'admin', (int)$admin['id']);
|
||||
redirect('/admin/index.php');
|
||||
} catch (Throwable $e) { $error = $e instanceof RuntimeException ? $e->getMessage() : '登录失败。'; }
|
||||
}
|
||||
?><!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>后台登录</title><link rel="stylesheet" href="/assets/app.css?v=20260721"><link rel="stylesheet" href="/assets/admin.css?v=20260721"></head><body class="admin-login"><main><div class="admin-login-brand"><span>金</span><div><b><?=e(setting('site_name'))?></b><small>运营管理后台</small></div></div><?php if($error):?><div class="alert error"><?=e($error)?></div><?php endif;?><form method="post" class="form-stack"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><label><span>管理员账号</span><input name="username" autocomplete="username" required autofocus></label><label><span>密码</span><input type="password" name="password" autocomplete="current-password" required></label><button class="btn primary">登录后台</button></form><a class="back-site" href="/index.php">← 返回用户端</a></main></body></html>
|
||||
Reference in New Issue
Block a user