
本文介绍如何在用户下注前安全校验账户余额,防止余额被扣成负数;通过预查询当前余额并与投注金额比对,实现前置风控,避免数据不一致和业务逻辑错误。
本文介绍如何在用户下注前安全校验账户余额,防止余额被扣成负数;通过预查询当前余额并与投注金额比对,实现前置风控,避免数据不一致和业务逻辑错误。
在处理涉及资金变动的业务(如体育投注)时,绝不能直接使用客户端传入的 balance 值进行计算和更新——该值极易被篡改(例如通过浏览器调试工具伪造 POST 数据),导致严重的安全漏洞和资损风险。正确做法是:以数据库中的实时余额为唯一可信源,在执行扣款前主动查询、严格校验。
以下是推荐的安全实现方案(使用 MySQLi 面向对象风格 + 预处理语句,兼顾安全性与可读性):
// 1. 获取并过滤用户输入(注意:stake 等数值应进一步验证为合法数字)
$stake = (float) filter_input(INPUT_POST, 'stake', FILTER_SANITIZE_NUMBER_FLOAT);
$payout = (float) filter_input(INPUT_POST, 'payout', FILTER_SANITIZE_NUMBER_FLOAT);
$todds = (float) filter_input(INPUT_POST, 'todds', FILTER_SANITIZE_NUMBER_FLOAT);
$invoiceid = filter_input(INPUT_POST, 'invoiceid', FILTER_SANITIZE_STRING);
$match = filter_input(INPUT_POST, 'match', FILTER_SANITIZE_STRING);
$selection = filter_input(INPUT_POST, 'selection', FILTER_SANITIZE_STRING);
$odd = (float) filter_input(INPUT_POST, 'odd', FILTER_SANITIZE_NUMBER_FLOAT);
$account = filter_input(INPUT_POST, 'account', FILTER_SANITIZE_STRING);
$kickoff = filter_input(INPUT_POST, 'kickoff', FILTER_SANITIZE_STRING);
// 2. 严格校验必要参数
if (!$stake || $stake <= 0 || !$invoiceid || empty($_SESSION['username'])) {
die("参数错误:投注金额无效或会话异常。");
}
// 3. 安全查询当前余额(使用预处理防止 SQL 注入)
$stmt = $link->prepare("SELECT balance FROM users WHERE username = ?");
$stmt->bind_param("s", $_SESSION['username']);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows !== 1) {
die("用户不存在或账户异常。");
}
$row = $result->fetch_assoc();
$current_balance = (float) $row['balance'];
// 4. 关键校验:余额是否充足?
if ($current_balance < $stake) {
$_SESSION['error'] = "余额不足,当前余额:¥" . number_format($current_balance, 2)
. ",需 ¥" . number_format($stake, 2) . "。";
header("Location: bet_form.php");
exit;
}
// 5. ✅ 余额充足,执行原子化扣款与记录(建议在事务中完成)
$link->autocommit(FALSE); // 启用事务
try {
// 扣减余额(WHERE 条件再次校验,防并发超扣)
$update = $link->prepare("UPDATE users SET balance = balance - ? WHERE username = ? AND balance >= ?");
$update->bind_param("dss", $stake, $_SESSION['username'], $stake);
$update->execute();
if ($update->affected_rows !== 1) {
throw new Exception("余额扣减失败:可能已被其他操作修改。");
}
// 更新 Session 中的余额(仅作展示,非权威值)
$_SESSION['balance'] = $current_balance - $stake;
// 插入交易凭证
$insert = $link->prepare(
"INSERT INTO `receipts` (`Match`, `Selection`, `Odd`, `Account`, `Stake Amount`, `Payout`, `Total Odds`, `invoiceid`, `Kickoff`)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
$insert->bind_param("ssdddddss", $match, $selection, $odd, $account, $stake, $payout, $todds, $invoiceid, $kickoff);
$insert->execute();
$link->commit();
$_SESSION['rec'] = $invoiceid;
$_SESSION['success'] = 1;
header("Location: welcome.php");
exit;
} catch (Exception $e) {
$link->rollback();
error_log("投注事务失败: " . $e->getMessage());
$_SESSION['error'] = "系统繁忙,请稍后重试。";
header("Location: bet_form.php");
exit;
}✅ 关键改进说明:
-
杜绝信任客户端数据:不再使用
$_POST['balance'],而是从数据库实时读取balance; -
双重校验机制:应用层判断 + SQL
WHERE balance >= ?条件,有效防御并发场景下的超卖; - 事务保障一致性:扣款与记账在同一个事务中完成,避免部分成功导致的数据不一致;
-
输入强类型过滤:使用
filter_input()和(float)强制转换,防止恶意字符串注入; - 清晰错误处理与用户反馈:余额不足时跳转至表单页并提示具体金额,提升体验。
⚠️ 重要提醒:
立即学习“PHP免费学习笔记(深入)”;
- 原代码中
mysqli_real_escape_string()对数值型字段防护效果有限,且无法防止逻辑绕过,必须弃用; -
$_SESSION['username']必须已通过登录认证初始化,否则存在越权风险; - 生产环境务必启用
error_reporting(0)或集中日志,避免敏感错误信息暴露给用户。
遵循以上实践,即可构建健壮、安全、符合金融级要求的余额校验流程。



















