1. Birthdays

实现一个 Web 应用,用于记录朋友的生日。
Birthdays 应用 项目文件结构

 tree
.
├── app.py
├── birthdays.db
├── static
│   └── styles.css
└── templates
    └── index.html

数据库结构

sqlite3 birthdays.db
sqlite> .schema
CREATE TABLE birthdays (
    id INTEGER,
    name TEXT,
    month INTEGER,
    day INTEGER,
    PRIMARY KEY(id)
);

我们的任务:完成一个网页应用程序的实现,让用户可以存储和跟踪生日。 后端(Python):接收表单提交的数据,存入数据库。
前端(HTML):制作一个表单让用户输入。
展示(Python + HTML):从数据库取出数据,并在网页上显示出来。
课程以提供了代码框架,我们要实现的细节如下:

  1. 当用户发起 GET 请求 / 时,程序应该以表格形式显示数据库中所有人员及其生日。
    • app.pyGET 请求处理逻辑添加查询 birthdays.db 库中所有人员及其生日的功能。并将这些数据传递给 index.html 模板展示。
    • index.html 添加逻辑,每个人的生日渲染为一行,每行数据包含两列,一列为 name,一列为 birthday。
  2. 当用户发起 POST 请求 / 时,应用需要向数据库添加一行新的数据,并重新渲染 index 页面。
    • index.html 中添加表单,允许用户输入姓名、生日月份和生日日期。确保表单通过 post 的方法提交到 / (其“action”)。
    • app.py 中,POST 请求处理逻辑中,根据用户提供的数据向 birthdays 表中添加一行。
      可选任务:
    • 添加删除/编辑生日条目的功能。
    • 添加任何你想添加的功能。

      分步实现

  3. TODO: Create a form for users to submit a name, a month, and a day
    回忆下 html 中添加表单的标签
    ```html
我们需要在这对标签内添加以下字段  
- `name`, 类型 `text`  
- `month`,类型 `number`  
- `day`, 类型 `number`  
还需要一个提交按钮,用以提交添加的生日。  
```html
<form>
    <input name="name" type="text">
    <input name="month" type="number">
    <input name="day" type="number">
    <input type="submit" value="Add Birthday">
</form>

现在,用户数据表单和提交按钮准备好了,提交到哪里呢?也就是说我们需要 action这个属性指定提交的目的地,根据前面任务分析,提交到 /路由。

<form action="/" method="post">
    <input name="name" type="text">
    <input name="month" type="number">
    <input name="day" type="number">
    <input type="submit" value="Add Birthday">
</form>

为了更完善,通常我们需要加上 placeholder 用以提醒用户输入。同时,为了确认用户与表单意图相符,通常需要添加一些客户端验证,比如,这里的 month 的范围应该为[1,12],day 的范围应该为[1,31]

<form action="/" method="post">
    <input name="name" placeholder="Name" type="text">
    <input name="month" placeholder="Month" type="number" min="1" max="12">
    <input name="day" placeholder="Day" type="number" min="1" max="31">
    <input type="submit" value="Add Birthday">
</form>
  1. TODO: Add the user’s entry into the database
    回顾下,Flask 提供了 request.form.get() 方法通过 POST 提交表单数据。因此,前端表单数据可以通过如下方式提交
    if request.method == "POST":,qpp.py 需要完成以下逻辑处理:
    # Access form data
    name = request.form.get("name")
    month = request.form.get("month")
    day = request.form.get("day")
    

    下一步就是将这些数据提交到数据库

  2. 访问数据库
    db = SQL("sqlite:///birthdays.db")
    
  3. 使用 INSERT 语句将数据插入到 birthdays 库的 birthdays中。
    db.execute("INSERT INTO birthdays (name, month, day) VALUES(?, ?, ?)", name, month, day)
    

    除此之外,实际项目需要考虑处理边界条件的逻辑。比如,这里的 name 是否为空,month,day 的范围是否符合逻辑。 app.py 这部分任务完整实现如下:
    ```Python

    Access form data

    name = request.form.get(“name”) if not name: return redirect(“/”)

month = request.form.get(“month”) if not month: return redirect(“/”) try: month = int(month) except ValueError: return redirect(“/”) if month < 1 or month > 12: return redirect(“/”)

day = request.form.get(“day”) if not day: return redirect(“/”) try: day = int(day) except ValueError: return redirect(“/”) if day < 1 or day > 31: return redirect(“/”)

Insert data into database

db.execute(“INSERT INTO birthdays (name, month, day) VALUES(?, ?, ?)”, name, month, day)

进一步优化,当用户提交非法数据是,不是直接返回到 index 页面,而是告知用户明确的错误信息,我们可以用一个 error.html 页面展示:  
```html
<!DOCTYPE html>

