1. 项目概述
作为一名有多年PHP开发经验的程序员,我想分享一个基于面向对象编程思想的ATM系统实现方案。这个项目不仅展示了PHP的面向对象特性,更体现了实际金融系统中常见的业务逻辑和安全考量。
在银行系统中,ATM是最基础也是最核心的业务终端之一。通过这个项目,我们可以学习如何将现实世界的银行业务抽象为代码模型,同时确保系统的安全性和可靠性。整个系统采用经典的MVC架构,但为了教学目的做了适当简化。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计
2.1 核心类设计
BankAccount类是整个系统的基础数据模型,它封装了银行账户的所有属性和基本操作:
php复制class BankAccount {
// 严格封装所有属性
private $accountNumber; // 账户号码(唯一标识)
private $accountHolder; // 账户持有人姓名
private $passwordHash; // 密码哈希值(非明文存储)
private $balance; // 账户余额(精确到分)
private $transactions; // 交易记录数组
// 构造函数初始化账户
public function __construct($accountNumber, $accountHolder, $password, $initialBalance = 0) {
$this->accountNumber = $accountNumber;
$this->accountHolder = $accountHolder;
$this->setPassword($password); // 密码加密存储
$this->balance = $initialBalance;
$this->transactions = [];
}
}
ATM类作为业务逻辑控制器,负责协调用户交互和账户操作:
php复制class ATM {
// 账户集合(实际项目中会使用数据库)
private $accounts = [];
// 当前登录用户
private $currentAccount = null;
// 最大登录尝试次数
const MAX_LOGIN_ATTEMPTS = 3;
// 当前登录尝试次数
private $loginAttempts = 0;
}
2.2 面向对象特性应用
封装性是本项目最重要的特性。所有账户属性都被设为private,外部只能通过定义好的公共方法访问:
php复制// 在BankAccount类中
public function getBalance() {
return $this->balance;
}
public function deposit($amount) {
// 验证金额有效性
if ($amount <= 0) {
throw new InvalidArgumentException("存款金额必须大于0");
}
$this->balance += $amount;
$this->recordTransaction('存款', $amount);
return true;
}
继承性虽然在这个基础版本中没有直接使用,但我们预留了扩展点:
php复制// 可以这样扩展VIP账户
class VIPAccount extends BankAccount {
private $creditLimit = 5000; // 信用额度
public function withdraw($amount) {
$available = $this->balance + $this->creditLimit;
if ($amount > $available) {
throw new Exception("超过可用额度");
}
$this->balance -= $amount;
$this->recordTransaction('取款', $amount);
}
}
多态性体现在不同账户类型可以有不同的业务逻辑实现,如上例中的VIPAccount重写了withdraw方法。
3. 核心功能实现
3.1 用户认证系统
安全的用户认证是ATM系统的第一道防线。我们采用以下安全措施:
- 密码哈希存储:使用PHP内置的password_hash函数
- 登录尝试限制:防止暴力破解
- 会话管理:记录登录状态
php复制// 在BankAccount类中
public function setPassword($password) {
if (strlen($password) < 6) {
throw new InvalidArgumentException("密码至少需要6位");
}
$this->passwordHash = password_hash($password, PASSWORD_DEFAULT);
}
public function verifyPassword($password) {
return password_verify($password, $this->passwordHash);
}
// 在ATM类中
public function login($accountNumber, $password) {
// 检查登录尝试次数
if ($this->loginAttempts >= self::MAX_LOGIN_ATTEMPTS) {
throw new Exception("登录尝试过多,请稍后再试");
}
if (!isset($this->accounts[$accountNumber])) {
$this->loginAttempts++;
throw new Exception("账户不存在");
}
$account = $this->accounts[$accountNumber];
if (!$account->verifyPassword($password)) {
$this->loginAttempts++;
throw new Exception("密码错误");
}
// 登录成功,重置尝试次数
$this->loginAttempts = 0;
$this->currentAccount = $account;
return true;
}
3.2 账户管理功能
账户注册
php复制public function register($accountHolder, $password, $initialDeposit = 0) {
// 生成唯一账户号码
$accountNumber = $this->generateAccountNumber();
// 验证初始存款
if ($initialDeposit < 0) {
throw new InvalidArgumentException("初始存款不能为负");
}
// 创建新账户
$account = new BankAccount($accountNumber, $accountHolder, $password, $initialDeposit);
// 添加到账户集合
$this->accounts[$accountNumber] = $account;
return $accountNumber;
}
private function generateAccountNumber() {
do {
// 生成11位账户号码:62开头 + 9位随机数
$accountNumber = '62' . str_pad(mt_rand(0, 999999999), 9, '0', STR_PAD_LEFT);
} while (isset($this->accounts[$accountNumber]));
return $accountNumber;
}
余额查询
php复制public function checkBalance() {
$this->requireLogin();
return $this->currentAccount->getBalance();
}
private function requireLogin() {
if ($this->currentAccount === null) {
throw new Exception("请先登录");
}
}
3.3 交易功能实现
存款操作
php复制public function deposit($amount) {
$this->requireLogin();
// 验证金额
if ($amount <= 0) {
throw new InvalidArgumentException("存款金额必须大于0");
}
// 执行存款
$this->currentAccount->deposit($amount);
// 记录系统日志
$this->logTransaction("存款", $amount);
return true;
}
取款操作
php复制public function withdraw($amount) {
$this->requireLogin();
// 验证金额
if ($amount <= 0) {
throw new InvalidArgumentException("取款金额必须大于0");
}
// 检查余额
$balance = $this->currentAccount->getBalance();
if ($amount > $balance) {
throw new Exception("余额不足");
}
// 执行取款
$this->currentAccount->withdraw($amount);
// 记录系统日志
$this->logTransaction("取款", $amount);
return true;
}
转账操作
php复制public function transfer($amount, $targetAccountNumber) {
$this->requireLogin();
// 验证目标账户
if (!isset($this->accounts[$targetAccountNumber])) {
throw new Exception("目标账户不存在");
}
$targetAccount = $this->accounts[$targetAccountNumber];
// 不能给自己转账
if ($targetAccount === $this->currentAccount) {
throw new Exception("不能向自己账户转账");
}
// 验证金额
if ($amount <= 0) {
throw new InvalidArgumentException("转账金额必须大于0");
}
// 检查余额
$balance = $this->currentAccount->getBalance();
if ($amount > $balance) {
throw new Exception("余额不足");
}
// 执行转账
$this->currentAccount->withdraw($amount);
$targetAccount->deposit($amount);
// 记录系统日志
$this->logTransaction("转账支出至".$targetAccountNumber, $amount);
return true;
}
3.4 交易记录系统
完善的交易记录是金融系统的基本要求:
php复制// 在BankAccount类中
private function recordTransaction($type, $amount) {
$transaction = [
'timestamp' => date('Y-m-d H:i:s'),
'type' => $type,
'amount' => $amount,
'balance' => $this->balance
];
// 限制交易记录数量,防止内存耗尽
if (count($this->transactions) > 100) {
array_shift($this->transactions);
}
array_push($this->transactions, $transaction);
}
public function getTransactionHistory($limit = 10) {
return array_slice($this->transactions, -$limit, $limit, true);
}
4. 异常处理与边界情况
4.1 自定义异常类
为了更好地处理不同类型的错误,我们可以定义专门的异常类:
php复制class ATMException extends Exception {
// 通用ATM异常
}
class AccountNotFoundException extends ATMException {
// 账户不存在异常
}
class InsufficientBalanceException extends ATMException {
// 余额不足异常
}
class InvalidAmountException extends ATMException {
// 无效金额异常
}
4.2 边界情况处理
在实际使用中,我们需要考虑各种边界情况:
php复制// 金额验证
if (!is_numeric($amount) || $amount <= 0) {
throw new InvalidAmountException("金额必须为正数");
}
// 余额检查(考虑浮点数精度问题)
$epsilon = 0.00001;
if (abs($amount - $this->balance) < $epsilon || $amount > $this->balance) {
throw new InsufficientBalanceException("余额不足");
}
// 账户状态检查
if ($this->isFrozen) {
throw new AccountFrozenException("账户已被冻结");
}
4.3 会话超时处理
php复制class ATM {
private $lastActivityTime;
const SESSION_TIMEOUT = 300; // 5分钟
public function __construct() {
$this->lastActivityTime = time();
}
private function checkSessionTimeout() {
if (time() - $this->lastActivityTime > self::SESSION_TIMEOUT) {
$this->logout();
throw new SessionTimeoutException("会话超时,请重新登录");
}
$this->lastActivityTime = time();
}
public function anyOperation() {
$this->checkSessionTimeout();
// 其他操作逻辑
}
}
5. 安全增强措施
5.1 密码安全
php复制// 密码强度验证
public static function validatePasswordStrength($password) {
if (strlen($password) < 8) {
return false;
}
if (!preg_match('/[A-Z]/', $password)) {
return false; // 需要大写字母
}
if (!preg_match('/[a-z]/', $password)) {
return false; // 需要小写字母
}
if (!preg_match('/[0-9]/', $password)) {
return false; // 需要数字
}
return true;
}
// 在设置密码时
public function setPassword($password) {
if (!self::validatePasswordStrength($password)) {
throw new WeakPasswordException("密码必须至少8位,包含大小写字母和数字");
}
$this->passwordHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
}
5.2 交易限额
php复制class BankAccount {
const DAILY_WITHDRAWAL_LIMIT = 5000; // 单日取款限额
private $todayWithdrawn = 0;
private $lastWithdrawalDate;
public function withdraw($amount) {
// 检查是否是同一天
$today = date('Y-m-d');
if ($this->lastWithdrawalDate !== $today) {
$this->todayWithdrawn = 0;
$this->lastWithdrawalDate = $today;
}
// 检查限额
if ($this->todayWithdrawn + $amount > self::DAILY_WITHDRAWAL_LIMIT) {
throw new WithdrawalLimitExceededException("超过单日取款限额");
}
// 执行取款
$this->balance -= $amount;
$this->todayWithdrawn += $amount;
$this->recordTransaction('取款', $amount);
}
}
5.3 操作日志
php复制class ATM {
private $systemLog = [];
const MAX_LOG_ENTRIES = 1000;
private function log($message) {
$entry = [
'timestamp' => date('Y-m-d H:i:s'),
'message' => $message,
'account' => $this->currentAccount ? $this->currentAccount->getAccountNumber() : null
];
if (count($this->systemLog) >= self::MAX_LOG_ENTRIES) {
array_shift($this->systemLog);
}
array_push($this->systemLog, $entry);
}
public function getSystemLog($limit = 50) {
return array_slice($this->systemLog, -$limit, $limit, true);
}
}
6. 数据库集成方案
虽然我们的示例使用内存存储,但在实际项目中需要数据库支持。以下是MySQL集成的示例:
6.1 数据库表设计
sql复制CREATE TABLE accounts (
account_number VARCHAR(20) PRIMARY KEY,
account_holder VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
balance DECIMAL(15,2) NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE transactions (
id INT AUTO_INCREMENT PRIMARY KEY,
account_number VARCHAR(20) NOT NULL,
transaction_type ENUM('存款','取款','转账收入','转账支出') NOT NULL,
amount DECIMAL(15,2) NOT NULL,
balance_after DECIMAL(15,2) NOT NULL,
related_account VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_number) REFERENCES accounts(account_number)
);
6.2 数据库操作类
php复制class Database {
private $pdo;
public function __construct($host, $dbname, $username, $password) {
$dsn = "mysql:host=$host;dbname=$dbname;charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$this->pdo = new PDO($dsn, $username, $password, $options);
}
public function getAccount($accountNumber) {
$stmt = $this->pdo->prepare("SELECT * FROM accounts WHERE account_number = ?");
$stmt->execute([$accountNumber]);
return $stmt->fetch();
}
public function createAccount($accountNumber, $accountHolder, $passwordHash, $initialBalance) {
$stmt = $this->pdo->prepare("
INSERT INTO accounts (account_number, account_holder, password_hash, balance)
VALUES (?, ?, ?, ?)
");
return $stmt->execute([$accountNumber, $accountHolder, $passwordHash, $initialBalance]);
}
// 其他数据库操作方法...
}
6.3 集成数据库的ATM类
php复制class DatabaseATM extends ATM {
private $db;
public function __construct(Database $db) {
$this->db = $db;
// 初始化时从数据库加载账户
$this->loadAccounts();
}
private function loadAccounts() {
// 从数据库加载所有账户到内存
// 实际项目中可能只需要加载当前会话需要的账户
}
public function register($accountHolder, $password, $initialDeposit = 0) {
// 生成账户号码
$accountNumber = $this->generateAccountNumber();
// 密码哈希
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
// 保存到数据库
$this->db->createAccount($accountNumber, $accountHolder, $passwordHash, $initialDeposit);
// 创建内存对象
$account = new BankAccount($accountNumber, $accountHolder, $password, $initialDeposit);
$this->accounts[$accountNumber] = $account;
return $accountNumber;
}
// 重写其他需要数据库操作的方法...
}
7. 测试与验证
7.1 单元测试示例
使用PHPUnit进行单元测试:
php复制class ATMTest extends PHPUnit\Framework\TestCase {
private $atm;
protected function setUp(): void {
$this->atm = new ATM();
$this->accountNumber = $this->atm->register("测试用户", "Test1234", 1000);
}
public function testLoginSuccess() {
$this->assertTrue($this->atm->login($this->accountNumber, "Test1234"));
}
public function testLoginWrongPassword() {
$this->expectException(Exception::class);
$this->atm->login($this->accountNumber, "WrongPass");
}
public function testDeposit() {
$this->atm->login($this->accountNumber, "Test1234");
$this->atm->deposit(500);
$this->assertEquals(1500, $this->atm->checkBalance());
}
// 更多测试用例...
}
7.2 集成测试场景
php复制// 创建ATM实例
$atm = new ATM();
// 测试场景1:新用户注册和基本操作
$account1 = $atm->register("张三", "ZhangSan123", 500);
$atm->login($account1, "ZhangSan123");
$atm->deposit(300);
$atm->withdraw(200);
$balance = $atm->checkBalance(); // 应该为600
// 测试场景2:转账操作
$account2 = $atm->register("李四", "LiSi456", 1000);
$atm->login($account1, "ZhangSan123");
$atm->transfer(300, $account2);
// 验证转账结果
$atm->login($account2, "LiSi456");
$balance2 = $atm->checkBalance(); // 应该为1300
7.3 性能测试考虑
对于性能敏感的场景,我们需要考虑:
- 对象缓存:频繁访问的账户对象可以缓存
- 批量操作:多个数据库操作合并执行
- 连接池:数据库连接复用
php复制class AccountCache {
private $cache = [];
private $db;
public function __construct(Database $db) {
$this->db = $db;
}
public function getAccount($accountNumber) {
if (!isset($this->cache[$accountNumber])) {
$accountData = $this->db->getAccount($accountNumber);
if (!$accountData) {
return null;
}
$this->cache[$accountNumber] = new BankAccount(
$accountData['account_number'],
$accountData['account_holder'],
'', // 密码不存储在对象中
$accountData['balance']
);
}
return $this->cache[$accountNumber];
}
public function flush() {
// 将缓存中的变更写回数据库
foreach ($this->cache as $account) {
if ($account->isDirty()) {
$this->db->updateAccount($account);
}
}
}
}
8. 项目扩展与优化
8.1 多币种支持
php复制class MultiCurrencyAccount extends BankAccount {
private $currency = 'CNY'; // 默认货币
private $exchangeRates = [
'USD' => 6.5,
'EUR' => 7.8,
// 其他汇率
];
public function setCurrency($currency) {
if (!array_key_exists($currency, $this->exchangeRates)) {
throw new InvalidArgumentException("不支持的货币类型");
}
$this->currency = $currency;
}
public function deposit($amount, $currency = null) {
$currency = $currency ?: $this->currency;
if ($currency !== 'CNY') {
if (!isset($this->exchangeRates[$currency])) {
throw new InvalidArgumentException("不支持的货币类型");
}
$amount = $amount * $this->exchangeRates[$currency];
}
parent::deposit($amount);
}
// 类似地重写其他方法...
}
8.2 支持支票存款
php复制class CheckDeposit {
private $checkNumber;
private $amount;
private $issuingBank;
private $status = 'pending'; // pending, cleared, bounced
public function __construct($checkNumber, $amount, $issuingBank) {
$this->checkNumber = $checkNumber;
$this->amount = $amount;
$this->issuingBank = $issuingBank;
}
public function clear() {
$this->status = 'cleared';
}
public function bounce() {
$this->status = 'bounced';
}
}
class BankAccount {
private $pendingChecks = [];
public function depositCheck(CheckDeposit $check) {
$this->pendingChecks[$check->getCheckNumber()] = $check;
$this->recordTransaction('支票存款待清算', $check->getAmount());
}
public function processCheck($checkNumber, $cleared = true) {
if (!isset($this->pendingChecks[$checkNumber])) {
throw new Exception("支票不存在");
}
$check = $this->pendingChecks[$checkNumber];
if ($cleared) {
$check->clear();
$this->balance += $check->getAmount();
$this->recordTransaction('支票存款已清算', $check->getAmount());
} else {
$check->bounce();
$this->recordTransaction('支票存款退票', -$check->getAmount());
}
unset($this->pendingChecks[$checkNumber]);
}
}
8.3 支持定期存款
php复制class TimeDeposit {
private $amount;
private $interestRate;
private $term; // in months
private $startDate;
private $maturityDate;
public function __construct($amount, $term, $interestRate) {
$this->amount = $amount;
$this->term = $term;
$this->interestRate = $interestRate;
$this->startDate = new DateTime();
$this->maturityDate = (new DateTime())->add(new DateInterval("P{$term}M"));
}
public function calculateInterest() {
$now = new DateTime();
if ($now < $this->maturityDate) {
return 0; // 未到期不计息
}
$years = $this->term / 12;
return $this->amount * $this->interestRate * $years;
}
}
class BankAccount {
private $timeDeposits = [];
public function createTimeDeposit($amount, $term, $interestRate) {
if ($amount > $this->balance) {
throw new Exception("余额不足");
}
$this->balance -= $amount;
$deposit = new TimeDeposit($amount, $term, $interestRate);
$this->timeDeposits[] = $deposit;
$this->recordTransaction('定期存款', -$amount);
}
public function settleTimeDeposit($index) {
if (!isset($this->timeDeposits[$index])) {
throw new Exception("定期存款不存在");
}
$deposit = $this->timeDeposits[$index];
$interest = $deposit->calculateInterest();
$total = $deposit->getAmount() + $interest;
$this->balance += $total;
unset($this->timeDeposits[$index]);
$this->recordTransaction('定期存款结算', $total);
return $total;
}
}
9. 实际部署考虑
9.1 安全性加固
- HTTPS加密:所有通信必须加密
- 输入验证:严格过滤所有用户输入
- 防CSRF:重要操作需要CSRF令牌
- 防XSS:输出时进行HTML转义
php复制// 输入过滤示例
public static function sanitizeInput($input) {
if (is_array($input)) {
return array_map([self, 'sanitizeInput'], $input);
}
// 去除前后空格
$input = trim($input);
// 防止SQL注入
$input = str_replace(["'", '"', '\\', "\0"], '', $input);
// 防止XSS
$input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
return $input;
}
// 在控制器中使用
$cleanInput = ATM::sanitizeInput($_POST);
9.2 性能优化
- OPcache:启用PHP OPcache加速
- 数据库索引:确保查询字段都有索引
- 缓存策略:频繁访问的数据使用Redis缓存
- 懒加载:非立即需要的数据延迟加载
php复制// Redis缓存示例
class AccountCache {
private $redis;
public function __construct() {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
public function getAccount($accountNumber) {
$cacheKey = "account:$accountNumber";
$accountData = $this->redis->get($cacheKey);
if ($accountData === false) {
// 从数据库加载
$accountData = $this->loadFromDatabase($accountNumber);
// 存入缓存,有效期1小时
$this->redis->setex($cacheKey, 3600, serialize($accountData));
} else {
$accountData = unserialize($accountData);
}
return $accountData;
}
}
9.3 日志与监控
完善的日志系统对于问题排查至关重要:
php复制class Logger {
const LEVEL_ERROR = 'ERROR';
const LEVEL_WARNING = 'WARNING';
const LEVEL_INFO = 'INFO';
private $logFile;
public function __construct($logFile) {
$this->logFile = $logFile;
}
public function log($level, $message, array $context = []) {
$entry = [
'timestamp' => date('Y-m-d H:i:s'),
'level' => $level,
'message' => $message,
'context' => $context,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'cli'
];
file_put_contents(
$this->logFile,
json_encode($entry) . PHP_EOL,
FILE_APPEND
);
}
public function error($message, array $context = []) {
$this->log(self::LEVEL_ERROR, $message, $context);
}
// 其他级别方法...
}
// 使用示例
$logger = new Logger('/var/log/atm.log');
$logger->error("登录失败", [
'account' => $accountNumber,
'attempts' => $loginAttempts
]);
10. 项目总结与经验分享
在实现这个PHP面向对象ATM系统的过程中,我积累了一些有价值的经验:
-
封装的重要性:将所有账户属性设为private,通过方法访问,这在后期添加验证逻辑时非常方便,不需要修改大量外部代码。
-
异常处理的实用性:定义专门的异常类可以让错误处理更加精确。比如在转账时,调用者可以明确知道是余额不足还是目标账户不存在导致的失败。
-
交易记录的完整性:保存完整的交易历史不仅是为了显示给用户看,更重要的是在出现争议时可以追溯。实际项目中,交易记录应该是不可变的。
-
密码安全:使用PHP内置的password_hash和password_verify函数比自己实现加密要安全得多,它们会自动处理盐值和使用适当的算法。
-
测试驱动开发:在实现转账功能前先写好测试用例,可以确保各种边界情况(如余额不足、自我转账等)都被正确处理。
在实际项目中,这个基础版本还可以进一步扩展:
- 添加Web界面或API接口
- 集成短信验证码等二次验证
- 支持指纹或面部识别等生物认证
- 实现账户冻结和解冻功能
- 添加管理员后台管理系统
这个项目很好地展示了如何将面向对象的原则应用于实际业务系统开发。通过合理的类设计和职责划分,代码既保持了清晰的结构,又具备了良好的扩展性。
