Menu
CoddyTech

银行系统

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 106 课,共 107 课。

challenge icon

挑战

简单

让我们构建一个管理账户、交易和金融操作的银行系统!你将创建一个健壮的系统,其中数据完整性至关重要:账户余额只能通过经过验证的方法更改,透支或金额无效等错误必须得到妥善处理。

你将把代码分布在 5 个文件中:

  • account.go:定义一个 Transactable 接口,其中包含 Deposit(amount float64) error 和 Withdraw(amount float64) error 方法。然后创建两种实现此接口的账户类型:

    一种 CheckingAccount,包含未导出的 id、holder 和 balance 字段。支票账户允许取款后余额恰好为零,但不能低于零。

    一种 SavingsAccount,包含相同的未导出字段,另外还有一个 minBalance 字段。储蓄账户必须保持至少 100.0 的余额:如果取款会使余额低于此阈值,就必须失败。

    这两种账户类型都需要构造函数(NewCheckingAccount 和 NewSavingsAccount)以及 Balance() float64 getter 方法。存款时,拒绝负数或零金额。取款时,拒绝负数或零金额,以及超过可用资金的金额。

  • transaction.go:创建一个 Transaction 结构体,用于记录账户活动。每笔交易都有一个 Type(字符串:"deposit" 或 "withdrawal")、Amount(float64)和 AccountID(字符串)。添加一个 NewTransaction 构造函数,以及一个 String() string 方法,该方法返回以下格式:[Type] $[Amount] on [AccountID],其中金额格式为两位小数。
  • bank.go:构建一个 Bank 结构体,用于管理多个账户并记录交易。按 ID 将账户存储在 map 中,并使用切片保存交易。实现以下内容:
    • NewBank() *Bank:初始化空集合
    • AddAccount(id string, account Transactable):注册一个账户
    • Deposit(accountID string, amount float64) error:向账户存款,并在成功时记录交易
    • Withdraw(accountID string, amount float64) error:从账户取款,并在成功时记录交易
    • Transfer(fromID, toID string, amount float64) error:在账户之间转账,并保证操作的原子性(如果取款成功但存款失败,则撤销取款)
    • GetBalance(accountID string) (float64, error):返回账户余额

    账户不存在时,返回相应的错误。

  • errors.go:为银行操作定义哨兵错误:
    • ErrAccountNotFound:账户 ID 不存在时
    • ErrInsufficientFunds:取款金额超过可用余额时
    • ErrInvalidAmount:存款或取款金额为零或负数时
    • ErrMinBalanceRequired:储蓄账户取款会导致余额低于最低余额时
  • main.go:创建一个银行,并根据输入处理操作。

    读取要创建的账户数量。对于每个账户,读取账户类型(checking 或 savings)、ID、持有人姓名和初始余额。创建相应的账户类型,并将其添加到银行中。

    然后读取操作数量。每个操作属于以下类型之一:

    • deposit [accountID] [amount]
    • withdraw [accountID] [amount]
    • transfer [fromID] [toID] [amount]
    • balance [accountID]

    对于每个操作,打印结果:存款、取款或转账成功时打印 OK;查询余额时打印格式为两位小数的余额;操作失败时,使用哨兵错误消息打印 Error: [message]。

将提供以下输入:

  • 账户数量,然后是每个账户的类型、ID、持有人、初始余额(每个账户 4 行)
  • 操作数量,然后是每个操作(每个操作 1 行)

例如,给定以下输入:

2
checking
ACC001
Alice
500.00
savings
ACC002
Bob
1000.00
6
deposit ACC001 200
balance ACC001
withdraw ACC002 950
withdraw ACC001 800
transfer ACC001 ACC002 100
balance ACC002

输出应为:

OK
700.00
Error: minimum balance required
Error: insufficient funds
OK
1000.00

再给定以下输入:

1
checking
C100
Charlie
100.00
4
withdraw C100 100
balance C100
deposit C100 -50
withdraw C999 50

输出应为:

OK
0.00
Error: invalid amount
Error: account not found

注意,Transactable 接口让 Bank 能够统一处理这两种账户类型,同时每种账户类型都能执行自己的规则。Transfer 方法展示了原子操作:如果操作中途出现问题,系统仍能保持一致!

亲手试试

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)

	// 读取账户数量
	scanner.Scan()
	numAccounts, _ := strconv.Atoi(scanner.Text())

	// 创建一个新银行
	bank := NewBank()

	// 读取并创建账户
	for i := 0; i < numAccounts; i++ {
		scanner.Scan()
		accountType := scanner.Text()
		scanner.Scan()
		id := scanner.Text()
		scanner.Scan()
		holder := scanner.Text()
		scanner.Scan()
		balance, _ := strconv.ParseFloat(scanner.Text(), 64)

		// TODO:根据 accountType 创建相应类型的账户
		// 并将其添加到银行
		_ = accountType
		_ = id
		_ = holder
		_ = balance
	}

	// 读取操作数量
	scanner.Scan()
	numOps, _ := strconv.Atoi(scanner.Text())

	// 处理每个操作
	for i := 0; i < numOps; i++ {
		scanner.Scan()
		parts := strings.Fields(scanner.Text())
		operation := parts[0]

		// TODO:处理每种操作类型:
		// - "deposit":向账户存款,输出 "OK" 或错误
		// - "withdraw":从账户取款,输出 "OK" 或错误
		// - "transfer":在账户之间转账,输出 "OK" 或错误
		// - "balance":获取余额并按保留 2 位小数的格式输出
		_ = operation
	}
}

面向对象编程中的所有课

自己练习: Go 在线编译器