Initial commit: 投注游戏平台初始化

This commit is contained in:
li
2026-02-25 01:26:58 +08:00
commit 77ca2cc8b3
275 changed files with 237479 additions and 0 deletions
Vendored Executable
BIN
View File
Binary file not shown.
+274
View File
@@ -0,0 +1,274 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class AdminController extends AdminBaseController {
/**
* 构造函数 - 验证管理员权限
*/
public function __construct() {
$this->checkLogin();
$this->checkAdmin();
}
/**
* 管理员列表页面
*/
public function index() {
$db = new Database();
// 只查询管理员角色的用户
$admins = $db->select('users', '*', [
'role' => 'admin',
'ORDER' => ['id' => 'DESC']
]);
$this->render('Admin/admin.php', [
'admins' => $admins,
'title' => '管理员管理'
]);
}
/**
* 获取单个管理员数据(用于编辑和详情)
*/
public function get($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的管理员ID'
]);
return;
}
$db = new Database();
$admin = $db->get('users', '*', [
'id' => $id,
'role' => 'admin' // 确保只获取管理员
]);
if ($admin) {
// 移除密码字段,避免泄露
unset($admin['password']);
echo json_encode([
'success' => true,
'data' => $admin
]);
} else {
echo json_encode([
'success' => false,
'message' => '管理员不存在'
]);
}
}
public function update() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
echo json_encode([
'success' => false,
'message' => '未接收到数据'
]);
return;
}
$db = new Database();
$id = isset($data['id']) ? (int)$data['id'] : 0;
$isEditMode = $id > 0;
$username = trim($data['username'] ?? '');
$email = trim($data['email'] ?? '');
$status = isset($data['status']) ? (int)$data['status'] : 0;
if (empty($username) || empty($email)) {
echo json_encode([
'success' => false,
'message' => '用户名和邮箱不能为空'
]);
return;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode([
'success' => false,
'message' => '邮箱格式不正确'
]);
return;
}
// 统一查重(排除当前 ID
$exists = $db->get('users', '*', [
'AND' => [
'OR' => [
'username' => $username,
'email' => $email
],
'id[!]' => $id
]
]);
if ($exists) {
if ($exists['username'] === $username) {
$msg = '用户名已存在';
} elseif ($exists['email'] === $email) {
$msg = '邮箱已被使用';
} else {
$msg = '用户名或邮箱已被占用';
}
echo json_encode([
'success' => false,
'message' => $msg
]);
return;
}
// 准备数据 - 强制角色为 admin
$adminData = [
'username' => $username,
'email' => $email,
'role' => 'admin', // 强制设置为管理员角色
'status' => $status,
'updated_at' => date('Y-m-d H:i:s')
];
try {
if ($isEditMode) {
// 验证是否真的是管理员
$existingAdmin = $db->get('users', 'id', [
'id' => $id,
'role' => 'admin'
]);
if (!$existingAdmin) {
echo json_encode([
'success' => false,
'message' => '该用户不是管理员'
]);
return;
}
// 如果密码不为空,更新密码
if (!empty($data['password'])) {
if (strlen($data['password']) < 8) {
echo json_encode([
'success' => false,
'message' => '密码长度至少8位'
]);
return;
}
$adminData['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
}
$db->update('users', $adminData, ['id' => $id]);
$message = '管理员更新成功';
} else {
// 新增时必须有密码
if (empty($data['password']) || strlen($data['password']) < 8) {
echo json_encode([
'success' => false,
'message' => '密码长度至少8位'
]);
return;
}
$adminData['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
$adminData['created_at'] = date('Y-m-d H:i:s');
$id = $db->insert('users', $adminData);
$message = '管理员创建成功';
}
echo json_encode([
'success' => true,
'message' => $message,
'data' => ['id' => $id]
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '操作失败:' . $e->getMessage()
]);
}
}
/**
* 删除管理员
*/
public function delete($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的管理员ID'
]);
return;
}
// 禁止删除自己
session_start();
if ($id == $_SESSION['user_id']) {
echo json_encode([
'success' => false,
'message' => '不能删除当前登录的管理员'
]);
return;
}
$db = new Database();
// 确保只删除管理员角色
$adminExists = $db->get('users', 'id', [
'id' => $id,
'role' => 'admin'
]);
if (!$adminExists) {
echo json_encode([
'success' => false,
'message' => '管理员不存在'
]);
return;
}
// 检查是否还有其他管理员
$adminCount = $db->count('users', [
'role' => 'admin'
]);
if ($adminCount <= 1) {
echo json_encode([
'success' => false,
'message' => '不能删除最后一个管理员'
]);
return;
}
try {
$db->delete('users', [
'id' => $id,
'role' => 'admin'
]);
echo json_encode([
'success' => true,
'message' => '管理员已删除'
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '删除失败:' . $e->getMessage()
]);
}
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class AgentController extends AdminBaseController {
private $db;
public function __construct(Database $db) { $this->db = $db; }
// 后台代理管理
public function index() {
$this->checkLogin(); $this->checkAdmin();
$agents = $this->db->select('agents', '*', ['ORDER' => ['id' => 'DESC']]);
foreach ($agents as &$a) {
$a['user'] = $this->db->get('users', ['username','balance'], ['id' => $a['user_id']]);
$a['player_count'] = $this->db->count('users', ['agent_id' => $a['id']]);
}
$this->render('Admin/agents.php', compact('agents'));
}
public function update() {
$this->checkLogin(); $this->checkAdmin();
$data = json_decode(file_get_contents('php://input'), true);
$id = (int)($data['id'] ?? 0);
if ($id > 0) {
$this->db->update('agents', [
'commission_rate' => floatval($data['commission_rate'] ?? 0),
'rebate_rate' => floatval($data['rebate_rate'] ?? 0),
'status' => (int)($data['status'] ?? 1),
], ['id' => $id]);
} else {
// 创建代理:先创建用户,再创建代理记录
$userId = (int)($data['user_id'] ?? 0);
if (!$userId) { $this->json(['status'=>'error','message'=>'User ID required']); return; }
$code = strtoupper(substr(md5(uniqid()), 0, 8));
$this->db->insert('agents', [
'user_id' => $userId,
'parent_id' => !empty($data['parent_id']) ? (int)$data['parent_id'] : null,
'level' => !empty($data['parent_id']) ? 2 : 1,
'agent_code' => $code,
'commission_rate' => floatval($data['commission_rate'] ?? 0),
'rebate_rate' => floatval($data['rebate_rate'] ?? 0),
'status' => 1,
]);
$this->db->update('users', ['role' => 'agent'], ['id' => $userId]);
}
$this->json(['status' => 'success']);
}
public function delete($id) {
$this->checkLogin(); $this->checkAdmin();
$agent = $this->db->get('agents', '*', ['id' => (int)$id]);
if ($agent) {
$this->db->update('users', ['role' => 'user', 'agent_id' => null], ['agent_id' => $agent['id']]);
$this->db->delete('agents', ['id' => (int)$id]);
}
$this->json(['status' => 'success']);
}
// 代理赔率设置
public function odds($agentId) {
$this->checkLogin(); $this->checkAdmin();
$odds = $this->db->select('agent_odds', '*', ['agent_id' => (int)$agentId]);
$this->json(['status' => 'success', 'data' => $odds]);
}
public function updateOdds() {
$this->checkLogin(); $this->checkAdmin();
$data = json_decode(file_get_contents('php://input'), true);
$agentId = (int)($data['agent_id'] ?? 0);
$odds = $data['odds'] ?? [];
$gameId = (int)($data['game_id'] ?? 0);
foreach ($odds as $o) {
$existing = $this->db->get('agent_odds', 'id', [
'agent_id' => $agentId, 'game_id' => $gameId,
'bet_type' => $o['bet_type'], 'bet_target' => $o['bet_target']
]);
if ($existing) {
$this->db->update('agent_odds', ['odds' => floatval($o['odds'])], ['id' => $existing]);
} else {
$this->db->insert('agent_odds', [
'agent_id' => $agentId, 'game_id' => $gameId,
'bet_type' => $o['bet_type'], 'bet_target' => $o['bet_target'],
'odds' => floatval($o['odds']),
]);
}
}
$this->json(['status' => 'success']);
}
// 佣金记录
public function commissions($agentId) {
$this->checkLogin(); $this->checkAdmin();
$records = $this->db->select('agent_commissions', '*', [
'agent_id' => (int)$agentId, 'ORDER' => ['id' => 'DESC'], 'LIMIT' => 100
]);
$this->json(['status' => 'success', 'data' => $records]);
}
private function json($data) { header('Content-Type: application/json'); echo json_encode($data); }
}
+179
View File
@@ -0,0 +1,179 @@
<?php
namespace App\Controllers\admin;
use \Db\Database;
class AuthController {
public function loginPage($error = '') {
include __DIR__ . '/../../Views/Admin/login.php';
}
public function loginSubmit() {
header('Content-Type: application/json');
$rawData = file_get_contents('php://input');
$data = json_decode($rawData, true);
// 检查 JSON 格式
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode([
'success' => false,
'message' => '提交的数据格式有误'
]);
return;
}
// 提取输入参数
$usernameOrEmail = trim($data['username_or_email'] ?? '');
$password = $data['password'] ?? '';
if (empty($usernameOrEmail) || empty($password)) {
echo json_encode([
'success' => false,
'message' => '请输入用户名/邮箱和密码'
]);
return;
}
$db = new Database();
// 查询用户(匹配用户名或邮箱,忽略邮箱大小写)
$user = $db->get('users', '*', [
'OR' => [
'username' => $usernameOrEmail,
'email' => strtolower($usernameOrEmail)
]
]);
if (!$user) {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
return;
}
// 检查是否禁用(假设 status=0 表示禁用)
if (isset($user['status']) && $user['status'] == 0) {
echo json_encode([
'success' => false,
'message' => '该账户未启用,请联系管理员'
]);
return;
}
// 验证密码
if (!password_verify($password, $user['password'])) {
echo json_encode([
'success' => false,
'message' => '密码错误'
]);
return;
}
// 设置会话
session_start();
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['last_activity'] = time();
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT'];
echo json_encode([
'success' => true,
'message' => '登录成功,正在跳转...',
'redirect' => '/admin/dashboard'
]);
}
public function logout() {
session_start();
session_destroy();
header('Location: /admin/login');
exit;
}
public function registerSubmit() {
header('Content-Type: application/json');
$rawData = file_get_contents('php://input');
$data = json_decode($rawData, true);
$username = trim($data['username'] ?? '');
$email = strtolower(trim($data['email'] ?? '')); // 统一转小写
$password = $data['password'] ?? '';
$confirm = $data['confirm_password'] ?? '';
if (!$username || !$email || !$password || !$confirm) {
echo json_encode([
'success' => false,
'message' => '请完整填写注册信息'
]);
return;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode([
'success' => false,
'message' => '邮箱格式不正确'
]);
return;
}
if ($password !== $confirm) {
echo json_encode([
'success' => false,
'message' => '两次密码输入不一致'
]);
return;
}
$db = new Database();
// 检查用户名或邮箱是否已存在(邮箱忽略大小写)
$existing = $db->get('users', '*', [
'OR' => [
'username' => $username,
'email' => $email
]
]);
if ($existing) {
if ($existing['username'] === $username) {
$msg = '用户名已存在';
} elseif (strtolower($existing['email']) === $email) {
$msg = '邮箱已存在';
} else {
$msg = '用户名或邮箱已存在';
}
echo json_encode([
'success' => false,
'message' => $msg
]);
return;
}
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$insertResult = $db->insert('users', [
'username' => $username,
'email' => $email, // 已转小写
'password' => $hashedPassword,
'role' => 'user', // 默认角色
'created_at' => date('Y-m-d H:i:s'),
]);
if ($insertResult) {
echo json_encode([
'success' => true,
'message' => '注册成功,请登录',
'redirect' => '/admin/login'
]);
} else {
echo json_encode([
'success' => false,
'message' => '注册失败,请稍后重试'
]);
}
}
}
@@ -0,0 +1,174 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
/**
* 自动开期设置控制器
*/
class AutoPeriodController extends AdminBaseController {
private $db;
public function __construct(Database $db) {
$this->db = $db;
}
/**
* 自动开期设置页面
*/
public function index() {
$this->checkLogin();
$this->checkAdmin();
// 获取所有游戏
$games = $this->db->select('games', '*', [
'status' => 1,
'ORDER' => ['id' => 'ASC']
]);
// 获取最近的自动开期日志
$logs = [];
try {
$logs = $this->db->select('auto_period_log', '*', [
'ORDER' => ['id' => 'DESC'],
'LIMIT' => 50
]);
} catch (\Throwable $e) {
// 表可能不存在
}
$this->render('Admin/auto_period.php', compact('games', 'logs'));
}
/**
* 更新游戏的自动开期设置
*/
public function update() {
$this->checkLogin();
$this->checkAdmin();
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data)) $data = $_POST;
$gameId = (int)($data['game_id'] ?? 0);
if ($gameId <= 0) {
echo json_encode(['success' => false, 'message' => '无效的游戏ID']);
return;
}
$game = $this->db->get('games', '*', ['id' => $gameId]);
if (!$game) {
echo json_encode(['success' => false, 'message' => '游戏不存在']);
return;
}
$updateData = [];
// 自动开期开关
if (isset($data['auto_period_enabled'])) {
$updateData['auto_period_enabled'] = (int)$data['auto_period_enabled'] ? 1 : 0;
}
// 每期时长(秒)
if (isset($data['period_duration'])) {
$duration = (int)$data['period_duration'];
if ($duration < 60) $duration = 60; // 最少1分钟
if ($duration > 3600) $duration = 3600; // 最多1小时
$updateData['period_duration'] = $duration;
}
// 封盘提前时间(秒)
if (isset($data['lock_before_end'])) {
$lockTime = (int)$data['lock_before_end'];
if ($lockTime < 5) $lockTime = 5;
if ($lockTime > 120) $lockTime = 120;
$updateData['lock_before_end'] = $lockTime;
}
if (empty($updateData)) {
echo json_encode(['success' => false, 'message' => '无更新内容']);
return;
}
$updateData['updated_at'] = date('Y-m-d H:i:s');
try {
$this->db->update('games', $updateData, ['id' => $gameId]);
echo json_encode([
'success' => true,
'message' => '设置已保存',
'data' => $updateData
]);
} catch (\Throwable $e) {
echo json_encode(['success' => false, 'message' => '保存失败: ' . $e->getMessage()]);
}
}
/**
* 批量启用/禁用
*/
public function toggleAll() {
$this->checkLogin();
$this->checkAdmin();
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$enabled = (int)($data['enabled'] ?? 0);
try {
$this->db->update('games', [
'auto_period_enabled' => $enabled,
'updated_at' => date('Y-m-d H:i:s')
], ['status' => 1]);
echo json_encode([
'success' => true,
'message' => $enabled ? '已全部启用自动开期' : '已全部关闭自动开期'
]);
} catch (\Throwable $e) {
echo json_encode(['success' => false, 'message' => '操作失败: ' . $e->getMessage()]);
}
}
/**
* 获取自动开期运行状态
*/
public function status() {
$this->checkLogin();
header('Content-Type: application/json');
$games = $this->db->select('games', ['id', 'name', 'type', 'auto_period_enabled', 'period_duration', 'lock_before_end'], [
'status' => 1,
'ORDER' => ['id' => 'ASC']
]);
// 每个游戏的当前期号
foreach ($games as &$g) {
$g['current_period'] = $this->db->get('periods', ['id', 'period_number', 'status', 'start_time', 'end_time'], [
'game_id' => $g['id'],
'status' => ['pending', 'locked', 'drawn'],
'ORDER' => ['id' => 'DESC']
]);
}
// 最近日志
$recentLogs = [];
try {
$recentLogs = $this->db->select('auto_period_log', '*', [
'ORDER' => ['id' => 'DESC'],
'LIMIT' => 10
]);
} catch (\Throwable $e) {}
echo json_encode([
'success' => true,
'data' => [
'games' => $games,
'logs' => $recentLogs,
'server_time' => date('Y-m-d H:i:s'),
]
]);
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class BetController extends AdminBaseController {
public function __construct() {
$this->checkLogin();
$this->checkAdmin();
}
/**
* 投注记录列表页面
*/
public function index() {
$db = new Database();
// 分页参数
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$limit = 20;
$offset = ($page - 1) * $limit;
// 筛选参数
$where = [];
if (isset($_GET['user_id']) && !empty($_GET['user_id'])) {
$where['bets.user_id'] = $_GET['user_id'];
}
if (isset($_GET['period_number']) && !empty($_GET['period_number'])) {
$where['bets.period_number'] = $_GET['period_number'];
}
if (isset($_GET['status']) && !empty($_GET['status'])) {
$where['bets.status'] = $_GET['status'];
}
// 排序
$where['ORDER'] = ['bets.id' => 'DESC'];
$where['LIMIT'] = [$offset, $limit];
try {
// 获取投注记录,关联用户表和游戏表
$bets = $db->select('bets', [
'[>]users' => ['user_id' => 'id'],
'[>]games' => ['game_id' => 'id']
], [
'bets.id',
'bets.user_id',
'users.username',
'users.balance', // 当前余额
'games.name(game_name)', // 游戏名称
'bets.period_number',
'bets.bet_type',
'bets.bet_value',
'bets.amount',
'bets.odds',
'bets.win_amount',
'bets.status',
'bets.created_at',
'bets.settled_at'
], $where);
// 获取总记录数用于分页(移除LIMIT)
unset($where['LIMIT']);
$total = $db->count('bets', [
'[>]users' => ['user_id' => 'id'],
'[>]games' => ['game_id' => 'id']
], '*', $where);
$totalPages = ceil($total / $limit);
} catch (\Throwable $e) {
// 记录错误以便调试
error_log("BetController Error: " . $e->getMessage());
$bets = [];
$total = 0;
$totalPages = 0;
}
$this->render('Admin/bets.php', [
'bets' => $bets,
'currentPage' => $page,
'totalPages' => $totalPages,
'title' => '投注记录'
]);
}
}
+734
View File
@@ -0,0 +1,734 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class DicePeriodController extends AdminBaseController {
private $gameType = 'dice';
public function __construct() {
$this->checkLogin();
$this->checkAdmin();
}
/**
* 骰子游戏期号列表页面
*/
public function index() {
$db = new Database();
try {
// 获取骰子游戏列表
$gamesList = $db->select('games', ['id', 'name'], [
'type' => $this->gameType,
'status' => 1,
'ORDER' => ['id' => 'ASC']
]);
if (!is_array($gamesList)) {
$gamesList = [];
}
// 获取所有骰子游戏的game_id
$gameIds = array_column($gamesList, 'id');
// 获取骰子游戏的期号列表
$periods = [];
if (!empty($gameIds)) {
$periods = $db->select('periods', '*', [
'game_id' => $gameIds,
'ORDER' => ['id' => 'DESC'],
'LIMIT' => 100
]);
}
if (!is_array($periods)) {
$periods = [];
}
// 构建游戏名称映射
$games = [];
foreach ($gamesList as $game) {
$games[$game['id']] = $game['name'];
}
// 为每个骰子游戏获取当前期号
$currentPeriods = [];
foreach ($gamesList as $game) {
$currentPeriod = $db->get('periods', '*', [
'game_id' => $game['id'],
'ORDER' => ['id' => 'DESC']
]);
if ($currentPeriod) {
$currentPeriods[$game['id']] = $currentPeriod;
}
}
} catch (\Throwable $e) {
$periods = [];
$games = [];
$gamesList = [];
$currentPeriods = [];
}
$this->render('Admin/dice_periods.php', [
'currentPeriods' => $currentPeriods,
'periods' => $periods,
'games' => $games,
'gamesList' => $gamesList,
'title' => '骰子游戏期号管理'
]);
}
/**
* 获取单个期号数据
*/
public function get($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
$db = new Database();
try {
$period = $db->get('periods', '*', [
'id' => $id
]);
if ($period) {
// 验证是否为骰子游戏期号
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
if ($game && $game['type'] === $this->gameType) {
echo json_encode([
'success' => true,
'data' => $period
]);
} else {
echo json_encode([
'success' => false,
'message' => '期号不属于骰子游戏'
]);
}
} else {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
}
} catch (\Throwable $e) {
echo json_encode([
'success' => false,
'message' => '获取数据失败:' . $e->getMessage()
]);
}
}
/**
* 录入开奖结果
*/
public function draw() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
if (empty($data)) {
echo json_encode([
'success' => false,
'message' => '未接收到数据'
]);
return;
}
$db = new Database();
$id = isset($data['id']) ? (int)$data['id'] : 0;
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
// 获取期号信息
$period = $db->get('periods', '*', ['id' => $id]);
if (!$period) {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
return;
}
// 验证是否为骰子游戏
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
if (!$game || $game['type'] !== $this->gameType) {
echo json_encode([
'success' => false,
'message' => '期号不属于骰子游戏'
]);
return;
}
// 检查期号状态
if ($period['status'] === 'settled') {
echo json_encode([
'success' => false,
'message' => '该期号已结算,无法修改'
]);
return;
}
$auto = isset($data['auto']) ? (bool)$data['auto'] : false;
// 骰子游戏开奖逻辑
if ($auto) {
$dice1 = rand(1, 6);
$dice2 = rand(1, 6);
$dice3 = rand(1, 6);
} else {
$dice1 = isset($data['dice1']) ? (int)$data['dice1'] : 0;
$dice2 = isset($data['dice2']) ? (int)$data['dice2'] : 0;
$dice3 = isset($data['dice3']) ? (int)$data['dice3'] : 0;
// 验证骰子点数
if ($dice1 < 1 || $dice1 > 6 || $dice2 < 1 || $dice2 > 6 || $dice3 < 1 || $dice3 > 6) {
echo json_encode([
'success' => false,
'message' => '骰子点数必须在1-6之间'
]);
return;
}
}
// 计算总和
$total = $dice1 + $dice2 + $dice3;
// 计算结果
$result = $this->calculateResult($dice1, $dice2, $dice3, $total);
// 获取当前登录的管理员ID(审核人)
$approvedBy = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
// 更新期号数据
$updateData = [
'dice1' => $dice1,
'dice2' => $dice2,
'dice3' => $dice3,
'total' => $total,
'result' => $result,
'status' => 'drawn',
'draw_time' => date('Y-m-d H:i:s'),
'approved_by' => $approvedBy,
'updated_at' => date('Y-m-d H:i:s')
];
$responseData = [
'dice1' => $dice1,
'dice2' => $dice2,
'dice3' => $dice3,
'total' => $total,
'result' => $result
];
try {
$db->update('periods', $updateData, ['id' => $id]);
echo json_encode([
'success' => true,
'message' => '开奖结果已录入',
'data' => $responseData
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '录入失败:' . $e->getMessage()
]);
}
}
/**
* 启动新一期(开始下注)
*/
public function start() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
$gameId = isset($data['game_id']) ? (int)$data['game_id'] : 0;
if ($gameId <= 0) {
echo json_encode([
'success' => false,
'message' => '请选择游戏'
]);
return;
}
$db = new Database();
// 验证游戏类型
$game = $db->get('games', ['type', 'stream_url'], ['id' => $gameId]);
if (!$game || $game['type'] !== $this->gameType) {
echo json_encode([
'success' => false,
'message' => '游戏不属于骰子游戏'
]);
return;
}
// 检查该游戏是否有未结束的期号
$activePeriod = $db->get('periods', '*', [
'game_id' => $gameId,
'status' => ['pending', 'locked', 'drawn']
]);
if ($activePeriod) {
echo json_encode([
'success' => false,
'message' => '该游戏当前有未结束的期号,无法开始新一轮'
]);
return;
}
// 生成期号
$periodNumber = $this->generatePeriodNumber($db, $gameId);
// 获取stream_url
$streamUrl = !empty($game['stream_url']) ? $game['stream_url'] : null;
$periodData = [
'period_number' => $periodNumber,
'status' => 'pending',
'game_id' => $gameId,
'stream_url' => $streamUrl,
'start_time' => date('Y-m-d H:i:s'),
'auto_generated' => 0,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
try {
$id = $db->insert('periods', $periodData);
echo json_encode([
'success' => true,
'message' => '新一期已启动,开始下注',
'data' => [
'id' => $id,
'period_number' => $periodNumber,
'start_time' => $periodData['start_time']
]
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '启动失败:' . $e->getMessage()
]);
}
}
/**
* 确认开奖并结算
*/
public function settle() {
ob_clean();
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
$id = isset($data['id']) ? (int)$data['id'] : 0;
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
$db = new Database();
// 获取期号信息
$period = $db->get('periods', '*', ['id' => $id]);
if (!$period) {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
return;
}
// 验证是否为骰子游戏
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
if (!$game || $game['type'] !== $this->gameType) {
echo json_encode([
'success' => false,
'message' => '期号不属于骰子游戏'
]);
return;
}
// 检查期号状态
if ($period['status'] !== 'drawn') {
echo json_encode([
'success' => false,
'message' => '该期号还未开奖,无法结算'
]);
return;
}
if ($period['status'] === 'settled') {
echo json_encode([
'success' => false,
'message' => '该期号已结算'
]);
return;
}
try {
// 开启事务
$db->medoo->pdo->beginTransaction();
// 获取本期所有待结算注单
$bets = $db->select('bets', '*', [
'period_id' => $id,
'status' => 'pending'
]);
// 遍历注单进行结算
if ($bets) {
foreach ($bets as $bet) {
$checkResult = $this->checkWin($bet, $period);
$isWin = $checkResult['win'];
$winAmount = $checkResult['amount'];
$now = date('Y-m-d H:i:s');
if ($isWin) {
$payout = $bet['amount'] + $winAmount;
// 更新注单状态
$db->update('bets', [
'status' => 'win',
'win_amount' => $winAmount,
'settled_at' => $now,
'updated_at' => $now
], ['id' => $bet['id']]);
// 更新用户余额
$db->update('users', [
'balance[+]' => $payout
], ['id' => $bet['user_id']]);
// 获取更新后的余额
$user = $db->get('users', ['balance'], ['id' => $bet['user_id']]);
$balanceAfter = $user['balance'];
$balanceBefore = $balanceAfter - $payout;
// 写入资金流水
$db->insert('transactions', [
'user_id' => $bet['user_id'],
'type' => 'win',
'amount' => $payout,
'balance_before' => $balanceBefore,
'balance_after' => $balanceAfter,
'related_id' => $bet['id'],
'description' => "中奖 - 期号: " . $period['period_number'],
'created_at' => $now
]);
} else {
// 未中奖
$db->update('bets', [
'status' => 'lose',
'win_amount' => 0,
'settled_at' => $now,
'updated_at' => $now
], ['id' => $bet['id']]);
}
}
}
// 更新期号状态为已结算
$db->update('periods', [
'status' => 'settled',
'updated_at' => date('Y-m-d H:i:s')
], ['id' => $id]);
// 提交事务
$db->medoo->pdo->commit();
echo json_encode([
'success' => true,
'message' => '结算成功,请点击"开始下注"启动下一期',
'data' => [
'current_period_id' => $id
]
]);
} catch (\Exception $e) {
// 回滚事务
if (isset($db->medoo->pdo)) {
$db->medoo->pdo->rollBack();
}
echo json_encode([
'success' => false,
'message' => '结算失败:' . $e->getMessage()
]);
}
}
/**
* 封盘
*/
public function lock() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
$id = isset($data['id']) ? (int)$data['id'] : 0;
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
$db = new Database();
// 获取期号信息
$period = $db->get('periods', '*', ['id' => $id]);
if (!$period) {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
return;
}
// 验证是否为骰子游戏
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
if (!$game || $game['type'] !== $this->gameType) {
echo json_encode([
'success' => false,
'message' => '期号不属于骰子游戏'
]);
return;
}
// 检查期号状态
if ($period['status'] !== 'pending') {
echo json_encode([
'success' => false,
'message' => '该期号状态不允许封盘'
]);
return;
}
try {
$db->update('periods', [
'status' => 'locked',
'end_time' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
], ['id' => $id]);
echo json_encode([
'success' => true,
'message' => '封盘成功'
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '封盘失败:' . $e->getMessage()
]);
}
}
/**
* 生成期号
*/
private function generatePeriodNumber($db, $gameId) {
$now = new \DateTime();
$dateStr = $now->format('Ymd');
$prefix = "G{$gameId}{$dateStr}";
try {
$lastPeriod = $db->get('periods', 'period_number', [
'period_number[~]' => $prefix . '%',
'ORDER' => ['period_number' => 'DESC']
]);
if ($lastPeriod) {
$lastSeq = substr($lastPeriod, strlen($prefix));
$newSeq = (int)$lastSeq + 1;
$seqStr = str_pad((string)$newSeq, 4, '0', STR_PAD_LEFT);
} else {
$seqStr = '0001';
}
return $prefix . $seqStr;
} catch (\Throwable $e) {
return $prefix . time();
}
}
/**
* 检查注单是否中奖
*/
private function checkWin($bet, $period) {
$betType = $bet['bet_type'];
$betValue = $bet['bet_value'];
$betAmount = (float)$bet['amount'];
$gameId = $period['game_id'];
$db = new Database();
// 获取赔率配置
static $oddsCache = [];
if (!isset($oddsCache[$gameId])) {
$oddsData = $db->select('game_odds', '*', ['game_id' => $gameId]);
$oddsCache[$gameId] = [];
foreach ($oddsData as $odd) {
$key = $odd['type'] . '_' . $odd['target'];
$oddsCache[$gameId][$key] = (float)$odd['odds'];
}
}
$isWin = false;
$payoutMultiplier = 0;
// 获取赔率
$getOdds = function($type, $target = 'all') use ($oddsCache, $gameId) {
$key = $type . '_' . $target;
if (isset($oddsCache[$gameId][$key])) {
return $oddsCache[$gameId][$key];
}
$allKey = $type . '_all';
return $oddsCache[$gameId][$allKey] ?? 0;
};
// 骰子游戏结算逻辑
$dice1 = (int)$period['dice1'];
$dice2 = (int)$period['dice2'];
$dice3 = (int)$period['dice3'];
$total = (int)$period['total'];
switch ($betType) {
case 'xiu': // 小 (4-10)
if ($dice1 == $dice2 && $dice2 == $dice3) {
if ($dice1 <= 3) $isWin = true;
} else {
if ($total >= 4 && $total <= 10) $isWin = true;
}
$payoutMultiplier = $getOdds('xiu');
break;
case 'tai': // 大 (11-17)
if ($dice1 == $dice2 && $dice2 == $dice3) {
if ($dice1 >= 4) $isWin = true;
} else {
if ($total >= 11 && $total <= 17) $isWin = true;
}
$payoutMultiplier = $getOdds('tai');
break;
case 'chan': // 偶数
if ($total % 2 == 0) $isWin = true;
$payoutMultiplier = $getOdds('chan');
break;
case 'le': // 奇数
if ($total % 2 != 0) $isWin = true;
$payoutMultiplier = $getOdds('le');
break;
case 'number': // 单数字 (Sum)
if ($total == (int)$betValue) {
$isWin = true;
$payoutMultiplier = $getOdds('sum', $betValue);
}
break;
case 'dice': // 单个骰子
$count = 0;
if ($dice1 == (int)$betValue) $count++;
if ($dice2 == (int)$betValue) $count++;
if ($dice3 == (int)$betValue) $count++;
if ($count > 0) {
$isWin = true;
$baseOdds = $getOdds('dice', $betValue);
$payoutMultiplier = $baseOdds * $count;
}
break;
case 'combo': // 豹子
if ($betValue === 'any_triple') {
if ($dice1 == $dice2 && $dice2 == $dice3) {
$isWin = true;
$payoutMultiplier = $getOdds('combo', 'any_triple');
}
} else {
if ($dice1 == $dice2 && $dice2 == $dice3 && $total == (int)$betValue) {
$isWin = true;
$payoutMultiplier = $getOdds('combo', 'specific_triple');
}
}
break;
}
if ($isWin && $payoutMultiplier > 0) {
return [
'win' => true,
'amount' => $betAmount * $payoutMultiplier
];
}
return ['win' => false, 'amount' => 0];
}
/**
* 计算开奖结果
*/
private function calculateResult($dice1, $dice2, $dice3, $total) {
// 检查是否为爆子
if ($dice1 === $dice2 && $dice2 === $dice3) {
if ($dice1 <= 3) {
return 'Xỉu';
} else {
return 'Tài';
}
}
// 普通大小判断
if ($total >= 4 && $total <= 10) {
return 'Xỉu';
} else {
return 'Tài';
}
}
}
@@ -0,0 +1,154 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class EmployeeController extends AdminBaseController {
private $db;
public function __construct(Database $db) { $this->db = $db; }
public function index() {
$this->checkLogin(); $this->checkAdmin();
$employees = $this->db->select('employees', '*', ['ORDER' => ['id' => 'DESC']]);
$this->render('Admin/employees.php', compact('employees'));
}
public function update() {
$this->checkLogin(); $this->checkAdmin();
$data = json_decode(file_get_contents('php://input'), true);
$id = (int)($data['id'] ?? 0);
$fields = [
'emp_code' => trim($data['emp_code'] ?? ''),
'username' => trim($data['username'] ?? ''),
'real_name' => trim($data['real_name'] ?? ''),
'shift' => $data['shift'] ?? 'all',
'permissions' => json_encode($data['permissions'] ?? ['deposit','withdraw']),
'status' => (int)($data['status'] ?? 1),
];
if ($id > 0) {
if (!empty($data['password'])) $fields['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
$this->db->update('employees', $fields, ['id' => $id]);
} else {
if (empty($data['password'])) { $this->json(['status'=>'error','message'=>'Password required']); return; }
$fields['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
$this->db->insert('employees', $fields);
}
$this->json(['status' => 'success']);
}
public function delete($id) {
$this->checkLogin(); $this->checkAdmin();
$this->db->delete('employees', ['id' => (int)$id]);
$this->json(['status' => 'success']);
}
// 员工登录(独立入口)
public function loginPage() {
include ROOT_PATH . 'App/Views/Admin/employee_login.php';
}
public function loginSubmit() {
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$db = new Database();
$emp = $db->get('employees', '*', ['username' => trim($data['username'] ?? '')]);
if (!$emp || !password_verify($data['password'] ?? '', $emp['password'])) {
echo json_encode(['success' => false, 'message' => 'Invalid credentials']); return;
}
if (!$emp['status']) {
echo json_encode(['success' => false, 'message' => 'Account disabled']); return;
}
// 班次检查
$hour = (int)date('H');
if ($emp['shift'] === 'day' && ($hour < 8 || $hour >= 20)) {
echo json_encode(['success' => false, 'message' => 'Not your shift']); return;
}
if ($emp['shift'] === 'night' && ($hour >= 8 && $hour < 20)) {
echo json_encode(['success' => false, 'message' => 'Not your shift']); return;
}
if (session_status() === PHP_SESSION_NONE) session_start();
$_SESSION['emp_id'] = $emp['id'];
$_SESSION['emp_code'] = $emp['emp_code'];
$_SESSION['emp_permissions'] = json_decode($emp['permissions'], true) ?: [];
$_SESSION['emp_last_activity'] = time();
$db->update('employees', ['last_login' => date('Y-m-d H:i:s')], ['id' => $emp['id']]);
echo json_encode(['success' => true, 'redirect' => '/employee/dashboard']);
}
// 员工操作面板(仅上下分)
public function dashboard() {
$this->checkEmployee();
$users = $this->db->select('users', ['id','username','balance','is_virtual'], [
'is_virtual' => 0, 'ORDER' => ['username' => 'ASC']
]);
$logs = $this->db->select('employee_logs', '*', [
'emp_id' => $_SESSION['emp_id'],
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 50
]);
include ROOT_PATH . 'App/Views/Admin/employee_dashboard.php';
}
public function adjustBalance() {
$this->checkEmployee();
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$userId = (int)($data['user_id'] ?? 0);
$action = $data['action'] ?? ''; // deposit / withdraw
$amount = abs(floatval($data['amount'] ?? 0));
$remark = trim($data['remark'] ?? '');
$perms = $_SESSION['emp_permissions'] ?? [];
if (!in_array($action, ['deposit','withdraw']) || !in_array($action, $perms)) {
echo json_encode(['success' => false, 'message' => 'No permission']); return;
}
if ($amount <= 0 || $userId <= 0) {
echo json_encode(['success' => false, 'message' => 'Invalid input']); return;
}
try {
$this->db->medoo->pdo->beginTransaction();
$stmt = $this->db->medoo->pdo->prepare("SELECT balance FROM users WHERE id = :id FOR UPDATE");
$stmt->execute([':id' => $userId]);
$user = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$user) { $this->db->medoo->pdo->rollBack(); echo json_encode(['success'=>false,'message'=>'User not found']); return; }
$old = (float)$user['balance'];
$new = $action === 'deposit' ? $old + $amount : $old - $amount;
if ($new < 0) { $this->db->medoo->pdo->rollBack(); echo json_encode(['success'=>false,'message'=>'Insufficient balance']); return; }
$this->db->update('users', ['balance' => $new], ['id' => $userId]);
$this->db->insert('transactions', [
'user_id' => $userId, 'type' => 'manual_' . $action,
'amount' => $action === 'deposit' ? $amount : -$amount,
'balance_before' => $old, 'balance_after' => $new,
'description' => "Employee {$_SESSION['emp_code']}: $remark",
'operator_id' => $_SESSION['emp_id'], 'operator_type' => 'employee',
'created_at' => date('Y-m-d H:i:s'),
]);
$this->db->insert('employee_logs', [
'emp_id' => $_SESSION['emp_id'], 'action' => $action,
'target_user_id' => $userId, 'amount' => $amount,
'remark' => $remark, 'created_at' => date('Y-m-d H:i:s'),
]);
$this->db->medoo->pdo->commit();
echo json_encode(['success' => true, 'new_balance' => $new]);
} catch (\Throwable $e) {
if ($this->db->medoo->pdo->inTransaction()) $this->db->medoo->pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Error']);
}
}
private function checkEmployee() {
if (session_status() === PHP_SESSION_NONE) session_start();
if (empty($_SESSION['emp_id'])) { header('Location: /employee/login'); exit; }
if (time() - ($_SESSION['emp_last_activity'] ?? 0) > 7200) {
unset($_SESSION['emp_id']); header('Location: /employee/login'); exit;
}
$_SESSION['emp_last_activity'] = time();
}
private function json($data) { header('Content-Type: application/json'); echo json_encode($data); }
}
+357
View File
@@ -0,0 +1,357 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class FinanceController extends AdminBaseController {
public function __construct() {
$this->checkLogin();
$this->checkAdmin();
}
/**
* 财务管理页面
*/
public function index() {
$db = new Database();
try {
// 获取资金流水列表
$transactions = $db->select('transactions', [
'[>]users' => ['user_id' => 'id']
], [
'transactions.id',
'transactions.user_id',
'users.username',
'transactions.type',
'transactions.amount',
'transactions.balance_before',
'transactions.balance_after',
'transactions.related_id',
'transactions.description',
'transactions.created_at'
], [
'ORDER' => ['transactions.id' => 'DESC'],
'LIMIT' => 100
]);
if (!is_array($transactions)) {
$transactions = [];
}
// 计算统计信息
$stats = $this->calculateStats($db);
} catch (\Throwable $e) {
$transactions = [];
$stats = [
'total_deposit' => 0,
'total_withdraw' => 0,
'total_bet' => 0,
'total_win' => 0,
'today_deposit' => 0,
'today_withdraw' => 0
];
}
$this->render('Admin/finance.php', [
'transactions' => $transactions,
'stats' => $stats,
'title' => '财务管理'
]);
}
/**
* 获取资金流水详情
*/
public function get($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的流水ID'
]);
return;
}
$db = new Database();
try {
$transaction = $db->get('transactions', [
'[>]users' => ['user_id' => 'id']
], [
'transactions.id',
'transactions.user_id',
'users.username',
'transactions.type',
'transactions.amount',
'transactions.balance_before',
'transactions.balance_after',
'transactions.related_id',
'transactions.description',
'transactions.created_at'
], [
'transactions.id' => $id
]);
if ($transaction) {
echo json_encode([
'success' => true,
'data' => $transaction
]);
} else {
echo json_encode([
'success' => false,
'message' => '流水记录不存在'
]);
}
} catch (\Throwable $e) {
echo json_encode([
'success' => false,
'message' => '获取数据失败:' . $e->getMessage()
]);
}
}
/**
* 处理充值
*/
public function deposit() {
header('Content-Type: application/json');
$rawData = file_get_contents('php://input');
$data = json_decode($rawData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode([
'success' => false,
'message' => '数据格式错误'
]);
return;
}
$userId = isset($data['user_id']) ? (int)$data['user_id'] : 0;
$amount = isset($data['amount']) ? floatval($data['amount']) : 0;
$description = trim($data['description'] ?? '');
if ($userId <= 0) {
echo json_encode([
'success' => false,
'message' => '无效的用户ID'
]);
return;
}
if ($amount <= 0) {
echo json_encode([
'success' => false,
'message' => '充值金额必须大于0'
]);
return;
}
$db = new Database();
try {
// 获取用户信息
$user = $db->get('users', ['id', 'balance', 'username'], ['id' => $userId]);
if (!$user) {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
return;
}
$balanceBefore = floatval($user['balance']);
$balanceAfter = $balanceBefore + $amount;
// 更新用户余额
$db->update('users', [
'balance' => $balanceAfter,
'updated_at' => date('Y-m-d H:i:s')
], ['id' => $userId]);
// 记录资金流水
$transactionId = $db->insert('transactions', [
'user_id' => $userId,
'type' => 'deposit',
'amount' => $amount,
'balance_before' => $balanceBefore,
'balance_after' => $balanceAfter,
'description' => $description ?: '管理员充值',
'created_at' => date('Y-m-d H:i:s')
]);
echo json_encode([
'success' => true,
'message' => '充值成功',
'data' => [
'transaction_id' => $transactionId,
'balance_before' => $balanceBefore,
'balance_after' => $balanceAfter
]
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '充值失败:' . $e->getMessage()
]);
}
}
/**
* 处理提现
*/
public function withdraw() {
header('Content-Type: application/json');
$rawData = file_get_contents('php://input');
$data = json_decode($rawData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode([
'success' => false,
'message' => '数据格式错误'
]);
return;
}
$userId = isset($data['user_id']) ? (int)$data['user_id'] : 0;
$amount = isset($data['amount']) ? floatval($data['amount']) : 0;
$description = trim($data['description'] ?? '');
if ($userId <= 0) {
echo json_encode([
'success' => false,
'message' => '无效的用户ID'
]);
return;
}
if ($amount <= 0) {
echo json_encode([
'success' => false,
'message' => '提现金额必须大于0'
]);
return;
}
$db = new Database();
try {
// 获取用户信息
$user = $db->get('users', ['id', 'balance', 'username'], ['id' => $userId]);
if (!$user) {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
return;
}
$balanceBefore = floatval($user['balance']);
// 检查余额是否充足
if ($balanceBefore < $amount) {
echo json_encode([
'success' => false,
'message' => '用户余额不足,当前余额:' . number_format($balanceBefore, 2)
]);
return;
}
$balanceAfter = $balanceBefore - $amount;
// 更新用户余额
$db->update('users', [
'balance' => $balanceAfter,
'updated_at' => date('Y-m-d H:i:s')
], ['id' => $userId]);
// 记录资金流水
$transactionId = $db->insert('transactions', [
'user_id' => $userId,
'type' => 'withdraw',
'amount' => -$amount, // 提现为负数
'balance_before' => $balanceBefore,
'balance_after' => $balanceAfter,
'description' => $description ?: '管理员提现',
'created_at' => date('Y-m-d H:i:s')
]);
echo json_encode([
'success' => true,
'message' => '提现成功',
'data' => [
'transaction_id' => $transactionId,
'balance_before' => $balanceBefore,
'balance_after' => $balanceAfter
]
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '提现失败:' . $e->getMessage()
]);
}
}
/**
* 计算财务统计
*/
private function calculateStats($db) {
try {
// 总充值 (包含普通充值和人工加款)
$totalDeposit = $db->sum('transactions', 'amount', [
'type' => ['deposit', 'manual_deposit']
]) ?: 0;
// 总提现(取绝对值,包含普通提现和人工扣款)
$totalWithdraw = abs($db->sum('transactions', 'amount', [
'type' => ['withdraw', 'manual_withdraw']
]) ?: 0);
// 总投注(取绝对值)
$totalBet = abs($db->sum('transactions', 'amount', [
'type' => 'bet'
]) ?: 0);
// 总中奖
$totalWin = $db->sum('transactions', 'amount', [
'type' => 'win'
]) ?: 0;
// 今日充值 (包含普通充值和人工加款)
$todayDeposit = $db->sum('transactions', 'amount', [
'type' => ['deposit', 'manual_deposit'],
'created_at[>=]' => date('Y-m-d 00:00:00')
]) ?: 0;
// 今日提现(取绝对值,包含普通提现和人工扣款)
$todayWithdraw = abs($db->sum('transactions', 'amount', [
'type' => ['withdraw', 'manual_withdraw'],
'created_at[>=]' => date('Y-m-d 00:00:00')
]) ?: 0);
return [
'total_deposit' => floatval($totalDeposit),
'total_withdraw' => floatval($totalWithdraw),
'total_bet' => floatval($totalBet),
'total_win' => floatval($totalWin),
'today_deposit' => floatval($todayDeposit),
'today_withdraw' => floatval($todayWithdraw)
];
} catch (\Throwable $e) {
return [
'total_deposit' => 0,
'total_withdraw' => 0,
'total_bet' => 0,
'total_win' => 0,
'today_deposit' => 0,
'today_withdraw' => 0
];
}
}
}
+414
View File
@@ -0,0 +1,414 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class GameController extends AdminBaseController {
public function __construct() {
$this->checkLogin();
$this->checkAdmin();
}
public function index() {
$db = new Database();
try {
$games = $db->select('games', '*', [
'ORDER' => ['id' => 'DESC']
]);
if (!is_array($games)) {
$games = [];
}
} catch (\Throwable $e) {
$games = [];
}
$this->render('Admin/games.php', [
'games' => $games,
'title' => '游戏管理'
]);
}
/**
* 获取单个游戏数据(用于编辑)
*/
public function get($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的游戏ID'
]);
return;
}
$db = new Database();
try {
$game = $db->get('games', '*', [
'id' => $id
]);
if ($game) {
echo json_encode([
'success' => true,
'data' => $game
]);
} else {
echo json_encode([
'success' => false,
'message' => '游戏不存在'
]);
}
} catch (\Throwable $e) {
echo json_encode([
'success' => false,
'message' => '获取数据失败:' . $e->getMessage()
]);
}
}
/**
* 创建或更新游戏
*/
public function update() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
echo json_encode([
'success' => false,
'message' => '未接收到数据'
]);
return;
}
$db = new Database();
$id = isset($data['id']) ? (int)$data['id'] : 0;
$isEditMode = $id > 0;
$name = trim($data['name'] ?? '');
$code = trim($data['code'] ?? '');
$streamUrl = trim($data['stream_url'] ?? '');
$type = trim($data['type'] ?? 'dice');
$sortOrder = isset($data['sort_order']) ? (int)$data['sort_order'] : 0;
$status = isset($data['status']) ? (int)$data['status'] : 1; // 默认启用
$featured = isset($data['featured']) ? (int)$data['featured'] : 1; // 默认显示在游戏大厅
$image = trim($data['image'] ?? '');
// 处理标签(复选框数组转逗号分隔字符串)
$tags = '';
if (isset($data['tags']) && is_array($data['tags'])) {
$tags = implode(',', array_map('trim', $data['tags']));
} elseif (isset($data['tags'])) {
$tags = trim($data['tags']);
}
if (empty($name)) {
echo json_encode([
'success' => false,
'message' => '游戏名称不能为空'
]);
return;
}
// 自动生成游戏标识(如果为空)
if (empty($code)) {
// 将中文转换为拼音或使用拼音首字母,这里简化为使用时间戳+随机数
// 实际可以使用拼音库,这里使用简化方式:去除特殊字符,转为小写,空格转下划线
$code = $this->generateGameCode($name, $db, $id);
}
// 检查code是否已存在(排除当前ID)
try {
$exists = $db->get('games', 'id', [
'code' => $code,
'id[!]' => $id
]);
if ($exists) {
// 如果已存在,添加随机后缀
$code = $code . '_' . time();
}
} catch (\Throwable $e) {
// 如果表不存在或其他错误,继续执行
}
// 准备数据
$gameData = [
'name' => $name,
'code' => $code,
'stream_url' => $streamUrl,
'type' => $type,
'tags' => $tags,
'sort_order' => $sortOrder,
'status' => $status,
'featured' => $featured,
'updated_at' => date('Y-m-d H:i:s')
];
// 如果提供了图片路径,则添加
if (!empty($image)) {
$gameData['image'] = $image;
}
try {
if ($isEditMode) {
$db->update('games', $gameData, ['id' => $id]);
$message = '游戏更新成功';
} else {
$gameData['created_at'] = date('Y-m-d H:i:s');
$id = $db->insert('games', $gameData);
$message = '游戏创建成功';
}
echo json_encode([
'success' => true,
'message' => $message,
'data' => ['id' => $id]
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '操作失败:' . $e->getMessage()
]);
}
}
/**
* 删除游戏
*/
public function delete($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的游戏ID'
]);
return;
}
$db = new Database();
try {
$db->delete('games', ['id' => $id]);
echo json_encode([
'success' => true,
'message' => '游戏已删除'
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '删除失败:' . $e->getMessage()
]);
}
}
/**
* 切换游戏启用状态
*/
public function toggleStatus($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的游戏ID'
]);
return;
}
$db = new Database();
try {
$game = $db->get('games', ['status'], ['id' => $id]);
if (!$game) {
echo json_encode([
'success' => false,
'message' => '游戏不存在'
]);
return;
}
$newStatus = $game['status'] ? 0 : 1;
$db->update('games', [
'status' => $newStatus,
'updated_at' => date('Y-m-d H:i:s')
], ['id' => $id]);
echo json_encode([
'success' => true,
'message' => $newStatus ? '游戏已启用' : '游戏已禁用',
'data' => ['status' => $newStatus]
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '操作失败:' . $e->getMessage()
]);
}
}
/**
* 获取游戏赔率配置
*/
public function getOdds($gameId) {
header('Content-Type: application/json');
if (empty($gameId) || !is_numeric($gameId)) {
echo json_encode([
'success' => false,
'message' => '无效的游戏ID'
]);
return;
}
$db = new Database();
try {
// Check if game exists
$game = $db->get('games', ['id', 'name'], ['id' => $gameId]);
if (!$game) {
echo json_encode([
'success' => false,
'message' => '游戏不存在'
]);
return;
}
// Get odds
$odds = $db->select('game_odds', '*', [
'game_id' => $gameId
]);
// If no odds found, return defaults structure (or empty array)
// The frontend should handle empty/missing odds
echo json_encode([
'success' => true,
'data' => [
'game' => $game,
'odds' => $odds
]
]);
} catch (\Throwable $e) {
echo json_encode([
'success' => false,
'message' => '获取赔率失败:' . $e->getMessage()
]);
}
}
/**
* 更新游戏赔率
*/
public function updateOdds() {
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data)) {
$data = $_POST;
}
$gameId = isset($data['game_id']) ? (int)$data['game_id'] : 0;
$oddsData = isset($data['odds']) ? $data['odds'] : [];
if (empty($gameId) || empty($oddsData)) {
echo json_encode([
'success' => false,
'message' => '参数不完整'
]);
return;
}
$db = new Database();
try {
// Begin transaction if supported (PDO usually does)
// For simple database wrapper, we just loop update
foreach ($oddsData as $item) {
$type = $item['type'];
$target = $item['target'];
$odds = floatval($item['odds']);
// Check if exists
$exists = $db->has('game_odds', [
'game_id' => $gameId,
'type' => $type,
'target' => $target
]);
if ($exists) {
$db->update('game_odds', [
'odds' => $odds,
'updated_at' => date('Y-m-d H:i:s')
], [
'game_id' => $gameId,
'type' => $type,
'target' => $target
]);
} else {
$db->insert('game_odds', [
'game_id' => $gameId,
'type' => $type,
'target' => $target,
'odds' => $odds,
'created_at' => date('Y-m-d H:i:s')
]);
}
}
echo json_encode([
'success' => true,
'message' => '赔率配置已保存'
]);
} catch (\Throwable $e) {
echo json_encode([
'success' => false,
'message' => '保存失败:' . $e->getMessage()
]);
}
}
/**
* 自动生成游戏标识
* @param string $name 游戏名称
* @param Database $db 数据库实例
* @param int $excludeId 排除的ID(编辑时使用)
* @return string 生成的游戏标识
*/
private function generateGameCode($name, $db, $excludeId = 0) {
// 去除特殊字符,转为小写,空格和中文标点转下划线
$code = strtolower($name);
// 将中文字符转为拼音(简化处理:直接使用拼音首字母或时间戳)
// 这里使用简化的方式:去除所有非字母数字字符,替换为下划线
$code = preg_replace('/[^a-z0-9_]+/', '_', $code);
$code = preg_replace('/_+/', '_', $code); // 多个下划线合并为一个
$code = trim($code, '_'); // 去除首尾下划线
// 如果转换后为空,使用时间戳
if (empty($code)) {
$code = 'game_' . time();
}
// 限制长度
if (strlen($code) > 50) {
$code = substr($code, 0, 50);
}
// 检查是否已存在,如果存在则添加随机数
try {
$exists = $db->get('games', 'id', [
'code' => $code,
'id[!]' => $excludeId
]);
if ($exists) {
$code = $code . '_' . substr(time(), -6); // 添加时间戳后缀
}
} catch (\Throwable $e) {
// 忽略错误
}
return $code;
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
use App\Core\PluginManager;
class HomeController extends AdminBaseController {
protected $pluginManager;
protected $db;
public function __construct() {
global $pluginManager;
$this->pluginManager = $pluginManager;
$this->checkLogin();
}
public function index() {
$plugins = $this->pluginManager->getPluginStatusList();
$stats = $this->calculateDashboardStats();
$this->render('Admin/dashboard.php', [
'plugins' => $plugins,
'stats' => $stats,
'title' => '控制台'
]);
}
private function calculateDashboardStats() {
$db = new Database();
$todayStart = date('Y-m-d 00:00:00');
try {
// 1. 今日投注总额
$todayBetAmount = $db->sum('bets', 'amount', [
'created_at[>=]' => $todayStart
]) ?: 0;
// 2. 今日已派彩金额 (已结算且中奖的)
$todayPayoutAmount = $db->sum('bets', 'win_amount', [
'settled_at[>=]' => $todayStart,
'status' => 'win'
]) ?: 0;
// 3. 待处理开奖 (已封盘 waiting for draw, 或 已开奖 waiting for settle)
// status: pending -> locked -> drawn -> settled
$pendingDrawCount = $db->count('periods', [
'status' => ['locked', 'drawn']
]);
// 4. 待处理提现 (目前没有提现申请表,暂定为0)
// 如果后续有 withdrawals 表或 transactions status 字段,需在此修改
$pendingWithdrawCount = 0;
return [
'today_bet_amount' => $todayBetAmount,
'today_payout_amount' => $todayPayoutAmount,
'pending_draw_count' => $pendingDrawCount,
'pending_withdraw_count' => $pendingWithdrawCount
];
} catch (\Throwable $e) {
return [
'today_bet_amount' => 0,
'today_payout_amount' => 0,
'pending_draw_count' => 0,
'pending_withdraw_count' => 0
];
}
}
}
+376
View File
@@ -0,0 +1,376 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database; // Medoo数据库实例
use \Exception;
class ImagesController extends AdminBaseController {
protected $db;
protected $uploadDir;
protected array $uploadConfig;
// 允许的图片MIME类型
const ALLOWED_MIME = [
'image/jpeg', 'image/png', 'image/gif',
'image/webp', 'image/svg+xml'
];
const MAX_UPLOAD_SIZE = 10485760; // 上传大小限制(10MB
const MAX_IMAGE_SIZE = 800; // 主图最大边长(等同缩略图大小)
public function __construct() {
$this->checkLogin(); // 登录验证
$this->db = new Database(); // 初始化数据库连接
$domain = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http")
. "://" . $_SERVER['HTTP_HOST'];
$this->uploadConfig = [
'upload_dir' => $_SERVER['DOCUMENT_ROOT'] . '/Storage/images/',
'url' => $domain . '/Storage/images/',
'max_size' => 10 * 1024 * 1024, // 10MB
'allowed_mimes' => [
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/icon'
],
'min_quality' => 60,
'max_quality' => 100,
'default_quality' => 80
];
}
/**
* 图片管理首页(仅渲染前端页面,数据使用AJAX加载)
*/
public function index() {
// 渲染页面模板
$this->render('Admin/images.php', [
'title' => '图片管理'
]);
}
/**
* 图片列表查询
*/
public function list() {
try {
// 处理分页参数
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$limit = isset($_GET['limit']) ?
min(100, max(1, (int)$_GET['limit'])) : 20;
$offset = ($page - 1) * $limit;
// 查询总数
$total = $this->db->count('images');
// 查询当前页数据
$data = $this->db->select('images', [
'id', 'name', 'url',
'size', 'width', 'height', 'upload_time'
], [
'LIMIT' => [$offset, $limit],
'ORDER' => ['upload_time' => 'DESC']
]);
// 返回结果
echo json_encode([
'status' => 'success',
'page' => $page,
'total' => $total,
'data' => $data ?: []
]);
exit;
} catch (Exception $e) {
$this->jsonError('获取数据失败: ' . $e->getMessage());
}
}
/**
* 图片上传处理
*/
public function upload() {
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
$this->jsonError('文件上传失败: ' . $this->getUploadError($_FILES['image']['error'] ?? -1));
}
$file = $_FILES['image'];
$params = [
'quality' => isset($_POST['quality']) ?
min($this->uploadConfig['max_quality'],
max($this->uploadConfig['min_quality'], (int)$_POST['quality'])) :
$this->uploadConfig['default_quality'],
'width' => isset($_POST['width']) && $_POST['width'] !== '' ? max(1, (int)$_POST['width']) : 0,
'height' => isset($_POST['height']) && $_POST['height'] !== '' ? max(1, (int)$_POST['height']) : 0
];
if ($file['size'] > $this->uploadConfig['max_size']) {
$this->jsonError("文件过大,最大支持" . $this->formatFileSize($this->uploadConfig['max_size']));
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $this->uploadConfig['allowed_mimes'])) {
$this->jsonError('不支持的文件类型,仅允许: ' . implode(', ', $this->uploadConfig['allowed_mimes']));
}
try {
$uploadDir = rtrim($this->uploadConfig['upload_dir'], '/') . '/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$originalName = basename($file['name']);
$originalExt = pathinfo($originalName, PATHINFO_EXTENSION);
$filename = date('YmdHis') . '_' . uniqid() . '.' . $originalExt;
$targetPath = $uploadDir . $filename;
list($origWidth, $origHeight) = getimagesize($file['tmp_name']);
list($newWidth, $newHeight, $srcX, $srcY, $cropW, $cropH) = $this->calculateDimensions(
$origWidth, $origHeight,
$params['width'], $params['height']
);
// 判断是否需要裁剪
if ($params['width'] > 0 && $params['height'] > 0) {
$success = $this->processImageWithCrop(
$file['tmp_name'], $mime, $targetPath,
$newWidth, $newHeight,
$srcX, $srcY, $cropW, $cropH,
$params['quality']
);
} else {
$success = $this->processImage(
$file['tmp_name'], $mime, $targetPath,
$newWidth, $newHeight, $params['quality']
);
}
if (!$success) {
throw new Exception('图片处理失败');
}
$record = [
'name' => $originalName,
'newname' => $filename,
'url' => rtrim($this->uploadConfig['url'], '/') . '/' . $filename,
'size' => filesize($targetPath),
'width' => $newWidth,
'height' => $newHeight,
'upload_time' => time()
];
$imageId = $this->db->insert('images', $record);
echo json_encode([
'status' => 'success',
'data' => $record + ['id' => $imageId]
]);
exit;
} catch (Exception $e) {
if (isset($targetPath) && file_exists($targetPath)) {
unlink($targetPath);
}
$this->jsonError($e->getMessage());
}
}
private function calculateDimensions($origW, $origH, $targetW, $targetH) {
if ($targetW <= 0 && $targetH <= 0) {
return [$origW, $origH, 0, 0, $origW, $origH];
}
// 只指定宽
if ($targetW > 0 && $targetH <= 0) {
$newW = $targetW;
$newH = (int)($origH * ($targetW / $origW));
return [$newW, $newH, 0, 0, $origW, $origH];
}
// 只指定高
if ($targetH > 0 && $targetW <= 0) {
$newH = $targetH;
$newW = (int)($origW * ($targetH / $origH));
return [$newW, $newH, 0, 0, $origW, $origH];
}
// 指定宽高 → 居中裁剪
$srcRatio = $origW / $origH;
$dstRatio = $targetW / $targetH;
if ($srcRatio > $dstRatio) {
$cropH = $origH;
$cropW = (int)($origH * $dstRatio);
$srcX = (int)(($origW - $cropW) / 2);
$srcY = 0;
} else {
$cropW = $origW;
$cropH = (int)($origW / $dstRatio);
$srcX = 0;
$srcY = (int)(($origH - $cropH) / 2);
}
return [$targetW, $targetH, $srcX, $srcY, $cropW, $cropH];
}
// 处理图片,带裁剪逻辑
private function processImageWithCrop($srcPath, $mime, $destPath, $width, $height, $srcX, $srcY, $cropW, $cropH, $quality) {
switch ($mime) {
case 'image/jpeg': $src = imagecreatefromjpeg($srcPath); break;
case 'image/png': $src = imagecreatefrompng($srcPath); imagesavealpha($src, true); break;
case 'image/gif': $src = imagecreatefromgif($srcPath); break;
case 'image/webp': $src = imagecreatefromwebp($srcPath); break;
default: throw new Exception("不支持的图片格式: {$mime}");
}
if (!$src) throw new Exception('无法解析图片内容');
$dest = imagecreatetruecolor($width, $height);
if ($mime === 'image/png' || $mime === 'image/gif') {
imagealphablending($dest, false);
imagesavealpha($dest, true);
$transparent = imagecolorallocatealpha($dest, 255, 255, 255, 127);
imagefilledrectangle($dest, 0, 0, $width, $height, $transparent);
}
imagecopyresampled(
$dest, $src,
0, 0,
$srcX, $srcY,
$width, $height,
$cropW, $cropH
);
$result = false;
switch ($mime) {
case 'image/jpeg': $result = imagejpeg($dest, $destPath, $quality); break;
case 'image/png': $result = imagepng($dest, $destPath); break;
case 'image/gif': $result = imagegif($dest, $destPath); break;
case 'image/webp': $result = imagewebp($dest, $destPath, $quality); break;
}
imagedestroy($src);
imagedestroy($dest);
return $result;
}
// 工具函数:图片处理(普通格式,不裁剪)
private function processImage($srcPath, $mime, $destPath, $width, $height, $quality) {
switch ($mime) {
case 'image/jpeg': $src = imagecreatefromjpeg($srcPath); break;
case 'image/png': $src = imagecreatefrompng($srcPath); imagesavealpha($src, true); break;
case 'image/gif': $src = imagecreatefromgif($srcPath); break;
case 'image/webp': $src = imagecreatefromwebp($srcPath); break;
default: throw new Exception("不支持的图片格式: {$mime}");
}
if (!$src) throw new Exception('无法解析图片内容');
$dest = imagecreatetruecolor($width, $height);
if ($mime === 'image/png' || $mime === 'image/gif') {
imagealphablending($dest, false);
imagesavealpha($dest, true);
$transparent = imagecolorallocatealpha($dest, 255, 255, 255, 127);
imagefilledrectangle($dest, 0, 0, $width, $height, $transparent);
}
imagecopyresampled($dest, $src, 0, 0, 0, 0, $width, $height, imagesx($src), imagesy($src));
$result = false;
switch ($mime) {
case 'image/jpeg': $result = imagejpeg($dest, $destPath, $quality); break;
case 'image/png': $result = imagepng($dest, $destPath); break;
case 'image/gif': $result = imagegif($dest, $destPath); break;
case 'image/webp': $result = imagewebp($dest, $destPath, $quality); break;
}
imagedestroy($src);
imagedestroy($dest);
return $result;
}
// 工具函数:上传错误信息
private function getUploadError($code) {
$errors = [
UPLOAD_ERR_INI_SIZE => '超过php.ini限制',
UPLOAD_ERR_FORM_SIZE => '超过表单限制',
UPLOAD_ERR_PARTIAL => '文件仅部分上传',
UPLOAD_ERR_NO_FILE => '未上传文件',
UPLOAD_ERR_NO_TMP_DIR => '缺少临时目录',
UPLOAD_ERR_CANT_WRITE => '写入文件失败',
UPLOAD_ERR_EXTENSION => '被扩展阻止'
];
return $errors[$code] ?? "未知错误(代码: {$code}";
}
// 工具函数:格式化文件大小
private function formatFileSize($bytes) {
if ($bytes < 1024) return $bytes . 'B';
if ($bytes < 1048576) return round($bytes / 1024, 1) . 'KB';
return round($bytes / 1048576, 1) . 'MB';
}
// 工具函数:JSON错误响应
private function jsonError($message) {
echo json_encode([
'status' => 'error',
'message' => $message
]);
exit;
}
public function delete() {
header('Content-Type: application/json');
// 获取 JSON 输入
$input = json_decode(file_get_contents('php://input'), true);
if (!isset($input['ids']) || !is_array($input['ids'])) {
echo json_encode(['status' => 'error', 'message' => '缺少参数']);
return;
}
// 将字符串数组转换为整数数组
$ids = array_map('intval', $input['ids']);
$ids = array_filter($ids); // 去除无效值
if (empty($ids)) {
echo json_encode(['status' => 'error', 'message' => '无效的 ID 列表']);
return;
}
try {
// 查询图片路径(如果你想同时删除图片文件)
$images = $this->db->select('images', ['id', 'newname'], [
'id' => $ids
]);
foreach ($images as $img) {
$file = $this->uploadConfig['upload_dir'] . $img['newname'];
if (file_exists($file)) {
@unlink($file);
}
}
// 删除数据库记录
$this->db->delete('images', ['id' => $ids]);
echo json_encode(['status' => 'success']);
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => '删除失败:' . $e->getMessage()]);
}
}
}
+776
View File
@@ -0,0 +1,776 @@
<?php
namespace App\Controllers\Admin;
class InstallController
{
protected $step;
protected $configFile = ROOT_PATH . "Db/config.php";
protected $installedLockFile = ROOT_PATH . "Db/installed.lock";
protected $db;
protected $message = '';
protected $messageType = ''; // success, error, warning
public function __construct()
{
// 关键修复:步骤4即使已安装也允许访问
$currentStep = $_POST['step'] ?? $_GET['step'] ?? 1;
// 只有非步骤4且已安装时才显示已安装页面
if ($currentStep != 4 && $this->isInstalled()) {
$this->step = 5; // 已安装状态
return;
}
// 处理消息提示
if (isset($_SESSION['install_message'])) {
$this->message = $_SESSION['install_message'];
$this->messageType = $_SESSION['install_message_type'];
unset($_SESSION['install_message'], $_SESSION['install_message_type']);
}
// 设置当前步骤
$this->step = $currentStep;
}
public function index()
{
// 已安装则显示提示(步骤4除外)
if ($this->step == 5) {
$this->renderHeader();
$this->alreadyInstalled();
$this->renderFooter();
return;
}
// 处理表单提交(在输出任何内容之前)
$this->handleSubmission();
// 输出页面
$this->renderHeader();
switch ($this->step) {
case 1:
$this->checkEnvironment();
break;
case 2:
$this->databaseForm();
break;
case 3:
$this->adminForm();
break;
case 4:
$this->finish();
break;
default:
$this->setMessage('非法步骤', 'error');
$this->showMessage();
}
$this->renderFooter();
}
// 处理表单提交
protected function handleSubmission()
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
return;
}
$currentStep = $_POST['step'] ?? 1;
switch ($currentStep) {
case 2:
$this->processDatabaseForm();
break;
case 3:
$this->processAdminForm();
break;
}
}
// 处理数据库表单提交
protected function processDatabaseForm()
{
$host = $_POST['db_host'] ?? '';
$name = $_POST['db_name'] ?? '';
$user = $_POST['db_user'] ?? '';
$pass = $_POST['db_pass'] ?? '';
try {
// 连接数据库服务器
$pdo = new \PDO("mysql:host=$host;charset=utf8mb4", $user, $pass);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// 检查数据库是否存在
$stmt = $pdo->query("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'");
$dbExists = $stmt->fetchColumn() !== false;
// 如果数据库存在且不为空,提示用户
if ($dbExists) {
$stmt = $pdo->query("USE $name");
$stmt = $pdo->query("SHOW TABLES");
if ($stmt->fetchColumn() !== false) {
$this->setSessionMessage(
"警告:数据库 '$name' 已存在且包含表。继续安装可能会覆盖现有数据。",
'warning'
);
return;
}
} else {
// 创建数据库
$pdo->exec("CREATE DATABASE `$name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
}
// 连接到指定数据库
$this->db = new \PDO("mysql:host=$host;dbname=$name;charset=utf8mb4", $user, $pass);
$this->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// 保存配置文件
$config = "<?php\nreturn [\n";
$config .= " 'db_host' => '" . addslashes($host) . "',\n";
$config .= " 'db_name' => '" . addslashes($name) . "',\n";
$config .= " 'db_user' => '" . addslashes($user) . "',\n";
$config .= " 'db_pass' => '" . addslashes($pass) . "',\n];\n";
if (file_put_contents($this->configFile, $config) === false) {
throw new \Exception("无法写入配置文件,请检查权限");
}
$this->setSessionMessage('数据库配置成功!即将进入管理员设置', 'success');
header("Location: ?step=3");
exit;
} catch (\Exception $e) {
$this->setSessionMessage('数据库配置失败:' . $e->getMessage(), 'error');
header("Location: ?step=2");
exit;
}
}
// 处理管理员表单提交
protected function processAdminForm()
{
try {
if (!file_exists($this->configFile)) {
throw new \Exception("数据库配置文件不存在,请先完成数据库设置");
}
$cfg = require $this->configFile;
$this->db = new \PDO(
"mysql:host={$cfg['db_host']};dbname={$cfg['db_name']};charset=utf8mb4",
$cfg['db_user'],
$cfg['db_pass']
);
$this->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// 创建images表
$this->db->exec("
CREATE TABLE IF NOT EXISTS `images` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(255) NOT NULL COMMENT '文件原名',
`newname` varchar(255) NOT NULL COMMENT '新文件名',
`url` text NOT NULL COMMENT 'URL',
`size` int(11) NOT NULL COMMENT '文件大小(字节)',
`width` int(11) NOT NULL COMMENT '宽度(像素)',
`height` int(11) NOT NULL COMMENT '高度(像素)',
`upload_time` int(11) NOT NULL COMMENT '上传时间戳',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// 创建plugins表
$this->db->exec("
CREATE TABLE IF NOT EXISTS `plugins` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '插件唯一标识,如 SamplePlugin',
`title` varchar(255) NOT NULL COMMENT '插件显示名称',
`description` text COMMENT '插件描述',
`version` varchar(20) DEFAULT NULL COMMENT '插件版本号',
`author` varchar(100) DEFAULT NULL COMMENT '作者',
`url` varchar(128) NOT NULL,
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '启用状态,1=启用,0=禁用',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件管理表';
");
// 创建users表
$this->db->exec("
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`email` varchar(100) NOT NULL,
`avatar` VARCHAR(128) DEFAULT NULL,
`balance` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '用户余额',
`status` int(11) NOT NULL DEFAULT '0',
`role` varchar(20) NOT NULL DEFAULT 'user',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$email = $_POST['email'] ?? ''; // 新增邮箱字段
// 验证逻辑更新
if (!$username) {
throw new \Exception("用户名不能为空");
}
if (!$password) {
throw new \Exception("密码不能为空");
}
if (strlen($password) < 8) {
throw new \Exception("密码长度不能少于8位");
}
if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \Exception("请输入有效的邮箱地址");
}
// 检查用户名是否已存在
$stmt = $this->db->prepare("SELECT id FROM users WHERE username = ?");
$stmt->execute([$username]);
if ($stmt->fetchColumn()) {
throw new \Exception("用户名 '$username' 已存在,请选择其他用户名");
}
// 检查邮箱是否已存在
$stmt = $this->db->prepare("SELECT id FROM users WHERE email = ?");
$stmt->execute([$email]);
if ($stmt->fetchColumn()) {
throw new \Exception("邮箱 '$email' 已被使用,请选择其他邮箱");
}
$hash = password_hash($password, PASSWORD_BCRYPT);
// 插入管理员数据(包含邮箱和管理员角色)
$stmt = $this->db->prepare("
INSERT INTO users (username, password, email, role, status)
VALUES (?, ?, ?, 'admin', 1)
");
$stmt->execute([$username, $hash, $email]);
// 创建临时标记文件
file_put_contents(ROOT_PATH . "Db/install_completed.tmp", date('Y-m-d H:i:s') . "\n");
$this->setSessionMessage('管理员账号创建成功!安装已完成', 'success');
header("Location: ?step=4");
exit;
} catch (\Exception $e) {
$this->setSessionMessage('管理员账号创建失败:' . $e->getMessage(), 'error');
header("Location: ?step=3");
exit;
}
}
// 渲染页面头部和样式
protected function renderHeader()
{
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统安装向导</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#10b981',
danger: '#ef4444',
warning: '#f59e0b',
neutral: '#f3f4f6',
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.step-active { @apply bg-primary text-white border-primary; }
.step-passed { @apply bg-secondary text-white border-secondary; }
.step-pending { @apply bg-gray-100 text-gray-400 border-gray-200; }
.card { @apply bg-white rounded-lg shadow-md overflow-hidden transition-all duration-300 hover:shadow-lg; }
.btn { @apply px-4 py-2 rounded-md font-medium transition-all duration-200; }
.btn-primary { @apply bg-primary text-white hover:bg-primary/90; }
.btn-secondary { @apply bg-gray-600 text-white hover:bg-gray-700; }
}
</style>
</head>
<body class="bg-gray-50 min-h-screen font-sans">
<div class="container mx-auto px-4 py-8 max-w-3xl">
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
<i class="fa fa-cogs text-2xl"></i>
</div>
<h1 class="text-3xl font-bold text-gray-800">系统安装向导</h1>
<p class="text-gray-500 mt-2">请按照步骤完成系统安装</p>
</div>
<!-- 步骤指示器 -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div class="flex flex-col items-center">
<div class="w-10 h-10 rounded-full border-2 flex items-center justify-center
<?php echo $this->step == 1 ? 'step-active' : ($this->step > 1 ? 'step-passed' : 'step-pending'); ?>">
<i class="fa fa-check"></i>
</div>
<span class="mt-2 text-sm font-medium text-gray-600">环境检测</span>
</div>
<div class="flex-1 h-1 mx-4 bg-gray-200 relative">
<div class="absolute inset-0 bg-primary transition-all duration-500
<?php echo $this->step > 1 ? 'w-full' : 'w-0'; ?>"></div>
</div>
<div class="flex flex-col items-center">
<div class="w-10 h-10 rounded-full border-2 flex items-center justify-center
<?php echo $this->step == 2 ? 'step-active' : ($this->step > 2 ? 'step-passed' : 'step-pending'); ?>">
<i class="fa fa-database"></i>
</div>
<span class="mt-2 text-sm font-medium text-gray-600">数据库设置</span>
</div>
<div class="flex-1 h-1 mx-4 bg-gray-200 relative">
<div class="absolute inset-0 bg-primary transition-all duration-500
<?php echo $this->step > 2 ? 'w-full' : 'w-0'; ?>"></div>
</div>
<div class="flex flex-col items-center">
<div class="w-10 h-10 rounded-full border-2 flex items-center justify-center
<?php echo $this->step == 3 ? 'step-active' : ($this->step > 3 ? 'step-passed' : 'step-pending'); ?>">
<i class="fa fa-user"></i>
</div>
<span class="mt-2 text-sm font-medium text-gray-600">管理员设置</span>
</div>
<div class="flex-1 h-1 mx-4 bg-gray-200 relative">
<div class="absolute inset-0 bg-primary transition-all duration-500
<?php echo $this->step > 3 ? 'w-full' : 'w-0'; ?>"></div>
</div>
<div class="flex flex-col items-center">
<div class="w-10 h-10 rounded-full border-2 flex items-center justify-center
<?php echo $this->step == 4 ? 'step-active' : ($this->step > 4 ? 'step-passed' : 'step-pending'); ?>">
<i class="fa fa-check-circle"></i>
</div>
<span class="mt-2 text-sm font-medium text-gray-600">完成</span>
</div>
</div>
</div>
<!-- 消息提示 -->
<?php $this->showMessage(); ?>
<div class="card p-6">
<?php
}
// 渲染页面底部
protected function renderFooter()
{
?>
</div>
<div class="mt-8 text-center text-gray-500 text-sm">
<p>© 2025 系统安装向导</p>
</div>
</div>
</body>
</html>
<?php
}
// 显示消息提示
protected function showMessage()
{
if (empty($this->message)) {
return;
}
$icon = '';
$class = '';
switch ($this->messageType) {
case 'success':
$icon = 'fa-check-circle';
$class = 'bg-green-50 border-green-200 text-green-700';
break;
case 'error':
$icon = 'fa-exclamation-circle';
$class = 'bg-red-50 border-red-200 text-red-700';
break;
case 'warning':
$icon = 'fa-exclamation-triangle';
$class = 'bg-yellow-50 border-yellow-200 text-yellow-700';
break;
default:
$icon = 'fa-info-circle';
$class = 'bg-blue-50 border-blue-200 text-blue-700';
}
echo "<div class='mb-6 p-4 border rounded-lg $class'>
<i class='fa $icon mr-2'></i>$this->message
</div>";
}
// 设置消息
protected function setMessage($message, $type = 'info')
{
$this->message = $message;
$this->messageType = $type;
}
// 设置会话消息(用于跳转后显示)
protected function setSessionMessage($message, $type = 'info')
{
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$_SESSION['install_message'] = $message;
$_SESSION['install_message_type'] = $type;
}
// 检查是否已安装
protected function isInstalled()
{
return file_exists($this->installedLockFile);
}
// 已安装提示
protected function alreadyInstalled()
{
echo "<div class='text-center py-8'>
<div class='inline-flex items-center justify-center w-20 h-20 rounded-full bg-warning/10 text-warning mb-6'>
<i class='fa fa-exclamation-triangle text-3xl'></i>
</div>
<h2 class='text-2xl font-bold text-gray-800 mb-3'>系统已安装</h2>
<p class='text-gray-600 mb-8 max-w-md mx-auto'>
检测到系统已经安装,如需重新安装,请先删除安装锁定文件:<br>
<code class='bg-gray-100 px-2 py-1 rounded text-sm'>{$this->installedLockFile}</code>
</p>
<div class='flex justify-center'>
<a href='/admin/login' class='btn btn-primary inline-flex items-center justify-center'>
<i class='fa fa-sign-in mr-2'></i> 登录后台
</a>
</div>
</div>";
}
// 第一步:检测环境
protected function checkEnvironment()
{
$phpVersion = PHP_VERSION;
$extensions = [
'pdo_mysql' => 'MySQL PDO 驱动(必须)',
'mbstring' => '多字节字符串支持(必须)',
'json' => 'JSON 解析支持(必须)',
'openssl' => 'OpenSSL 加密扩展(必须)',
'fileinfo' => '文件信息扩展(必须)',
'pdo' => 'PDO 基础扩展(可选,一般随 pdo_mysql 自动启用)'
];
$writableDirs = [
ROOT_PATH . "Db" => "数据库配置目录",
ROOT_PATH . "Storage" => "文件存储目录"
];
// 检查PHP版本
$phpVersionOk = version_compare($phpVersion, '8.0.0', '>=');
// 检查扩展
$allExtensionsOk = true;
$extensionResults = [];
foreach ($extensions as $ext => $desc) {
$installed = extension_loaded($ext);
if (!$installed && $ext !== 'pdo') { // pdo 可选
$allExtensionsOk = false;
}
$extensionResults[] = [
'name' => $ext,
'desc' => $desc,
'installed' => $installed
];
}
// 检查目录写权限(真实写入测试)
$allDirsWritable = true;
$dirResults = [];
foreach ($writableDirs as $dir => $desc) {
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$testFile = $dir . '/.test';
$canWrite = @file_put_contents($testFile, 'test') !== false;
if ($canWrite) {
unlink($testFile);
} else {
$allDirsWritable = false;
}
$dirResults[] = [
'path' => $dir,
'desc' => $desc,
'writable' => $canWrite
];
}
// 检查 PHP 配置
$fileUploadsOk = ini_get('file_uploads');
echo "<h2 class='text-2xl font-bold text-gray-800 mb-6 flex items-center'>
<i class='fa fa-server text-primary mr-3'></i>环境检测
</h2>";
// PHP版本检查
echo "<div class='mb-4 p-4 border rounded-lg " . ($phpVersionOk ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50') . "'>
<div class='flex justify-between items-center'>
<div>
<h3 class='font-medium text-gray-800'>PHP版本</h3>
<p class='text-sm text-gray-600 mt-1'>需要PHP 8.0.0或更高版本</p>
</div>
<span class='text-lg " . ($phpVersionOk ? 'text-secondary' : 'text-danger') . "'>
" . ($phpVersionOk ? '✅' : '❌') . "
</span>
</div>
<p class='mt-2 text-sm text-gray-700'>当前版本: $phpVersion</p>
</div>";
// 扩展检查
echo "<div class='mb-4'>
<h3 class='font-medium text-gray-800 mb-3'>必要扩展</h3>
<div class='grid grid-cols-1 md:grid-cols-2 gap-3'>";
foreach ($extensionResults as $ext) {
echo "<div class='p-3 border rounded-lg " . ($ext['installed'] ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50') . "'>
<div class='flex justify-between items-center'>
<span>{$ext['desc']} ({$ext['name']})</span>
<span class='" . ($ext['installed'] ? 'text-secondary' : 'text-danger') . "'>
" . ($ext['installed'] ? '✅' : '❌') . "
</span>
</div>";
if (!$ext['installed']) {
echo "<p class='text-xs text-gray-500 mt-1'>Linux 安装命令示例: <code>apt install php-{$ext['name']}</code></p>";
}
echo "</div>";
}
echo "</div></div>";
// 文件权限检查
echo "<div class='mb-6'>
<h3 class='font-medium text-gray-800 mb-3'>文件权限</h3>
<div class='space-y-3'>";
foreach ($dirResults as $dir) {
echo "<div class='p-3 border rounded-lg " . ($dir['writable'] ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50') . "'>
<div class='flex justify-between items-center'>
<span class='text-sm truncate max-w-[70%]'>{$dir['desc']} ({$dir['path']})</span>
<span class='" . ($dir['writable'] ? 'text-secondary' : 'text-danger') . "'>
" . ($dir['writable'] ? '✅' : '❌') . "
</span>
</div>
</div>";
}
echo "</div></div>";
// PHP 配置检查
echo "<div class='mb-6 p-4 border rounded-lg " . ($fileUploadsOk ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50') . "'>
<div class='flex justify-between items-center'>
<span>文件上传 (file_uploads)</span>
<span class='" . ($fileUploadsOk ? 'text-secondary' : 'text-danger') . "'>
" . ($fileUploadsOk ? '✅' : '❌') . "
</span>
</div>
</div>";
// 下一步按钮
$allChecksPassed = $phpVersionOk && $allExtensionsOk && $allDirsWritable && $fileUploadsOk;
echo "<div class='flex justify-end mt-8'>";
if ($allChecksPassed) {
echo "<a href='?step=2' class='btn btn-primary flex items-center'>
<span>下一步:数据库设置</span>
<i class='fa fa-arrow-right ml-2'></i>
</a>";
} else {
echo "<button class='btn bg-gray-300 text-gray-500 cursor-not-allowed' disabled>
<span>请先解决所有问题</span>
<i class='fa fa-arrow-right ml-2'></i>
</button>";
}
echo "</div>";
}
// 第二步:数据库表单
protected function databaseForm()
{
echo "<h2 class='text-2xl font-bold text-gray-800 mb-6 flex items-center'>
<i class='fa fa-database text-primary mr-3'></i>数据库设置
</h2>";
echo '<form method="post" class="space-y-4">
<input type="hidden" name="step" value="2">
<div>
<label for="db_host" class="block text-sm font-medium text-gray-700 mb-1">数据库主机</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
<i class="fa fa-server -mt-5"></i>
</div>
<input type="text" id="db_host" name="db_host" value="localhost" required
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
<p class="mt-1 text-xs text-gray-500">通常为 localhost 或 127.0.0.1</p>
</div>
</div>
<div>
<label for="db_name" class="block text-sm font-medium text-gray-700 mb-1">数据库名称</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
<i class="fa fa-database"></i>
</div>
<input type="text" id="db_name" name="db_name" required
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
</div>
</div>
<div>
<label for="db_user" class="block text-sm font-medium text-gray-700 mb-1">数据库用户名</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
<i class="fa fa-user"></i>
</div>
<input type="text" id="db_user" name="db_user" required
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
</div>
</div>
<div>
<label for="db_pass" class="block text-sm font-medium text-gray-700 mb-1">数据库密码</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
<i class="fa fa-lock -mt-5"></i>
</div>
<input type="password" id="db_pass" name="db_pass"
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
<p class="mt-1 text-xs text-gray-500">如果数据库无密码,请留空</p>
</div>
</div>
<div class="flex justify-between mt-8">
<a href="?step=1" class="btn btn-secondary flex items-center">
<i class="fa fa-arrow-left mr-2"></i>
<span>上一步</span>
</a>
<button type="submit" class="btn btn-primary flex items-center">
<span>保存并测试</span>
<i class="fa fa-arrow-right ml-2"></i>
</button>
</div>
</form>';
}
// 第三步:创建管理员
protected function adminForm()
{
// 检查配置文件是否存在
if (!file_exists($this->configFile)) {
$this->setMessage('数据库配置文件不存在,请先完成数据库设置', 'error');
$this->showMessage();
echo "<div class='flex justify-start mt-8'>
<a href='?step=2' class='btn btn-secondary flex items-center'>
<i class='fa fa-arrow-left mr-2'></i>
<span>返回数据库设置</span>
</a>
</div>";
return;
}
echo '<h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fa fa-user text-primary mr-3"></i>创建管理员账号
</h2>';
echo '<form method="post" class="space-y-4">
<input type="hidden" name="step" value="3">
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">管理员用户名</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
<i class="fa fa-user-circle"></i>
</div>
<input type="text" id="username" name="username" required
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
</div>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
<i class="fa fa-user-circle"></i>
</div>
<input type="text" id="email" name="email" required
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
</div>
</div> <div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">管理员密码</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
<i class="fa fa-shield"></i>
</div>
<input type="password" id="password" name="password" required
class="w-full pl-10 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all">
<p class="mt-1 text-xs text-gray-500">请设置强密码(至少8位,包含字母和数字)</p>
</div>
</div>
<div class="flex justify-between mt-8">
<a href="?step=2" class="btn btn-secondary flex items-center">
<i class="fa fa-arrow-left mr-2"></i>
<span>上一步</span>
</a>
<button type="submit" class="btn btn-primary flex items-center">
<span>创建管理员</span>
<i class="fa fa-check ml-2"></i>
</button>
</div>
</form>';
}
// 第四步:完成安装
protected function finish()
{
// 关键修复:在完成页面创建最终的安装锁定文件
if (file_exists(ROOT_PATH . "Db/install_completed.tmp")) {
rename(ROOT_PATH . "Db/install_completed.tmp", $this->installedLockFile);
}
echo "<div class='text-center py-8'>
<div class='inline-flex items-center justify-center w-20 h-20 rounded-full bg-secondary/10 text-secondary mb-6'>
<i class='fa fa-check-circle text-3xl'></i>
</div>
<h2 class='text-2xl font-bold text-gray-800 mb-3'>安装完成!</h2>
<p class='text-gray-600 mb-8 max-w-md mx-auto'>管理员账号已创建,系统安装成功,您可以登录后台开始使用了。</p>
<div class='flex flex-col sm:flex-row justify-center gap-4'>
<a href='/admin/login' class='btn btn-primary inline-flex items-center justify-center'>
<i class='fa fa-sign-in mr-2'></i> 登录后台
</a>
<a href='/' class='btn btn-secondary inline-flex items-center justify-center'>
<i class='fa fa-home mr-2'></i> 访问首页
</a>
</div>
</div>";
}
}
@@ -0,0 +1,308 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use App\Core\PK10Algorithm;
use Db\Database;
class PK10PeriodController extends AdminBaseController {
private $db;
public function __construct(Database $db) {
$this->db = $db;
}
public function index() {
$this->checkLogin();
$gameId = $this->getGameId();
$periods = $this->db->select('periods', '*', [
'game_id' => $gameId,
'ORDER' => ['id' => 'DESC'],
'LIMIT' => 50
]);
// 附加pk10_results
foreach ($periods as &$p) {
$p['pk10'] = $this->db->get('pk10_results', '*', ['period_id' => $p['id']]);
}
$current = $this->db->get('periods', '*', [
'game_id' => $gameId,
'status[!]' => 'settled',
'ORDER' => ['id' => 'DESC']
]);
if ($current) {
$current['pk10'] = $this->db->get('pk10_results', '*', ['period_id' => $current['id']]);
}
$stats = [
'total' => $this->db->count('periods', ['game_id' => $gameId]),
'pending' => $this->db->count('periods', ['game_id' => $gameId, 'status' => 'pending']),
'drawn' => $this->db->count('periods', ['game_id' => $gameId, 'status' => 'drawn']),
'settled' => $this->db->count('periods', ['game_id' => $gameId, 'status' => 'settled']),
];
$this->render('Admin/pk10_periods.php', compact('periods', 'current', 'stats', 'gameId'));
}
public function start() {
$this->checkLogin();
$gameId = $this->getGameId();
// 检查是否有未结算期号
$active = $this->db->get('periods', 'id', [
'game_id' => $gameId,
'status[!]' => 'settled'
]);
if ($active) {
$this->json(['status' => 'error', 'message' => 'There is an active period. Please settle it first.']);
return;
}
$periodNumber = PK10Algorithm::generatePeriodNumber($gameId);
$this->db->insert('periods', [
'game_id' => $gameId,
'period_number' => $periodNumber,
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
]);
$this->json(['status' => 'success', 'message' => 'New period started', 'period_number' => $periodNumber]);
}
public function lock() {
$this->checkLogin();
$data = json_decode(file_get_contents('php://input'), true);
$periodId = (int)($data['period_id'] ?? 0);
$period = $this->db->get('periods', '*', ['id' => $periodId]);
if (!$period || $period['status'] !== 'pending') {
$this->json(['status' => 'error', 'message' => 'Invalid period or status']);
return;
}
$this->db->update('periods', ['status' => 'locked'], ['id' => $periodId]);
$this->json(['status' => 'success', 'message' => 'Period locked']);
}
public function draw() {
$this->checkLogin();
$data = json_decode(file_get_contents('php://input'), true);
$periodId = (int)($data['period_id'] ?? 0);
$manual = $data['manual'] ?? false;
$period = $this->db->get('periods', '*', ['id' => $periodId]);
if (!$period || !in_array($period['status'], ['pending', 'locked'])) {
$this->json(['status' => 'error', 'message' => 'Invalid period or status']);
return;
}
$gameId = $period['game_id'];
if ($manual && !empty($data['result'])) {
// 手动补开奖
$result = array_map('intval', $data['result']);
if (count($result) !== 10 || count(array_unique($result)) !== 10) {
$this->json(['status' => 'error', 'message' => 'Result must be 10 unique numbers 1-10']);
return;
}
} else {
// 自动开奖(带放水机制)
$waterConfig = [];
$wc = $this->db->select('water_control', '*', ['game_id' => $gameId, 'enabled' => 1]);
foreach ($wc as $w) {
$waterConfig[$w['bet_type']] = (float)$w['win_rate_pct'];
}
$bets = $this->db->select('bets', '*', [
'period_id' => $periodId,
'status' => 'pending',
'is_virtual' => 0
]);
if (!empty($waterConfig) && !empty($bets)) {
$result = PK10Algorithm::generateControlledResult($bets, $waterConfig);
} else {
$result = PK10Algorithm::generateResult();
}
}
$sum = $result[0] + $result[1];
$resultJson = json_encode($result);
$this->db->medoo->pdo->beginTransaction();
try {
$this->db->update('periods', [
'status' => 'drawn',
'result' => $resultJson,
'draw_time' => date('Y-m-d H:i:s'),
], ['id' => $periodId]);
// 删除旧结果(如果手动修改)
$this->db->delete('pk10_results', ['period_id' => $periodId]);
$this->db->insert('pk10_results', [
'period_id' => $periodId,
'rank_1' => $result[0], 'rank_2' => $result[1], 'rank_3' => $result[2],
'rank_4' => $result[3], 'rank_5' => $result[4], 'rank_6' => $result[5],
'rank_7' => $result[6], 'rank_8' => $result[7], 'rank_9' => $result[8],
'rank_10' => $result[9], 'champion_sum' => $sum,
]);
$this->db->medoo->pdo->commit();
} catch (\Throwable $e) {
$this->db->medoo->pdo->rollBack();
$this->json(['status' => 'error', 'message' => 'Draw failed: ' . $e->getMessage()]);
return;
}
$this->json(['status' => 'success', 'message' => 'Draw completed', 'result' => $result, 'sum' => $sum]);
}
public function settle() {
$this->checkLogin();
$data = json_decode(file_get_contents('php://input'), true);
$periodId = (int)($data['period_id'] ?? 0);
$period = $this->db->get('periods', '*', ['id' => $periodId]);
if (!$period || $period['status'] !== 'drawn') {
$this->json(['status' => 'error', 'message' => 'Period must be drawn first']);
return;
}
$result = json_decode($period['result'], true);
if (!$result || count($result) !== 10) {
$this->json(['status' => 'error', 'message' => 'Invalid result data']);
return;
}
$bets = $this->db->select('bets', '*', ['period_id' => $periodId, 'status' => 'pending']);
if (empty($bets)) {
$this->db->update('periods', ['status' => 'settled'], ['id' => $periodId]);
$this->json(['status' => 'success', 'message' => 'No bets to settle', 'wins' => 0, 'losses' => 0]);
return;
}
$wins = 0; $losses = 0; $totalPayout = 0;
$this->db->medoo->pdo->beginTransaction();
try {
foreach ($bets as $bet) {
$isWin = PK10Algorithm::checkWin($result, $bet['bet_type'], $bet['bet_value']);
$betAmount = (float)$bet['amount'];
$odds = (float)$bet['odds'];
if ($isWin) {
$winAmount = $betAmount * $odds;
$payout = $betAmount + $winAmount;
$this->db->update('bets', [
'status' => 'win',
'win_amount' => $winAmount,
], ['id' => $bet['id']]);
// 非虚拟账户才更新余额
if (!(int)$bet['is_virtual']) {
$stmt = $this->db->medoo->pdo->prepare("SELECT balance FROM users WHERE id = :id FOR UPDATE");
$stmt->execute([':id' => $bet['user_id']]);
$user = $stmt->fetch(\PDO::FETCH_ASSOC);
$newBalance = (float)$user['balance'] + $payout;
$this->db->update('users', ['balance' => $newBalance], ['id' => $bet['user_id']]);
$this->db->insert('transactions', [
'user_id' => $bet['user_id'],
'type' => 'win',
'amount' => $payout,
'balance_before' => $user['balance'],
'balance_after' => $newBalance,
'related_id' => $periodId,
'description' => 'PK10 Win - Period ' . $period['period_number'],
'is_virtual' => $bet['is_virtual'],
'created_at' => date('Y-m-d H:i:s'),
]);
$totalPayout += $payout;
}
$wins++;
} else {
$this->db->update('bets', ['status' => 'lose', 'win_amount' => 0], ['id' => $bet['id']]);
$losses++;
}
}
// 计算代理佣金
$this->settleAgentCommissions($bets, $periodId);
$this->db->update('periods', ['status' => 'settled'], ['id' => $periodId]);
$this->db->medoo->pdo->commit();
} catch (\Throwable $e) {
$this->db->medoo->pdo->rollBack();
$this->json(['status' => 'error', 'message' => 'Settle failed: ' . $e->getMessage()]);
return;
}
$this->json(['status' => 'success', 'message' => 'Settlement completed',
'wins' => $wins, 'losses' => $losses, 'total_payout' => $totalPayout]);
}
private function settleAgentCommissions(array $bets, int $periodId) {
$agentBets = [];
foreach ($bets as $bet) {
if ((int)$bet['is_virtual']) continue;
$user = $this->db->get('users', ['agent_id'], ['id' => $bet['user_id']]);
if (!$user || !$user['agent_id']) continue;
$agentId = $user['agent_id'];
if (!isset($agentBets[$agentId])) $agentBets[$agentId] = 0;
$agentBets[$agentId] += (float)$bet['amount'];
}
foreach ($agentBets as $agentId => $totalBet) {
$agent = $this->db->get('agents', '*', ['id' => $agentId, 'status' => 1]);
if (!$agent) continue;
$commission = $totalBet * $agent['commission_rate'] / 100;
if ($commission <= 0) continue;
$this->db->insert('agent_commissions', [
'agent_id' => $agentId,
'from_user_id' => 0,
'period_id' => $periodId,
'bet_amount' => $totalBet,
'commission' => $commission,
'type' => 'bet',
'created_at' => date('Y-m-d H:i:s'),
]);
// 佣金加到代理用户余额
$agentUserId = $agent['user_id'];
$stmt = $this->db->medoo->pdo->prepare("SELECT balance FROM users WHERE id = :id FOR UPDATE");
$stmt->execute([':id' => $agentUserId]);
$au = $stmt->fetch(\PDO::FETCH_ASSOC);
$newBal = (float)$au['balance'] + $commission;
$this->db->update('users', ['balance' => $newBal], ['id' => $agentUserId]);
$this->db->insert('transactions', [
'user_id' => $agentUserId, 'type' => 'commission',
'amount' => $commission, 'balance_before' => $au['balance'],
'balance_after' => $newBal, 'related_id' => $periodId,
'description' => 'Agent commission', 'created_at' => date('Y-m-d H:i:s'),
]);
}
}
public function get($id) {
$this->checkLogin();
$period = $this->db->get('periods', '*', ['id' => (int)$id]);
if ($period) {
$period['pk10'] = $this->db->get('pk10_results', '*', ['period_id' => $period['id']]);
}
$this->json(['status' => 'success', 'data' => $period]);
}
private function getGameId(): int {
$game = $this->db->get('games', 'id', ['code' => 'pk10']);
return $game ? (int)$game : 0;
}
private function json(array $data) {
header('Content-Type: application/json');
echo json_encode($data);
}
}
+957
View File
@@ -0,0 +1,957 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class PeriodController extends AdminBaseController {
public function __construct() {
$this->checkLogin();
$this->checkAdmin();
}
/**
* 期号列表页面
*/
public function index() {
$db = new Database();
try {
// 获取所有期号列表
$periods = $db->select('periods', '*', [
'ORDER' => ['id' => 'DESC'],
'LIMIT' => 100
]);
if (!is_array($periods)) {
$periods = [];
}
// 获取游戏列表(用于关联显示)
$games = [];
$gamesList = [];
try {
$gamesList = $db->select('games', ['id', 'name'], [
'status' => 1,
'ORDER' => ['id' => 'ASC']
]);
if (is_array($gamesList)) {
foreach ($gamesList as $game) {
$games[$game['id']] = $game['name'];
}
}
} catch (\Throwable $e) {
// 忽略错误
}
// 为每个游戏获取当前期号
$currentPeriods = [];
foreach ($gamesList as $game) {
$currentPeriod = $db->get('periods', '*', [
'game_id' => $game['id'],
'ORDER' => ['id' => 'DESC']
]);
if ($currentPeriod) {
$currentPeriods[$game['id']] = $currentPeriod;
}
}
} catch (\Throwable $e) {
$periods = [];
$games = [];
$gamesList = [];
$currentPeriods = [];
}
$this->render('Admin/periods.php', [
'currentPeriods' => $currentPeriods, // 改为多个游戏的当前期号
'periods' => $periods,
'games' => $games,
'gamesList' => $gamesList, // 完整的游戏列表
'title' => '期号管理'
]);
}
/**
* 获取单个期号数据
*/
public function get($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
$db = new Database();
try {
$period = $db->get('periods', '*', [
'id' => $id
]);
if ($period) {
echo json_encode([
'success' => true,
'data' => $period
]);
} else {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
}
} catch (\Throwable $e) {
echo json_encode([
'success' => false,
'message' => '获取数据失败:' . $e->getMessage()
]);
}
}
/**
* 创建期号
*/
public function create() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
echo json_encode([
'success' => false,
'message' => '未接收到数据'
]);
return;
}
$db = new Database();
$gameId = isset($data['game_id']) ? (int)$data['game_id'] : null;
// 如果关联了游戏,从游戏中获取直播流地址
$streamUrl = null;
if ($gameId) {
try {
$game = $db->get('games', ['stream_url'], ['id' => $gameId]);
if ($game && !empty($game['stream_url'])) {
$streamUrl = $game['stream_url'];
}
} catch (\Throwable $e) {
// 忽略错误,继续使用 null
}
}
// 获取当前登录的管理员ID
$createdBy = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
// 生成期号
$periodNumber = $this->generatePeriodNumber($db, $gameId);
// 准备数据
$periodData = [
'period_number' => $periodNumber,
'status' => 'pending',
'game_id' => $gameId ?: null,
'stream_url' => $streamUrl,
'start_time' => date('Y-m-d H:i:s'),
'created_by' => $createdBy,
'auto_generated' => 0, // 手动创建
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
try {
$id = $db->insert('periods', $periodData);
echo json_encode([
'success' => true,
'message' => '期号创建成功',
'data' => ['id' => $id, 'period_number' => $periodNumber]
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '创建失败:' . $e->getMessage()
]);
}
}
/**
* 录入开奖结果
*/
public function draw() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
if (empty($data)) {
echo json_encode([
'success' => false,
'message' => '未接收到数据'
]);
return;
}
$db = new Database();
$id = isset($data['id']) ? (int)$data['id'] : 0;
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
// 获取期号信息
$period = $db->get('periods', '*', ['id' => $id]);
if (!$period) {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
return;
}
// 检查期号状态
if ($period['status'] === 'settled') {
echo json_encode([
'success' => false,
'message' => '该期号已结算,无法修改'
]);
return;
}
// 获取游戏类型
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
$gameType = $game['type'] ?? 'dice';
$auto = isset($data['auto']) ? (bool)$data['auto'] : false;
// 根据游戏类型处理开奖结果
if ($gameType === 'xocdia') {
// Xóc Đĩa 开奖逻辑
if ($auto) {
// 自动生成:4个硬币随机红/白
$coins = [];
for ($i = 0; $i < 4; $i++) {
$coins[] = (rand(0, 1) === 0) ? 'red' : 'white';
}
} else {
// 手动输入:从前端获取
$coins = isset($data['coins']) ? $data['coins'] : [];
if (!is_array($coins) || count($coins) !== 4) {
echo json_encode([
'success' => false,
'message' => 'Xóc Đĩa 必须提供4个硬币的颜色(red/white'
]);
return;
}
// 验证颜色
foreach ($coins as $coin) {
if (!in_array($coin, ['red', 'white'])) {
echo json_encode([
'success' => false,
'message' => '硬币颜色只能是 red 或 white'
]);
return;
}
}
}
// 计算结果
$redCount = count(array_filter($coins, fn($c) => $c === 'red'));
$result = $this->calculateXocdiaResult($redCount);
// 获取当前登录的管理员ID(审核人)
$approvedBy = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
// 更新期号数据
$updateData = [
'result' => json_encode($coins), // 存储JSON数组
'dice1' => $redCount, // 复用字段:红色数量
'dice2' => 4 - $redCount, // 复用字段:白色数量
'dice3' => null,
'total' => null,
'status' => 'drawn',
'draw_time' => date('Y-m-d H:i:s'),
'approved_by' => $approvedBy,
'updated_at' => date('Y-m-d H:i:s')
];
$responseData = [
'coins' => $coins,
'red_count' => $redCount,
'white_count' => 4 - $redCount,
'result' => $result
];
} else {
// 骰子游戏开奖逻辑(原逻辑)
if ($auto) {
$dice1 = rand(1, 6);
$dice2 = rand(1, 6);
$dice3 = rand(1, 6);
} else {
$dice1 = isset($data['dice1']) ? (int)$data['dice1'] : 0;
$dice2 = isset($data['dice2']) ? (int)$data['dice2'] : 0;
$dice3 = isset($data['dice3']) ? (int)$data['dice3'] : 0;
// 验证骰子点数
if ($dice1 < 1 || $dice1 > 6 || $dice2 < 1 || $dice2 > 6 || $dice3 < 1 || $dice3 > 6) {
echo json_encode([
'success' => false,
'message' => '骰子点数必须在1-6之间'
]);
return;
}
}
// 计算总和
$total = $dice1 + $dice2 + $dice3;
// 计算结果
$result = $this->calculateResult($dice1, $dice2, $dice3, $total);
// 获取当前登录的管理员ID(审核人)
$approvedBy = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
// 更新期号数据
$updateData = [
'dice1' => $dice1,
'dice2' => $dice2,
'dice3' => $dice3,
'total' => $total,
'result' => $result,
'status' => 'drawn',
'draw_time' => date('Y-m-d H:i:s'),
'approved_by' => $approvedBy,
'updated_at' => date('Y-m-d H:i:s')
];
$responseData = [
'dice1' => $dice1,
'dice2' => $dice2,
'dice3' => $dice3,
'total' => $total,
'result' => $result
];
}
try {
$db->update('periods', $updateData, ['id' => $id]);
echo json_encode([
'success' => true,
'message' => '开奖结果已录入',
'data' => $responseData
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '录入失败:' . $e->getMessage()
]);
}
}
/**
* 启动新一期(开始下注)
*/
public function start() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
$gameId = isset($data['game_id']) ? (int)$data['game_id'] : 0;
if ($gameId <= 0) {
echo json_encode([
'success' => false,
'message' => '请选择游戏'
]);
return;
}
$db = new Database();
// 检查该游戏是否有未结束的期号
$activePeriod = $db->get('periods', '*', [
'game_id' => $gameId,
'status' => ['pending', 'locked', 'drawn']
]);
if ($activePeriod) {
echo json_encode([
'success' => false,
'message' => '该游戏当前有未结束的期号,无法开始新一轮'
]);
return;
}
// 生成期号
$periodNumber = $this->generatePeriodNumber($db, $gameId);
// 从游戏配置取stream_url
$streamUrl = null;
$game = $db->get('games', ['stream_url'], ['id' => $gameId]);
if ($game && !empty($game['stream_url'])) {
$streamUrl = $game['stream_url'];
}
$periodData = [
'period_number' => $periodNumber,
'status' => 'pending',
'game_id' => $gameId,
'stream_url' => $streamUrl,
'start_time' => date('Y-m-d H:i:s'),
'auto_generated' => 0, // 手动点击开始
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
try {
$id = $db->insert('periods', $periodData);
echo json_encode([
'success' => true,
'message' => '新一期已启动,开始下注',
'data' => [
'id' => $id,
'period_number' => $periodNumber,
'start_time' => $periodData['start_time']
]
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '启动失败:' . $e->getMessage()
]);
}
}
/**
* 确认开奖并结算(自动创建下一期)
*/
public function settle() {
ob_clean();
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
$id = isset($data['id']) ? (int)$data['id'] : 0;
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
$db = new Database();
// 获取期号信息
$period = $db->get('periods', '*', ['id' => $id]);
if (!$period) {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
return;
}
// 检查期号状态
if ($period['status'] !== 'drawn') {
echo json_encode([
'success' => false,
'message' => '该期号还未开奖,无法结算'
]);
return;
}
if ($period['status'] === 'settled') {
echo json_encode([
'success' => false,
'message' => '该期号已结算'
]);
return;
}
try {
// 开启事务
$db->medoo->pdo->beginTransaction();
// 1. 获取本期所有待结算注单
$bets = $db->select('bets', '*', [
'period_id' => $id,
'status' => 'pending'
]);
// 2. 遍历注单进行结算
if ($bets) {
foreach ($bets as $bet) {
$checkResult = $this->checkWin($bet, $period);
$isWin = $checkResult['win'];
$winAmount = $checkResult['amount']; // 纯赢金额
$now = date('Y-m-d H:i:s');
if ($isWin) {
$payout = $bet['amount'] + $winAmount; // 本金 + 盈利
// 更新注单状态
$db->update('bets', [
'status' => 'win',
'win_amount' => $winAmount,
'settled_at' => $now,
'updated_at' => $now
], ['id' => $bet['id']]);
// 更新用户余额
$db->update('users', [
'balance[+]' => $payout
], ['id' => $bet['user_id']]);
// 获取更新后的余额(用于记录流水)
$user = $db->get('users', ['balance'], ['id' => $bet['user_id']]);
$balanceAfter = $user['balance'];
$balanceBefore = $balanceAfter - $payout;
// 写入资金流水
$db->insert('transactions', [
'user_id' => $bet['user_id'],
'type' => 'win',
'amount' => $payout,
'balance_before' => $balanceBefore,
'balance_after' => $balanceAfter,
'related_id' => $bet['id'],
'description' => "中奖 - 期号: " . $period['period_number'],
'created_at' => $now
]);
} else {
// 未中奖
$db->update('bets', [
'status' => 'lose',
'win_amount' => 0,
'settled_at' => $now,
'updated_at' => $now
], ['id' => $bet['id']]);
}
}
}
// 更新期号状态为已结算
$db->update('periods', [
'status' => 'settled',
'updated_at' => date('Y-m-d H:i:s')
], ['id' => $id]);
// 移除自动创建下一期的逻辑,改为由管理员手动点击"开始下注"
/*
// 自动创建下一期
$nextPeriodNumber = $this->generatePeriodNumber($db);
// 如果当前期号没有 stream_url,尝试从关联游戏中获取
$nextStreamUrl = $period['stream_url'];
if (empty($nextStreamUrl) && !empty($period['game_id'])) {
try {
$game = $db->get('games', ['stream_url'], ['id' => $period['game_id']]);
if ($game && !empty($game['stream_url'])) {
$nextStreamUrl = $game['stream_url'];
}
} catch (\Throwable $e) {
// 忽略错误,继续使用原值
}
}
$nextPeriodData = [
'period_number' => $nextPeriodNumber,
'status' => 'pending',
'game_id' => $period['game_id'],
'stream_url' => $nextStreamUrl,
'start_time' => date('Y-m-d H:i:s'),
'auto_generated' => 1, // 自动生成
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
$nextPeriodId = $db->insert('periods', $nextPeriodData);
*/
$nextPeriodId = 0;
$nextPeriodNumber = '';
// 提交事务
$db->medoo->pdo->commit();
echo json_encode([
'success' => true,
'message' => '结算成功,请点击"开始下注"启动下一期',
'data' => [
'current_period_id' => $id,
// 'next_period_id' => $nextPeriodId,
// 'next_period_number' => $nextPeriodNumber
]
]);
} catch (\Exception $e) {
// 回滚事务
if (isset($db->medoo->pdo)) {
$db->medoo->pdo->rollBack();
}
echo json_encode([
'success' => false,
'message' => '结算失败:' . $e->getMessage()
]);
}
}
/**
* 封盘
*/
public function lock() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
$id = isset($data['id']) ? (int)$data['id'] : 0;
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
$db = new Database();
// 获取期号信息
$period = $db->get('periods', '*', ['id' => $id]);
if (!$period) {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
return;
}
// 检查期号状态
if ($period['status'] !== 'pending') {
echo json_encode([
'success' => false,
'message' => '该期号状态不允许封盘'
]);
return;
}
try {
$db->update('periods', [
'status' => 'locked',
'end_time' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
], ['id' => $id]);
echo json_encode([
'success' => true,
'message' => '封盘成功'
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '封盘失败:' . $e->getMessage()
]);
}
}
/**
* 生成期号
* 格式:G{GameID}{YYYYMMDD}{NNNN}
* 例如:G1202512240001
*/
private function generatePeriodNumber($db, $gameId = null) {
// 如果没有指定游戏ID,默认使用1
$gameId = $gameId ?: 1;
$now = new \DateTime();
$dateStr = $now->format('Ymd');
$prefix = "G{$gameId}{$dateStr}";
// 查询当天该游戏最大的期号
try {
$lastPeriod = $db->get('periods', 'period_number', [
'period_number[~]' => $prefix . '%',
'ORDER' => ['period_number' => 'DESC']
]);
if ($lastPeriod) {
// 提取序号并+1
$lastSeq = substr($lastPeriod, strlen($prefix));
$newSeq = (int)$lastSeq + 1;
$seqStr = str_pad((string)$newSeq, 4, '0', STR_PAD_LEFT);
} else {
// 当天第一期
$seqStr = '0001';
}
return $prefix . $seqStr;
} catch (\Throwable $e) {
// 发生错误时回退到时间戳随机数,防止阻塞
return $prefix . time();
}
}
/**
* 检查注单是否中奖
*/
private function checkWin($bet, $period) {
$betType = $bet['bet_type'];
$betValue = $bet['bet_value'];
$betAmount = (float)$bet['amount'];
$gameId = $period['game_id'] ?? 1;
$db = new Database();
// 获取游戏类型
$game = $db->get('games', ['type'], ['id' => $gameId]);
$gameType = $game['type'] ?? 'dice';
// 获取该游戏的所有赔率配置
static $oddsCache = [];
if (!isset($oddsCache[$gameId])) {
$oddsData = $db->select('game_odds', '*', ['game_id' => $gameId]);
$oddsCache[$gameId] = [];
foreach ($oddsData as $odd) {
$key = $odd['type'] . '_' . $odd['target'];
$oddsCache[$gameId][$key] = (float)$odd['odds'];
}
}
$isWin = false;
$payoutMultiplier = 0;
// 辅助函数:获取赔率
$getOdds = function($type, $target = 'all') use ($oddsCache, $gameId) {
$key = $type . '_' . $target;
// 如果找不到特定target的赔率,尝试查找 'all'
if (isset($oddsCache[$gameId][$key])) {
return $oddsCache[$gameId][$key];
}
$allKey = $type . '_all';
return $oddsCache[$gameId][$allKey] ?? 0;
};
// Xóc Đĩa 游戏结算逻辑
if ($gameType === 'xocdia') {
// 从 result 字段解析硬币颜色数组
$coins = json_decode($period['result'], true);
if (!is_array($coins) || count($coins) !== 4) {
return ['win' => false, 'amount' => 0];
}
$redCount = count(array_filter($coins, fn($c) => $c === 'red'));
$whiteCount = 4 - $redCount;
switch ($betType) {
case 'chan': // 双(偶): 4红 或 4白 或 2红2白
if (in_array($redCount, [0, 2, 4])) {
$isWin = true;
$payoutMultiplier = $getOdds('chan', $betValue);
}
break;
case 'le': // 单(奇): 3红1白 或 3白1红
if (in_array($redCount, [1, 3])) {
$isWin = true;
$payoutMultiplier = $getOdds('le', $betValue);
}
break;
case 'exact': // 精确颜色组合
switch ($betValue) {
case '4red':
if ($redCount === 4) $isWin = true;
break;
case '4white':
if ($redCount === 0) $isWin = true;
break;
case '3red1white':
if ($redCount === 3) $isWin = true;
break;
case '3white1red':
if ($redCount === 1) $isWin = true;
break;
}
if ($isWin) {
$payoutMultiplier = $getOdds('exact', $betValue);
}
break;
}
if ($isWin && $payoutMultiplier > 0) {
return [
'win' => true,
'amount' => $betAmount * $payoutMultiplier
];
}
return ['win' => false, 'amount' => 0];
}
// 骰子游戏结算逻辑(原逻辑)
$dice1 = (int)$period['dice1'];
$dice2 = (int)$period['dice2'];
$dice3 = (int)$period['dice3'];
$total = (int)$period['total'];
switch ($betType) {
case 'xiu': // 小 (4-10)
// 爆子处理:1-3为小,4-6为大
if ($dice1 == $dice2 && $dice2 == $dice3) {
if ($dice1 <= 3) $isWin = true;
} else {
if ($total >= 4 && $total <= 10) $isWin = true;
}
$payoutMultiplier = $getOdds('xiu');
break;
case 'tai': // 大 (11-17)
// 爆子处理:1-3为小,4-6为大
if ($dice1 == $dice2 && $dice2 == $dice3) {
if ($dice1 >= 4) $isWin = true;
} else {
if ($total >= 11 && $total <= 17) $isWin = true;
}
$payoutMultiplier = $getOdds('tai');
break;
case 'chan': // 偶数
if ($total % 2 == 0) $isWin = true;
$payoutMultiplier = $getOdds('chan');
break;
case 'le': // 奇数
if ($total % 2 != 0) $isWin = true;
$payoutMultiplier = $getOdds('le');
break;
case 'number': // 单数字 (Sum)
if ($total == (int)$betValue) {
$isWin = true;
$payoutMultiplier = $getOdds('sum', $betValue);
}
break;
case 'dice': // 单个骰子
$count = 0;
if ($dice1 == (int)$betValue) $count++;
if ($dice2 == (int)$betValue) $count++;
if ($dice3 == (int)$betValue) $count++;
if ($count > 0) {
$isWin = true;
// 单个骰子赔率通常是 1:1, 1:2, 1:3
// 但数据库中只存了基础赔率 (e.g. 0.97 or 1.0)
// 这里我们假设数据库存的是 1赔X 的 X
// 如果是双骰或三骰,通常规则是:
// 1个: 1倍
// 2个: 2倍
// 3个: 3倍
// 我们读取基础赔率,然后乘以数量
$baseOdds = $getOdds('dice', $betValue);
$payoutMultiplier = $baseOdds * $count;
}
break;
case 'combo': // 豹子 (Specific Triple)
// betValue 存储的是总和 (3, 6, ..., 18) 或 'any_triple'
if ($betValue === 'any_triple') {
if ($dice1 == $dice2 && $dice2 == $dice3) {
$isWin = true;
$payoutMultiplier = $getOdds('combo', 'any_triple');
}
} else {
if ($dice1 == $dice2 && $dice2 == $dice3 && $total == (int)$betValue) {
$isWin = true;
$payoutMultiplier = $getOdds('combo', 'specific_triple');
}
}
break;
}
if ($isWin && $payoutMultiplier > 0) {
return [
'win' => true,
'amount' => $betAmount * $payoutMultiplier
];
}
return ['win' => false, 'amount' => 0];
}
/**
* 计算开奖结果(骰子游戏)
* @param int $dice1 骰子1点数
* @param int $dice2 骰子2点数
* @param int $dice3 骰子3点数
* @param int $total 总和
* @return string 结果:Xỉu/Tài/Bão
*/
private function calculateResult($dice1, $dice2, $dice3, $total) {
// 检查是否为爆子(三个骰子相同)
if ($dice1 === $dice2 && $dice2 === $dice3) {
// 爆子根据点数判断:1-3为Xỉu,4-6为Tài
if ($dice1 <= 3) {
return 'Xỉu';
} else {
return 'Tài';
}
}
// 普通大小判断:4-10点为小(Xỉu),11-17点为大(Tài)
if ($total >= 4 && $total <= 10) {
return 'Xỉu';
} else {
return 'Tài';
}
}
/**
* 计算 Xóc Đĩa 开奖结果
* @param int $redCount 红色硬币数量
* @return string 结果描述
*/
private function calculateXocdiaResult($redCount) {
switch ($redCount) {
case 0:
return '4 Trắng (Chẵn)';
case 1:
return '3 Trắng 1 Đỏ (Lẻ)';
case 2:
return '2 Trắng 2 Đỏ (Chẵn)';
case 3:
return '3 Đỏ 1 Trắng (Lẻ)';
case 4:
return '4 Đỏ (Chẵn)';
default:
return 'Không xác định';
}
}
}
+217
View File
@@ -0,0 +1,217 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
use App\Core\PluginManager;
class PluginController extends AdminBaseController {
protected $pluginManager;
protected $db;
public function __construct() {
global $pluginManager;
$this->pluginManager = $pluginManager;
$this->db = $this->pluginManager->getDB();
$this->checkLogin();
$this->checkAdmin();
}
public function manage() {
$plugins = $this->pluginManager->getPluginStatusList();
$icons = $this->pluginManager->getAllPluginIcons();
foreach ($plugins as &$plugin) {
$pluginName = $plugin['name']; // 获取插件名
$plugin['icon'] = $icons[$pluginName] ?? 'fa fa-plug';
}
unset($plugin);
$this->render('Admin/plugins.php', [
'plugins' => $plugins,
'title' => '插件管理'
]);
}
public function upload() {
header('Content-Type: application/json');
if (!isset($_FILES['plugin_zip']) || $_FILES['plugin_zip']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => '插件上传失败']);
return;
}
$zipPath = $_FILES['plugin_zip']['tmp_name'];
$fileName = basename($_FILES['plugin_zip']['name']);
$pluginName = pathinfo($fileName, PATHINFO_FILENAME);
$pluginDir = __DIR__ . '/../../Plugins/' . $pluginName;
if (is_dir($pluginDir)) {
http_response_code(409);
echo json_encode(['success' => false, 'message' => '插件已存在,请先删除或改名']);
return;
}
if (!mkdir($pluginDir, 0777, true) && !is_dir($pluginDir)) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => '无法创建插件目录']);
return;
}
$zip = new \ZipArchive;
if ($zip->open($zipPath) === TRUE) {
$zip->extractTo($pluginDir);
$zip->close();
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => '解压失败']);
return;
}
if (!file_exists($pluginDir . '/mian.php')) {
$this->deleteFolder($pluginDir);
http_response_code(400);
echo json_encode(['success' => false, 'message' => '无效插件包,缺少 mian.php']);
return;
}
$this->pluginManager->clearCache();
$plugins = $this->pluginManager->scanPlugins();
if (!isset($plugins[$pluginName])) {
$this->deleteFolder($pluginDir);
http_response_code(400);
echo json_encode([
'success' => false,
'message' =>$this->pluginManager->getLastError() ?: "插件缺少必要信息,请检查插件是否规范!"
]);
return;
}
echo json_encode(['success' => true, 'message' => '插件上传成功']);
}
public function toggleStatus($name) {
header('Content-Type: application/json');
$installedPlugins = $this->pluginManager->getInstalledPlugins();
if (!isset($installedPlugins[$name])) {
echo json_encode(['success' => false, 'message' => "插件 {$name} 不存在"]);
return;
}
$currentStatus = $installedPlugins[$name]['status'];
$newStatus = $currentStatus ? 0 : 1;
$this->db->update('plugins', ['status' => $newStatus], ['name' => $name]);
$msg = $newStatus ? "插件 {$name} 已启用" : "插件 {$name} 已禁用";
echo json_encode(['success' => true, 'message' => $msg]);
}
public function install($name) {
header('Content-Type: application/json');
$allPlugins = $this->pluginManager->getAllPlugins();
if (!isset($allPlugins[$name])) {
echo json_encode(['success' => false, 'message' => "插件 {$name} 不存在"]);
return;
}
$result = $this->pluginManager->installPlugin($name);
if ($result !== true) {
echo json_encode(['success' => false, 'message' => $result]);
return;
}
$pluginFile = $allPlugins[$name]['path'] . '/mian.php';
if (file_exists($pluginFile)) {
$pluginInfo = require $pluginFile;
if (isset($pluginInfo['activate']) && is_callable($pluginInfo['activate'])) {
call_user_func($pluginInfo['activate'], $this->db);
}
}
echo json_encode(['success' => true, 'message' => "插件 {$name} 安装成功"]);
}
public function uninstall($name) {
header('Content-Type: application/json');
$allPlugins = $this->pluginManager->getAllPlugins();
if (!isset($allPlugins[$name])) {
echo json_encode(['success' => false, 'message' => "插件 {$name} 不存在"]);
return;
}
$pluginFile = $allPlugins[$name]['path'] . '/mian.php';
if (file_exists($pluginFile)) {
$pluginInfo = require $pluginFile;
if (isset($pluginInfo['deactivate']) && is_callable($pluginInfo['deactivate'])) {
call_user_func($pluginInfo['deactivate'], $this->db);
}
}
$this->pluginManager->uninstallPlugin($name);
echo json_encode(['success' => true, 'message' => "插件 {$name} 已卸载。"]);
}
public function delete($name) {
if (!$name) {
return json_encode(['success' => false, 'message' => '插件名称不能为空']);
}
$pluginDir = PLUGIN_PATH . $name;
// 判断插件目录是否存在
if (!is_dir($pluginDir)) {
return json_encode(['success' => false, 'message' => '插件目录不存在']);
}
// 尝试删除目录
if ($this->deleteFolder($pluginDir)) {
echo json_encode(['success' => true, 'message' => "插件 {$name} 已卸载并删除"]);
} else {
echo json_encode(['success' => false, 'message' => "插件 {$name} 删除失败,请检查权限或是否被占用"]);
}
}
private function deleteFolder($dir) {
if (!file_exists($dir)) {
return true; // 不存在当作删除成功
}
// 如果是文件或符号链接,直接删除
if (is_file($dir) || is_link($dir)) {
return @unlink($dir);
}
// 扫描目录内容
$items = array_diff(scandir($dir), ['.', '..']);
foreach ($items as $item) {
$path = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($path)) {
if (!$this->deleteFolder($path)) {
return false; // 子目录删除失败
}
} else {
if (!@unlink($path)) {
return false; // 删除文件失败
}
}
}
// 删除目录本身
return @rmdir($dir);
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class ReportController extends AdminBaseController {
private $db;
public function __construct(Database $db) { $this->db = $db; }
public function index() {
$this->checkLogin(); $this->checkAdmin();
$dateFrom = $_GET['from'] ?? date('Y-m-d', strtotime('-7 days'));
$dateTo = $_GET['to'] ?? date('Y-m-d');
$range = [$dateFrom . ' 00:00:00', $dateTo . ' 23:59:59'];
// 总投注(排除虚拟)
$totalBet = $this->db->sum('bets', 'amount', [
'is_virtual' => 0, 'created_at[<>]' => $range
]) ?: 0;
// 总派奖
$totalWin = $this->db->sum('bets', 'win_amount', [
'is_virtual' => 0, 'status' => 'win', 'created_at[<>]' => $range
]) ?: 0;
// 平台利润
$profit = $totalBet - $totalWin;
// 总充值
$totalDeposit = $this->db->sum('transactions', 'amount', [
'is_virtual' => 0, 'type[~]' => '%deposit%', 'amount[>]' => 0,
'created_at[<>]' => $range
]) ?: 0;
// 总提现
$totalWithdraw = abs($this->db->sum('transactions', 'amount', [
'is_virtual' => 0, 'type[~]' => '%withdraw%', 'amount[<]' => 0,
'created_at[<>]' => $range
]) ?: 0);
// 代理佣金
$totalCommission = $this->db->sum('agent_commissions', 'commission', [
'created_at[<>]' => $range
]) ?: 0;
// 反水
$totalRebate = $this->db->sum('agent_commissions', 'commission', [
'type' => 'rebate', 'created_at[<>]' => $range
]) ?: 0;
// 用户统计
$totalUsers = $this->db->count('users', ['is_virtual' => 0]);
$newUsers = $this->db->count('users', [
'is_virtual' => 0, 'created_at[<>]' => $range
]);
// 每日明细
$dailyStats = [];
$start = new \DateTime($dateFrom);
$end = new \DateTime($dateTo);
$end->modify('+1 day');
$interval = new \DateInterval('P1D');
$period = new \DatePeriod($start, $interval, $end);
foreach ($period as $day) {
$d = $day->format('Y-m-d');
$dr = [$d . ' 00:00:00', $d . ' 23:59:59'];
$dailyStats[] = [
'date' => $d,
'bets' => $this->db->sum('bets', 'amount', ['is_virtual'=>0,'created_at[<>]'=>$dr]) ?: 0,
'wins' => $this->db->sum('bets', 'win_amount', ['is_virtual'=>0,'status'=>'win','created_at[<>]'=>$dr]) ?: 0,
'deposits' => $this->db->sum('transactions', 'amount', ['is_virtual'=>0,'type[~]'=>'%deposit%','amount[>]'=>0,'created_at[<>]'=>$dr]) ?: 0,
'withdraws' => abs($this->db->sum('transactions', 'amount', ['is_virtual'=>0,'type[~]'=>'%withdraw%','amount[<]'=>0,'created_at[<>]'=>$dr]) ?: 0),
];
}
// 代理报表
$agentStats = [];
$agents = $this->db->select('agents', '*', ['status' => 1]);
foreach ($agents as $ag) {
$playerIds = $this->db->select('users', 'id', ['agent_id' => $ag['id'], 'is_virtual' => 0]);
$agBet = 0; $agWin = 0;
if (!empty($playerIds)) {
$agBet = $this->db->sum('bets', 'amount', ['user_id' => $playerIds, 'is_virtual'=>0, 'created_at[<>]'=>$range]) ?: 0;
$agWin = $this->db->sum('bets', 'win_amount', ['user_id' => $playerIds, 'is_virtual'=>0, 'status'=>'win', 'created_at[<>]'=>$range]) ?: 0;
}
$agComm = $this->db->sum('agent_commissions', 'commission', ['agent_id'=>$ag['id'],'created_at[<>]'=>$range]) ?: 0;
$agentStats[] = [
'agent' => $ag,
'user' => $this->db->get('users', ['username'], ['id' => $ag['user_id']]),
'players' => count($playerIds),
'bets' => $agBet, 'wins' => $agWin, 'commission' => $agComm,
'profit' => $agBet - $agWin - $agComm,
];
}
$this->render('Admin/reports.php', compact(
'dateFrom','dateTo','totalBet','totalWin','profit',
'totalDeposit','totalWithdraw','totalCommission','totalRebate',
'totalUsers','newUsers','dailyStats','agentStats'
));
}
private function json($data) { header('Content-Type: application/json'); echo json_encode($data); }
}
+273
View File
@@ -0,0 +1,273 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
use \Exception;
class SettingsController extends AdminBaseController {
protected $db;
protected $uploadConfig;
public function __construct() {
$this->checkLogin();
$this->db = new Database();
$this->initSettingsTable();
$this->uploadConfig = [
'upload_dir' => dirname(dirname(dirname(__DIR__))) . '/Static/img/',
'max_size' => 5 * 1024 * 1024, // 5MB
'allowed_mimes' => [
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/x-icon', 'image/vnd.microsoft.icon'
]
];
}
/**
* 初始化系统设置表
*/
private function initSettingsTable() {
try {
$this->db->query("
CREATE TABLE IF NOT EXISTS `system_settings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`setting_key` varchar(100) NOT NULL,
`setting_value` text,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_setting_key` (`setting_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
} catch (Exception $e) {
// 表已存在或其他错误,忽略
}
}
/**
* 系统设置页面
*/
public function index() {
$settings = $this->getAllSettings();
$this->render('Admin/settings.php', [
'title' => '系统设置',
'settings' => $settings
]);
}
/**
* 获取所有设置
*/
public function get() {
header('Content-Type: application/json');
try {
$settings = $this->getAllSettings();
echo json_encode([
'status' => 'success',
'data' => $settings
]);
exit;
} catch (Exception $e) {
$this->jsonError('获取设置失败: ' . $e->getMessage());
}
}
/**
* 保存系统设置
*/
public function save() {
header('Content-Type: application/json');
$this->checkAdmin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->jsonError('仅支持POST请求');
}
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
$data = $_POST;
}
// 处理logo上传(只支持site_logo和site_favicon
$logoTypes = ['site_logo', 'site_favicon'];
foreach ($logoTypes as $logoType) {
$fileKey = $logoType . '_file';
if (isset($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) {
$_POST['logo_type'] = $logoType;
$logoUrl = $this->handleLogoUpload($_FILES[$fileKey]);
$this->saveSetting($logoType, $logoUrl);
}
}
// 保存其他设置
$allowedKeys = [
'site_title', 'site_description', 'site_keywords',
'site_logo', 'site_favicon', 'site_copyright',
'smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass',
'smtp_from', 'smtp_from_name', 'smtp_encryption',
];
foreach ($allowedKeys as $key) {
if (isset($data[$key])) {
$this->saveSetting($key, $data[$key]);
}
}
// 清除SettingsHelper缓存
\App\Core\SettingsHelper::clearCache();
echo json_encode([
'status' => 'success',
'message' => '设置保存成功',
'data' => $this->getAllSettings()
]);
exit;
} catch (Exception $e) {
$this->jsonError('保存设置失败: ' . $e->getMessage());
}
}
/**
* 处理logo上传
*/
private function handleLogoUpload($file) {
// 验证文件大小
if ($file['size'] > $this->uploadConfig['max_size']) {
throw new Exception('文件过大,最大支持5MB');
}
// 验证文件类型
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $this->uploadConfig['allowed_mimes'])) {
throw new Exception('不支持的文件类型,仅允许: JPG, PNG, GIF, WEBP, ICO');
}
// 确保上传目录存在
$uploadDir = rtrim($this->uploadConfig['upload_dir'], '/') . '/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// 生成文件名
$originalName = basename($file['name']);
$originalExt = pathinfo($originalName, PATHINFO_EXTENSION);
$filename = 'logo_' . date('YmdHis') . '_' . uniqid() . '.' . $originalExt;
$targetPath = $uploadDir . $filename;
// 移动文件
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
throw new Exception('文件上传失败');
}
// 返回相对路径(不含域名,兼容任何域名/端口)
return '/Static/img/' . $filename;
}
/**
* 获取单个设置值
*/
private function getSetting($key, $default = '') {
try {
$setting = $this->db->get('system_settings', 'setting_value', [
'setting_key' => $key
]);
return $setting !== false ? $setting : $default;
} catch (Exception $e) {
return $default;
}
}
/**
* 保存单个设置
*/
private function saveSetting($key, $value) {
try {
$existing = $this->db->get('system_settings', 'id', [
'setting_key' => $key
]);
if ($existing) {
// 更新
$this->db->update('system_settings', [
'setting_value' => $value
], [
'setting_key' => $key
]);
} else {
// 插入
$this->db->insert('system_settings', [
'setting_key' => $key,
'setting_value' => $value
]);
}
} catch (Exception $e) {
throw new Exception('保存设置失败: ' . $e->getMessage());
}
}
/**
* 获取所有设置(以关联数组形式返回)
*/
private function getAllSettings() {
try {
$settings = $this->db->select('system_settings', ['setting_key', 'setting_value']);
$result = [];
foreach ($settings as $setting) {
$result[$setting['setting_key']] = $setting['setting_value'];
}
// 设置默认值
$defaults = [
'site_title' => 'Tài Xỉu Online - TM68',
'site_description' => 'Trang game tài xỉu đổi thưởng, nạp rút nhanh chóng',
'site_keywords' => 'tài xỉu, game online, đổi thưởng',
'site_logo' => '/Static/tm68/logo_tm68.png.png',
'site_favicon' => '/Static/css/favicon.ico',
'site_copyright' => '© 2024 TM68. All rights reserved.'
];
foreach ($defaults as $key => $default) {
if (!isset($result[$key])) {
$result[$key] = $default;
}
}
return $result;
} catch (Exception $e) {
return [];
}
}
/**
* SMTP 测试发信
*/
public function smtpTest() {
header('Content-Type: application/json');
$this->checkAdmin();
$data = json_decode(file_get_contents('php://input'), true);
$email = trim($data['email'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->jsonError('Invalid email');
}
\App\Core\SettingsHelper::clearCache();
$result = \App\Core\Mailer::test($email);
echo json_encode($result);
exit;
}
/**
* JSON错误响应
*/
private function jsonError($message) {
echo json_encode([
'status' => 'error',
'message' => $message
]);
exit;
}
}
+359
View File
@@ -0,0 +1,359 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class UserController extends AdminBaseController {
/**
* 构造函数 - 验证管理员权限
*/
public function __construct() {
$this->checkLogin();
}
/**
* 用户列表页面
*/
public function index() {
$this->checkAdmin();
$db = new Database();
// 只查询普通用户,排除管理员
$users = $db->select('users', '*', [
'role[!]' => 'admin',
'ORDER' => ['id' => 'DESC']
]);
$this->render('Admin/user.php', [
'users' => $users,
'title' => '用户管理'
]);
}
/**
* 获取单个用户数据(用于编辑和详情)
*/
public function get($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的用户ID'
]);
return;
}
$db = new Database();
$user = $db->get('users', '*', [
'id' => $id
]);
if ($user) {
// 移除密码字段,避免泄露
unset($user['password']);
echo json_encode([
'success' => true,
'data' => $user
]);
} else {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
}
}
public function update() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
echo json_encode([
'success' => false,
'message' => '未接收到数据'
]);
return;
}
$db = new Database();
$id = isset($data['id']) ? (int)$data['id'] : 0;
$isEditMode = $id > 0;
$username = trim($data['username'] ?? '');
$email = trim($data['email'] ?? '');
$role = $data['role'] ?? 'user';
$status = isset($data['status']) ? (int)$data['status'] : 0;
if (empty($username) || empty($email)) {
echo json_encode([
'success' => false,
'message' => '用户名和邮箱不能为空'
]);
return;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode([
'success' => false,
'message' => '邮箱格式不正确'
]);
return;
}
// 统一查重(排除当前 ID
$exists = $db->get('users', '*', [
'AND' => [
'OR' => [
'username' => $username,
'email' => $email
],
'id[!]' => $id
]
]);
if ($exists) {
if ($exists['username'] === $username) {
$msg = '用户名已存在';
} elseif ($exists['email'] === $email) {
$msg = '邮箱已被使用';
} else {
$msg = '用户名或邮箱已被占用';
}
echo json_encode([
'success' => false,
'message' => $msg
]);
return;
}
// 准备数据
$userData = [
'username' => $username,
'email' => $email,
'role' => $role,
'status' => $status,
'updated_at' => date('Y-m-d H:i:s')
];
try {
if ($isEditMode) {
// 如果密码不为空,更新密码
if (!empty($data['password'])) {
if (strlen($data['password']) < 8) {
echo json_encode([
'success' => false,
'message' => '密码长度至少8位'
]);
return;
}
$userData['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
}
$db->update('users', $userData, ['id' => $id]);
$message = '用户更新成功';
} else {
// 新增时必须有密码
if (empty($data['password']) || strlen($data['password']) < 8) {
echo json_encode([
'success' => false,
'message' => '密码长度至少8位'
]);
return;
}
$userData['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
$userData['created_at'] = date('Y-m-d H:i:s');
$id = $db->insert('users', $userData);
$message = '用户创建成功';
}
echo json_encode([
'success' => true,
'message' => $message,
'data' => ['id' => $id]
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '操作失败:' . $e->getMessage()
]);
}
}
/**
* 调整用户余额
*/
public function adjustBalance($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的用户ID'
]);
return;
}
$rawData = file_get_contents('php://input');
$data = json_decode($rawData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode([
'success' => false,
'message' => '数据格式错误'
]);
return;
}
$amount = isset($data['amount']) ? floatval($data['amount']) : 0;
$action = $data['action'] ?? ''; // 'increase' or 'decrease'
if ($amount <= 0) {
echo json_encode([
'success' => false,
'message' => '金额必须大于0'
]);
return;
}
if (!in_array($action, ['increase', 'decrease'])) {
echo json_encode([
'success' => false,
'message' => '无效的操作类型'
]);
return;
}
$db = new Database();
// 获取当前用户信息
$user = $db->get('users', ['id', 'balance', 'username'], [
'id' => $id,
'role[!]' => 'admin' // 只能调整普通用户的余额
]);
if (!$user) {
echo json_encode([
'success' => false,
'message' => '用户不存在或无法操作'
]);
return;
}
$currentBalance = floatval($user['balance'] ?? 0);
$newBalance = $action === 'increase'
? $currentBalance + $amount
: $currentBalance - $amount;
// 检查减少后余额不能为负数
if ($newBalance < 0) {
echo json_encode([
'success' => false,
'message' => '余额不足,当前余额:' . number_format($currentBalance, 2)
]);
return;
}
try {
$db->medoo->pdo->beginTransaction();
// 更新用户余额
$db->update('users', [
'balance' => $newBalance,
'updated_at' => date('Y-m-d H:i:s')
], ['id' => $id]);
// 写入资金流水
// 修改这里:使用 manual_deposit 和 manual_withdraw 区分人工操作
$transactionType = $action === 'increase' ? 'manual_deposit' : 'manual_withdraw';
$transactionAmount = $action === 'increase' ? $amount : -$amount;
$description = $action === 'increase' ? '管理员手动增加余额' : '管理员手动减少余额';
$db->insert('transactions', [
'user_id' => $id,
'type' => $transactionType,
'amount' => $transactionAmount,
'balance_before' => $currentBalance,
'balance_after' => $newBalance,
'description' => $description,
'created_at' => date('Y-m-d H:i:s')
]);
$db->medoo->pdo->commit();
$actionText = $action === 'increase' ? '增加' : '减少';
echo json_encode([
'success' => true,
'message' => "余额{$actionText}成功",
'data' => [
'old_balance' => $currentBalance,
'new_balance' => $newBalance,
'amount' => $amount
]
]);
} catch (\Exception $e) {
$db->medoo->pdo->rollBack();
echo json_encode([
'success' => false,
'message' => '操作失败:' . $e->getMessage()
]);
}
}
/**
* 删除用户
*/
public function delete($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的用户ID'
]);
return;
}
// 禁止删除自己
session_start();
if ($id == $_SESSION['user_id']) {
echo json_encode([
'success' => false,
'message' => '不能删除当前登录用户'
]);
return;
}
$db = new Database();
$userExists = $db->get('users', 'id', [
'id' => $id
]);
if (!$userExists) {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
return;
}
try {
$db->delete('users', [
'id' => $id
]);
echo json_encode([
'success' => true,
'message' => '用户已删除'
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '删除失败:' . $e->getMessage()
]);
}
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class VirtualAccountController extends AdminBaseController {
private $db;
public function __construct(Database $db) { $this->db = $db; }
public function index() {
$this->checkLogin(); $this->checkAdmin();
$virtuals = $this->db->select('users', '*', ['is_virtual' => 1, 'ORDER' => ['id' => 'DESC']]);
$this->render('Admin/virtual_accounts.php', compact('virtuals'));
}
public function create() {
$this->checkLogin(); $this->checkAdmin();
$data = json_decode(file_get_contents('php://input'), true);
$username = trim($data['username'] ?? '');
if (empty($username)) { $this->json(['status'=>'error','message'=>'Username required']); return; }
if ($this->db->get('users', 'id', ['username' => $username])) {
$this->json(['status'=>'error','message'=>'Username exists']); return;
}
$this->db->insert('users', [
'username' => $username,
'password' => password_hash($data['password'] ?? '123456', PASSWORD_DEFAULT),
'email' => $username . '@virtual.local',
'role' => 'user', 'status' => 1, 'is_virtual' => 1,
'email_verified' => 1,
'balance' => floatval($data['balance'] ?? 100000),
'created_at' => date('Y-m-d H:i:s'),
]);
$this->json(['status' => 'success']);
}
public function adjustBalance() {
$this->checkLogin(); $this->checkAdmin();
$data = json_decode(file_get_contents('php://input'), true);
$id = (int)($data['id'] ?? 0);
$amount = floatval($data['amount'] ?? 0);
$user = $this->db->get('users', '*', ['id' => $id, 'is_virtual' => 1]);
if (!$user) { $this->json(['status'=>'error','message'=>'Not found']); return; }
$new = (float)$user['balance'] + $amount;
$this->db->update('users', ['balance' => $new], ['id' => $id]);
$this->json(['status' => 'success', 'new_balance' => $new]);
}
public function delete($id) {
$this->checkLogin(); $this->checkAdmin();
$this->db->delete('users', ['id' => (int)$id, 'is_virtual' => 1]);
$this->json(['status' => 'success']);
}
private function json($data) { header('Content-Type: application/json'); echo json_encode($data); }
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class WaterController extends AdminBaseController {
private $db;
public function __construct(Database $db) { $this->db = $db; }
public function index() {
$this->checkLogin(); $this->checkAdmin();
$gameId = $this->getGameId();
$configs = $this->db->select('water_control', '*', ['game_id' => $gameId]);
$limits = $this->db->select('bet_limits', '*', ['game_id' => $gameId]);
$this->render('Admin/water_control.php', compact('configs', 'limits', 'gameId'));
}
public function updateWater() {
$this->checkLogin(); $this->checkAdmin();
$data = json_decode(file_get_contents('php://input'), true);
foreach ($data['items'] ?? [] as $item) {
$existing = $this->db->get('water_control', 'id', [
'game_id' => (int)$item['game_id'], 'bet_type' => $item['bet_type']
]);
$fields = [
'win_rate_pct' => floatval($item['win_rate_pct']),
'enabled' => (int)($item['enabled'] ?? 0),
];
if ($existing) {
$this->db->update('water_control', $fields, ['id' => $existing]);
} else {
$fields['game_id'] = (int)$item['game_id'];
$fields['bet_type'] = $item['bet_type'];
$this->db->insert('water_control', $fields);
}
}
$this->json(['status' => 'success']);
}
public function updateLimits() {
$this->checkLogin(); $this->checkAdmin();
$data = json_decode(file_get_contents('php://input'), true);
foreach ($data['items'] ?? [] as $item) {
$existing = $this->db->get('bet_limits', 'id', [
'game_id' => (int)$item['game_id'], 'bet_type' => $item['bet_type']
]);
$fields = [
'min_amount' => floatval($item['min_amount']),
'max_amount' => floatval($item['max_amount']),
'max_per_period' => floatval($item['max_per_period'] ?? 500000),
];
if ($existing) {
$this->db->update('bet_limits', $fields, ['id' => $existing]);
} else {
$fields['game_id'] = (int)$item['game_id'];
$fields['bet_type'] = $item['bet_type'];
$this->db->insert('bet_limits', $fields);
}
}
$this->json(['status' => 'success']);
}
private function getGameId(): int {
if (!empty($_GET['game_id'])) return (int)$_GET['game_id'];
return (int)($this->db->get('games', 'id', ['code' => 'pk10']) ?: 0);
}
private function json($d) { header('Content-Type: application/json'); echo json_encode($d); }
}
+714
View File
@@ -0,0 +1,714 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class XocdiaPeriodController extends AdminBaseController {
private $gameType = 'xocdia';
public function __construct() {
$this->checkLogin();
$this->checkAdmin();
}
/**
* Xóc Đĩa 游戏期号列表页面
*/
public function index() {
$db = new Database();
try {
// 获取 Xóc Đĩa 游戏列表
$gamesList = $db->select('games', ['id', 'name'], [
'type' => $this->gameType,
'status' => 1,
'ORDER' => ['id' => 'ASC']
]);
if (!is_array($gamesList)) {
$gamesList = [];
}
// 获取所有 Xóc Đĩa 游戏的game_id
$gameIds = array_column($gamesList, 'id');
// 获取 Xóc Đĩa 游戏的期号列表
$periods = [];
if (!empty($gameIds)) {
$periods = $db->select('periods', '*', [
'game_id' => $gameIds,
'ORDER' => ['id' => 'DESC'],
'LIMIT' => 100
]);
}
if (!is_array($periods)) {
$periods = [];
}
// 构建游戏名称映射
$games = [];
foreach ($gamesList as $game) {
$games[$game['id']] = $game['name'];
}
// 为每个 Xóc Đĩa 游戏获取当前期号
$currentPeriods = [];
foreach ($gamesList as $game) {
$currentPeriod = $db->get('periods', '*', [
'game_id' => $game['id'],
'ORDER' => ['id' => 'DESC']
]);
if ($currentPeriod) {
$currentPeriods[$game['id']] = $currentPeriod;
}
}
} catch (\Throwable $e) {
$periods = [];
$games = [];
$gamesList = [];
$currentPeriods = [];
}
$this->render('Admin/xocdia_periods.php', [
'currentPeriods' => $currentPeriods,
'periods' => $periods,
'games' => $games,
'gamesList' => $gamesList,
'title' => 'Xóc Đĩa 游戏期号管理'
]);
}
/**
* 获取单个期号数据
*/
public function get($id) {
header('Content-Type: application/json');
if (empty($id) || !is_numeric($id)) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
$db = new Database();
try {
$period = $db->get('periods', '*', [
'id' => $id
]);
if ($period) {
// 验证是否为 Xóc Đĩa 游戏期号
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
if ($game && $game['type'] === $this->gameType) {
echo json_encode([
'success' => true,
'data' => $period
]);
} else {
echo json_encode([
'success' => false,
'message' => '期号不属于 Xóc Đĩa 游戏'
]);
}
} else {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
}
} catch (\Throwable $e) {
echo json_encode([
'success' => false,
'message' => '获取数据失败:' . $e->getMessage()
]);
}
}
/**
* 录入开奖结果
*/
public function draw() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
if (empty($data)) {
echo json_encode([
'success' => false,
'message' => '未接收到数据'
]);
return;
}
$db = new Database();
$id = isset($data['id']) ? (int)$data['id'] : 0;
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
// 获取期号信息
$period = $db->get('periods', '*', ['id' => $id]);
if (!$period) {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
return;
}
// 验证是否为 Xóc Đĩa 游戏
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
if (!$game || $game['type'] !== $this->gameType) {
echo json_encode([
'success' => false,
'message' => '期号不属于 Xóc Đĩa 游戏'
]);
return;
}
// 检查期号状态
if ($period['status'] === 'settled') {
echo json_encode([
'success' => false,
'message' => '该期号已结算,无法修改'
]);
return;
}
$auto = isset($data['auto']) ? (bool)$data['auto'] : false;
// Xóc Đĩa 开奖逻辑
if ($auto) {
// 自动生成:4个硬币随机红/白
$coins = [];
for ($i = 0; $i < 4; $i++) {
$coins[] = (rand(0, 1) === 0) ? 'red' : 'white';
}
} else {
// 手动输入:从前端获取
$coins = isset($data['coins']) ? $data['coins'] : [];
if (!is_array($coins) || count($coins) !== 4) {
echo json_encode([
'success' => false,
'message' => 'Xóc Đĩa 必须提供4个硬币的颜色(red/white'
]);
return;
}
// 验证颜色
foreach ($coins as $coin) {
if (!in_array($coin, ['red', 'white'])) {
echo json_encode([
'success' => false,
'message' => '硬币颜色只能是 red 或 white'
]);
return;
}
}
}
// 计算结果
$redCount = count(array_filter($coins, fn($c) => $c === 'red'));
$result = $this->calculateXocdiaResult($redCount);
// 获取当前登录的管理员ID(审核人)
$approvedBy = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
// 更新期号数据
$updateData = [
'result' => json_encode($coins),
'dice1' => $redCount,
'dice2' => 4 - $redCount,
'dice3' => null,
'total' => null,
'status' => 'drawn',
'draw_time' => date('Y-m-d H:i:s'),
'approved_by' => $approvedBy,
'updated_at' => date('Y-m-d H:i:s')
];
$responseData = [
'coins' => $coins,
'red_count' => $redCount,
'white_count' => 4 - $redCount,
'result' => $result
];
try {
$db->update('periods', $updateData, ['id' => $id]);
echo json_encode([
'success' => true,
'message' => '开奖结果已录入',
'data' => $responseData
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '录入失败:' . $e->getMessage()
]);
}
}
/**
* 启动新一期(开始下注)
*/
public function start() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
$gameId = isset($data['game_id']) ? (int)$data['game_id'] : 0;
if ($gameId <= 0) {
echo json_encode([
'success' => false,
'message' => '请选择游戏'
]);
return;
}
$db = new Database();
// 验证游戏类型
$game = $db->get('games', ['type', 'stream_url'], ['id' => $gameId]);
if (!$game || $game['type'] !== $this->gameType) {
echo json_encode([
'success' => false,
'message' => '游戏不属于 Xóc Đĩa 游戏'
]);
return;
}
// 检查该游戏是否有未结束的期号
$activePeriod = $db->get('periods', '*', [
'game_id' => $gameId,
'status' => ['pending', 'locked', 'drawn']
]);
if ($activePeriod) {
echo json_encode([
'success' => false,
'message' => '该游戏当前有未结束的期号,无法开始新一轮'
]);
return;
}
// 生成期号
$periodNumber = $this->generatePeriodNumber($db, $gameId);
// 获取stream_url
$streamUrl = !empty($game['stream_url']) ? $game['stream_url'] : null;
$periodData = [
'period_number' => $periodNumber,
'status' => 'pending',
'game_id' => $gameId,
'stream_url' => $streamUrl,
'start_time' => date('Y-m-d H:i:s'),
'auto_generated' => 0,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
try {
$id = $db->insert('periods', $periodData);
echo json_encode([
'success' => true,
'message' => '新一期已启动,开始下注',
'data' => [
'id' => $id,
'period_number' => $periodNumber,
'start_time' => $periodData['start_time']
]
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '启动失败:' . $e->getMessage()
]);
}
}
/**
* 确认开奖并结算
*/
public function settle() {
ob_clean();
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
$id = isset($data['id']) ? (int)$data['id'] : 0;
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
$db = new Database();
// 获取期号信息
$period = $db->get('periods', '*', ['id' => $id]);
if (!$period) {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
return;
}
// 验证是否为 Xóc Đĩa 游戏
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
if (!$game || $game['type'] !== $this->gameType) {
echo json_encode([
'success' => false,
'message' => '期号不属于 Xóc Đĩa 游戏'
]);
return;
}
// 检查期号状态
if ($period['status'] !== 'drawn') {
echo json_encode([
'success' => false,
'message' => '该期号还未开奖,无法结算'
]);
return;
}
if ($period['status'] === 'settled') {
echo json_encode([
'success' => false,
'message' => '该期号已结算'
]);
return;
}
try {
// 开启事务
$db->medoo->pdo->beginTransaction();
// 获取本期所有待结算注单
$bets = $db->select('bets', '*', [
'period_id' => $id,
'status' => 'pending'
]);
// 遍历注单进行结算
if ($bets) {
foreach ($bets as $bet) {
$checkResult = $this->checkWin($bet, $period);
$isWin = $checkResult['win'];
$winAmount = $checkResult['amount'];
$now = date('Y-m-d H:i:s');
if ($isWin) {
$payout = $bet['amount'] + $winAmount;
// 更新注单状态
$db->update('bets', [
'status' => 'win',
'win_amount' => $winAmount,
'settled_at' => $now,
'updated_at' => $now
], ['id' => $bet['id']]);
// 更新用户余额
$db->update('users', [
'balance[+]' => $payout
], ['id' => $bet['user_id']]);
// 获取更新后的余额
$user = $db->get('users', ['balance'], ['id' => $bet['user_id']]);
$balanceAfter = $user['balance'];
$balanceBefore = $balanceAfter - $payout;
// 写入资金流水
$db->insert('transactions', [
'user_id' => $bet['user_id'],
'type' => 'win',
'amount' => $payout,
'balance_before' => $balanceBefore,
'balance_after' => $balanceAfter,
'related_id' => $bet['id'],
'description' => "中奖 - 期号: " . $period['period_number'],
'created_at' => $now
]);
} else {
// 未中奖
$db->update('bets', [
'status' => 'lose',
'win_amount' => 0,
'settled_at' => $now,
'updated_at' => $now
], ['id' => $bet['id']]);
}
}
}
// 更新期号状态为已结算
$db->update('periods', [
'status' => 'settled',
'updated_at' => date('Y-m-d H:i:s')
], ['id' => $id]);
// 提交事务
$db->medoo->pdo->commit();
echo json_encode([
'success' => true,
'message' => '结算成功,请点击"开始下注"启动下一期',
'data' => [
'current_period_id' => $id
]
]);
} catch (\Exception $e) {
// 回滚事务
if (isset($db->medoo->pdo)) {
$db->medoo->pdo->rollBack();
}
echo json_encode([
'success' => false,
'message' => '结算失败:' . $e->getMessage()
]);
}
}
/**
* 封盘
*/
public function lock() {
header('Content-Type: application/json');
$data = $_POST;
if (empty($data)) {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
}
$id = isset($data['id']) ? (int)$data['id'] : 0;
if ($id <= 0) {
echo json_encode([
'success' => false,
'message' => '无效的期号ID'
]);
return;
}
$db = new Database();
// 获取期号信息
$period = $db->get('periods', '*', ['id' => $id]);
if (!$period) {
echo json_encode([
'success' => false,
'message' => '期号不存在'
]);
return;
}
// 验证是否为 Xóc Đĩa 游戏
$game = $db->get('games', ['type'], ['id' => $period['game_id']]);
if (!$game || $game['type'] !== $this->gameType) {
echo json_encode([
'success' => false,
'message' => '期号不属于 Xóc Đĩa 游戏'
]);
return;
}
// 检查期号状态
if ($period['status'] !== 'pending') {
echo json_encode([
'success' => false,
'message' => '该期号状态不允许封盘'
]);
return;
}
try {
$db->update('periods', [
'status' => 'locked',
'end_time' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
], ['id' => $id]);
echo json_encode([
'success' => true,
'message' => '封盘成功'
]);
} catch (\Exception $e) {
echo json_encode([
'success' => false,
'message' => '封盘失败:' . $e->getMessage()
]);
}
}
/**
* 生成期号
*/
private function generatePeriodNumber($db, $gameId) {
$now = new \DateTime();
$dateStr = $now->format('Ymd');
$prefix = "G{$gameId}{$dateStr}";
try {
$lastPeriod = $db->get('periods', 'period_number', [
'period_number[~]' => $prefix . '%',
'ORDER' => ['period_number' => 'DESC']
]);
if ($lastPeriod) {
$lastSeq = substr($lastPeriod, strlen($prefix));
$newSeq = (int)$lastSeq + 1;
$seqStr = str_pad((string)$newSeq, 4, '0', STR_PAD_LEFT);
} else {
$seqStr = '0001';
}
return $prefix . $seqStr;
} catch (\Throwable $e) {
return $prefix . time();
}
}
/**
* 检查注单是否中奖
*/
private function checkWin($bet, $period) {
$betType = $bet['bet_type'];
$betValue = $bet['bet_value'];
$betAmount = (float)$bet['amount'];
$gameId = $period['game_id'];
$db = new Database();
// 获取赔率配置
static $oddsCache = [];
if (!isset($oddsCache[$gameId])) {
$oddsData = $db->select('game_odds', '*', ['game_id' => $gameId]);
$oddsCache[$gameId] = [];
foreach ($oddsData as $odd) {
$key = $odd['type'] . '_' . $odd['target'];
$oddsCache[$gameId][$key] = (float)$odd['odds'];
}
}
$isWin = false;
$payoutMultiplier = 0;
// 获取赔率
$getOdds = function($type, $target = 'all') use ($oddsCache, $gameId) {
$key = $type . '_' . $target;
if (isset($oddsCache[$gameId][$key])) {
return $oddsCache[$gameId][$key];
}
$allKey = $type . '_all';
return $oddsCache[$gameId][$allKey] ?? 0;
};
// Xóc Đĩa 游戏结算逻辑
$coins = json_decode($period['result'], true);
if (!is_array($coins) || count($coins) !== 4) {
return ['win' => false, 'amount' => 0];
}
$redCount = count(array_filter($coins, fn($c) => $c === 'red'));
$whiteCount = 4 - $redCount;
switch ($betType) {
case 'chan': // 双(偶): 4红 或 4白 或 2红2白
if (in_array($redCount, [0, 2, 4])) {
$isWin = true;
$payoutMultiplier = $getOdds('chan', $betValue);
}
break;
case 'le': // 单(奇): 3红1白 或 3白1红
if (in_array($redCount, [1, 3])) {
$isWin = true;
$payoutMultiplier = $getOdds('le', $betValue);
}
break;
case 'exact': // 精确颜色组合
switch ($betValue) {
case '4red':
if ($redCount === 4) $isWin = true;
break;
case '4white':
if ($redCount === 0) $isWin = true;
break;
case '3red1white':
if ($redCount === 3) $isWin = true;
break;
case '3white1red':
if ($redCount === 1) $isWin = true;
break;
}
if ($isWin) {
$payoutMultiplier = $getOdds('exact', $betValue);
}
break;
}
if ($isWin && $payoutMultiplier > 0) {
return [
'win' => true,
'amount' => $betAmount * $payoutMultiplier
];
}
return ['win' => false, 'amount' => 0];
}
/**
* 计算 Xóc Đĩa 开奖结果
*/
private function calculateXocdiaResult($redCount) {
switch ($redCount) {
case 0:
return '4 Trắng (Chẵn)';
case 1:
return '3 Trắng 1 Đỏ (Lẻ)';
case 2:
return '2 Trắng 2 Đỏ (Chẵn)';
case 3:
return '3 Đỏ 1 Trắng (Lẻ)';
case 4:
return '4 Đỏ (Chẵn)';
default:
return 'Không xác định';
}
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
class ApiController {
public function data() {
$db = Database::get();
$data = $db->select('sample_table', '*');
header('Content-Type: application/json');
echo json_encode($data);
}
}
+179
View File
@@ -0,0 +1,179 @@
<?php
namespace App\Controllers\Web;
use App\Core\WebBaseController;
use App\Core\I18n;
use Db\Database;
class AgentController extends WebBaseController {
private function getAgent() {
$db = new Database();
$agent = $db->get('agents', '*', ['user_id' => $this->getCurrentUserId(), 'status' => 1]);
if (!$agent) { header('Location: /'); exit; }
return $agent;
}
// 代理后台主页
public function dashboard() {
$this->checkWebLogin();
$agent = $this->getAgent();
I18n::init();
$db = new Database();
$user = $db->get('users', '*', ['id' => $this->getCurrentUserId()]);
$players = $db->select('users', ['id','username','balance','created_at','status'], [
'agent_id' => $agent['id'], 'is_virtual' => 0, 'ORDER' => ['id' => 'DESC']
]);
$today = date('Y-m-d');
$todayRange = [$today . ' 00:00:00', $today . ' 23:59:59'];
$todayComm = $db->sum('agent_commissions', 'commission', [
'agent_id' => $agent['id'], 'created_at[<>]' => $todayRange
]) ?: 0;
$totalComm = $db->sum('agent_commissions', 'commission', ['agent_id' => $agent['id']]) ?: 0;
// 玩家总投注额
$totalBets = $db->sum('bets', 'amount', ['agent_id' => $agent['id'], 'is_virtual' => 0]) ?: 0;
// 下级代理
$subAgents = [];
if ($agent['level'] == 1) {
$subAgents = $db->select('agents', '*', ['parent_id' => $agent['id']]);
foreach ($subAgents as &$sa) {
$sa['user'] = $db->get('users', ['username','balance'], ['id' => $sa['user_id']]);
$sa['player_count'] = $db->count('users', ['agent_id' => $sa['id']]);
}
}
$page = 'home';
extract(compact('agent','user','players','todayComm','totalComm','totalBets','subAgents','page'));
include __DIR__ . '/../../Views/Web/agent_dashboard.php';
}
// 赔率管理页
public function odds() {
$this->checkWebLogin();
$agent = $this->getAgent();
I18n::init();
$db = new Database();
$user = $db->get('users', '*', ['id' => $this->getCurrentUserId()]);
$game = $db->get('games', '*', ['code' => 'pk10']);
// 系统赔率
$sysOdds = $db->select('game_odds', '*', ['game_id' => $game['id']]);
$sysMap = [];
foreach ($sysOdds as $s) $sysMap[$s['type'].'_'.$s['target']] = (float)$s['odds'];
// 上级赔率
$parentMap = $sysMap;
if ($agent['parent_id']) {
$po = $db->select('agent_odds', '*', ['agent_id' => $agent['parent_id'], 'game_id' => $game['id']]);
foreach ($po as $p) $parentMap[$p['bet_type'].'_'.$p['bet_target']] = (float)$p['odds'];
}
// 当前代理赔率
$myOdds = $db->select('agent_odds', '*', ['agent_id' => $agent['id'], 'game_id' => $game['id']]);
$myMap = [];
foreach ($myOdds as $m) $myMap[$m['bet_type'].'_'.$m['bet_target']] = (float)$m['odds'];
$page = 'odds';
extract(compact('agent','user','game','sysMap','parentMap','myMap','page'));
include __DIR__ . '/../../Views/Web/agent_dashboard.php';
}
// 保存赔率
public function setOdds() {
$this->checkWebLogin();
$agent = $this->getAgent();
header('Content-Type: application/json');
$db = new Database();
$data = json_decode(file_get_contents('php://input'), true);
$gameId = (int)($data['game_id'] ?? 0);
// 上级赔率限制
$parentOdds = [];
if ($agent['parent_id']) {
$po = $db->select('agent_odds', '*', ['agent_id' => $agent['parent_id'], 'game_id' => $gameId]);
foreach ($po as $p) $parentOdds[$p['bet_type'].'_'.$p['bet_target']] = (float)$p['odds'];
}
if (empty($parentOdds)) {
$so = $db->select('game_odds', '*', ['game_id' => $gameId]);
foreach ($so as $s) $parentOdds[$s['type'].'_'.$s['target']] = (float)$s['odds'];
}
foreach ($data['odds'] ?? [] as $o) {
$key = $o['bet_type'].'_'.$o['bet_target'];
$newOdds = floatval($o['odds']);
if (isset($parentOdds[$key]) && $newOdds > $parentOdds[$key]) {
echo json_encode(['success'=>false,'message'=>"赔率不能超过上级: $key 上限 ".$parentOdds[$key]]);
return;
}
$existing = $db->get('agent_odds', 'id', [
'agent_id'=>$agent['id'],'game_id'=>$gameId,'bet_type'=>$o['bet_type'],'bet_target'=>$o['bet_target']
]);
if ($existing) {
$db->update('agent_odds', ['odds'=>$newOdds], ['id'=>$existing]);
} else {
$db->insert('agent_odds', [
'agent_id'=>$agent['id'],'game_id'=>$gameId,
'bet_type'=>$o['bet_type'],'bet_target'=>$o['bet_target'],'odds'=>$newOdds
]);
}
}
echo json_encode(['success' => true, 'message' => '赔率已更新']);
}
// 玩家投注记录
public function bets() {
$this->checkWebLogin();
$agent = $this->getAgent();
I18n::init();
$db = new Database();
$user = $db->get('users', '*', ['id' => $this->getCurrentUserId()]);
$betRecords = $db->select('bets', '*', [
'agent_id' => $agent['id'], 'is_virtual' => 0,
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 200
]);
foreach ($betRecords as &$b) {
$b['username'] = $db->get('users', 'username', ['id' => $b['user_id']]) ?: '?';
}
$page = 'bets';
extract(compact('agent','user','betRecords','page'));
include __DIR__ . '/../../Views/Web/agent_dashboard.php';
}
// 佣金明细
public function commissions() {
$this->checkWebLogin();
$agent = $this->getAgent();
I18n::init();
$db = new Database();
$user = $db->get('users', '*', ['id' => $this->getCurrentUserId()]);
$records = $db->select('agent_commissions', '*', [
'agent_id' => $agent['id'], 'ORDER' => ['id' => 'DESC'], 'LIMIT' => 200
]);
foreach ($records as &$r) {
$r['username'] = $db->get('users', 'username', ['id' => $r['from_user_id']]) ?: '?';
}
$page = 'commissions';
extract(compact('agent','user','records','page'));
include __DIR__ . '/../../Views/Web/agent_dashboard.php';
}
// 佣金APIJSON
public function commissionsApi() {
$this->checkWebLogin();
$agent = $this->getAgent();
header('Content-Type: application/json');
$db = new Database();
$records = $db->select('agent_commissions', '*', [
'agent_id' => $agent['id'], 'ORDER' => ['id' => 'DESC'], 'LIMIT' => 100
]);
echo json_encode(['success' => true, 'data' => $records]);
}
}
+187
View File
@@ -0,0 +1,187 @@
<?php
namespace App\Controllers\Web;
use Db\Database;
use App\Core\WebBaseController;
use App\Core\I18n;
class AuthController extends WebBaseController {
public function loginPage() {
if ($this->isLoggedIn()) { header('Location: /'); exit; }
I18n::init();
include __DIR__ . '/../../Views/Web/login.php';
}
public function loginSubmit() {
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$username = trim($data['username'] ?? '');
$password = $data['password'] ?? '';
$remember = $data['remember'] ?? false;
if (empty($username) || empty($password)) {
echo json_encode(['success' => false, 'message' => 'Please fill in all fields']);
return;
}
$db = new Database();
$user = $db->get('users', '*', ['username' => $username]);
if (!$user || !password_verify($password, $user['password'])) {
echo json_encode(['success' => false, 'message' => 'Invalid username or password']);
return;
}
if (isset($user['status']) && $user['status'] == 0) {
echo json_encode(['success' => false, 'message' => 'Account disabled']);
return;
}
// 邮箱验证检查(虚拟账户跳过)
if (empty($user['is_virtual']) && isset($user['email_verified']) && !$user['email_verified']) {
echo json_encode(['success' => false, 'message' => 'Please verify your email first', 'need_verify' => true]);
return;
}
if (session_status() === PHP_SESSION_NONE) session_start();
$_SESSION['web_user_id'] = $user['id'];
$_SESSION['web_username'] = $user['username'];
$_SESSION['web_role'] = $user['role'] ?? 'user';
$_SESSION['web_last_activity'] = time();
$_SESSION['web_ip'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['web_ua'] = $_SERVER['HTTP_USER_AGENT'];
if ($remember) $_SESSION['web_remember'] = true;
if (!empty($user['lang'])) $_SESSION['lang'] = $user['lang'];
echo json_encode(['success' => true, 'message' => 'Login successful', 'redirect' => '/']);
}
public function registerSubmit() {
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$username = trim($data['username'] ?? '');
$password = $data['password'] ?? '';
$confirmPassword = $data['confirm_password'] ?? '';
$email = trim($data['email'] ?? '');
$inviteCode = trim($data['invite_code'] ?? '');
$verifyCode = trim($data['verify_code'] ?? '');
if (empty($username) || empty($password) || empty($email)) {
echo json_encode(['success' => false, 'message' => 'Please fill in all required fields']);
return;
}
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
echo json_encode(['success' => false, 'message' => 'Username: 3-20 chars, letters/numbers/underscore only']);
return;
}
if (strlen($password) < 6) {
echo json_encode(['success' => false, 'message' => 'Password must be at least 6 characters']);
return;
}
if ($password !== $confirmPassword) {
echo json_encode(['success' => false, 'message' => 'Passwords do not match']);
return;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => 'Invalid email address']);
return;
}
$db = new Database();
// 唯一性检查
if ($db->get('users', 'id', ['username' => $username])) {
echo json_encode(['success' => false, 'message' => 'Username already exists']);
return;
}
if ($db->get('users', 'id', ['email' => $email])) {
echo json_encode(['success' => false, 'message' => 'Email already registered']);
return;
}
// 验证邮箱验证码
$ev = $db->get('email_verifications', '*', [
'email' => $email, 'code' => $verifyCode, 'used' => 0,
'expires_at[>]' => date('Y-m-d H:i:s'),
'ORDER' => ['id' => 'DESC']
]);
if (!$ev) {
echo json_encode(['success' => false, 'message' => 'Invalid or expired verification code']);
return;
}
// 处理邀请码 → 代理关联
$agentId = null;
if (!empty($inviteCode)) {
$agent = $db->get('agents', '*', ['agent_code' => $inviteCode, 'status' => 1]);
if ($agent) $agentId = $agent['id'];
}
$db->insert('users', [
'username' => $username,
'password' => password_hash($password, PASSWORD_DEFAULT),
'email' => $email,
'role' => 'user',
'status' => 1,
'email_verified' => 1,
'agent_id' => $agentId,
'lang' => 'en',
'created_at' => date('Y-m-d H:i:s'),
]);
// 标记验证码已用
$db->update('email_verifications', ['used' => 1], ['id' => $ev['id']]);
echo json_encode(['success' => true, 'message' => 'Registration successful', 'redirect' => '/login']);
}
public function sendVerifyCode() {
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$email = trim($data['email'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => 'Invalid email']);
return;
}
// 频率限制: 60秒内只能发一次
$db = new Database();
$recent = $db->get('email_verifications', '*', [
'email' => $email,
'created_at[>]' => date('Y-m-d H:i:s', time() - 60),
]);
if ($recent) {
echo json_encode(['success' => false, 'message' => 'Please wait 60 seconds']);
return;
}
$code = str_pad(mt_rand(0, 999999), 6, '0', STR_PAD_LEFT);
$db->insert('email_verifications', [
'email' => $email,
'code' => $code,
'expires_at' => date('Y-m-d H:i:s', time() + 600), // 10分钟
'created_at' => date('Y-m-d H:i:s'),
]);
// 发送邮件
$subject = 'Email Verification Code';
$body = "Your verification code is: <b>$code</b><br>Valid for 10 minutes.";
$sent = \App\Core\Mailer::send($email, $subject, $body);
echo json_encode(['success' => true, 'message' => 'Verification code sent']);
}
public function logout() {
if (session_status() === PHP_SESSION_NONE) session_start();
$keys = ['web_user_id','web_username','web_role','web_last_activity','web_ip','web_ua','web_remember','lang'];
foreach ($keys as $k) unset($_SESSION[$k]);
header('Location: /login');
exit;
}
private function isLoggedIn(): bool {
if (session_status() === PHP_SESSION_NONE) session_start();
return isset($_SESSION['web_user_id']);
}
}
+138
View File
@@ -0,0 +1,138 @@
<?php
namespace App\Controllers\Web;
use App\Core\WebBaseController;
use Db\Database;
class BetController extends WebBaseController {
public function placeBet() {
$this->checkWebLogin();
header('Content-Type: application/json');
$userId = $this->getCurrentUserId();
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['bets']) || empty($data['period_number']) || empty($data['game_id'])) {
echo json_encode(['success' => false, 'message' => 'Missing required fields']);
return;
}
$gameId = (int)$data['game_id'];
$periodNumber = $data['period_number'];
$bets = $data['bets'];
$db = new Database();
// 验证期号
$period = $db->get('periods', '*', ['period_number' => $periodNumber, 'game_id' => $gameId]);
if (!$period || $period['status'] !== 'pending') {
echo json_encode(['success' => false, 'message' => 'Period is not open for betting']);
return;
}
// 赔率映射
$oddsList = $db->select('game_odds', '*', ['game_id' => $gameId]);
$oddsMap = [];
foreach ($oddsList as $o) {
$oddsMap[$o['type'] . '_' . $o['target']] = (float)$o['odds'];
}
// 用户信息
$user = $db->get('users', '*', ['id' => $userId]);
$isVirtual = (int)($user['is_virtual'] ?? 0);
// 代理专属赔率覆盖
if (!empty($user['agent_id'])) {
$agentOdds = $db->select('agent_odds', '*', ['agent_id' => $user['agent_id'], 'game_id' => $gameId]);
foreach ($agentOdds as $ao) {
$oddsMap[$ao['bet_type'] . '_' . $ao['bet_target']] = (float)$ao['odds'];
}
}
// 限额
$limitsRaw = $db->select('bet_limits', '*', ['game_id' => $gameId]);
$limits = [];
foreach ($limitsRaw as $l) { $limits[$l['bet_type']] = $l; }
// 验证投注
$totalAmount = 0;
$validBets = [];
foreach ($bets as $bet) {
$type = $bet['type'] ?? '';
$target = $bet['value'] ?? $bet['target'] ?? '';
$amount = floatval($bet['amount'] ?? 0);
if ($amount <= 0) continue;
$key = $type . '_' . $target;
$odds = $oddsMap[$key] ?? null;
if ($odds === null) {
echo json_encode(['success' => false, 'message' => "Invalid bet: $key"]);
return;
}
// 限额检查
if (isset($limits[$type])) {
if ($amount < (float)$limits[$type]['min_amount']) {
echo json_encode(['success' => false, 'message' => "Min bet for $type: " . $limits[$type]['min_amount']]);
return;
}
if ($amount > (float)$limits[$type]['max_amount']) {
echo json_encode(['success' => false, 'message' => "Max bet for $type: " . $limits[$type]['max_amount']]);
return;
}
}
$totalAmount += $amount;
$validBets[] = ['type' => $type, 'target' => $target, 'amount' => $amount, 'odds' => $odds];
}
if (empty($validBets)) {
echo json_encode(['success' => false, 'message' => 'No valid bets']);
return;
}
// 事务
try {
$db->medoo->pdo->beginTransaction();
$stmt = $db->medoo->pdo->prepare("SELECT balance FROM users WHERE id = :id FOR UPDATE");
$stmt->execute([':id' => $userId]);
$userRow = $stmt->fetch(\PDO::FETCH_ASSOC);
$currentBalance = (float)$userRow['balance'];
if ($currentBalance < $totalAmount) {
$db->medoo->pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'Insufficient balance']);
return;
}
$newBalance = $currentBalance - $totalAmount;
$db->update('users', ['balance' => $newBalance], ['id' => $userId]);
$db->insert('transactions', [
'user_id' => $userId, 'type' => 'bet', 'amount' => -$totalAmount,
'balance_before' => $currentBalance, 'balance_after' => $newBalance,
'related_id' => $period['id'], 'description' => ($db->get('games', 'name', ['id' => $gameId]) ?: 'Game') . ' Bet - ' . $periodNumber,
'is_virtual' => $isVirtual, 'created_at' => date('Y-m-d H:i:s'),
]);
foreach ($validBets as $vb) {
$db->insert('bets', [
'user_id' => $userId, 'game_id' => $gameId,
'period_number' => $periodNumber, 'period_id' => $period['id'],
'bet_type' => $vb['type'], 'bet_value' => $vb['target'],
'amount' => $vb['amount'], 'odds' => $vb['odds'],
'status' => 'pending', 'is_virtual' => $isVirtual,
'agent_id' => $user['agent_id'] ?? null,
'created_at' => date('Y-m-d H:i:s'),
]);
}
$db->medoo->pdo->commit();
echo json_encode(['success' => true, 'message' => 'Bet placed successfully', 'new_balance' => $newBalance]);
} catch (\Throwable $e) {
if ($db->medoo->pdo->inTransaction()) $db->medoo->pdo->rollBack();
echo json_encode(['success' => false, 'message' => 'System error']);
}
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Controllers\Web;
use App\Core\WebBaseController;
use App\Core\GameFactory;
use App\Core\I18n;
use Db\Database;
class GameController extends WebBaseController {
public function play($code) {
$this->checkWebLogin();
I18n::init();
$db = new Database();
$userId = $this->getCurrentUserId();
$user = $db->get('users', ['id','username','balance','lang'], ['id' => $userId]);
$game = $db->get('games', '*', ['code' => $code, 'status' => 1]);
if (!$game) { header('Location: /'); return; }
$isAgent = !!$db->get('agents', 'id', ['user_id' => $userId, 'status' => 1]);
extract([
'user' => $user ?: ['id' => $userId, 'username' => $this->getCurrentUsername(), 'balance' => 0],
'game' => $game, 'isAgent' => $isAgent,
]);
$viewFile = __DIR__ . "/../../Views/Web/{$code}.php";
if (!file_exists($viewFile)) $viewFile = __DIR__ . "/../../Views/Web/game_generic.php";
include $viewFile;
}
public function stats($code) {
$this->checkWebLogin();
I18n::init();
$db = new Database();
$game = $db->get('games', '*', ['code' => $code, 'status' => 1]);
if (!$game) { header('Location: /'); return; }
$gameType = $game['type'] ?? $code;
$algoClass = GameFactory::getAlgorithm($gameType);
$resultTable = $algoClass::getResultTable();
$gameId = $game['id'];
$periods = $db->select('periods', ['id','period_number'], [
'game_id' => $gameId, 'status' => 'settled',
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 100
]);
$results = [];
foreach ($periods as $p) {
if ($resultTable) {
$row = $db->get($resultTable, '*', ['period_id' => $p['id']]);
} else {
$row = $db->get('periods', '*', ['id' => $p['id']]);
}
if ($row) { $row['period_number'] = $p['period_number']; $results[] = $row; }
}
extract(compact('game', 'results'));
$viewFile = __DIR__ . "/../../Views/Web/{$code}_stats.php";
if (!file_exists($viewFile)) $viewFile = __DIR__ . "/../../Views/Web/game_stats_generic.php";
include $viewFile;
}
}
+170
View File
@@ -0,0 +1,170 @@
<?php
namespace App\Controllers\Web;
use App\Core\WebBaseController;
use App\Core\I18n;
use Db\Database;
class HomeController extends WebBaseController {
public function index() {
$this->checkWebLogin();
I18n::init();
$db = new Database();
$userId = $this->getCurrentUserId();
$user = $db->get('users', ['id','username','balance','lang'], ['id' => $userId]);
$game = $db->get('games', '*', ['code' => 'pk10', 'status' => 1]);
extract([
'user' => $user ?: ['id' => $userId, 'username' => $this->getCurrentUsername(), 'balance' => 0],
'game' => $game,
]);
include __DIR__ . '/../../Views/Web/home.php';
}
public function pk10Game() {
$this->checkWebLogin();
I18n::init();
$db = new Database();
$userId = $this->getCurrentUserId();
$user = $db->get('users', ['id','username','balance','lang'], ['id' => $userId]);
$game = $db->get('games', '*', ['code' => 'pk10', 'status' => 1]);
$isAgent = !!$db->get('agents', 'id', ['user_id' => $userId, 'status' => 1]);
extract([
'user' => $user ?: ['id' => $userId, 'username' => $this->getCurrentUsername(), 'balance' => 0],
'game' => $game,
'isAgent' => $isAgent,
]);
include __DIR__ . '/../../Views/Web/pk10.php';
}
public function profile() {
$this->checkWebLogin();
I18n::init();
$db = new Database();
$userId = $this->getCurrentUserId();
$user = $db->get('users', '*', ['id' => $userId]);
// 最近交易
$transactions = $db->select('transactions', '*', [
'user_id' => $userId, 'is_virtual' => 0,
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 20
]);
// 最近投注
$bets = $db->select('bets', '*', [
'user_id' => $userId,
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 20
]);
extract(compact('user', 'transactions', 'bets'));
include __DIR__ . '/../../Views/Web/profile.php';
}
public function bindUsdt() {
$this->checkWebLogin();
header('Content-Type: application/json');
$db = new Database();
$userId = $this->getCurrentUserId();
$data = json_decode(file_get_contents('php://input'), true);
$address = trim($data['usdt_address'] ?? '');
// TRC20地址验证: 以T开头, 34字符, base58
if (!preg_match('/^T[1-9A-HJ-NP-Za-km-z]{33}$/', $address)) {
echo json_encode(['success' => false, 'message' => 'Invalid TRC20 address']);
return;
}
// 唯一性
$exists = $db->get('users', 'id', ['usdt_address' => $address, 'id[!]' => $userId]);
if ($exists) {
echo json_encode(['success' => false, 'message' => 'Address already bound to another account']);
return;
}
$db->update('users', ['usdt_address' => $address, 'usdt_chain' => 'TRC20'], ['id' => $userId]);
echo json_encode(['success' => true, 'message' => 'Address bound successfully']);
}
public function setLang() {
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$lang = $data['lang'] ?? 'en';
if (isset(I18n::LANGUAGES[$lang])) {
$_SESSION['lang'] = $lang;
setcookie('lang', $lang, time() + 86400 * 365, '/');
$db = new Database();
$userId = $this->getCurrentUserId();
if ($userId) $db->update('users', ['lang' => $lang], ['id' => $userId]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Invalid language']);
}
}
public function lottery() {
$this->checkWebLogin();
I18n::init();
$db = new Database();
$gameCode = $_GET['game'] ?? 'pk10';
$game = $db->get('games', '*', ['code' => $gameCode, 'status' => 1]);
if (!$game) $game = $db->get('games', '*', ['code' => 'pk10', 'status' => 1]);
$gameId = $game['id'] ?? 0;
$gameType = $game['type'] ?? 'pk10';
$algoClass = \App\Core\GameFactory::getAlgorithm($gameType);
$resultTable = $algoClass::getResultTable();
$periods = $db->select('periods', '*', [
'game_id' => $gameId, 'status' => 'settled',
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 50
]);
foreach ($periods as &$p) {
if ($resultTable) {
$pk = $db->get($resultTable, '*', ['period_id' => $p['id']]);
if ($pk) { foreach ($pk as $k => $v) $p[$k] = $v; }
}
}
unset($p);
extract(compact('game', 'periods'));
include __DIR__ . '/../../Views/Web/lottery.php';
}
public function pk10Stats() {
$this->checkWebLogin();
I18n::init();
$db = new Database();
$game = $db->get('games', '*', ['code' => 'pk10', 'status' => 1]);
$gameId = $game['id'] ?? 0;
// 最近100期已结算结果
$periods = $db->select('periods', ['id','period_number'], [
'game_id' => $gameId, 'status' => 'settled',
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 100
]);
$results = [];
foreach ($periods as $p) {
$pk = $db->get('pk10_results', '*', ['period_id' => $p['id']]);
if ($pk) {
$pk['period_number'] = $p['period_number'];
$results[] = $pk;
}
}
extract(compact('game', 'results'));
include __DIR__ . '/../../Views/Web/pk10_stats.php';
}
public function details() {
$this->checkWebLogin();
I18n::init();
$db = new Database();
$userId = $this->getCurrentUserId();
$settled = $db->select('bets', '*', [
'user_id' => $userId, 'status[!]' => 'pending',
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 50
]);
$pending = $db->select('bets', '*', [
'user_id' => $userId, 'status' => 'pending',
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 50
]);
extract(compact('settled', 'pending'));
include __DIR__ . '/../../Views/Web/details.php';
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
namespace App\Controllers\Web;
use App\Core\WebBaseController;
use App\Core\GameFactory;
use Db\Database;
class PeriodController extends WebBaseController {
public function getCurrent() {
header('Content-Type: application/json');
$db = new Database();
$gameId = (int)($_GET['game_id'] ?? 0);
if (!$gameId) {
$gameId = (int)($db->get('games', 'id', ['code' => 'pk10']) ?: 0);
}
if (!$gameId) {
echo json_encode(['success' => false, 'message' => 'Game not found']);
return;
}
// 获取游戏类型和算法
$game = $db->get('games', ['id', 'type', 'period_duration', 'lock_before_end'], ['id' => $gameId]);
$gameType = $game['type'] ?? 'pk10';
$algoClass = GameFactory::getAlgorithm($gameType);
$resultTable = $algoClass::getResultTable();
$period = $db->get('periods', '*', [
'game_id' => $gameId,
'ORDER' => ['id' => 'DESC']
]);
$now = time();
$countdown = 0;
$lockCountdown = 0;
if ($period && in_array($period['status'], ['pending', 'locked'])) {
$start = strtotime($period['start_time'] ?? $period['created_at']);
$countdown = max(0, !empty($period['end_time']) ? strtotime($period['end_time']) - $now : 300 - ($now - $start));
$periodDuration = (int)($game['period_duration'] ?? 300);
$lockBefore = (int)($game['lock_before_end'] ?? 30);
$lockCountdown = max(0, ($periodDuration - $lockBefore) - ($now - $start));
}
// 获取结果的通用方法
$getResult = function($periodRow) use ($db, $resultTable, $algoClass) {
if (!$periodRow) return null;
if ($resultTable) {
return $db->get($resultTable, '*', ['period_id' => $periodRow['id']]);
}
// dice/xocdia: 结果在 periods 表中
return $periodRow;
};
// ====== 最近开奖结果 ======
$lastResult = null;
$lastResultStatus = null;
$lastDrawn = $db->get('periods', '*', [
'game_id' => $gameId,
'status' => ['drawn', 'settled'],
'ORDER' => ['id' => 'DESC']
]);
if ($lastDrawn) {
$pk10 = $getResult($lastDrawn);
if ($pk10) {
$lastResult = $pk10;
$lastResult['period_number'] = $lastDrawn['period_number'];
$lastResultStatus = $lastDrawn['status'];
}
}
// ====== locked 状态预开奖结果 ======
$raceResult = null;
if ($period && $period['status'] === 'locked' && !empty($period['result'])) {
$pk10Current = $getResult($period);
if ($pk10Current) {
$raceResult = $pk10Current;
$raceResult['period_number'] = $period['period_number'];
}
}
// history
$history = [];
$recentPeriods = $db->select('periods', ['id','period_number','result','status'], [
'game_id' => $gameId,
'status' => ['drawn', 'settled'],
'ORDER' => ['id' => 'DESC'],
'LIMIT' => 10
]);
foreach ($recentPeriods as $s) {
$pk = $getResult($s);
if ($pk) { $pk['period_number'] = $s['period_number']; $history[] = $pk; }
}
// user bets + balance
$myBets = [];
$balance = null;
$userId = $this->getWebUserId();
if ($userId && $period) {
$betsRaw = $db->select('bets', ['id', 'bet_type', 'bet_value', 'amount', 'odds', 'status', 'win_amount'], [
'user_id' => $userId,
'period_id' => $period['id'],
'ORDER' => ['id' => 'DESC']
]);
$myBets = $betsRaw ?: [];
$userRow = $db->get('users', ['balance'], ['id' => $userId]);
$balance = $userRow ? (float)$userRow['balance'] : null;
}
$lastMyBets = [];
if ($userId && $lastDrawn && $lastDrawn['status'] === 'settled') {
$lastBetsRaw = $db->select('bets', ['id', 'bet_type', 'bet_value', 'amount', 'odds', 'status', 'win_amount'], [
'user_id' => $userId,
'period_id' => $lastDrawn['id'],
'ORDER' => ['id' => 'DESC']
]);
$lastMyBets = $lastBetsRaw ?: [];
}
echo json_encode([
'success' => true,
'data' => [
'id' => $period['id'] ?? 0,
'period_number' => $period['period_number'] ?? '',
'status' => $period['status'] ?? 'none',
'remaining_seconds' => $countdown,
'lock_countdown' => $lockCountdown,
'auto_generated' => (int)($period['auto_generated'] ?? 0),
'server_time' => date('Y-m-d H:i:s'),
],
'last_result' => $lastResult,
'last_result_status' => $lastResultStatus,
'race_result' => $raceResult,
'history' => $history,
'my_bets' => $myBets,
'last_my_bets' => $lastMyBets,
'balance' => $balance,
]);
}
private function getWebUserId(): int {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
return (int)($_SESSION['web_user_id'] ?? 0);
}
}