<html lang="en">
    <head>
        <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@500&display=swap" rel="stylesheet">
        <link href="/static/styles.css" rel="stylesheet">
        <title>Error</title>
    </head>
    <body>
        <div class="header">
            <h1>Error</h1>
        </div>
        <div class="container">
            <div class="section">
                <h2>{ { message } }</h2>
                <p><a href="/">Go back</a></p>
            </div>
        </div>
    </body>
</html>

app.py 优化如下:

import os

from cs50 import SQL
from flask import Flask, flash, jsonify, redirect, render_template, request, session

# Configure application
app = Flask(__name__)

# Ensure templates are auto-reloaded
app.config["TEMPLATES_AUTO_RELOAD"] = True

# Configure CS50 Library to use SQLite database
db = SQL("sqlite:///birthdays.db")


@app.after_request
def after_request(response):
    """Ensure responses aren't cached"""
    response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    response.headers["Expires"] = 0
    response.headers["Pragma"] = "no-cache"
    return response


@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "POST":

        # TODO: Add the user's entry into the database
        # 1. 获取用户提交的前端表单数据
        name = request.form.get("name")
        month = request.form.get("month")
        day = request.form.get("day")
        #####  验证数据 #######
        # 2. 验证 name(注意:name.strip() 返回空字符串时为 False)
        if not name or not name.strip():
            return render_template("error.html", message="Name cannot be empty")
        # 3. 验证 month,day 是否存在
        if not month or not day:
            return render_template("error.html",message="Month and day are required")
        # 4. 将 month,day 转换为整数,并捕获异常
        try:
            month = int(month)
            day = int(day)
        except ValueError:
            return render_template("error.html", message="Month and day must be numbers")
        # 验证 month,day 的数值范围
        if month < 1 or month > 12:
            return render_template("error.html",message="month must be between 1 and 12")
        if day < 1 or day > 31:
            return render_template("error.html", message="day must be between 1 and 31")
        # 更严格的数值范围验证
        days_in_month = {
            1: 31, 2: 29, 3: 31, 4: 30, 5: 31, 6: 30,
            7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31
        }
        if day > days_in_month[month]:
            return render_template("error.html",message=f"Month {month} doesn't have {day} days")
        # 清除 name 首尾空格
        name = name.strip()
        # 插入数据库
        db.execute("INSERT INTO birthdays (name, month, day) VALUES (?, ?, ?)", name, month, day)
        return redirect("/")

    else:
        # TODO: Display the entries in the database on index.html
        # GET 请求,从数据库查询所有数据
        birthdays = db.execute("SELECT * FROM birthdays")
        return render_template("index.html", birthdays=birthdays)

