# -*- coding: utf-8 -*-
"""
================================================================
酒店客房管理系统（酒店管理专业 AI 教学版）—— Web 网页版
================================================================
适用课程：酒店管理信息系统、酒店数字化运营、Python 程序设计（酒店专业方向）
运行环境：Python 3.8+，仅需安装 Flask（一个轻量 Web 框架）
教学目标：帮助酒店管理专业学生理解酒店客房核心业务流程，掌握
          数字化管理系统的设计逻辑，同时入门 Python Web 编程。

【与原桌面版（tkinter + pandas）的关系】
  本程序是将《酒店客房管理系统（酒店管理专业 AI 教学版）》桌面程序
  改写为「网页版」的完整教学程序：业务逻辑、房型、价格、会员折扣等
  数据完全一致，仅将 GUI 交互（tkinter 窗口）替换为浏览器网页（Flask）。
  为降低零基础学生的环境门槛，本网页版用 Python 内置列表/字典代替 pandas，
  数据分析逻辑等价，可对照原桌面版逐模块学习。

【如何运行】
  1) 安装依赖：  pip install flask
  2) 启动程序：  python hotel_room_web_lesson3.py
  3) 打开浏览器访问： http://127.0.0.1:5000

【系统核心功能】
  1. 房态统计看板     —— 实时计算总客房数、可售房数、入住率、各类房态数量
  2. 空房智能推荐     —— 按预算 / 房型 / 楼层多条件筛选空闲客房
  3. 客房信息总览     —— 表格化展示全量客房（房号/房型/价格/楼层/状态）
  4. 入住登记办理     —— 录入信息、会员折扣自动计算、房态自动更新
  5. 退房办理         —— 结算、房态转「待清洁」、入住信息归档删除
  6. 入住客户管理     —— 表格展示所有在住客户与预估房费
  7. 运营数据分析     —— 入住率、可售房均价、会员等级分布
================================================================
"""

import os
import json

from flask import Flask, request, render_template_string, redirect, url_for, flash, get_flashed_messages
from markupsafe import escape

# ------------------------------------------------------------------
# 一、应用与业务数据初始化（教学重点：酒店业务数据的结构化设计）
# ------------------------------------------------------------------
app = Flask(__name__)
app.secret_key = "hotel-room-teaching-demo"  # flash 消息需要

# 会员等级折扣配置（教学用，贴合酒店实际业务）
MEMBER_DISCOUNT = {
    "普通": 1.0,
    "银卡": 0.95,
    "金卡": 0.88,
    "钻石卡": 0.8,
}

# 房态种类（符合酒店行业通用分类）
ROOM_STATUS = ["空闲", "入住", "待清洁", "维修"]

# 数据文件：用于把内存数据持久化到本地，重启程序不丢失
DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "hotel_data.json")


def init_rooms():
    """初始化 20 间客房基础数据（房号/房型/价格/楼层/状态）。
    完全对应原桌面版的数据：10 楼 101-110、20 楼 201-210。"""
    room_nos = [str(n) for n in range(101, 111)] + [str(n) for n in range(201, 211)]
    room_types = ["大床房", "双床房", "套房", "大床房", "双床房",
                  "大床房", "双床房", "套房", "大床房", "双床房",
                  "大床房", "双床房", "套房", "大床房", "双床房",
                  "大床房", "双床房", "套房", "大床房", "双床房"]
    prices = [398, 428, 888, 418, 438, 388, 418, 868, 378, 408,
              458, 488, 988, 438, 468, 428, 458, 968, 418, 448]
    floors = [10] * 10 + [20] * 10
    statuses = ["空闲", "空闲", "空闲", "空闲", "空闲",
                "空闲", "空闲", "待清洁", "空闲", "维修",
                "空闲", "入住", "待清洁", "空闲", "维修",
                "空闲", "入住", "待清洁", "入住", "维修"]
    rooms = []
    for i in range(20):
        rooms.append({
            "房号": room_nos[i],
            "房型": room_types[i],
            "价格(元)": prices[i],
            "楼层": floors[i],
            "状态": statuses[i],
        })
    return rooms


# 全局业务数据（内存中）
ROOMS = init_rooms()
CHECKINS = []  # 在住客户：房号/姓名/联系电话/入住天数/会员等级/预估房费(元)


# ------------------------------------------------------------------
# 二、数据持久化（教学重点：内存数据如何落盘保存）
# ------------------------------------------------------------------
def save_data():
    """把当前业务数据保存到本地 JSON 文件。"""
    with open(DATA_FILE, "w", encoding="utf-8") as f:
        json.dump({"rooms": ROOMS, "checkins": CHECKINS}, f, ensure_ascii=False, indent=2)


def load_data():
    """启动时尝试从 JSON 文件恢复数据（若存在）。"""
    global ROOMS, CHECKINS
    if os.path.exists(DATA_FILE):
        try:
            with open(DATA_FILE, "r", encoding="utf-8") as f:
                data = json.load(f)
            ROOMS = data.get("rooms", ROOMS)
            CHECKINS = data.get("checkins", [])
        except Exception:
            pass  # 数据文件损坏时回退到初始数据


def reset_data():
    """恢复初始数据（对应原桌面版「刷新所有数据」）。"""
    global ROOMS, CHECKINS
    ROOMS = init_rooms()
    CHECKINS = []
    save_data()


# ------------------------------------------------------------------
# 三、核心业务逻辑函数（教学重点：酒店业务逻辑的代码实现）
# ------------------------------------------------------------------
def find_room(room_no):
    """按房号查找客房，找不到返回 None。"""
    for r in ROOMS:
        if r["房号"] == room_no:
            return r
    return None


def room_stats():
    """房态统计：总客房数、各类房态数量、入住率、可售房均价。

    核心教学公式：入住率 = 已入住房间数 / 酒店总客房数 * 100%
    """
    total = len(ROOMS)
    counts = {s: 0 for s in ROOM_STATUS}
    for r in ROOMS:
        counts[r["状态"]] = counts.get(r["状态"], 0) + 1

    occupied = counts["入住"]
    occupancy_rate = round(occupied / total * 100, 1) if total else 0.0

    # 可售房 = 状态为「空闲」的客房
    sellable = [r for r in ROOMS if r["状态"] == "空闲"]
    avg_price = round(sum(r["价格(元)"] for r in sellable) / len(sellable), 2) if sellable else 0.0

    return {
        "total": total,
        "counts": counts,
        "sellable": len(sellable),
        "occupancy_rate": occupancy_rate,
        "avg_price": avg_price,
    }


def recommend_rooms(budget, room_type, floor):
    """空房智能推荐：从空闲房中按预算/房型/楼层筛选。"""
    result = []
    for r in ROOMS:
        if r["状态"] != "空闲":
            continue
        if budget is not None and r["价格(元)"] > budget:
            continue
        if room_type and r["房型"] != room_type:
            continue
        if floor is not None and r["楼层"] != floor:
            continue
        result.append(r)
    return result


