469 lines
20 KiB
PHP
469 lines
20 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
$config = require dirname(__DIR__) . '/config.php';
|
|
if (empty($config['installed'])) {
|
|
if (PHP_SAPI !== 'cli' && !headers_sent()) {
|
|
header('Location: /install.php', true, 302);
|
|
exit;
|
|
}
|
|
throw new RuntimeException('系统尚未安装,请访问 /install.php。');
|
|
}
|
|
date_default_timezone_set($config['timezone']);
|
|
|
|
if (PHP_SAPI !== 'cli' && !headers_sent()) {
|
|
header('X-Content-Type-Options: nosniff');
|
|
header('X-Frame-Options: SAMEORIGIN');
|
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
header("Permissions-Policy: camera=(), microphone=(), geolocation=()");
|
|
}
|
|
|
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
ini_set('session.use_strict_mode', '1');
|
|
ini_set('session.use_only_cookies', '1');
|
|
session_name($config['session_name']);
|
|
session_set_cookie_params([
|
|
'lifetime' => 0,
|
|
'path' => '/',
|
|
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
]);
|
|
session_start();
|
|
}
|
|
|
|
function db(): PDO
|
|
{
|
|
static $pdo;
|
|
if ($pdo instanceof PDO) {
|
|
return $pdo;
|
|
}
|
|
|
|
global $config;
|
|
$db = $config['db'];
|
|
$dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=%s', $db['host'], $db['port'], $db['database'], $db['charset']);
|
|
$pdo = new PDO($dsn, $db['username'], $db['password'], [
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
]);
|
|
return $pdo;
|
|
}
|
|
|
|
function now(): string
|
|
{
|
|
return date('Y-m-d H:i:s');
|
|
}
|
|
|
|
function e(mixed $value): string
|
|
{
|
|
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
}
|
|
|
|
function csrf_token(): string
|
|
{
|
|
if (empty($_SESSION['csrf'])) {
|
|
$_SESSION['csrf'] = bin2hex(random_bytes(24));
|
|
}
|
|
return $_SESSION['csrf'];
|
|
}
|
|
|
|
function verify_csrf(): void
|
|
{
|
|
$token = (string) ($_POST['csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
|
|
if (!hash_equals((string) ($_SESSION['csrf'] ?? ''), $token)) {
|
|
throw new RuntimeException('页面已过期,请刷新后重试。');
|
|
}
|
|
}
|
|
|
|
function setting(string $key, string $default = ''): string
|
|
{
|
|
$stmt = db()->prepare('SELECT `value` FROM settings WHERE `key` = ?');
|
|
$stmt->execute([$key]);
|
|
$value = $stmt->fetchColumn();
|
|
return $value === false ? $default : (string) $value;
|
|
}
|
|
|
|
function current_user(bool $fresh = false): ?array
|
|
{
|
|
static $cached = false;
|
|
if (!$fresh && is_array($cached)) {
|
|
return $cached;
|
|
}
|
|
$id = (int) ($_SESSION['user_id'] ?? 0);
|
|
if ($id < 1) {
|
|
return null;
|
|
}
|
|
$stmt = db()->prepare('SELECT * FROM users WHERE id = ?');
|
|
$stmt->execute([$id]);
|
|
$user = $stmt->fetch();
|
|
if (!$user || (int) $user['status'] !== 1) {
|
|
unset($_SESSION['user_id']);
|
|
return null;
|
|
}
|
|
$cached = $user;
|
|
return $user;
|
|
}
|
|
|
|
function current_admin(): ?array
|
|
{
|
|
$id = (int) ($_SESSION['admin_id'] ?? 0);
|
|
if ($id < 1) {
|
|
return null;
|
|
}
|
|
$stmt = db()->prepare('SELECT id, username FROM admins WHERE id = ?');
|
|
$stmt->execute([$id]);
|
|
return $stmt->fetch() ?: null;
|
|
}
|
|
|
|
function require_user(): array
|
|
{
|
|
$user = current_user();
|
|
if (!$user) {
|
|
header('Location: /index.php?view=login');
|
|
exit;
|
|
}
|
|
return $user;
|
|
}
|
|
|
|
function require_admin(): array
|
|
{
|
|
$admin = current_admin();
|
|
if (!$admin) {
|
|
header('Location: /admin/login.php');
|
|
exit;
|
|
}
|
|
return $admin;
|
|
}
|
|
|
|
function flash(string $type, string $message): void
|
|
{
|
|
$_SESSION['flash'] = ['type' => $type, 'message' => $message];
|
|
}
|
|
|
|
function take_flash(): ?array
|
|
{
|
|
$flash = $_SESSION['flash'] ?? null;
|
|
unset($_SESSION['flash']);
|
|
return is_array($flash) ? $flash : null;
|
|
}
|
|
|
|
function redirect(string $url): never
|
|
{
|
|
header('Location: ' . $url);
|
|
exit;
|
|
}
|
|
|
|
function client_ip(): string
|
|
{
|
|
return substr((string) ($_SERVER['REMOTE_ADDR'] ?? ''), 0, 45);
|
|
}
|
|
|
|
function auth_limit_key(string $identifier): string
|
|
{
|
|
return hash('sha256', mb_strtolower(trim($identifier)));
|
|
}
|
|
|
|
function assert_auth_not_limited(string $scope, string $identifier, int $maximum = 5, int $windowSeconds = 900): void
|
|
{
|
|
$cutoff = date('Y-m-d H:i:s', time() - $windowSeconds);
|
|
$stmt = db()->prepare('SELECT COUNT(*) FROM auth_attempts WHERE scope=? AND succeeded=0 AND attempted_at>=? AND (identifier_hash=? OR ip_address=?)');
|
|
$stmt->execute([$scope, $cutoff, auth_limit_key($identifier), client_ip()]);
|
|
if ((int) $stmt->fetchColumn() >= $maximum) {
|
|
throw new RuntimeException('尝试次数过多,请稍后再试。');
|
|
}
|
|
}
|
|
|
|
function record_auth_attempt(string $scope, string $identifier, bool $succeeded): void
|
|
{
|
|
$pdo = db();
|
|
$key = auth_limit_key($identifier);
|
|
$ip = client_ip();
|
|
if ($succeeded) {
|
|
$stmt = $pdo->prepare('DELETE FROM auth_attempts WHERE scope=? AND (identifier_hash=? OR ip_address=?)');
|
|
$stmt->execute([$scope, $key, $ip]);
|
|
return;
|
|
}
|
|
$stmt = $pdo->prepare('INSERT INTO auth_attempts(scope,identifier_hash,ip_address,succeeded,attempted_at) VALUES(?,?,?,0,?)');
|
|
$stmt->execute([$scope, $key, $ip, now()]);
|
|
if (random_int(1, 100) === 1) {
|
|
$pdo->prepare('DELETE FROM auth_attempts WHERE attempted_at<?')->execute([date('Y-m-d H:i:s', time() - 604800)]);
|
|
}
|
|
}
|
|
|
|
function admin_audit(PDO $pdo, string $action, string $targetType = '', string|int $targetId = '', mixed $before = null, mixed $after = null): void
|
|
{
|
|
$adminId = (int) ($_SESSION['admin_id'] ?? 0);
|
|
if ($adminId < 1) return;
|
|
$encode = static fn(mixed $value): ?string => $value === null ? null : json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR);
|
|
$stmt = $pdo->prepare('INSERT INTO admin_audit_logs(admin_id,action,target_type,target_id,before_json,after_json,ip_address,created_at) VALUES(?,?,?,?,?,?,?,?)');
|
|
$stmt->execute([$adminId, mb_substr($action, 0, 50), mb_substr($targetType, 0, 40), mb_substr((string) $targetId, 0, 64), $encode($before), $encode($after), client_ip(), now()]);
|
|
}
|
|
|
|
function create_ledger(PDO $pdo, int $userId, string $type, float $amount, float $before, float $after, string $note, ?string $reference = null): void
|
|
{
|
|
$stmt = $pdo->prepare('INSERT INTO balance_logs (user_id, type, amount, balance_before, balance_after, note, reference_no, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
|
|
$stmt->execute([$userId, $type, $amount, $before, $after, $note, $reference, now()]);
|
|
}
|
|
|
|
function order_no(string $prefix = 'RC'): string
|
|
{
|
|
return $prefix . date('YmdHis') . strtoupper(bin2hex(random_bytes(3)));
|
|
}
|
|
|
|
function generate_invite_code(PDO $pdo): string
|
|
{
|
|
$stmt = $pdo->prepare('SELECT 1 FROM users WHERE invite_code=?');
|
|
do {
|
|
$code = strtoupper(bin2hex(random_bytes(5)));
|
|
$stmt->execute([$code]);
|
|
} while ($stmt->fetchColumn());
|
|
return $code;
|
|
}
|
|
|
|
function redeem_recharge_card(int $userId, string $code, string $note = '充值卡兑换'): array
|
|
{
|
|
$code = strtoupper(trim($code));
|
|
if (!preg_match('/^[A-Z0-9]{4,32}$/', $code)) throw new RuntimeException('充值卡不存在或不可用。');
|
|
$pdo = db();
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$lock = ' FOR UPDATE';
|
|
$stmt = $pdo->prepare('SELECT * FROM recharge_cards WHERE code=?'.$lock);
|
|
$stmt->execute([$code]);
|
|
$card = $stmt->fetch();
|
|
if (!$card) throw new RuntimeException('充值卡不存在或不可用。');
|
|
|
|
$isShared = ($card['card_type'] ?? 'single') === 'shared';
|
|
if ($isShared) {
|
|
if ($card['status'] !== 'active') throw new RuntimeException('该公共充值卡已停用。');
|
|
$stmt = $pdo->prepare('SELECT 1 FROM recharge_card_redemptions WHERE card_id=? AND user_id=?');
|
|
$stmt->execute([$card['id'], $userId]);
|
|
if ($stmt->fetchColumn()) throw new RuntimeException('您已经使用过该公共充值卡,每个账号仅可兑换一次。');
|
|
} elseif ($card['status'] !== 'unused') {
|
|
throw new RuntimeException('充值卡不存在或已使用。');
|
|
}
|
|
|
|
$stmt = $pdo->prepare('SELECT balance FROM users WHERE id=?'.$lock);
|
|
$stmt->execute([$userId]);
|
|
$balance = $stmt->fetchColumn();
|
|
if ($balance === false) throw new RuntimeException('用户不存在。');
|
|
$amount = round((float)$card['amount'], 2);
|
|
if ($amount <= 0) throw new RuntimeException('充值卡金额无效。');
|
|
$before = (float)$balance;
|
|
$after = round($before + $amount, 2);
|
|
|
|
if ($isShared) {
|
|
try {
|
|
$pdo->prepare('INSERT INTO recharge_card_redemptions(card_id,user_id,amount,created_at) VALUES(?,?,?,?)')->execute([$card['id'], $userId, $amount, now()]);
|
|
} catch (PDOException $e) {
|
|
if ((string)$e->getCode() === '23000') throw new RuntimeException('您已经使用过该公共充值卡,每个账号仅可兑换一次。');
|
|
throw $e;
|
|
}
|
|
} else {
|
|
$pdo->prepare("UPDATE recharge_cards SET status='used',used_by=?,used_at=? WHERE id=? AND status='unused'")->execute([$userId, now(), $card['id']]);
|
|
}
|
|
|
|
$pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after, now(), $userId]);
|
|
$reference = $isShared ? 'SC'.$card['id'].'U'.$userId : $code;
|
|
create_ledger($pdo, $userId, $isShared ? 'shared_card_recharge' : 'card_recharge', $amount, $before, $after, $note, $reference);
|
|
credit_recharge_commissions($pdo, $userId, $amount, $isShared ? 'shared_card' : 'card', $reference);
|
|
$pdo->commit();
|
|
return ['amount'=>$amount, 'card_type'=>$isShared ? 'shared' : 'single'];
|
|
} catch (Throwable $e) {
|
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
function create_user_account(string $username, string $password, string $phone = '', string $inviteCode = ''): array
|
|
{
|
|
$pdo = db();
|
|
$ip = client_ip();
|
|
$inviteCode = strtoupper(trim($inviteCode));
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$lock = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? ' FOR UPDATE' : '';
|
|
$inviter = null;
|
|
if ($inviteCode !== '') {
|
|
$stmt = $pdo->prepare('SELECT * FROM users WHERE invite_code=? AND status=1'.$lock);
|
|
$stmt->execute([$inviteCode]);
|
|
$inviter = $stmt->fetch();
|
|
if (!$inviter) throw new RuntimeException('邀请码无效,请核对后重试。');
|
|
}
|
|
|
|
$bonus = max(0, (float)setting('register_bonus', '0'));
|
|
$code = generate_invite_code($pdo);
|
|
try {
|
|
$stmt = $pdo->prepare('INSERT INTO users(username,password_hash,phone,invite_code,inviter_id,register_ip,balance,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,1,?,?)');
|
|
$stmt->execute([$username,password_hash($password,PASSWORD_DEFAULT),$phone,$code,$inviter['id']??null,$ip,$bonus,now(),now()]);
|
|
} catch (PDOException $e) {
|
|
if ((string)$e->getCode() === '23000') throw new RuntimeException('该账号已被注册。');
|
|
throw $e;
|
|
}
|
|
$userId = (int)$pdo->lastInsertId();
|
|
if ($bonus > 0) create_ledger($pdo,$userId,'register_bonus',$bonus,0,$bonus,'新用户注册奖励');
|
|
|
|
$referralStatus = null;
|
|
if ($inviter) {
|
|
$reward = round(max(0,(float)setting('invite_reward','0')),2);
|
|
$inviterIp = (string)($inviter['register_ip'] ?: $inviter['last_login_ip']);
|
|
$status = $reward > 0 ? 'rewarded' : 'qualified';
|
|
$reason = '';
|
|
if ($ip === '') {
|
|
$status = 'rejected'; $reason = '无法识别注册 IP';
|
|
} elseif ($inviterIp !== '' && hash_equals($inviterIp,$ip)) {
|
|
$status = 'rejected'; $reason = '邀请人与被邀请人 IP 相同';
|
|
} elseif ($reward <= 0) {
|
|
$reason = '注册奖励未启用';
|
|
}
|
|
|
|
$qualified = false;
|
|
if (in_array($status,['rewarded','qualified'],true)) {
|
|
try {
|
|
$pdo->prepare('INSERT INTO referral_rewards(inviter_id,invitee_id,invitee_ip,reward_ip,reward_amount,status,reason,created_at) VALUES(?,?,?,?,?,?,?,?)')
|
|
->execute([$inviter['id'],$userId,$ip,$ip,$status === 'rewarded' ? $reward : 0,$status,$reason,now()]);
|
|
$qualified = true;
|
|
} catch (PDOException $e) {
|
|
if ((string)$e->getCode() !== '23000') throw $e;
|
|
$status = 'rejected'; $reason = '该 IP 已建立过有效邀请关系';
|
|
}
|
|
}
|
|
if (!$qualified) {
|
|
$pdo->prepare('INSERT INTO referral_rewards(inviter_id,invitee_id,invitee_ip,reward_ip,reward_amount,status,reason,created_at) VALUES(?,?,?,NULL,0,?,?,?)')
|
|
->execute([$inviter['id'],$userId,$ip,$status,$reason,now()]);
|
|
} elseif ($status === 'rewarded') {
|
|
$before = (float)$inviter['balance'];
|
|
$after = $before + $reward;
|
|
$pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after,now(),$inviter['id']]);
|
|
create_ledger($pdo,(int)$inviter['id'],'invite_reward',$reward,$before,$after,'成功邀请用户 '.$username,'INV'.$userId);
|
|
}
|
|
$referralStatus = $status;
|
|
}
|
|
$pdo->commit();
|
|
return ['user_id'=>$userId,'invite_code'=>$code,'referral_status'=>$referralStatus];
|
|
} catch (Throwable $e) {
|
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
function referral_link_qualified(PDO $pdo, int $inviteeId, int $inviterId): bool
|
|
{
|
|
$stmt = $pdo->prepare("SELECT status FROM referral_rewards WHERE invitee_id=? AND inviter_id=? AND status IN ('rewarded','qualified')");
|
|
$stmt->execute([$inviteeId,$inviterId]);
|
|
return (bool)$stmt->fetchColumn();
|
|
}
|
|
|
|
function credit_recharge_commissions(PDO $pdo, int $userId, float $amount, string $sourceType, string $sourceRef): void
|
|
{
|
|
if ($amount <= 0) return;
|
|
$lock = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? ' FOR UPDATE' : '';
|
|
$stmt = $pdo->prepare('SELECT id,inviter_id FROM users WHERE id=?'.$lock);
|
|
$stmt->execute([$userId]);
|
|
$rechargingUser = $stmt->fetch();
|
|
if (!$rechargingUser || !(int)$rechargingUser['inviter_id']) return;
|
|
|
|
$directId = (int)$rechargingUser['inviter_id'];
|
|
if (!referral_link_qualified($pdo,$userId,$directId)) return;
|
|
credit_recharge_commission($pdo,$userId,$directId,1,$amount,(float)setting('direct_commission_rate','10'),$sourceType,$sourceRef);
|
|
|
|
$stmt = $pdo->prepare('SELECT inviter_id FROM users WHERE id=?'.$lock);
|
|
$stmt->execute([$directId]);
|
|
$secondId = (int)$stmt->fetchColumn();
|
|
if ($secondId > 0 && referral_link_qualified($pdo,$directId,$secondId)) {
|
|
credit_recharge_commission($pdo,$userId,$secondId,2,$amount,(float)setting('second_commission_rate','1'),$sourceType,$sourceRef);
|
|
}
|
|
}
|
|
|
|
function credit_recharge_commission(PDO $pdo, int $rechargingUserId, int $beneficiaryId, int $level, float $rechargeAmount, float $rate, string $sourceType, string $sourceRef): void
|
|
{
|
|
$rate = min(100.0,max(0.0,$rate));
|
|
$commission = round($rechargeAmount * $rate / 100,2);
|
|
if ($commission <= 0) return;
|
|
try {
|
|
$stmt = $pdo->prepare('INSERT INTO recharge_commissions(source_type,source_ref,recharging_user_id,beneficiary_user_id,level,rate,recharge_amount,commission_amount,created_at) VALUES(?,?,?,?,?,?,?,?,?)');
|
|
$stmt->execute([$sourceType,mb_substr($sourceRef,0,40),$rechargingUserId,$beneficiaryId,$level,$rate,$rechargeAmount,$commission,now()]);
|
|
} catch (PDOException $e) {
|
|
if ((string)$e->getCode() === '23000') return;
|
|
throw $e;
|
|
}
|
|
$commissionId = (int)$pdo->lastInsertId();
|
|
$lock = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? ' FOR UPDATE' : '';
|
|
$stmt = $pdo->prepare('SELECT balance FROM users WHERE id=?'.$lock);
|
|
$stmt->execute([$beneficiaryId]);
|
|
$before = (float)$stmt->fetchColumn();
|
|
$after = $before + $commission;
|
|
$pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after,now(),$beneficiaryId]);
|
|
create_ledger($pdo,$beneficiaryId,'recharge_commission_l'.$level,$commission,$before,$after,$level===1?'一级下线充值佣金':'二级下线充值佣金','COM'.$commissionId);
|
|
}
|
|
|
|
function reverse_recharge_order(PDO $pdo, int $orderId, string $note): void
|
|
{
|
|
$note = mb_substr(trim($note), 0, 200);
|
|
if ($note === '') throw new RuntimeException('请填写冲正原因。');
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$lock = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? ' FOR UPDATE' : '';
|
|
$stmt = $pdo->prepare('SELECT * FROM recharge_orders WHERE id=?'.$lock);
|
|
$stmt->execute([$orderId]);
|
|
$order = $stmt->fetch();
|
|
if (!$order || $order['status'] !== 'approved') throw new RuntimeException('仅可冲正已到账且未冲正的订单。');
|
|
|
|
$stmt = $pdo->prepare("SELECT * FROM recharge_commissions WHERE source_type='order' AND source_ref=? AND reversed_at IS NULL ORDER BY id".$lock);
|
|
$stmt->execute([$order['order_no']]);
|
|
$commissions = $stmt->fetchAll();
|
|
$deductions = [(int) $order['user_id'] => (float) $order['amount']];
|
|
foreach ($commissions as $commission) {
|
|
$beneficiaryId = (int) $commission['beneficiary_user_id'];
|
|
$deductions[$beneficiaryId] = ($deductions[$beneficiaryId] ?? 0) + (float) $commission['commission_amount'];
|
|
}
|
|
ksort($deductions);
|
|
$ids = array_keys($deductions);
|
|
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
|
$stmt = $pdo->prepare('SELECT id,balance FROM users WHERE id IN ('.$placeholders.') ORDER BY id'.$lock);
|
|
$stmt->execute($ids);
|
|
$balances = [];
|
|
foreach ($stmt->fetchAll() as $row) $balances[(int) $row['id']] = (float) $row['balance'];
|
|
foreach ($deductions as $userId => $amount) {
|
|
if (!array_key_exists($userId, $balances) || $balances[$userId] + 0.00001 < $amount) {
|
|
throw new RuntimeException('相关用户余额不足,无法自动冲正,请先核对资金。');
|
|
}
|
|
}
|
|
|
|
foreach ($commissions as $commission) {
|
|
$userId = (int) $commission['beneficiary_user_id'];
|
|
$amount = (float) $commission['commission_amount'];
|
|
$before = $balances[$userId];
|
|
$after = round($before - $amount, 2);
|
|
$pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after, now(), $userId]);
|
|
$pdo->prepare('UPDATE recharge_commissions SET reversed_by=?,reversed_at=? WHERE id=? AND reversed_at IS NULL')->execute([(int) $_SESSION['admin_id'], now(), $commission['id']]);
|
|
create_ledger($pdo, $userId, 'commission_reversal', -$amount, $before, $after, '充值佣金冲正', 'RVC'.$commission['id']);
|
|
$balances[$userId] = $after;
|
|
}
|
|
|
|
$userId = (int) $order['user_id'];
|
|
$amount = (float) $order['amount'];
|
|
$before = $balances[$userId];
|
|
$after = round($before - $amount, 2);
|
|
$pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after, now(), $userId]);
|
|
create_ledger($pdo, $userId, 'recharge_reversal', -$amount, $before, $after, '充值订单冲正:'.$note, 'RV'.$order['order_no']);
|
|
$pdo->prepare("UPDATE recharge_orders SET status='reversed',reversed_by=?,reversed_at=?,reversal_note=? WHERE id=? AND status='approved'")->execute([(int) $_SESSION['admin_id'], now(), $note, $orderId]);
|
|
admin_audit($pdo, 'recharge_reversal', 'recharge_order', $orderId, ['status' => 'approved', 'amount' => $amount], ['status' => 'reversed', 'reason' => $note]);
|
|
$pdo->commit();
|
|
} catch (Throwable $exception) {
|
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
function settings_map(): array
|
|
{
|
|
$rows = db()->query('SELECT `key`, `value` FROM settings')->fetchAll();
|
|
$settings = [];
|
|
foreach ($rows as $row) {
|
|
$settings[(string) $row['key']] = (string) $row['value'];
|
|
}
|
|
return $settings;
|
|
}
|