OK,现在写的部分(更新数据)已经完成了,下面实现读的部分(读取展示数据)
对应 app.pyelse 部分应该完成以下逻辑处理:

  1. 查询所有生日数据
    birthdays = db.execute("SELECT * FROM birthdays")
    
  2. 渲染模板,并将 birthdays 变量传递给 index.html
    return render_template("index.html", birthdays=birthdays)
    

    对应的 index.html 中第二部分 TODO 需要完成以下逻辑处理:

                     <tbody>
                         <!-- 遍历后端传过来的 birthdays 列表 -->
                         { % for birthday in birthdays % }
                             <tr>
                                 <!-- 显示名字 -->
                                 <td>{ { birthday.name } }</td>
                                 <!-- 显示日期 -->
                                 <td>{ { birthday.month } }/{ { birthday.day } }</td>
                             </tr>
                         { % endfor % }
                     </tbody>
    

    演示

    演示验证我们的 Birthdays 应用

  3. 运行 flask run
    浏览器中打开页面
    初始页面
    SELECT * FROM birthdays;查询数据库可以看到确实有三条数据。
  4. 添加数据
    添加数据
    查询数据库,也有了上面新插入的数据
    SELECT * FROM birthdays WHERE name="John";
    +----+------+-------+-----+
    | id | name | month | day |
    +----+------+-------+-----+
    | 4  | John | 3     | 10  |
    +----+------+-------+-----+
    

    同时控制台日志也显示插入成功
    控制台日志

  5. 插入一条非法数据,返回错误提示
    错误提示
    这只是简单 demo 演示,实际生产环境中我们部署这样的web 程序还有其它考虑。

    2. Finance

    实现一个网站,用户可以通过该网站“购买”和“出售”股票,类似于下面的示例。 Finance 应用

    问题背景

    如果你不太清楚买卖股票(即公司股份)是什么意思,可以来这里学习教程(还挺有意思,做CS作业,还附赠股票课程)。
    项目文件结构

    week9/finance/ $ tree
    .
    ├── app.py
    ├── finance.db
    ├── helpers.py
    ├── requirements.txt
    ├── static
    │   ├── favicon.ico
    │   ├── I_heart_validator.png
    │   └── styles.css
    └── templates
     ├── apology.html
     ├── layout.html
     └── login.html
    

    查看数据库

    sqlite3 finance.db
    sqlite> .schema
    CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username TEXT NOT NULL, hash TEXT NOT NULL, cash NUMERIC NOT NULL DEFAULT 10000.00);
    CREATE TABLE sqlite_sequence(name,seq);
    CREATE UNIQUE INDEX username ON users (username);
    

    执行 flask run 查看初始页面,我们看到一个登录页面,看一下就好,现在不要做任何操作。
    首先,根据课程页面和课程提供的源码分析项目。这是一个标准的MVC架构项目。

    项目概述

    C$50 Finance - 一个让用户可以”买卖”股票的网站,使用真实的股票价格数据。

    核心功能

  6. 注册/登录 - 用户账户系统
  7. 查询股票 - 查看股票实时价格
  8. 买入股票 - 用模拟资金购买股票
  9. 卖出股票 - 出售持有的股票
  10. 查看投资组合 - 显示持有的股票和总资产
  11. 交易历史 - 查看所有买卖记录

    一、理解现有架构与设计数据库

    在写任何 Python 代码之前,我们必须先看懂现有的代码,并设计好数据的存储方式。

    1. 理解核心文件

    • app.py
      ```Python

      所有路由:

    • /login (登录)
    • /logout (登出)
    • /register (注册) - 你需要实现
    • /quote (查询股票) - 你需要实现
    • /buy (买入) - 你需要实现
    • /sell (卖出) - 你需要实现
    • / (首页/投资组合) - 你需要实现
    • /history (历史记录) - 你需要实现 ```
      项目已经实现了 /login (登录) 和 /logout (登出)。而且已经写好了禁止缓存 after_request 和装饰器 login_required
    • helpers.py
      这个文件提供了三个重要的函数。
      ```Python def apology(message, code=400): “"”渲染错误页面””” # 用于显示错误信息

def login_required(f): “"”装饰器:要求用户登录””” # 用于保护需要登录的路由

def lookup(symbol): “"”查询股票价格””” # 输入: “AAPL” (股票代码) # 输出: {“name”: “Apple Inc.”, “price”: 150.25, “symbol”: “AAPL”} # 如果找不到返回 None

def usd(value): “"”格式化为美元””” # 输入: 1234.56 # 输出: “$1,234.56”

### 2. 设计数据库  
由前面查询数据库信息可知,目前的 `finance.db` 只有一张 `users` 表,用来存用户名、密码哈希和现金。   
通过前面的分析我们知道**需要记录用户买卖了什么股票**。  
因此,需要创建一张新的表,**交易记录表 `transactions`**。每行记录应该包含以下:  
1. **id**:交易记录的唯一编号。  
2. **user_id**:哪个用户做的交易,关联 users 表的 id。  
3. **symbol**:买卖了那支股票?(TEXT)  
4. **shares**:买了多少股?(INTEGER),如果是卖出,可以存负数,或者加一个 `type` 字段(buy/sell),为了简化逻辑,建议**买入存正数,卖出存负数**。   
5. **price**:当时的成交单价是多少?(NUMERIC)  
6. **timestamp**: 什么时候买的?(DATETIME,默认值为当前时间)。  
终端执行 `sqlite3 finance.db` 打开 `finance.db` 库,写入下面的建表语句:  
```sql
CREATE TABLE transactions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    symbol TEXT NOT NULL,
    shares INTEGER NOT NULL,
    price NUMERIC NOT NULL,
    transacted_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY(user_id) REFERENCES users(id)
);

提示:建立好表后,用 .schema transactions 确认一下。
另外,为了加快查询,创建索引

CREATE INDEX idx_user_transactions ON transactions(user_id);
CREATE INDEX idx_user_symbol ON transactions(user_id, symbol);

二、实现注册功能

用户需要先有账号才能登录,所以注册功能可以仿照登录功能的实现。