def check_in(room_no, name, phone, days, member):
    """入住登记：校验 → 计算折扣房费 → 更新房态 → 写入客户信息。
    返回 (是否成功, 提示信息)。"""
    room = find_room(room_no)
    if room is None:
        return False, "房号不存在，请核对后重新输入。"
    if room["状态"] != "空闲":
        return False, "房间「%s」当前状态为「%s」，不可入住。" % (room_no, room["状态"])

    # 会员折扣计算：预估房费 = 房价 × 入住天数 × 折扣
    discount = MEMBER_DISCOUNT.get(member, 1.0)
    fee = round(room["价格(元)"] * days * discount, 2)

    room["状态"] = "入住"
    CHECKINS.append({
        "房号": room_no,
        "姓名": name,
        "联系电话": phone,
        "入住天数": days,
        "会员等级": member,
        "预估房费(元)": fee,
    })
    save_data()
    return True, "入住登记成功：房号 %s，预估房费 ¥%.2f（%s 折扣 %.0f%%）" % (
        room_no, fee, member, discount * 100)


def check_out(room_no):
    """退房办理：校验 → 结算 → 房态转「待清洁」→ 删除入住信息。
    返回 (是否成功, 提示信息, 结算信息或None)。"""
    room = find_room(room_no)
    if room is None:
        return False, "房号不存在，请核对后重新输入。", None
    if room["状态"] != "入住":
        return False, "房间「%s」当前状态为「%s」，不是入住状态，无法退房。" % (room_no, room["状态"]), None

    settled = None
    for i, c in enumerate(CHECKINS):
        if c["房号"] == room_no:
            settled = c
            del CHECKINS[i]
            break

    room["状态"] = "待清洁"
    save_data()
    if settled:
        return True, "退房办理成功：房号 %s，应收房费 ¥%.2f。" % (room_no, settled["预估房费(元)"]), settled
    return True, "退房办理成功：房号 %s，已转为「待清洁」。" % room_no, settled


def occupancy_analysis():
    """运营数据分析：入住率、可售房均价。"""
    stats = room_stats()
    return {
        "total": stats["total"],
        "occupied": stats["counts"]["入住"],
        "occupancy_rate": stats["occupancy_rate"],
        "avg_price": stats["avg_price"],
    }


def member_distribution():
    """会员等级分布：统计在住客户各会员等级人数。"""
    dist = {}
    for c in CHECKINS:
        m = c["会员等级"]
        dist[m] = dist.get(m, 0) + 1
    return dist


# ------------------------------------------------------------------
# 四、网页样式与布局（教学重点：Web 页面的 HTML/CSS 基础）
# ------------------------------------------------------------------
CSS = """
* { box-sizing: border-box; }
body { margin: 0; font-family: -apple-system, "Segoe UI", "PingFang SC",
  "Microsoft YaHei", sans-serif; background: #f5f6f8; color: #1a1d24; line-height: 1.6; }
header { background: #fff; border-bottom: 1px solid #e6e8ec; position: sticky; top: 0; z-index: 10; }
.nav { max-width: 1080px; margin: 0 auto; padding: 0 20px; display: flex;
  align-items: center; height: 60px; gap: 4px; flex-wrap: wrap; }
.brand { font-weight: 700; font-size: 17px; margin-right: 16px; color: #1d4ed8; }
.nav a { color: #4a5160; text-decoration: none; padding: 7px 13px; border-radius: 8px;
  font-size: 14px; }
.nav a:hover { background: #eef2ff; color: #1d4ed8; }
.nav a.on { background: #1d4ed8; color: #fff; }
main { max-width: 1080px; margin: 24px auto 60px; padding: 0 20px; }
h1 { font-size: 22px; margin: 0 0 4px; }
h2 { font-size: 17px; margin: 0 0 14px; }
.sub { color: #8a919e; font-size: 13px; margin: 0 0 20px; }
.flash { padding: 10px 14px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }
.flash.ok { background: #e7f6ec; color: #157347; border: 1px solid #b7e4c7; }
.flash.err { background: #fdecec; color: #b3261e; border: 1px solid #f3c1c1; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin-bottom: 24px; }
.card { background: #fff; border: 1px solid #e6e8ec; border-radius: 12px; padding: 16px 18px; }
.card .k { font-size: 13px; color: #8a919e; }
.card .v { font-size: 26px; font-weight: 700; margin-top: 4px; }
.card .v.big { color: #1d4ed8; }
.panel { background: #fff; border: 1px solid #e6e8ec; border-radius: 12px; padding: 20px 22px; margin-bottom: 20px; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #eef0f3; }
th { background: #fafbfc; color: #4a5160; font-weight: 600; white-space: nowrap; }
tr:hover td { background: #fafbff; }
.badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; }
.badge.空闲 { background: #e7f6ec; color: #157347; }
.badge.入住 { background: #eef2ff; color: #1d4ed8; }
.badge.待清洁 { background: #fff4e5; color: #b45309; }
.badge.维修 { background: #fdecec; color: #b3261e; }
form.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
label { font-size: 13px; color: #4a5160; display: block; margin-bottom: 6px; }
input, select { width: 100%; padding: 9px 12px; border: 1px solid #d6dae1; border-radius: 8px;
  font-size: 14px; }
input:focus, select:focus { outline: none; border-color: #1d4ed8; }
.btn { display: inline-block; border: none; background: #1d4ed8; color: #fff; padding: 10px 20px;
  border-radius: 8px; font-size: 14px; cursor: pointer; text-decoration: none; }
.btn:hover { background: #1e40af; }
.btn.ghost { background: #fff; color: #4a5160; border: 1px solid #d6dae1; }
.mt { margin-top: 16px; }
.muted { color: #8a919e; }
.foot { text-align: center; color: #b0b6c0; font-size: 12px; margin-top: 40px; }
"""


def base(title, body_html):
    """渲染基础页面骨架（导航 + 内容）。"""
    nav_items = [
        ("/", "房态看板"),
        ("/rooms", "客房总览"),
        ("/recommend", "空房推荐"),
        ("/checkin", "入住登记"),
        ("/checkout", "退房办理"),
        ("/customers", "客户管理"),
        ("/analytics", "数据分析"),
        ("/structures", "控制结构实验"),
    ]
    if request.path == "/structures":
        nav_items = [("/structures", "客户需求练习"), ("/", "酒店管理拓展")]
    nav = "".join(
        '<a href="%s"%s>%s</a>' % (url, ' class="on"' if request.path == url else "", text)
        for url, text in nav_items
    )
    return """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>%s - 酒店客房管理系统</title>
<style>%s</style>
</head>
<body>
<header><nav class="nav"><span class="brand">🏨 酒店客房管理系统</span>%s</nav></header>
<main>%s</main>
<div class="foot">酒店客房管理系统（酒店管理专业 AI 教学版）· 网页版教学程序</div>
</body>
</html>""" % (title, CSS, nav, body_html)


def render_flashes():
    """渲染 flash 消息（成功绿色 / 失败红色）。"""
    html = ""
    for category, message in get_flashed_messages(with_categories=True):
        cls = "ok" if category == "ok" else "err"
        html += '<div class="flash %s">%s</div>' % (cls, escape(message))
    return html