1. 前端(register.html

  • 创建一个表单,包含用户名(username,text),密码(password,text),确认(confirmation
  • 提交到 /register,方法 post

    2. 后端(app.py -> def register):

  • GET 请求:渲染 register.html
  • POST 请求:
    1. 获取用户输入的用户名,密码(request.form.get())等。
    2. 校验
      • 用户名是否为空? return apology(...)
      • 密码是否为空? return apology(...)
      • 两次密码是否一致? return apology(...)
    3. 查询数据库,用户名是否已存在?
      这个有两种写法:
      1. 索引的主动查询("SELECT * FROM users WHERE username = ?", username
      2. 唯一约束的被动捕获(try ... except
        新手建议方法1,简单直接。
    4. 哈希密码:千万不能存明文密码。使用 werkzeug.security 里的 generate_password_hash
    5. 插入数据库:("INSERT INTO users ...”)
    6. 登录 (可选): 可以直接把用户存入 session["user_id"],或者简单的 return redirect("/login") 让用户自己登录。

      核心代码

  • /register
      if request.method == "POST":
          username = request.form.get("username")
          password = request.form.get("password")
          confirmation = request.form.get("confirmation")
          # check 
          # 1. check username and password is not empty
          # if not username or not password or not confirmation:
          if not username:
              return apology("must provide username", 400)
          if not password:
              return apology("must provide password", 400)
          if not confirmation:
              return apology("must provide confirmation", 400)
          # 2.check if passwords match for the confirmation
          if password != confirmation:
              return apology("confirmation does not match pawword", 400)
          # check if username has already taken
          existing = db.execute("SELECT * FROM users WHERE username = ?", username)
          if existing:
              return apology("username has already been taken", 400)
          # insert user account into database
          hash_password = generate_password_hash(password)
          # db.execute 对于 INSERT 语句返回新插入行的 id
          user_id = db.execute("INSERT INTO users (username, hash) VALUES (?, ?)", username, hash_password)
          session["user_id"] = user_id
          return redirect("/")
      else: # GET 
          return render_template("register.html")
    
  • templates/register.html
      <form action="/register" method="post">
          <div class="mb-3">
              <input autocomplete="off" autofocus class="form-control mx-auto w-auto" 
                     name="username" placeholder="Username" type="text">
          </div>
          <div class="mb-3">
              <input class="form-control mx-auto w-auto" 
                     name="password" placeholder="Password" type="password">
          </div>
          <div class="mb-3">
              <input class="form-control mx-auto w-auto" 
                     name="confirmation" placeholder="Confirm Password" type="password">
          </div>
          <button class="btn btn-primary" type="submit">Register</button>
      </form>
    

    验证

    对于这种复杂的项目,一定每实现一个功能就验证一下,确保子功能的正确性,不要想着毕其功于一役。否则,等你全部实现完,再调试,难度非常大。 测试,通过日志调试
    调试日志
    注册功能测试
    查询数据库 users 表可以看到用户账户已添加到数据库
    查询用户

三、查价功能(quote

这是最简单的业务功能。

1. 前端

  • quote.html:一个简单的表单,输入股票代码(symbol),提交按钮。

    2. 后端

    app.py/def quote()

    • POST 请求
      1. 获取 symbol
      2. 调用 helpers.py 中的 lookup(symbol)
      3. 如果查无股,返回 apology
      4. 如果成功,渲染一个新页面,把股票名字,代码,价格展示到页面。
  • 提示:可以使用 usd 过滤器在 HTML 中美化价格,例如 { { price | usd } }

    核心代码

  • /quote
      if request.method == "POST":
          symbol = request.form.get("symbol")
          if not symbol:
              return apology("must provide symbol", 400)
          # 注意:symbol 是字符串,不是函数,所以是 symbol.upper() 而不是 symbol().upper()
          stock = lookup(symbol.upper())
          if stock is None:
              return apology("invalid symbol", 400)
          return render_template("quoted.html", stock=stock)
      else:
          return render_template("quote.html")
    
  • templates/quoted.html
      <h2>Stock Quote</h2>
      <p>
          A share of { { stock.name } } ({ { stock.symbol } }) costs { { stock.price | usd } }.
      </p>
      <a href="/quote">Quote Another</a>
    
  • templates/quote.html
      <form action="/quote" method="post">
          <div class="mb-3">
              <input autocomplete="off" autofocus class="form-control mx-auto w-auto" 
                     name="symbol" placeholder="Symbol" type="text">
          </div>
          <button class="btn btn-primary" type="submit">Quote</button>
      </form>
    

    测试

    quote 功能测试

    四、买入功能

  • /buy,这是逻辑最复杂的一个任务,因为涉及到钱和货的流转。

    1. 前端(buy.html

    • 表单,输入 symbol(text) 和 shares(number)
    • 注意,shares 的值必须是整数。

      2. 后端(app.py -> def buy()

    • 获取数据:股票代码和购买份额。
    • 检验
      • 用户是否正确提交了 symbolshares
      • 股票代码是否有效(调用 lookup 查询)。
      • 购买份额的数值范围是否合法(是否正整数)。
        Python技巧: if not shares.isdigit() 或者用 try/except 转换 int
    • 计算
      • 查询当前该股票价格 price
      • 计算总共花费 total_cost = price * shares
    • 查询
      • 查询当前用户账户还有多上钱:"SELECT cash FROM users WHERE id = usr_id"
    • 判断
      • 如果 cash < total_costreturn apology("Can't afford")
    • 执行交易(原子操作)
      • 扣钱:更新 users 表,"UPDATE users SET cash = cash - total_cost WHERE id = user_id"
      • 记账:更新 transactions 表,记录购买的股票,份额,当时购买单价。

        核心代码

  • /buy
      if request.method == "POST":
          symbol = request.form.get("symbol")
          shares = request.form.get("shares")
          # check data
          # 1. check symbol and shares are not empty
          if not symbol:
              return apology("must provide symbol", 400)
          if not shares:
              return apology("must provide shares", 400)
          # 2. check if shares is a positive integer
          try:
              shares = int(shares)
              if shares <= 0:
                  return apology("shares must be positive", 400)
          except ValueError:
              return apology("shares must be a positive integer", 400)
          # get the stock through lookup
          stock = lookup(symbol)
          if stock is None:
              return apology("invalid symbol", 400)
          # compute total cost to buy the stock
          total_cost = shares * stock["price"]
          # check if the user has enough cash to buy the stock
          user = db.execute("SELECT cash FROM users WHERE id = ?", session["user_id"])
          cash = user[0]["cash"]
          if cash < total_cost:
              return apology("you do not have enough cash to buy the stock", 400)
          # update the user's cash
          db.execute("UPDATE users SET cash = cash - ? WHERE id = ?", total_cost, session["user_id"])
          # record transactions(注意:字段名是 user_id,不是 id)
          db.execute("INSERT INTO transactions (user_id, symbol, shares, price) VALUES (?, ?, ?, ?)",
                     session["user_id"], stock["symbol"], shares, stock["price"])
          # flash a message
          flash(f"Bought {shares} shares of {symbol} for {usd(total_cost)}")
          return redirect("/")
      else:
          return render_template("buy.html")
    
  • templates/buy.html
      <form action="/buy" method="post">
          <div class="mb-3">
              <input autocomplete="off" autofocus class="form-control mx-auto w-auto" 
                     name="symbol" placeholder="Symbol" type="text">
          </div>
          <div class="mb-3">
              <input class="form-control mx-auto w-auto" 
                     min="1" name="shares" placeholder="Shares" type="number">
          </div>
          <button class="btn btn-primary" type="submit">Buy</button>
      </form>
    

    验证

    前面可知 单价 $668.77,现在我们买20股,被告知cannot afford。因为每个用户默认初始 cash 为1000。看来我们只能先买一股用去测试了。 当前只有两个用户,所以我在交易后后台查询数据库就没有使用 WHERE指令,可以看出 cash 金额发生了变化。
    cash 不足时 buy 成功

    五、首页 (index)

    投资组合,难点在于数据聚合。
    首页需要展示当前用户的“投资组合”:比如你持有多少 Apple?多少 Google?现在的总市值是多少?
    这需要一些复杂的逻辑,因为数据库 transactions 存的是流水账 买+10,买+5,卖-3)。
    需要使用 Python 或者 SQL 进行聚合操作。

    后端逻辑

    1. 查询交易记录:找出当前用户所有交易记录。
    • 建议 SQL:SELECT symbol, SUM(shares) as total_shares FROM transactions WHERE user_id = ? GROUP BY symbol HAVING total_shares > 0
    • 说明:SUM(shares) 会自动处理买入(正)和卖出(负)的抵消。HAVING total_shares > 0 过滤掉那些已经卖光了的股票。
      2. 准备表格数据
    • 创建一个空列表 portfolio
    • 遍历刚才查到的每一行数据
      • 拿到 symbol
      • 再次调用 lookup 查询当前最新价格
      • 计算该股票的总市值(total_share * current_price
      • 把这些信息整理好放入 portfolio 列表。
        1. 查询现金:查 users 表中的余额。
        2. 计算总资产:现金 + 当前股票的总市值。
        3. 渲染页面: 把 portfolio、cash、total 传给 index.html 进行表格展示。

          核心代码

  • /
      # 查询用户持有的股票(注意:字段名是 user_id,SQL 需要逗号分隔)
      portfolio = db.execute("""
          SELECT symbol, SUM(shares) as total_shares
          FROM transactions
          WHERE user_id = ?
          GROUP BY symbol
          HAVING SUM(shares) > 0
      """, session["user_id"])
      # get the cash the user has
      user = db.execute("SELECT cash FROM users WHERE id = ?", session["user_id"])
      cash = user[0]["cash"]
      # compute the total value of the user's portfolio
      total_value = cash  # set total_value initially to the cash the user has
      for stock in portfolio:
          quote = lookup(stock["symbol"])
          stock["name"] = quote["name"]
          stock["price"] = quote["price"]
          stock["total"] = stock["price"] * stock["total_shares"]
          total_value += stock["total"]
      return render_template("index.html", portfolio=portfolio, cash=cash, total_value=total_value)
    
  • templates/index.html
      <h2>Portfolio</h2>
      <table class="table table-striped">
          <thead>
              <tr>
                  <th>Symbol</th>
                  <th>Name</th>
                  <th>Shares</th>
                  <th>Price</th>
                  <th>Total</th>
              </tr>
          </thead>
          <tbody>
              { % for stock in portfolio % }
              <tr>
                  <td>{ { stock.symbol } }</td>
                  <td>{ { stock.name } }</td>
                  <td>{ { stock.total_shares } }</td>
                  <td>{ { stock.price | usd } }</td>
                  <td>{ { stock.total | usd } }</td>
              </tr>
              { % endfor % }
          </tbody>
          <tfoot>
              <tr>
                  <td colspan="4"><strong>Cash</strong></td>
                  <td>{ { cash | usd } }</td>
              </tr>
              <tr>
                  <td colspan="4"><strong>Total</strong></td>
                  <td><strong>{ { total | usd } }</strong></td>
              </tr>
          </tfoot>
      </table>
    

    六、卖出(sell)

    逻辑和 buy 类似,但是方向是反的。

    前端逻辑:

    可以做一个下拉菜单(<select>),显示当前用户持有的股票(需要把用户当前持有的股票传递给前端)。

    后端逻辑

    • 校验:用户是否持有该股票,持有数量 >= 卖出数量?
    • 执行
      • users 表改用的 cash 增加
      • transactions 表插入记录(shares 记为负数)。
  • /sell(核心代码)
      if request.method == "POST":
          symbol = request.form.get("symbol")
          shares = request.form.get("shares")
          # check data from user
          if not symbol or not shares:
              return apology("must provide symbol and shares", 400)
          # check if the shares value is integer
          try:
              shares = int(shares)
              if shares <= 0:
                  return apology("shares must be positive", 400)
          except ValueError:
              return apology("shares must be a positive integer", 400)
          # get how many shares the user holds(注意:字段名是 user_id)
          holdings = db.execute("""
              SELECT SUM(shares) as total_shares
              FROM transactions
              WHERE user_id = ? AND symbol = ?
          """, session["user_id"], symbol)
    
          # check if the user owns any shares of the stock
          if not holdings or holdings[0]["total_shares"] is None or holdings[0]["total_shares"] <= 0:
              return apology("you do not own any shares of the stock", 400)
          # check if the user has enough shares to sell
          if holdings[0]["total_shares"] < shares:
              return apology("you do not have enough shares to sell", 400)
          # get the current price of the stock
          stock = lookup(symbol)
          if stock is None:
              return apology("invalid symbol", 400)
          # compute the total value
          total_sale = shares * stock["price"]
          # update the user's cash
          db.execute("UPDATE users SET cash = cash + ? WHERE id = ?", total_sale, session["user_id"])
          # record transactions(注意:卖出时 shares 存为负数 -shares)
          db.execute("INSERT INTO transactions (user_id, symbol, shares, price) VALUES (?, ?, ?, ?)",
                     session["user_id"], symbol, -shares, stock["price"])
          flash(f"Sold {shares} shares of {symbol} for {usd(total_sale)}")
          return redirect("/")
      else:  # GET request
          # 获取用户持有的股票列表
          stocks = db.execute("""
              SELECT symbol
              FROM transactions
              WHERE user_id = ?
              GROUP BY symbol
              HAVING SUM(shares) > 0
          """, session["user_id"])
          return render_template("sell.html", stocks=stocks)
    
  • templates/sell.html ```html { % extends “layout.html” % }

{ % block title % } Sell { % endblock % }

{ % block main % } <form action="/sell" method="post"> <div class="mb-3"> </div> <div class="mb-3"> </div> </form> { % endblock % }

## 七、交易历史   
很简单的一步。  
1. 直接  `SELECT * FROM transactions WHERE user_id = ?`  
2. 传给前端表单展示。  
### 核心代码  
- `/history`  
```Python
    transactions = db.execute("""
        SELECT symbol, shares, price, transacted_timestamp
        FROM transactions
        WHERE user_id = ?
        ORDER BY transacted_timestamp DESC
    """, session["user_id"])

    return render_template("history.html", transactions=transactions)
  • templates/history.html
      <h2>Transaction History</h2>
      <table class="table table-striped">
          <thead>
              <tr>
                  <th>Symbol</th>
                  <th>Shares</th>
                  <th>Price</th>
                  <th>Transacted</th>
              </tr>
          </thead>
          <tbody>
              { % for transaction in transactions % }
              <tr>
                  <td>{ { transaction.symbol } }</td>
                  <td>
                      { % if transaction.shares > 0 % }
                          <span class="text-success">+{ { transaction.shares } }</span>
                      { % else % }
                          <span class="text-danger">{ { transaction.shares } }</span>
                      { % endif % }
                  </td>
                  <td>{ { transaction.price | usd } }</td>
                  <td>{ { transaction.timestamp } }</td>
              </tr>
              { % endfor % }
          </tbody>
      </table>
    

    额外功能

    1. 添加更改密码功能

    后端逻辑

    1. 接收用户输入的新密码,旧密码,确认
    2. 校验
    • 校验输入不为空
    • 查询数据库中旧密码是否正确
      3. 更新数据库中 users 表。

      代码

  • /change_password
    @app.route("/change_password", methods=["GET", "POST"])
    @login_required
    def change_password():
      """Change user password"""
        
      if request.method == "POST":
          old_password = request.form.get("old_password")
          new_password = request.form.get("new_password")
          confirmation = request.form.get("confirmation")
            
          # 验证输入
          if not old_password or not new_password or not confirmation:
              return apology("must fill all fields", 400)
            
          if new_password != confirmation:
              return apology("passwords do not match", 400)
            
          # 验证旧密码
          user = db.execute("SELECT hash FROM users WHERE id = ?", session["user_id"])
          if not check_password_hash(user[0]["hash"], old_password):
              return apology("invalid old password", 400)
            
          # 更新密码
          new_hash = generate_password_hash(new_password)
          db.execute("UPDATE users SET hash = ? WHERE id = ?", 
                     new_hash, session["user_id"])
            
          flash("Password changed successfully!")
          return redirect("/")
        
      else:
          return render_template("change_password.html")
    
  • templates/change_password.html
    ```html { % extends “layout.html” % }

{ % block title % } Change Password { % endblock % }

{ % block main % } <h2>Change Password</h2> <form action="/change_password" method="post"> <div class="mb-3"> </div> <div class="mb-3"> </div> <div class="mb-3"> </div> </form> { % endblock % }

### 2. 添加充值功能   
#### 后端逻辑  
这个逻辑也很简单,就是更新 users 表中用户的 cash 金额  
#### 核心代码   
- `/add_cash`  
```Python
@app.route("/add_cash", methods=["GET", "POST"])
@login_required
def add_cash():
    """Add cash to account"""
    
    if request.method == "POST":
        amount = request.form.get("amount")
        
        try:
            amount = float(amount)
            if amount <= 0:
                return apology("amount must be positive", 400)
        except ValueError:
            return apology("invalid amount", 400)
        
        # 添加现金
        db.execute("UPDATE users SET cash = cash + ? WHERE id = ?", 
                   amount, session["user_id"])
        
        flash(f"Added {usd(amount)} to your account!")
        return redirect("/")
    
    else:
        return render_template("add_cash.html")
  • templates/add_cash.html
      <h2>Add Cash</h2>
      <form action="/add_cash" method="post">
          <div class="mb-3">
              <input autocomplete="off" autofocus class="form-control mx-auto w-auto" 
                     name="amount" placeholder="Amount (USD)" type="number" 
                     min="0.01" step="0.01" required>
          </div>
          <button class="btn btn-primary" type="submit">Add Cash</button>
      </form>
        
      <div class="mt-4 text-muted">
          <small>
              <p>Note: This is a simulation. In a real application, this would integrate with a payment gateway.</p>
              <p>Minimum amount: $0.01</p>
          </small>
      </div>
    

    附录

    结合 finance.db 解释一下此前SQL课程中没有遇到的知识。

    sqlite3 finance.db
    sqlite> .schema
    CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username TEXT NOT NULL, hash TEXT NOT NULL, cash NUMERIC NOT NULL DEFAULT 10000.00);
    CREATE TABLE sqlite_sequence(name,seq);
    CREATE UNIQUE INDEX username ON users (username);
    

    可以看到,除了 CREATE 了一张 users表,还有两条 CREATE 指令。
    这是一个非常敏锐的观察!你能注意到这三条指令,说明你查看 .schema 时非常仔细。

这两条指令分别代表了 SQLite 数据库的两个重要机制:自动增长计数器唯一性索引

它们对 Finance 项目意味着什么呢?

1. CREATE TABLE sqlite_sequence(name,seq);

这条指令是 SQLite 自动生成的,通常你不需要手动去碰它。

  • 它的来源: 在 users 表的定义中,可以看到了 id INTEGER PRIMARY KEY AUTOINCREMENT。当你使用了 AUTOINCREMENT(自动增长)关键字时,SQLite 就必须找个地方拿小本本记下来:“users 表的 ID 目前发到了第几号?
  • 它的作用sqlite_sequence 就是那个“小本本”。
    • name: 表的名字(比如 “users”)。
    • seq: 当前最大的序号(比如你已经注册了 5 个用户,这里就是 5)。
  • 为什么要它: 如果没有它,假设你删除了 ID 为 5 的用户,下一个新注册的用户可能会重用 ID 5。但有了 AUTOINCREMENT 和这张表,SQLite 会确保 ID 永远向上增加(下一个是 6),不会回退。
  • 给新手的建议完全忽略它。它是数据库内部维护秩序的“幕后工作人员”,你在写 Python 代码或 SQL 查询时,永远不需要直接操作这张表。

2. CREATE UNIQUE INDEX username ON users (username);

这条指令非常重要,它直接关系到 Register (注册) 功能的代码逻辑。

它包含两个概念:INDEX (索引)UNIQUE (唯一)

概念 A:索引 (INDEX) —— 为了速度

想象一下,如果 users 表里有 100 万个用户,当用户登录时,数据库需要查找 “alice”。如果没有索引,数据库必须从第 1 行扫描到第 100 万行(全表扫描),速度极慢。 INDEX 就像书籍背后的“索引页”。它让数据库能够以极快的速度(通常是对数时间复杂度)定位到 “alice” 在哪里。

概念 B:唯一 (UNIQUE) —— 为了规则

这是对项目影响最大的部分。UNIQUE 关键字给 username 这一列加了一把锁:数据库层面上禁止出现重复的用户名

  • 如果不加这一行: 你可以注册一个叫 “alice” 的用户,然后再注册一个叫 “alice” 的用户。数据库里会有两个 “alice”,当你用 SELECT * FROM users WHERE username = 'alice' 时,会造成混乱(到底谁是真正的 alice?)。
  • 加了这一行后: 当你试图插入一个已经存在的名字时,SQLite 会直接报错(抛出一个异常)。

对 Finance 项目开发的指导意义

app.py 实现 register 功能时,这行 SQL 决定了你如何处理“用户名已存在”的情况。

有两种写法:

写法 1(利用索引的主动查询): 利用索引查询很快的特性,先查再插。

# 先去数据库问问:这个名字有人用了吗?
rows = db.execute("SELECT * FROM users WHERE username = ?", username)

# 如果查到了结果,说明被占用了
if len(rows) > 0:
    return apology("Username already exists")

# 如果没查到,再插入
db.execute("INSERT INTO users ...")

写法 2(利用唯一约束的被动捕获 - 更高级的写法): 直接尝试插入,如果数据库报错(因为违反了 UNIQUE 规则),就说明重名了。

try:
    db.execute("INSERT INTO users (username, hash) VALUES(?, ?)", username, hash_password)
except ValueError: 
    # 注意:CS50 的 SQL 库在违反 UNIQUE 约束时可能表现不同,
    # 很多时候简单起见,新手推荐用 "写法 1"。
    return apology("Username already taken")

总结

  1. sqlite_sequence: 数据库的自动计数器。不用管它
  2. UNIQUE INDEX: “守门员”。它保证了所有用户的用户名都是独一无二的,并且让查找用户变得飞快。在写 registerlogin 功能时,正是依赖这个机制来确保系统正常的。

常见错误提醒

在实现 Finance 项目时,以下是一些容易犯的错误:

错误类型 错误示例 正确写法
SQL 字段名错误 WHERE id = ? WHERE user_id = ?(transactions 表)
SQL 语法错误 SELECT symbol SUM(shares) SELECT symbol, SUM(shares)(缺少逗号)
INSERT 字段名错误 INSERT INTO transactions (id, ...) INSERT INTO transactions (user_id, ...)
卖出 shares 符号 shares(正数) -shares卖出必须存负数
函数调用语法 lookup(symbol().upper()) lookup(symbol.upper())(symbol 是字符串,不是函数)
拼写错误 session["usre_id"] session["user_id"]
db.execute 返回值 user = db.execute("INSERT...") 然后 user["id"] INSERT 返回的是新 id 值,直接用 user_id = db.execute(...)

⚠️ 调试技巧:每次操作后用 sqlite3 finance.db 查询数据库,验证数据是否正确写入。