# ------------------------------------------------------------------
# 五、路由（教学重点：Web 请求如何映射到业务逻辑）
# ------------------------------------------------------------------
@app.route("/")
def index():
    stats = room_stats()
    body = render_flashes() + render_template_string("""
<h1>房态看板</h1>
<p class="sub">酒店前台实时房态全局概览 · 数据更新时间：{{ now }}</p>
<div class="cards">
  <div class="card"><div class="k">总客房数</div><div class="v">{{ stats.total }}</div></div>
  <div class="card"><div class="k">可售房数</div><div class="v big">{{ stats.sellable }}</div></div>
  <div class="card"><div class="k">实时入住率</div><div class="v">{{ stats.occupancy_rate }}%</div></div>
  <div class="card"><div class="k">空房</div><div class="v">{{ stats.counts["空闲"] }}</div></div>
  <div class="card"><div class="k">入住</div><div class="v">{{ stats.counts["入住"] }}</div></div>
  <div class="card"><div class="k">待清洁</div><div class="v">{{ stats.counts["待清洁"] }}</div></div>
  <div class="card"><div class="k">维修</div><div class="v">{{ stats.counts["维修"] }}</div></div>
</div>
<div class="panel">
  <h2>快捷操作</h2>
  <a class="btn" href="/recommend">空房推荐</a>
  <a class="btn" href="/checkin">入住登记</a>
  <a class="btn" href="/checkout">退房办理</a>
  <a class="btn ghost" href="/analytics">运营分析</a>
</div>
""", stats=stats, now=__import__("datetime").datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    return base("房态看板", body)


@app.route("/rooms")
def rooms():
    body = render_flashes() + render_template_string("""
<h1>客房信息总览</h1>
<p class="sub">全量客房档案与实时状态 · 房态流转规则：空闲 → 入住 → 待清洁 → 空闲</p>
<div class="panel">
<table>
<thead><tr><th>房号</th><th>房型</th><th>价格(元)</th><th>楼层</th><th>状态</th></tr></thead>
<tbody>
{% for r in rooms %}
<tr><td>{{ r["房号"] }}</td><td>{{ r["房型"] }}</td><td>{{ r["价格(元)"] }}</td>
<td>{{ r["楼层"] }}楼</td><td><span class="badge {{ r['状态'] }}">{{ r["状态"] }}</span></td></tr>
{% endfor %}
</tbody></table>
</div>
""", rooms=ROOMS)
    return base("客房总览", body)


@app.route("/recommend", methods=["GET", "POST"])
def recommend():
    results = None
    cond = {}
    if request.method == "POST":
        try:
            budget = request.form.get("budget", "").strip()
            budget = int(budget) if budget else None
        except ValueError:
            budget = None
        room_type = request.form.get("room_type", "").strip() or None
        floor_raw = request.form.get("floor", "").strip()
        floor = int(floor_raw) if floor_raw else None
        cond = {"budget": budget, "room_type": room_type, "floor": floor}
        results = recommend_rooms(budget, room_type, floor)

    room_types = sorted({r["房型"] for r in ROOMS})
    floors = sorted({r["楼层"] for r in ROOMS})
    body = render_flashes() + render_template_string("""
<h1>空房智能推荐</h1>
<p class="sub">按客户预算、房型、楼层多条件筛选空闲客房（收益管理与需求匹配）</p>
<div class="panel">
<form method="post" class="grid">
  <div><label>最高预算（元，可留空）</label><input type="number" name="budget"
    value="{{ cond.budget if cond.budget is not none else '' }}"></div>
  <div><label>需求房型（可选）</label>
    <select name="room_type"><option value="">不限</option>
    {% for t in room_types %}<option value="{{ t }}" {{ 'selected' if cond.room_type == t }}>{{ t }}</option>{% endfor %}
    </select></div>
  <div><label>目标楼层（可选）</label>
    <select name="floor"><option value="">不限</option>
    {% for f in floors %}<option value="{{ f }}" {{ 'selected' if cond.floor == f }}>{{ f }}楼</option>{% endfor %}
    </select></div>
  <div><label>&nbsp;</label><button class="btn" type="submit">一键推荐空房</button></div>
</form>
</div>
{% if results is not none %}
<div class="panel">
<h2>推荐结果（共 {{ results|length }} 间）</h2>
{% if results %}
<table><thead><tr><th>房号</th><th>房型</th><th>价格(元)</th><th>楼层</th><th>状态</th></tr></thead>
<tbody>{% for r in results %}
<tr><td>{{ r["房号"] }}</td><td>{{ r["房型"] }}</td><td>{{ r["价格(元)"] }}</td>
<td>{{ r["楼层"] }}楼</td><td><span class="badge 空闲">空闲</span></td></tr>
{% endfor %}</tbody></table>
{% else %}<p class="muted">没有符合条件的空闲客房，请放宽筛选条件。</p>{% endif %}
</div>
{% endif %}
""", cond=cond, room_types=room_types, floors=floors, results=results)
    return base("空房推荐", body)


@app.route("/checkin", methods=["GET", "POST"])
def checkin():
    if request.method == "POST":
        room_no = request.form.get("room_no", "").strip()
        name = request.form.get("name", "").strip()
        phone = request.form.get("phone", "").strip()
        days_raw = request.form.get("days", "").strip()
        member = request.form.get("member", "普通")

        if not room_no or not name or not phone or not days_raw:
            flash("请完整填写房号、姓名、联系电话和入住天数。", "err")
            return redirect(url_for("checkin"))
        try:
            days = int(days_raw)
            if days <= 0:
                raise ValueError
        except ValueError:
            flash("入住天数必须为正整数。", "err")
            return redirect(url_for("checkin"))

        ok, msg = check_in(room_no, name, phone, days, member)
        flash(msg, "ok" if ok else "err")
        return redirect(url_for("checkin"))

    body = render_flashes() + render_template_string("""
<h1>入住登记办理</h1>
<p class="sub">录入客户信息，系统自动校验房态并按会员等级计算折扣房费</p>
<div class="panel">
<form method="post" class="grid">
  <div><label>房号</label><input name="room_no" placeholder="如 101"></div>
  <div><label>姓名</label><input name="name" placeholder="客户姓名"></div>
  <div><label>联系电话</label><input name="phone" placeholder="手机号"></div>
  <div><label>入住天数</label><input type="number" name="days" value="1" min="1"></div>
  <div><label>会员等级</label>
    <select name="member">{% for m, d in discounts.items() %}
      <option value="{{ m }}">{{ m }}（{{ '{:.0f}'.format(d*100) }}% 折扣）</option>{% endfor %}
    </select></div>
  <div><label>&nbsp;</label><button class="btn" type="submit">办理入住</button></div>
</form>
</div>
""", discounts=MEMBER_DISCOUNT)
    return base("入住登记", body)


@app.route("/checkout", methods=["GET", "POST"])
def checkout():
    if request.method == "POST":
        room_no = request.form.get("room_no", "").strip()
        if not room_no:
            flash("请输入退房房号。", "err")
            return redirect(url_for("checkout"))
        ok, msg, _ = check_out(room_no)
        flash(msg, "ok" if ok else "err")
        return redirect(url_for("checkout"))

    body = render_flashes() + render_template_string("""
<h1>退房办理</h1>
<p class="sub">输入退房房号，系统自动校验、结算，并将房态转为「待清洁」</p>
<div class="panel">
<form method="post" class="grid">
  <div><label>退房房号</label><input name="room_no" placeholder="如 202"></div>
  <div><label>&nbsp;</label><button class="btn" type="submit">办理退房</button></div>
</form>
</div>
<div class="panel">
<h2>当前在住客户</h2>
{% if checkins %}
<table><thead><tr><th>房号</th><th>姓名</th><th>联系电话</th><th>入住天数</th>
<th>会员等级</th><th>预估房费(元)</th></tr></thead>
<tbody>{% for c in checkins %}
<tr><td>{{ c["房号"] }}</td><td>{{ c["姓名"] }}</td><td>{{ c["联系电话"] }}</td>
<td>{{ c["入住天数"] }}</td><td>{{ c["会员等级"] }}</td><td>{{ c["预估房费(元)"] }}</td></tr>
{% endfor %}</tbody></table>
{% else %}<p class="muted">暂无在住客户。</p>{% endif %}
</div>
""", checkins=CHECKINS)
    return base("退房办理", body)


@app.route("/customers")
def customers():
    body = render_flashes() + render_template_string("""
<h1>入住客户管理</h1>
<p class="sub">集中管理所有在住客户信息，方便前台快速查询与核对</p>
<div class="panel">
{% if checkins %}
<table><thead><tr><th>房号</th><th>姓名</th><th>联系电话</th><th>入住天数</th>
<th>会员等级</th><th>预估房费(元)</th></tr></thead>
<tbody>{% for c in checkins %}
<tr><td>{{ c["房号"] }}</td><td>{{ c["姓名"] }}</td><td>{{ c["联系电话"] }}</td>
<td>{{ c["入住天数"] }}</td><td>{{ c["会员等级"] }}</td><td>{{ c["预估房费(元)"] }}</td></tr>
{% endfor %}</tbody></table>
{% else %}<p class="muted">暂无在住客户。</p>{% endif %}
</div>
""", checkins=CHECKINS)
    return base("客户管理", body)


@app.route("/analytics")
def analytics():
    occ = occupancy_analysis()
    dist = member_distribution()
    body = render_flashes() + render_template_string("""
<h1>运营数据分析</h1>
<p class="sub">酒店运营核心 KPI 与客户结构分析</p>
<div class="cards">
  <div class="card"><div class="k">总客房数</div><div class="v">{{ occ.total }}</div></div>
  <div class="card"><div class="k">入住房间数</div><div class="v">{{ occ.occupied }}</div></div>
  <div class="card"><div class="k">入住率</div><div class="v big">{{ occ.occupancy_rate }}%</div></div>
  <div class="card"><div class="k">可售房均价(元)</div><div class="v">{{ occ.avg_price }}</div></div>
</div>
<div class="panel">
<h2>会员等级分布</h2>
{% if dist %}
<table><thead><tr><th>会员等级</th><th>在住人数</th><th>占比</th></tr></thead>
<tbody>{% for m, n in dist.items() %}
<tr><td>{{ m }}</td><td>{{ n }}</td><td>{{ '%.1f'|format(n / dist.values()|sum * 100) }}%</td></tr>
{% endfor %}</tbody></table>
{% else %}<p class="muted">暂无在住客户，无法统计会员分布。</p>{% endif %}
</div>
<form method="post" action="/reset" class="mt">
  <button class="btn ghost" type="submit" onclick="return confirm('确定要重置所有数据吗？');">重置所有数据</button>
</form>
""", occ=occ, dist=dist)
    return base("数据分析", body)


@app.route("/reset", methods=["POST"])
def reset():
    reset_data()
    flash("已重置所有数据，恢复初始房态。", "ok")
    return redirect(url_for("index"))


# ------------------------------------------------------------------
# 六、启动入口

# ------------------------------------------------------------------
# 六、第三次课：用同一个六间客房案例学习顺序、分支与循环
# 与第二章的 20 间 PMS 业务数据相互独立，实验不会办理真实入住或改房态。
# ------------------------------------------------------------------
LESSON3_ROOMS = [
    {"room": "101", "type": "大床房", "price": 398, "status": "空闲", "floor": 10},
    {"room": "102", "type": "双床房", "price": 428, "status": "入住", "floor": 10},
    {"room": "103", "type": "套房", "price": 888, "status": "待清洁", "floor": 10},
    {"room": "104", "type": "大床房", "price": 418, "status": "空闲", "floor": 11},
    {"room": "105", "type": "双床房", "price": 438, "status": "维修", "floor": 11},
    {"room": "106", "type": "大床房", "price": 468, "status": "空闲", "floor": 12},
]
LESSON3_MODES = {"sequence": "顺序：先后做什么", "branch": "分支：这一间选不选", "loop": "循环：逐间检查"}
LESSON3_ACTIONS = {"view", "apply", "next", "prev", "finish", "reset", "preset_a", "preset_b", "preset_c", "preset_d"}
LESSON3_PRESETS = {
    "preset_a": {"budget": "450", "room_type": "大床房", "floor": "11"},
    "preset_b": {"budget": "400", "room_type": "大床房", "floor": "11"},
    "preset_c": {"budget": "418", "room_type": "大床房", "floor": "11"},
    "preset_d": {"budget": "450", "room_type": "大床房", "floor": ""},
}


def lesson3_checks(room, budget, room_type, floor):
    """四个条件都必须为 True；楼层为 None 表示不限制楼层。"""
    return [
        {"label": "房态", "condition": 'room["status"] == "空闲"',
         "value": room["status"] == "空闲", "actual": room["status"]},
        {"label": "预算", "condition": 'room["price"] <= budget',
         "value": room["price"] <= budget, "actual": "%s <= %s" % (room["price"], budget)},
        {"label": "房型", "condition": 'room["type"] == room_type',
         "value": room["type"] == room_type, "actual": room["type"]},
        {"label": "楼层", "condition": 'floor is None or room["floor"] == floor',
         "value": floor is None or room["floor"] == floor,
         "actual": "%s 楼；%s" % (room["floor"], "楼层不限" if floor is None else "要求 %s 楼" % floor)},
    ]


def lesson3_code(budget=450, room_type="大床房", floor=11):
    """返回可直接复制运行的第三章课堂代码；每一行缩进均为 4 个空格。"""
    code = ["rooms = ["]
    for room in LESSON3_ROOMS:
        code.append("    " + repr(room) + ",")
    code.extend([
        "]",
        "budget = %s" % budget,
        "room_type = %r" % room_type,
        "floor = %r  # None 表示楼层不限" % floor,
        "result = []  # 只在 for 循环前初始化一次",
        "for room in rooms:",
        '    if (room["status"] == "空闲"',
        '        and room["price"] <= budget',
        '        and room["type"] == room_type',
        '        and (floor is None or room["floor"] == floor)):',
        '        result.append(room)',
        "",
        "if result:",
        '    print("符合条件的客房：", result)',
        "else:",
        '    print("本次条件下没有匹配的空闲客房，请调整条件。")',
    ])
    return code


def lesson3_trace(budget, room_type, floor, mode="loop", selected_room="104"):
    """纯函数：生成可 JSON 序列化的执行轨迹，不改 PMS 或课堂客房数据。

    step 字段：title、explanation、current_room（房号或 None）、checks、
    result（已追加的完整客房字典列表）、room_numbers（房号摘要）、
    code_line（1 起始）、phase、matched。
    sequence 看准备工作的先后；branch 只检验选中的一间；loop 遍历六间。
    """
    if isinstance(budget, bool) or not isinstance(budget, int) or not 0 <= budget <= 100000:
        raise ValueError("预算必须是 0–100000 的整数。")
    if room_type not in {"大床房", "双床房", "套房"}:
        raise ValueError("房型必须选择大床房、双床房或套房。")
    if floor is not None and (isinstance(floor, bool) or not isinstance(floor, int) or floor not in (10, 11, 12)):
        raise ValueError("楼层只能是 10、11、12，或留空表示不限。")
    if mode not in LESSON3_MODES:
        raise ValueError("请选择顺序、分支或循环模式。")
    if selected_room not in {room["room"] for room in LESSON3_ROOMS}:
        raise ValueError("请选择 101–106 中的房号。")
    steps = []
    result = []

    def add(title, explanation, code_line, phase, room=None, checks=None):
        steps.append({"title": title, "explanation": explanation,
                      "current_room": room["room"] if room else None,
                      "checks": checks or [], "result": [dict(item) for item in result],
                      "room_numbers": [item["room"] for item in result],
                      "code_line": code_line, "phase": phase,
                      "matched": all(check["value"] for check in checks) if checks else None})

    def finish(scope):
        if result:
            add("输出结果", "%s，符合条件的房号：%s。" % (scope, "、".join(item["room"] for item in result)), 21, "output")
        else:
            add("输出结果", "%s，本次条件下没有匹配的空闲客房，请调整条件。" % scope, 23, "output")

    if mode == "sequence":
        add("1. 准备数据", "把六间客房放入列表 rooms，每间房用一个字典记录。", 1, "prepare")
        add("2. 设置客人需求", "依次设置预算、房型和楼层；空闲房是必须满足的条件。", 9, "needs")
        add("3. 准备结果容器", "在循环之前创建空列表 result = []，用来收集合适的房号。", 12, "initialize")
        add("4. 开始逐间检查", "准备完成后进入 for；每间房再用 if 判断，符合条件才追加。", 13, "take")
        for room in LESSON3_ROOMS:
            if all(check["value"] for check in lesson3_checks(room, budget, room_type, floor)):
                result.append(room)
        finish("六间客房全部检查后")
        return steps

    add("初始化结果列表", "result = [] 在 for 前执行一次；后续追加的房号会保留下来。", 12, "initialize")
    selected = [room for room in LESSON3_ROOMS if room["room"] == selected_room]
    targets = selected if mode == "branch" else LESSON3_ROOMS
    for index, room in enumerate(targets, start=1):
        checks = lesson3_checks(room, budget, room_type, floor)
        prefix = "第 %s / 6 间：" % index if mode == "loop" else "当前选中："
        add(prefix + "取出 " + room["room"],
            "room 指向 %s 号房；先看数据，再做判断。" % room["room"], 13, "take", room)
        matched = all(check["value"] for check in checks)
        add("if 判断：%s 号房" % room["room"],
            "四个条件通过 and 连接，全部为 True 才入选。当前整体判断为 %s。" % matched,
            14, "check", room, checks)
        if matched:
            result.append(room)
            add("True → 追加 %s" % room["room"],
                "执行 result.append(room)，把 %s 的完整客房字典加入列表；已有客房继续保留。" % room["room"],
                18, "append", room, checks)
        else:
            failed = "、".join(check["label"] for check in checks if not check["value"])
            add("False → 跳过 %s" % room["room"],
                "%s 条件不符合；不执行 append，结果列表保持不变。" % failed,
                14, "skip", room, checks)
    finish("只检查选中的 %s 号房后" % selected_room if mode == "branch" else "六间客房全部检查后")
    return steps


def parse_lesson3_options(values):
    """校验浏览器表单；返回有效参数、原表单、错误列表。"""
    defaults = {"budget": "450", "room_type": "大床房", "floor": "11", "mode": "loop",
                "selected_room": "104", "cursor": "0", "action": "view"}
    form = {key: str(values.get(key, default)).strip() for key, default in defaults.items()}
    errors = []
    options = {"budget": 450, "room_type": "大床房", "floor": 11,
               "mode": "loop", "selected_room": "104", "cursor": 0, "action": "view"}
    # 先限制字符串长度，再转换整数；拒绝小数、负号、指数和超长输入。
    raw_budget = form["budget"]
    if len(raw_budget) > 6 or not raw_budget.isascii() or not raw_budget.isdigit() or int(raw_budget) > 100000:
        errors.append("预算必须是 0–100000 的整数，例如 450；不能留空或输入小数。")
    else:
        options["budget"] = int(raw_budget)
    if form["room_type"] not in {"大床房", "双床房", "套房"}:
        errors.append("房型必须选择大床房、双床房或套房。")
    else:
        options["room_type"] = form["room_type"]
    if form["floor"] not in {"", "10", "11", "12"}:
        errors.append("楼层只能是 10、11、12，或留空表示不限。")
    else:
        options["floor"] = int(form["floor"]) if form["floor"] else None
    if form["mode"] not in LESSON3_MODES:
        errors.append("实验模式无效，请选择顺序、分支或循环。")
    else:
        options["mode"] = form["mode"]
    if form["selected_room"] not in {room["room"] for room in LESSON3_ROOMS}:
        errors.append("当前房号必须在 101–106 之间。")
    else:
        options["selected_room"] = form["selected_room"]
    if len(form["cursor"]) > 3 or not form["cursor"].isascii() or not form["cursor"].isdigit() or int(form["cursor"]) > 200:
        errors.append("执行步数无效，请使用 0–200 之间的整数。")
    else:
        options["cursor"] = int(form["cursor"])
    if form["action"] not in LESSON3_ACTIONS:
        errors.append("操作无效，请使用页面上的实验按钮。")
    else:
        options["action"] = form["action"]
    return options, form, errors


def lesson3_recommendation(budget, room_type, floor):
    """返回完整筛选结果与逐间解释；课堂 GUI 展示房号摘要，实际保留完整字典。"""
    result = lesson3_trace(budget, room_type, floor)[-1]["result"]
    decisions = []
    for room in LESSON3_ROOMS:
        reasons = []
        if room["status"] != "空闲":
            reasons.append("房态是%s，必须是空闲" % room["status"])
        if room["price"] > budget:
            reasons.append("价格 %s 元，超过预算 %s 元" % (room["price"], budget))
        if room["type"] != room_type:
            reasons.append("房型是%s，需求是%s" % (room["type"], room_type))
        if floor is not None and room["floor"] != floor:
            reasons.append("floor 为 %s 楼，需求是 %s 楼" % (room["floor"], floor))
        decisions.append({"room": dict(room), "matched": not reasons, "reasons": reasons,
                          "checks": lesson3_checks(room, budget, room_type, floor)})
    return {"result": result, "room_numbers": [room["room"] for room in result],
            "decisions": decisions}


LESSON3_CSS = """
.nav { height:auto; min-height:60px; padding-top:8px; padding-bottom:8px; }
.lesson-intro { background:linear-gradient(110deg,#eef5ff,#f0faf5); }
.lesson-intro h1 { font-size:25px; }
.lesson-intro p { margin:8px 0 0; }
.lesson-grid { display:grid; grid-template-columns:minmax(0,1.02fr) minmax(0,1fr); gap:18px; }
.lesson-grid>* { min-width:0; }
.lesson-form { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }
.lesson-form .wide { grid-column:1/-1; }
.lesson-actions { display:flex; gap:8px; flex-wrap:wrap; }
.lesson-actions .btn { padding:9px 13px; }
.lesson-meta { display:flex; flex-wrap:wrap; gap:8px; margin:14px 0 0; }
.lesson-chip { font-size:12px; background:#fff; border:1px solid #d7e6ed; border-radius:99px; padding:3px 10px; }
.lesson-hint { font-size:13px; color:#556375; }
.lesson-step { border-left:5px solid #25824d; background:#f1faf4; padding:14px; border-radius:10px; }
.lesson-step.sequence { border-color:#2563eb; background:#eff6ff; }
.lesson-step.branch { border-color:#d97706; background:#fffbeb; }
.lesson-step h3 { margin:3px 0 8px; font-size:19px; }
.lesson-step p { margin:4px 0; }
.lesson-result { font:700 20px ui-monospace,Consolas,monospace; color:#17643b; padding:10px 0; overflow-wrap:anywhere; }
.lesson-checks { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; margin-top:12px; }
.lesson-check { padding:9px; border-radius:8px; border:1px solid #a4d4b9; background:#effaf3; font-size:13px; }
.lesson-check.fail { border-color:#ecc3b3; background:#fff2eb; }
.lesson-check code { font-size:11px; overflow-wrap:anywhere; }
.lesson-table-wrap { overflow-x:auto; }
.lesson-table td,.lesson-table th { padding:8px; white-space:nowrap; }
.lesson-table tr.current td { background:#fff0c2; border-top:2px solid #b77913; border-bottom:2px solid #b77913; font-weight:700; }
.lesson-table tr.selected td:first-child { color:#166534; font-weight:700; }
.lesson-code { background:#152237; color:#dce6f4; padding:12px 0; border-radius:10px; overflow:auto; font:12px/1.85 ui-monospace,Consolas,monospace; tab-size:4; }
.lesson-code-line { display:block; white-space:pre; padding:0 10px; min-width:max-content; border-left:4px solid transparent; }
.lesson-code-line.active { background:#364a61; border-left-color:#fde047; color:white; }
.lesson-line-no { display:inline-block; width:24px; color:#96a5b8; user-select:none; }
.lesson-code-line.setup { color:#9ec7ff; }
.lesson-code-line.branch { color:#ffd08b; }
.lesson-code-line.loop { color:#99e0b0; }
.lesson-flow { width:100%; height:auto; max-height:300px; display:block; margin:12px auto; }
.lesson-flow .node { fill:#f7fafc; stroke:#91a2b4; stroke-width:1.6; }
.lesson-flow .hot { fill:#fff0bf; stroke:#aa7016; stroke-width:3; }
.lesson-flow text { fill:#243347; font:14px sans-serif; text-anchor:middle; }
.lesson-flow .small { font-size:11px; }
.lesson-flow path,.lesson-flow line { stroke:#60758b; stroke-width:1.7; fill:none; }
.lesson-summary { font-size:13px; padding:12px; border-radius:8px; background:#f4f7fa; margin-top:12px; }
.lesson-presets { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:9px; margin:14px 0; }
.lesson-presets button { width:100%; text-align:left; background:#f3f7fc; border:1px solid #cddbe8; padding:12px; color:#243b56; border-radius:9px; cursor:pointer; font:inherit; }
.lesson-presets strong,.lesson-presets span { display:block; }
.lesson-presets span { font-size:12px; margin-top:5px; }
.lesson-demand { background:#eef5ff; border:1px solid #c6dbf3; padding:13px; border-radius:9px; }
.lesson-result-cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:12px; margin-top:15px; }
.lesson-room-card { background:#eef9f2; border:2px solid #8cc7a5; border-radius:10px; padding:16px; }
.lesson-room-card strong { color:#17643b; font-size:26px; }
.lesson-room-card p { margin:5px 0; }
.lesson-empty { margin-top:14px; border-left:5px solid #b67718; padding:14px; background:#fffaed; border-radius:8px; }
.lesson-empty h3 { margin:0 0 7px; }
.lesson-decision.pass { color:#17643b; font-weight:700; }
#recommendation-result .lesson-table th:last-child,#recommendation-result .lesson-table td:last-child { white-space:normal; min-width:220px; width:52%; overflow-wrap:anywhere; }
details.panel>summary { cursor:pointer; font-size:17px; font-weight:700; color:#234766; }
details.panel>summary+.lesson-hint { margin-top:12px; }
.lesson-answers { margin:14px 0 0; padding-left:21px; }
.lesson-answers li { margin:8px 0; }
.lesson-reflection { width:100%; min-height:150px; border:1px solid #bdcbd9; padding:12px; border-radius:8px; font:inherit; }
@media(max-width:760px){ .lesson-grid{grid-template-columns:1fr} .lesson-form{grid-template-columns:repeat(2,minmax(0,1fr))} .panel{padding:15px} .lesson-intro h1{font-size:22px} }
@media(max-width:760px){ .lesson-presets{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media(max-width:440px){ .lesson-form{grid-template-columns:1fr} .lesson-checks{grid-template-columns:1fr} .nav a{font-size:12px;padding:5px 7px} }
"""
CSS += LESSON3_CSS

LESSON3_TEMPLATE = """
<section class="panel lesson-intro">
<h1>第三次课 · 帮客人选到合适的客房</h1>
<p>修改客人的<strong>预算、房型、楼层</strong>，查看推荐，再用自己的话解释结果。</p>
<div class="lesson-meta"><span class="lesson-chip">90 分钟：讲解 35 · 实操 50 · 回顾 5</span><span class="lesson-chip">直接操作 GUI</span><span class="lesson-chip">截图 + 解释 → 提交课堂派</span></div>
<p class="lesson-hint">固定使用第三章 6 间教学客房，与第二章系统的 20 间业务客房互不影响。本次实操通过选择条件完成。</p>
</section>
<form method="post" action="/structures">
<section class="panel">
<h2>1. 选择课堂任务，或自己设置需求</h2>
<div class="lesson-presets">
<button name="action" value="preset_a" type="submit" formnovalidate><strong>任务 A · 基准需求</strong><span>450 元 / 大床房 / 11 楼</span></button>
<button name="action" value="preset_b" type="submit" formnovalidate><strong>任务 B · 预算降低</strong><span>400 元 / 大床房 / 11 楼</span></button>
<button name="action" value="preset_c" type="submit" formnovalidate><strong>任务 C · 刚好够住</strong><span>418 元 / 大床房 / 11 楼</span></button>
<button name="action" value="preset_d" type="submit" formnovalidate><strong>任务 D · 放宽楼层</strong><span>450 元 / 大床房 / 不限楼层</span></button>
</div>
<div class="lesson-form">
<div><label for="budget">最高预算（元，整数）</label><input id="budget" name="budget" inputmode="numeric" value="{{ form.budget }}" maxlength="6" required></div>
<div><label for="room_type">需求房型</label><select id="room_type" name="room_type">{% for kind in ['大床房','双床房','套房'] %}<option value="{{ kind }}" {{ 'selected' if options.room_type == kind }}>{{ kind }}</option>{% endfor %}</select></div>
<div><label for="floor">目标楼层</label><select id="floor" name="floor"><option value="" {{ 'selected' if options.floor is none }}>不限楼层</option>{% for number in [10,11,12] %}<option value="{{ number }}" {{ 'selected' if options.floor == number }}>{{ number }} 楼</option>{% endfor %}</select></div>
<div class="wide"><button class="btn" name="action" value="view" type="submit">查看推荐结果</button></div>
</div>
<p class="lesson-hint">必须满足“房态为空闲”。楼层依据 floor 字段判断，不根据房号猜测。</p>
{% if errors %}<div class="flash err mt" role="alert"><strong>请先修正输入：</strong><ul>{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul></div>{% endif %}
</section>
<input type="hidden" name="cursor" value="{{ cursor }}">
{% if recommendation %}
<section class="panel" id="recommendation-result">
<h2>2. 查看推荐结果</h2>
<div class="lesson-demand"><strong>本次客户需求：</strong>最高 {{ options.budget }} 元 / {{ options.room_type }} / {{ '不限楼层' if options.floor is none else (options.floor|string + ' 楼') }} / 必须空闲。</div>
{% if recommendation.result %}
<div class="lesson-result-cards">{% for room in recommendation.result %}<article class="lesson-room-card"><strong>{{ room.room }} 号房</strong><p>{{ room.type }} · {{ room.price }} 元 / 晚</p><p>楼层：{{ room.floor }} 楼 · {{ room.status }}</p><p>四项条件全部满足 ✓</p></article>{% endfor %}</div>
<p><strong>本次推荐 {{ recommendation.result|length }} 间：</strong>{{ recommendation.room_numbers|join('、') }}。</p>
{% else %}<div class="lesson-empty"><h3>本次需求下没有匹配的空闲客房</h3><p>请尝试提高预算、换房型或放宽楼层。<strong>无结果不等于酒店满房</strong>，只是没有客房同时满足这一次的全部条件。</p></div>{% endif %}
<p class="lesson-hint">每间推荐客房都必须同时满足全部需求；房型、价格、房态与楼层可在下表核对。</p>
<div class="lesson-table-wrap"><table class="lesson-table"><thead><tr><th>房号</th><th>房型 / 价格</th><th>房态 / floor</th><th>本次为什么入选或不入选</th></tr></thead><tbody>
{% for item in recommendation.decisions %}<tr><td>{{ item.room.room }}</td><td>{{ item.room.type }} / {{ item.room.price }} 元</td><td>{{ item.room.status }} / {{ item.room.floor }} 楼</td><td>{% if item.matched %}<span class="lesson-decision pass">入选：四项条件全部满足</span>{% else %}<strong>不入选：</strong>{{ item.reasons|join('；') }}{% endif %}</td></tr>{% endfor %}
</tbody></table></div>
<p class="lesson-hint">截图建议：把“本次客户需求”、推荐卡或无匹配提示，以及理由表一起截入画面。</p>
</section>
{% endif %}
<details class="panel" {{ 'open' if options.action in ['apply','next','prev','finish','reset'] }}>
<summary>看懂程序怎样工作 · 顺序、分支、循环（可展开观察）</summary>
<p class="lesson-hint">先完成上面的选房任务。想知道程序怎样作出判断，再选择一种结构，逐步观察。</p>
<div class="lesson-form">
<div><label for="mode">观察哪种结构</label><select id="mode" name="mode">{% for value,label in modes.items() %}<option value="{{ value }}" {{ 'selected' if options.mode == value }}>{{ label }}</option>{% endfor %}</select></div>
<div><label for="selected_room">分支：只观察这间房</label><select id="selected_room" name="selected_room">{% for room in rooms %}<option value="{{ room.room }}" {{ 'selected' if options.selected_room == room.room }}>{{ room.room }} 号房</option>{% endfor %}</select></div>
<div><label>观察准备</label><button class="btn ghost" name="action" value="apply" type="submit">应用条件并重置</button></div>
</div>
{% if step %}
<div class="lesson-step {{ options.mode }} mt"><small>观察进度 {{ cursor + 1 }} / {{ trace|length }} · {{ modes[options.mode] }}</small><h3>{{ step.title }}</h3><p>{{ step.explanation }}</p><div class="lesson-result">已收集房号摘要 = {{ step.room_numbers }}</div><p class="lesson-hint">实际 result 保存完整客房字典；这里用房号帮助观察变化。</p></div>
<div class="lesson-actions mt"><button class="btn ghost" name="action" value="prev" type="submit" {{ 'disabled' if cursor == 0 }}>上一步</button><button class="btn" name="action" value="next" type="submit" {{ 'disabled' if cursor == trace|length - 1 }}>下一步</button><button class="btn ghost" name="action" value="finish" type="submit">运行到最后</button><button class="btn ghost" name="action" value="reset" type="submit">重新开始</button></div>
{% if step.checks %}<div class="lesson-checks">{% for check in step.checks %}<div class="lesson-check {{ 'fail' if not check.value }}"><strong>{{ check.label }}：{{ 'True · 通过' if check.value else 'False · 不通过' }}</strong><br>{{ check.actual }}</div>{% endfor %}</div>{% endif %}
{% if options.mode == 'sequence' %}
<svg class="lesson-flow" viewBox="0 0 620 116" role="img" aria-label="顺序流程图：准备数据、设置需求、初始化结果、开始检查，依次向右执行">
<defs><marker id="arr" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#60758b"/></marker></defs>
{% for label,phase in [('准备数据','prepare'),('设置需求','needs'),('result = []','initialize'),('开始检查','take')] %}<rect class="node {{ 'hot' if step.phase == phase }}" x="{{ 8 + loop.index0 * 155 }}" y="36" width="139" height="48" rx="6"/><text x="{{ 77 + loop.index0 * 155 }}" y="65">{{ label }}</text>{% if not loop.last %}<line x1="{{ 147 + loop.index0 * 155 }}" y1="60" x2="{{ 161 + loop.index0 * 155 }}" y2="60" marker-end="url(#arr)"/>{% endif %}{% endfor %}
</svg>
{% else %}
<svg class="lesson-flow" viewBox="0 0 600 310" role="img" aria-label="{% if options.mode == 'loop' %}循环流程图：有下一间则取出客房、判断，符合则追加、不符则跳过，然后回到有下一间；没有下一间则输出{% else %}分支流程图：取出选中的客房，四个条件全真则追加，否则跳过，最后输出{% endif %}">
<defs><marker id="arr" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#60758b"/></marker></defs>
{% if options.mode == 'loop' %}
<polygon class="node {{ 'hot' if step.phase == 'initialize' }}" points="110,8 197,39 110,70 23,39"/><text x="110" y="44">有下一间？</text>
<path d="M197,39 H278 V77" marker-end="url(#arr)"/><text x="228" y="31" class="small">有</text>
<path d="M23,39 H12 V280 H221" marker-end="url(#arr)"/><text x="34" y="180" class="small">没有</text>
{% else %}<ellipse class="node" cx="300" cy="24" rx="70" ry="20"/><text x="300" y="29">选中的一间房</text><line x1="300" y1="44" x2="300" y2="77" marker-end="url(#arr)"/>{% endif %}
<rect class="node {{ 'hot' if step.phase == 'take' }}" x="221" y="79" width="158" height="38" rx="5"/><text x="300" y="104">取出客房 room</text>
<line x1="300" y1="117" x2="300" y2="136" marker-end="url(#arr)"/>
<polygon class="node {{ 'hot' if step.phase == 'check' }}" points="300,138 403,175 300,212 197,175"/><text x="300" y="180">四个条件全真？</text>
<path d="M197,175 H110 V227" marker-end="url(#arr)"/><text x="147" y="164" class="small">True</text>
<path d="M403,175 H490 V227" marker-end="url(#arr)"/><text x="450" y="164" class="small">False</text>
<rect class="node {{ 'hot' if step.phase == 'append' }}" x="41" y="229" width="138" height="36" rx="5"/><text x="110" y="252">追加客房</text>
<rect class="node {{ 'hot' if step.phase == 'skip' }}" x="421" y="229" width="138" height="36" rx="5"/><text x="490" y="252">跳过，不追加</text>
{% if options.mode == 'loop' %}<path d="M41,247 H30 V82 H110 V70" marker-end="url(#arr)"/><path d="M559,247 H580 V4 H110 V8" marker-end="url(#arr)"/><text x="555" y="73" class="small">下一轮</text>
{% else %}<path d="M110,265 V280 H221" marker-end="url(#arr)"/><path d="M490,265 V280 H379" marker-end="url(#arr)"/>{% endif %}
<rect class="node {{ 'hot' if step.phase == 'output' }}" x="223" y="262" width="154" height="36" rx="17"/><text x="300" y="285">输出 result</text>
</svg>
{% endif %}

<div class="lesson-table-wrap"><table class="lesson-table"><thead><tr><th>room</th><th>type</th><th>price</th><th>status</th><th>floor</th></tr></thead><tbody>{% for room in rooms %}<tr class="{{ 'current' if step.current_room == room.room }} {{ 'selected' if room.room in step.room_numbers }}"><td>{{ room.room }}{{ ' ← 当前' if step.current_room == room.room }}</td><td>{{ room.type }}</td><td>{{ room.price }}</td><td>{{ room.status }}</td><td>{{ room.floor }}</td></tr>{% endfor %}</tbody></table></div>
<p class="lesson-hint">蓝色顺序：先收集需求，再准备结果；橙色分支：符合就选，不符合就跳过；绿色循环：每间都检查一遍。</p>
{% endif %}
</details>
</form>
<details class="panel"><summary>教师参考代码（可展开，学生无需输入代码）</summary><p class="lesson-hint">result 在 for 前初始化；result.append(room) 追加完整客房字典。高亮行对应刚才展开观察的位置。</p><div class="lesson-code" aria-label="教师参考代码">{% for line in code %}<span class="lesson-code-line {{ 'active' if step and loop.index == step.code_line }} {{ 'setup' if loop.index <= 12 else ('loop' if loop.index in [13,18] else 'branch') }}"><span class="lesson-line-no">{{ loop.index }}</span>{{ line }}</span>{% endfor %}</div></details>
<section class="panel"><h2>3. 课堂练习与课堂派提交</h2><p><strong>50 分钟实操：</strong>15 分钟完成 A，认识界面并解释基准需求；15 分钟比较 B、C 的预算变化；10 分钟完成 D，观察取消楼层限制后的结果；10 分钟自拟需求、交流解释并整理截图。最后用 5 分钟回顾。</p><ol class="lesson-answers"><li><strong>A：</strong>450 元、大床房、11 楼。截一张结果图，解释为什么只有 104 入选。</li><li><strong>B：</strong>把预算改成 400 元，其余不变。截一张结果图，解释为什么无匹配，以及为什么不能说酒店满房。</li><li><strong>C：</strong>预算改成 418 元，其余同 A。截一张结果图，解释为什么“刚好等于预算”仍能入选。</li><li><strong>D：</strong>450 元、大床房、不限楼层。截一张结果图，解释为什么多出 101，106 为什么仍不入选。</li><li><strong>自主需求：</strong>自己设置预算、房型和楼层，先预测，再查看结果，用一两句话解释。</li></ol><p><strong>交到课堂派：</strong>A / B / C / D 各 1 张截图及对应解释，最后用生活语言分别说明顺序、分支和循环各做什么。自主需求可作为补充，不额外要求截图。按教师在课堂派发布的作业入口提交。</p><p class="lesson-hint">解释句式：客人要求……；我看到……；因为……，所以……。顺序像按步骤接待；分支像判断是否满足需求；循环像按房间清单逐间核对。</p><p><a href="/recommend">返回第二章空房推荐</a> · <a href="/">返回酒店业务看板</a></p></section>
"""


@app.route("/structures", methods=["GET", "POST"])
def structures():
    """无 JavaScript 也可用的服务器端表单步进实验。"""
    raw_values = request.form if request.method == "POST" else request.args
    values = raw_values.to_dict()
    if values.get("action") in LESSON3_PRESETS:
        values.update(LESSON3_PRESETS[values["action"]])
        values["cursor"] = "0"
    options, form, errors = parse_lesson3_options(values)
    trace = []
    cursor = 0
    step = None
    recommendation = None
    if not errors:
        recommendation = lesson3_recommendation(options["budget"], options["room_type"], options["floor"])
        trace = lesson3_trace(options["budget"], options["room_type"], options["floor"],
                              options["mode"], options["selected_room"])
        cursor = min(options["cursor"], len(trace) - 1)
        action = options["action"]
        if action in {"view", "apply", "reset"} or action in LESSON3_PRESETS:
            cursor = 0
        elif action == "next":
            cursor = min(cursor + 1, len(trace) - 1)
        elif action == "prev":
            cursor = max(cursor - 1, 0)
        elif action == "finish":
            cursor = len(trace) - 1
        step = trace[cursor]
    body = render_template_string(LESSON3_TEMPLATE, options=options, form=form, errors=errors,
                                  trace=trace, cursor=cursor, step=step, rooms=LESSON3_ROOMS,
                                  recommendation=recommendation,
                                  code=lesson3_code(options["budget"], options["room_type"], options["floor"]),
                                  modes=LESSON3_MODES)
    return base("第三次课 · 控制结构实验", body)


# ------------------------------------------------------------------
# 七、启动入口：导入模块时不会启动服务
# ------------------------------------------------------------------
if __name__ == "__main__":
    load_data()
    print("酒店客房管理系统 · 第二章业务与第三章控制结构实验")
    print("浏览器打开：http://127.0.0.1:5000/structures")
    app.run(host="127.0.0.1", port=int(os.environ.get("PORT", 5000)), debug=False)
