<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>youki的笔记</title><description> </description><link>https://youki.bbroot.com/</link><language>zh_CN</language><item><title>初识 Kubernetes：从 Service 与 CoreDNS 理解服务发现</title><link>https://youki.bbroot.com/posts/tools/intro-to-k8s/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/tools/intro-to-k8s/</guid><description>从 Service、CoreDNS 到完整调用链，理解 K8s 服务发现的核心机制与单机部署的本质区别</description><pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近在 K8s 集群里部署多服务时，最让人意外的是&lt;strong&gt;服务之间居然可以不写 IP、不改代码地互相找到对方&lt;/strong&gt;。这篇笔记拆解这套机制是怎么实现的：Service 如何当稳定的&quot;门牌号&quot;、CoreDNS 怎么当集群内的&quot;黄页&quot;、以及这套设计与单机 &lt;code&gt;localhost&lt;/code&gt; 部署的本质区别。&lt;/p&gt;
&lt;h2&gt;1. 先理清 K8s 的几个基本对象&lt;/h2&gt;
&lt;p&gt;讲服务发现之前，先把几个会反复出现的对象过一遍，避免后面混在一起。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;一句话解释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pod&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;K8s 调度的最小单位，里面跑着一个或多个容器。每个 Pod 有自己的 IP，但这个 IP &lt;strong&gt;不稳定&lt;/strong&gt;——重启就换。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;用来声明&quot;我要跑 N 个相同的 Pod&quot;，K8s 会自动维护这个数量（挂了会重启）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一组 Pod 的&lt;strong&gt;稳定访问入口&lt;/strong&gt;。给它一个名字 + ClusterIP，后面所有 Pod 都通过这个名字访问它。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NodePort / Ingress&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;把 Service 暴露到集群外部的两种方式（前者用节点端口，后者用 HTTP 路由）。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;记住一句话：&lt;strong&gt;Pod 是临时的，Service 是稳定的。&lt;/strong&gt; 后面所有机制都围绕这个差异展开。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Service：一个虚拟的、稳定的&quot;门牌号&quot;&lt;/h2&gt;
&lt;p&gt;创建 &lt;code&gt;Service&lt;/code&gt; 时，Kubernetes 会在集群内部为它分配一个&lt;strong&gt;固定的虚拟 IP&lt;/strong&gt;（ClusterIP）。这个 IP 永远不会变——只要 Service 存在，它就在那里。&lt;/p&gt;
&lt;p&gt;假设后端被部署成 3 份 Pod：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 3   # 3 个相同的 Pod
  template:
    spec:
      containers:
        - name: backend
          image: my-backend:1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个 Pod 启动后会被分到自己的随机 IP，比如 &lt;code&gt;10.244.1.5&lt;/code&gt;、&lt;code&gt;10.244.2.3&lt;/code&gt;、&lt;code&gt;10.244.3.8&lt;/code&gt;。如果前端直接写这些 IP，&lt;strong&gt;任何一个 Pod 重启，IP 就变了，前端就找不到了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;解决办法是再创建一个 &lt;code&gt;Service&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector:
    app: backend         # 选中上面那 3 个 Pod
  ports:
    - port: 80
      targetPort: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;K8s 会做两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;给这个 Service 分一个&lt;strong&gt;固定 ClusterIP&lt;/strong&gt;（比如 &lt;code&gt;10.96.100.1&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;在背后维护一个&lt;strong&gt;负载均衡器&lt;/strong&gt;，把发往 &lt;code&gt;10.96.100.1:80&lt;/code&gt; 的请求，均匀转发到那 3 个 Pod 上。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;于是前端只需要记住 &lt;code&gt;backend:80&lt;/code&gt;（或者 &lt;code&gt;10.96.100.1:80&lt;/code&gt;）这一个地址，就能访问到后端的任意副本。&lt;strong&gt;后端 Pod 增减、重启、IP 变化，前端完全不感知。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. CoreDNS：一个内置的&quot;黄页&quot;&lt;/h2&gt;
&lt;p&gt;让前端去记 &lt;code&gt;10.96.100.1&lt;/code&gt; 这种纯数字地址不现实。Kubernetes 内建了一个 DNS 服务（CoreDNS），相当于&lt;strong&gt;集群内部的电话本&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个叫 &lt;code&gt;backend&lt;/code&gt; 的 Service 后，CoreDNS 会自动注册一条记录：&lt;code&gt;backend&lt;/code&gt; → &lt;code&gt;10.96.100.1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;前端 Pod 里的应用发起 &lt;code&gt;http://backend:80&lt;/code&gt; 请求时，容器里的 DNS 解析器会自动把 &lt;code&gt;backend&lt;/code&gt; 翻译成 &lt;code&gt;10.96.100.1&lt;/code&gt;，流量就精准抵达了后端的 Service。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;前端 Pod 里的应用：GET http://backend:80/api
   ↓ DNS 解析
CoreDNS：backend → 10.96.100.1
   ↓ 流量转发
backend Service (10.96.100.1) → 负载均衡到某个后端 Pod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这就是&quot;服务发现&quot;的本质：调用方只需要知道服务名，不需要知道它有几个副本、IP 是什么、跑在哪台机器上。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 一个完整的调用链路&lt;/h2&gt;
&lt;p&gt;假设集群里有 3 个服务：数据库、后端、前端。完整通信过程是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 数据库
apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  selector:
    app: mysql
  ports:
    - port: 3306

---
# 2. 后端（3 副本）
apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector:
    app: backend
  ports:
    - port: 80
      targetPort: 8080

---
# 3. 前端（NodePort 暴露给外部）
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  type: NodePort
  selector:
    app: frontend
  ports:
    - port: 80
      nodePort: 30080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后端代码里，&lt;strong&gt;数据库连接地址直接写成 &lt;code&gt;db:3306&lt;/code&gt;&lt;/strong&gt;，不用关心 MySQL 的 Pod IP：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 后端代码示例
import mysql.connector
conn = mysql.connector.connect(
    host=&quot;db&quot;,       # 直接用 Service 名
    port=3306,
    user=&quot;app&quot;,
    password=&quot;xxx&quot;,
    database=&quot;app_db&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前端代码里，&lt;strong&gt;API 请求地址直接写成 &lt;code&gt;backend:80&lt;/code&gt;&lt;/strong&gt;，也不用关心后端有几个副本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 前端代码示例
fetch(&quot;http://backend:80/api/users&quot;)
  .then(res =&amp;gt; res.json())
  .then(data =&amp;gt; console.log(data));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户从浏览器访问 &lt;code&gt;http://Master节点IP:30080&lt;/code&gt; 时，完整的链路是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;用户浏览器
  ↓ http://MasterIP:30080
前端 Service (NodePort)
  ↓ 负载均衡到某个前端 Pod
前端 Pod 发起 GET http://backend:80/api
  ↓ CoreDNS 解析 backend → 10.96.100.1
后端 Service
  ↓ 负载均衡到某个后端 Pod
后端 Pod 连接 mysql -h db
  ↓ CoreDNS 解析 db → 10.96.x.x
数据库 Service
  ↓ 连上 MySQL Pod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;整个过程中，没有一个地方出现了具体的 Pod IP 或节点 IP。&lt;/strong&gt; 服务之间完全通过 Service 名称通信，K8s 自动处理负载均衡、高可用和故障恢复。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 和单机 &lt;code&gt;localhost&lt;/code&gt; 部署的本质区别&lt;/h2&gt;
&lt;p&gt;单机上跑多服务时，连接地址通常是这样写的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 单机部署：写死 localhost
conn = mysql.connector.connect(
    host=&quot;localhost&quot;,  # 必须是本机
    port=3306,
    ...
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要把后端拆到另一台机器上，要改的东西很多：IP 地址、端口、可能还有防火墙、配置中心……&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;单机 &lt;code&gt;localhost&lt;/code&gt; 方式&lt;/th&gt;
&lt;th&gt;Kubernetes Service 方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;服务地址&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;固定写死 IP/端口&lt;/td&gt;
&lt;td&gt;通过虚拟 IP + DNS 动态发现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;扩展性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无法横向扩展（端口冲突）&lt;/td&gt;
&lt;td&gt;随时增减副本，前端无感知&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;故障恢复&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;进程挂了就断了&lt;/td&gt;
&lt;td&gt;副本自动重新调度，Service 自动转发到健康实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;部署灵活性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;所有服务绑死在同一台机器&lt;/td&gt;
&lt;td&gt;服务可以分布在集群的任何节点上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;代码改动&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;迁移机器要改 IP/配置&lt;/td&gt;
&lt;td&gt;一行不用改，Service 名不变即可&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;用一句话总结这个差异：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;单机部署是&quot;我告诉你地址&quot;，Kubernetes 是&quot;告诉我你叫什么，我帮你找到地址&quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;后者让服务之间的通信变得松耦合、高可用，也更符合微服务架构的理念。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 写给自己的小结&lt;/h2&gt;
&lt;p&gt;这次在集群里做多服务部署实验，&lt;strong&gt;最关键的认知升级不是&quot;我会写 yaml 了&quot;，而是理解了&quot;为什么 Pod IP 不重要&quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;只要 Service 还在、名字没变，Pod 怎么重建、IP 怎么换、节点怎么调度，对调用方来说都是透明的。这种&quot;调用方不关心被调用方物理位置&quot;的设计，是分布式系统能横向扩展的基石。&lt;/p&gt;
&lt;p&gt;K8s 把这套机制内化到了平台层——&lt;strong&gt;调用方一行代码不用改，平台就把寻址、负载均衡、故障恢复全做了&lt;/strong&gt;。这在单台服务器上根本无法做到：单机部署时，进程地址是写死的、端口是冲突的、机器是绑死的，&lt;strong&gt;这套&quot;靠名字找服务&quot;的能力在单台服务器上根本无从发挥&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>CLI Agent 实践指南：从 Claude Code 到通用方法论</title><link>https://youki.bbroot.com/posts/tools/cli-practice-guide/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/tools/cli-practice-guide/</guid><description>以 Claude Code 为主例，对比 Codex CLI / GitHub Copilot CLI，提炼 CLI Agent 编程助手的通用方法：Plan 模式、必学命令、上下文管理、安全权限、实践经验</description><pubDate>Sat, 25 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这一两年里 CLI Agent 编程助手换了好几轮——从 Claude Code 到Codex ，中间还试过 Copilot CLI——但用久之后发现，三家的&quot;骨架&quot;几乎一样：plan 模式、文件注入、shell、压缩、恢复、回退、模型切换、权限管控。基本是通用的，差异只在命令名。&lt;/p&gt;
&lt;p&gt;这篇以 Claude Code 为主线，对照 Codex CLI 和 GitHub Copilot CLI，把用得上的方法整理成 5 节：Plan 模式、命令、上下文管理、安全权限、实践经验。读完之后应该能在任何一个 CLI Agent 上快速上手。&lt;/p&gt;
&lt;h2&gt;0. 三家命令速查&lt;/h2&gt;
&lt;p&gt;先把骨架放在一起。下面的表格在三家之间通用，命令名差异不大。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;能力&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Codex CLI&lt;/th&gt;
&lt;th&gt;Copilot CLI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;进入 Plan 模式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Shift+Tab&lt;/code&gt; → &lt;code&gt;plan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Shift+Tab&lt;/code&gt; → &lt;code&gt;Read-only&lt;/code&gt;（Approval mode）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Shift+Tab&lt;/code&gt; 或 &lt;code&gt;/plan &amp;lt;描述&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;外置编辑器写计划&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl+G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl+G&lt;/code&gt;（&lt;code&gt;$EDITOR&lt;/code&gt;/&lt;code&gt;$VISUAL&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl+Y&lt;/code&gt;（Markdown 编辑器）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;注入文件&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@文件路径&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@&lt;/code&gt; + &lt;code&gt;Tab&lt;/code&gt; 模糊搜索&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@路径&lt;/code&gt; 或 &lt;code&gt;@图片.png&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;会话内跑 shell&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;!命令&lt;/code&gt; + &lt;code&gt;Ctrl+B&lt;/code&gt; 后台&lt;/td&gt;
&lt;td&gt;&lt;code&gt;!命令&lt;/code&gt;（受 Approval mode 约束）&lt;/td&gt;
&lt;td&gt;通过 &lt;code&gt;shell(...)&lt;/code&gt; 权限调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;清空上下文&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/clear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/clear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/clear&lt;/code&gt; 或 &lt;code&gt;/new&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;侧边问（不污染主历史）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/btw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;摘要压缩&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/compact [focus...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;API 层自动&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/compact&lt;/code&gt;（手动）/ 默认自动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;恢复会话&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/resume&lt;/code&gt;、&lt;code&gt;claude -c&lt;/code&gt;、&lt;code&gt;claude -r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;codex resume&lt;/code&gt; / &lt;code&gt;codex resume --last&lt;/code&gt; / &lt;code&gt;codex resume &amp;lt;id&amp;gt;&lt;/code&gt; / &lt;code&gt;codex exec resume&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/session&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;回退 checkpoint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rewind&lt;/code&gt; + &lt;code&gt;Esc+Esc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Esc&lt;/code&gt;×2 编辑上一条消息（=fork 入口）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/session checkpoints&lt;/code&gt; + &lt;code&gt;双 Esc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;切换模型&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/model&lt;/code&gt; + &lt;code&gt;Alt+P&lt;/code&gt; + &lt;code&gt;--effort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/model&lt;/code&gt;（含 Auto / Opus 4.5 / Sonnet 4.5 / GPT-5.2 Codex）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;看上下文用量&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/statusline&lt;/code&gt; 自定义&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/context&lt;/code&gt; + &lt;code&gt;/session files&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;权限管理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/permissions&lt;/code&gt; + &lt;code&gt;settings.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/permissions&lt;/code&gt; 切 Approval mode + &lt;code&gt;config.toml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--allow-tool/--deny-tool/--available-tools&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;跳过所有权限&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--dangerously-skip-permissions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--yolo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--yolo&lt;/code&gt; / &lt;code&gt;--allow-all&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;多仓库/多目录&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--add-dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;codex --cd&lt;/code&gt; + &lt;code&gt;--add-dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/add-dir&lt;/code&gt; + &lt;code&gt;/list-dirs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;并行子代理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude agents&lt;/code&gt; + agent teams&lt;/td&gt;
&lt;td&gt;&lt;code&gt;codex agents&lt;/code&gt; + &lt;code&gt;/fork&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/fleet&lt;/code&gt; + &lt;code&gt;/task&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;会话存档位置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.claude/projects/&amp;lt;project&amp;gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.codex/sessions/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.copilot/session-state/{id}/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;后面所有方法论都围绕这张表展开。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. Plan 模式：所有工作的起点&lt;/h2&gt;
&lt;h3&gt;1.1 决策树：能用一句话描述 diff 吗？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;能&lt;/strong&gt; → 不用进 Plan，直接让 Agent 改。例：修 typo、加日志、改变量名、调整一个 import。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不能&lt;/strong&gt;（涉及多文件、跨模块、不熟悉的代码、不确定的方案）→ &lt;strong&gt;必须进 Plan 模式&lt;/strong&gt;让 Agent 先只读、不写。&lt;/p&gt;
&lt;p&gt;Plan 模式的核心价值是&lt;strong&gt;把&quot;问题理解&quot;和&quot;动手&quot;在时间上分开&lt;/strong&gt;——这两件事分得越开，返工率越低。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Claude Code
Shift+Tab  # 在 default / acceptEdits / plan / auto / dontAsk / bypassPermissions 之间循环

# Codex
Shift+Tab  # 在 Read-only / Auto / Full Access 之间循环，选 Read-only = Plan 模式

# Copilot
Shift+Tab  # 或 /plan Implement password reset flow with email link
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 Plan 阶段的 3 个高阶玩法&lt;/h3&gt;
&lt;h4&gt;1）用外置编辑器把计划当合同&lt;/h4&gt;
&lt;p&gt;三家都能在 Plan 阶段用外置编辑器写计划。意义在于：把&quot;硬约束&quot;直接写进计划文件里，让计划成为可审查的合同。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Claude Code / Codex
Ctrl+G  # 打开 $EDITOR 编辑当前 plan

# Copilot
Ctrl+Y  # 在 Markdown 编辑器中打开 plan.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;典型约束写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Plan: 实现密码重置流程

### 涉及文件
- src/api/auth.ts
- src/services/email.ts
- tests/auth/test_reset.py

### 约束（必须遵守）
- 保留向后兼容（旧的 /login 流程不能动）
- 先写测试再写实现
- 不要改数据库 schema
- 使用项目里现成的 token-manager.ts，不要引入 passport.js

### 验证
- `pytest tests/auth/ -k reset` 全部通过
- `mypy src/auth/` 无错误
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写完之后保存退出，Agent 会按&quot;合同&quot;实现。&lt;/p&gt;
&lt;h4&gt;2）计划里显式列验证步骤&lt;/h4&gt;
&lt;p&gt;没写验收标准的计划 = 100% 会变成&quot;看起来做完了&quot;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;### 验证
1. Run `pytest tests/auth/ -k reset` and ensure all 3 new cases pass.
2. Run `mypy src/auth/` to confirm no type errors.
3. Open a PR with title &quot;feat(auth): password reset flow&quot;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Agent 擅长&quot;对照 plan 自检&quot;——前提是把验证步骤写进去。&lt;/p&gt;
&lt;h3&gt;1.3 三家 Plan 命令对照&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;步骤&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Copilot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;进 Plan&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Shift+Tab&lt;/code&gt; → &lt;code&gt;plan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Shift+Tab&lt;/code&gt; → &lt;code&gt;Read-only&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Shift+Tab&lt;/code&gt; 或 &lt;code&gt;/plan &amp;lt;描述&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写计划&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl+G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl+G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl+Y&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;让 Agent 继续&lt;/td&gt;
&lt;td&gt;&quot;Proceed with the plan&quot;&lt;/td&gt;
&lt;td&gt;&quot;proceed&quot; / &quot;go&quot;&lt;/td&gt;
&lt;td&gt;&quot;Proceed with the plan&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中止 Plan&lt;/td&gt;
&lt;td&gt;再按 &lt;code&gt;Shift+Tab&lt;/code&gt; 退出&lt;/td&gt;
&lt;td&gt;切到 &lt;code&gt;Auto&lt;/code&gt; 或 &lt;code&gt;Full Access&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;再按 &lt;code&gt;Shift+Tab&lt;/code&gt; 退出&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 需要掌握的 7 个命令&lt;/h2&gt;
&lt;p&gt;按使用频率排序。这 7 个命令覆盖 90% 的日常使用，其余命令是&quot;知道有就行，需要时查 &lt;code&gt;/help&lt;/code&gt;&quot;。&lt;/p&gt;
&lt;h3&gt;2.1 &lt;code&gt;@文件&lt;/code&gt; / &lt;code&gt;@路径/&lt;/code&gt; — 精准注入上下文&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@src/api/auth.ts       # 单文件
@src/api/auth/         # 整个目录
@../docs/spec.md       # 项目外的文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以直接拖进cli，都支持得不错&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心原则：让 Agent 按需读相关部分。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CLAUDE.md / AGENTS.md 里也支持 &lt;code&gt;@docs/git-instructions.md&lt;/code&gt; 语法引用其他文件，最多 4 层引用。三家都有这个特性，行为一致。&lt;/p&gt;
&lt;h3&gt;2.2 &lt;code&gt;!命令&lt;/code&gt; — 会话内跑 shell&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;! npm test              # Claude Code
! git status
! ls -la src/

# 进阶
! long-running-task     # 长任务
Ctrl+B                  # 转后台（Claude Code 特有），不阻塞主对话
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;上下文行为&lt;/strong&gt;：输出会回到主对话——只在确实需要 Agent 看到结果时用；纯查看用 &lt;code&gt;!&lt;/code&gt; 更顺手。&lt;/li&gt;
&lt;li&gt;粘贴以 &lt;code&gt;!&lt;/code&gt; 开头的内容会自动进入 shell 模式（Claude Code 行为）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Codex 和 Copilot 都有 shell 工具，能用 &lt;code&gt;!git diff&lt;/code&gt;、&lt;code&gt;!cat file&lt;/code&gt; 直接验证。&lt;/p&gt;
&lt;h3&gt;2.3 &lt;code&gt;/clear&lt;/code&gt; — 切任务时清空上下文&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/clear                  # 清空 + 当前对话进 /resume 选择器
/clear feature-auth-work   # 给被清掉的对话打标签（Claude Code 特有）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用节奏：每完成一个独立任务 → &lt;code&gt;/clear&lt;/code&gt;。一次会话只做一件事。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;官方原话警告过一种反模式——&quot;kitchen sink session&quot;：一个会话混入不相关任务，等要清理 context 时已经一团糟。&lt;/p&gt;
&lt;h3&gt;2.4 &lt;code&gt;/compact [focus...]&lt;/code&gt; — 长会话摘要压缩&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/compact                            # 压缩整个对话
/compact focus on the API design    # 带方向压缩
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键事实&lt;/strong&gt;（Claude Code 文档原话）：&quot;Project-root CLAUDE.md survives compaction: after &lt;code&gt;/compact&lt;/code&gt;, Claude re-reads it from disk and re-inject it.&quot; —— 项目根的 &lt;code&gt;CLAUDE.md&lt;/code&gt; &lt;strong&gt;不会&lt;/strong&gt;因压缩丢失；但&lt;strong&gt;子目录的&lt;/strong&gt; CLAUDE.md &lt;strong&gt;会&lt;/strong&gt;丢失，直到下次读到该目录才重新加载。&lt;/p&gt;
&lt;p&gt;何时用 &lt;code&gt;/compact&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对话长了但任务没完 → &lt;code&gt;/compact focus on X&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;同一任务上&lt;strong&gt;修 Agent 同一错误超过 2 次&lt;/strong&gt; → 立刻 &lt;code&gt;/clear&lt;/code&gt;（用更好的 prompt 重开，不要继续累积修正）。&lt;/li&gt;
&lt;li&gt;Codex 隐式做了这件事，没有显式 &lt;code&gt;/compact&lt;/code&gt;，但可以 &lt;code&gt;/fork&lt;/code&gt; 派生新会话。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.5 &lt;code&gt;/resume&lt;/code&gt; — 恢复历史会话&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Claude Code
/resume                       # 打开选择器
/resume feature-auth          # 按名恢复
claude -c                     # 续当前目录下最近对话
claude -r &amp;lt;id&amp;gt;                # 按 ID 恢复

# Codex
codex resume
codex resume --last
codex resume &amp;lt;id&amp;gt;

# Copilot
/resume                       # 打开 ~/.copilot/session-state/ 选择器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;配合 git worktree 的高阶用法&lt;/strong&gt;：每个 worktree 一个 &lt;code&gt;codex resume&lt;/code&gt; / &lt;code&gt;claude -r &amp;lt;id&amp;gt;&lt;/code&gt; 会话。切分支 = 切会话，context 完全隔离。&lt;/p&gt;
&lt;h3&gt;2.6 &lt;code&gt;/rewind&lt;/code&gt; / &lt;code&gt;/session&lt;/code&gt; — 回退到 checkpoint&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Claude Code
/rewind          # 弹出检查点选择器
Esc+Esc          # 空输入时弹 rewind 菜单（含 &quot;Summarize from here&quot; / &quot;Summarize up to here&quot; 选项）

# Copilot
/session                  # 查看当前会话信息
/session checkpoints      # 列出所有检查点
/session checkpoints 3    # 查看第 3 个检查点内容
/session files            # 列出当前会话临时文件
双 Esc                     # 在 composer 为空时打开 rewind picker

# Codex
Esc × 2（composer 为空时）  # **编辑上一条用户消息** —— 这也是 fork / 分叉的入口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Copilot 警告&lt;/strong&gt;：&quot;Rewinding cannot be undone. Once you roll back to a snapshot, all snapshots and session history after that point are permanently removed.&quot; —— 回退是&lt;strong&gt;不可逆&lt;/strong&gt;的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;code&gt;/rewind&lt;/code&gt; 会&lt;strong&gt;同时回退 Agent 改的、手动改的、shell 命令产生的所有变更&lt;/strong&gt;，包括之后新增的文件。回退前先 &lt;code&gt;git status&lt;/code&gt; 确认工作区状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Codex 关键差异&lt;/strong&gt;：Codex 没有 &lt;code&gt;/rewind&lt;/code&gt; 概念，而是把&quot;分叉&quot;和&quot;回退&quot;合在一起——&lt;code&gt;Esc&lt;/code&gt;×2 编辑上一条消息后，从那个时间点分叉新会话；不满意就丢，老会话仍在 &lt;code&gt;/resume&lt;/code&gt; 里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.7 &lt;code&gt;/model&lt;/code&gt; + &lt;code&gt;--effort&lt;/code&gt; — 切模型与思考强度&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Claude Code
/model                # 打开选择器
Alt+P                 # 切换模型不丢失当前 prompt（更快的快捷键）
--effort low/medium/high/xhigh/max    # 调节思考强度

# Codex
/model
codex --model gpt-5.5

# Copilot
/model                # 含 Auto / Opus 4.5 / Sonnet 4.5 / GPT-5.2 Codex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心技巧&lt;/strong&gt;：让主模型去调度subagent，subagent可以指定使用不同的模型，节约用量&lt;/p&gt;
&lt;h3&gt;2.8 锦上添花&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;哪家&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/btw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;侧边问题&lt;/strong&gt;，答案不进入主对话历史；纯提问用，访问不到工具——完美的&quot;等下，X 那个 Y 是否考虑过&quot;场景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/statusline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;自定义状态栏持续追踪上下文用量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/goal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;跨会话条件门控（评估器每轮重新检查）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/diff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;交互式 diff 查看器，左右键切&quot;当前 git diff&quot;和&quot;单 turn diff&quot;，审 PR 前必看&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/code-review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;内置代码评审（在全新子代理中评审当前 diff）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/recap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;按需生成会话摘要（默认自动每 3 turn 触发）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/delegate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Copilot&lt;/td&gt;
&lt;td&gt;把当前会话转给 &lt;strong&gt;GitHub 云端 Copilot&lt;/strong&gt;，云端代理会创建 PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/fleet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Copilot&lt;/td&gt;
&lt;td&gt;启动&lt;strong&gt;并行子代理&lt;/strong&gt;加速大任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/task&lt;/code&gt; / &lt;code&gt;/review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Copilot&lt;/td&gt;
&lt;td&gt;显式触发子代理（review 有 4 种预设：base branch / uncommitted / commit / custom）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude -p &quot;...&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;非交互模式&lt;/strong&gt;，配 &lt;code&gt;--output-format json&lt;/code&gt; / &lt;code&gt;--output-format stream-json --verbose&lt;/code&gt; 给 CI、pre-commit、脚本用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codex exec &quot;...&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;非交互执行；&lt;code&gt;codex exec resume --last &quot;Fix race&quot;&lt;/code&gt; 续上次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codex cloud exec --attempts 3 &quot;...&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;云端 best-of-N&lt;/strong&gt;（1-4 次并发尝试）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codex features enable unified_exec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;启用新功能（如 &lt;code&gt;unified_exec&lt;/code&gt; 执行模式、&lt;code&gt;shell_snapshot&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codex app-server --listen ws://...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;启动 App Server，远程 TUI 通过 WebSocket 连接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude agents&lt;/code&gt; / &lt;code&gt;codex agents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;三家&lt;/td&gt;
&lt;td&gt;打开多代理视图，监控并行后台会话&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;claude -c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;续当前目录下最近对话（最快的工作流快捷方式）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/add-dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Copilot / Codex&lt;/td&gt;
&lt;td&gt;多仓库/多目录工作流，加额外目录进 Agent 可访问范围&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 上下文管理：context window&lt;/h2&gt;
&lt;p&gt;Anthropic 官方：&quot;Most best practices are based on one constraint: Claude&apos;s context window fills up fast, and performance degrades as it fills.&quot;&lt;/p&gt;
&lt;p&gt;所有&quot;上下文最佳实践&quot;都围绕&quot;别浪费它&quot;展开。&lt;/p&gt;
&lt;h3&gt;3.1 4 种&quot;喂&quot;上下文的姿势&lt;/h3&gt;
&lt;p&gt;从高到低：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@文件路径&lt;/code&gt;&lt;/strong&gt;（最优）—— 引用文件，Agent 按需读相关部分。&lt;strong&gt;Claude Code 与 Copilot&lt;/strong&gt; 支持 &lt;code&gt;@path/to/file&lt;/code&gt; 完整路径；&lt;strong&gt;Codex&lt;/strong&gt; 的 &lt;code&gt;@&lt;/code&gt; 触发的是&lt;strong&gt;模糊文件搜索&lt;/strong&gt;（&lt;code&gt;@&lt;/code&gt; + &lt;code&gt;Tab&lt;/code&gt; 选中），再 &lt;code&gt;Enter&lt;/code&gt; 填入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;粘贴图片/截图/错误堆栈&lt;/strong&gt; —— OCR 不必，人类也读图。三家都支持&lt;strong&gt;拖放图片&lt;/strong&gt;到 CLI 输入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cat error.log | claude&lt;/code&gt;&lt;/strong&gt;（管道输入） —— 适合一次性文本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;整文件复制&lt;/strong&gt;（最差）—— 1000 行代码塞进 prompt，挤占 context。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Claude Code 特有技巧&lt;/strong&gt;：&lt;code&gt;/permissions&lt;/code&gt; 把常用文档域名（&lt;code&gt;docs.example.com&lt;/code&gt;）加白名单，让 Agent 自己去拉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通用技巧&lt;/strong&gt;：&lt;code&gt;gh&lt;/code&gt; CLI 是 GitHub 上下文最高效的获取方式——Anthropic 文档原话：&quot;Without &lt;code&gt;gh&lt;/code&gt;, Claude can still use the GitHub API, but unauthenticated requests often hit rate limits.&quot;&lt;/p&gt;
&lt;h3&gt;3.2 3 个清理层次&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;触发场景&lt;/th&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;效果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent 走偏了一点&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Esc+Esc&lt;/code&gt; 局部回退 + 改 prompt 继续&lt;/td&gt;
&lt;td&gt;撤回最近几轮（Codex 是 fork 入口）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;想问个不相关的边角问题&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/btw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;侧边问，&lt;strong&gt;答案不进入主对话历史&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;对话长了但任务没完&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/compact focus on X&lt;/code&gt;（Claude/Copilot）&lt;/td&gt;
&lt;td&gt;保留当前任务，&lt;strong&gt;只压缩历史&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;任务完成要开始新任务&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/clear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;完全清空，原对话进 &lt;code&gt;/resume&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键节奏&lt;/strong&gt;：同一任务上&lt;strong&gt;修 Agent 同一错误超过 2 次&lt;/strong&gt; → 立刻 &lt;code&gt;/clear&lt;/code&gt; 用更好的 prompt 重开。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;官方原话：&quot;If you&apos;ve corrected Claude more than twice on the same issue in one session, the context is cluttered with failed approaches.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;干净会话 + 更好的 prompt 永远胜过&quot;长会话 + 累积修正&quot;。&lt;/p&gt;
&lt;h3&gt;3.3 4 层记忆文件 + 一键生成&lt;/h3&gt;
&lt;p&gt;把&quot;每次都要说的事情&quot;挪到磁盘，让 Agent 在每个新会话里&quot;已经知道项目是谁搭的、约定是什么&quot;——这是从&quot;工具&quot;到&quot;协作者&quot;的关键升级。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Code 的一键生成&lt;/strong&gt;：在项目根跑 &lt;code&gt;/init&lt;/code&gt; —— Agent 分析项目结构自动生成 &lt;code&gt;CLAUDE.md&lt;/code&gt; 起始模板。&lt;/p&gt;
&lt;p&gt;加载顺序（从广到窄）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;作用域&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Copilot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;用户全局&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.codex/AGENTS.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.copilot/copilot-instructions.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用户全局覆盖&lt;/td&gt;
&lt;td&gt;（local 后缀）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.codex/AGENTS.override.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;（&lt;code&gt;.override&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;项目共享&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./CLAUDE.md&lt;/code&gt; 或 &lt;code&gt;./.claude/CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./AGENTS.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;项目模块化&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./.claude/rules/*.md&lt;/code&gt;（支持 &lt;code&gt;paths&lt;/code&gt; frontmatter）&lt;/td&gt;
&lt;td&gt;每层一个 AGENTS.md&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.github/instructions/**/*.instructions.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;项目个人&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./CLAUDE.local.md&lt;/code&gt;（gitignore）&lt;/td&gt;
&lt;td&gt;子目录 &lt;code&gt;AGENTS.override.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;本机范围&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;团队组织&lt;/td&gt;
&lt;td&gt;Managed policy 文件&lt;/td&gt;
&lt;td&gt;&lt;code&gt;project_doc_fallback_filenames&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;组织托管规则&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Copilot 加载顺序的关键规则&lt;/strong&gt;：&quot;Repository instructions always take precedence over user instructions&quot;（仓库级指令&lt;strong&gt;始终优先于&lt;/strong&gt;全局指令）。&lt;/p&gt;
&lt;p&gt;加载行为：&lt;strong&gt;不是覆盖而是拼接&lt;/strong&gt;（Anthropic 原话：&quot;All discovered files are concatenated into context rather than overriding each other&quot;）。子目录的 CLAUDE.md &lt;strong&gt;只在 Agent 读该子目录文件时才按需加载&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CLAUDE.md 里支持 &lt;code&gt;@&lt;/code&gt; 导入语法&lt;/strong&gt;：可以在 CLAUDE.md 中写 &lt;code&gt;See @README.md for project overview and @package.json for available npm commands.&lt;/code&gt;，Agent 会按需展开。最多 4 层引用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;大小上限&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude Code &lt;code&gt;MEMORY.md&lt;/code&gt; 启动时只加载&lt;strong&gt;前 200 行 / 25KB&lt;/strong&gt;（自动记忆机制）。&lt;/li&gt;
&lt;li&gt;Codex 合并总大小上限是 &lt;code&gt;project_doc_max_bytes = 32 KiB&lt;/code&gt;（默认）。&lt;/li&gt;
&lt;li&gt;复杂规则不要写进 CLAUDE.md，改用 PreToolUse &lt;strong&gt;hook&lt;/strong&gt; 强制约束。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.4 跨会话：CLAUDE.md vs Auto memory vs Codex Memories&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;CLAUDE.md&lt;/strong&gt;（人工写）—— 由开发者维护、每会话完整加载。&lt;strong&gt;应只放&quot;普适指令&quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Auto memory&lt;/strong&gt;（Claude Code v2.1.59+，Agent 自己学） —— Agent 自己记录 build commands、调试经验、架构笔记。存到 &lt;code&gt;~/.claude/projects/&amp;lt;project&amp;gt;/memory/&lt;/code&gt;，&lt;strong&gt;首次 200 行 / 25KB 启动时加载&lt;/strong&gt;，详情文件按需读。&lt;strong&gt;默认开启&lt;/strong&gt;，可用 &lt;code&gt;/memory&lt;/code&gt; 命令切换。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Codex Memories / Chronicle&lt;/strong&gt;（Codex 0.140+ 新增） —— 文档侧栏已列出 &lt;code&gt;/codex/memories&lt;/code&gt; 和 &lt;code&gt;/codex/memories/chronicle&lt;/code&gt; 入口，对应 OpenAI 的持久记忆子系统。Chronicle 给出&quot;会话历史洞察&quot;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关键事实（Claude Code 文档原话）：&quot;Both are loaded at the start of every conversation. Claude treats them as context, not enforced configuration. To block an action regardless of what Claude decides, use a PreToolUse hook instead.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;记忆文件只是上下文不是规则&lt;/strong&gt;——要强制约束必须用 hook，不是写文档。&lt;/p&gt;
&lt;h3&gt;3.5 长任务的会话状态恢复&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;适用&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A. worktree + 会话隔离&lt;/td&gt;
&lt;td&gt;大功能 / 长期任务&lt;/td&gt;
&lt;td&gt;每个 git worktree 一个 &lt;code&gt;codex resume&lt;/code&gt; / &lt;code&gt;claude -r &amp;lt;id&amp;gt;&lt;/code&gt; 会话。切分支 = 切会话，context 完全隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B. CLAUDE.md 写&quot;怎么续&quot;&lt;/td&gt;
&lt;td&gt;中等任务&lt;/td&gt;
&lt;td&gt;在指令文件里写&quot;如果中断了，请先 &lt;code&gt;git log -5&lt;/code&gt; 和 &lt;code&gt;git status&lt;/code&gt; 再继续&quot;——把&quot;怎么续&quot;作为冷启动指令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C. 连续 5 小时不清&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;强烈不推荐&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;后期 Agent 必然开始&quot;忘记&quot;早期约束、变笨、产出重复修复&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 安全：deny 永远第一&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;永远不要让 Agent 默认拥有破坏性权限&lt;/strong&gt;。&quot;给它 1 个权限 = 让它能用到 1 个&quot; 是错误模型；正确模型是&quot;默认拒绝 + 显式白名单&quot;。&lt;/p&gt;
&lt;h3&gt;4.1 三级危险模型&lt;/h3&gt;
&lt;p&gt;把工具按破坏半径分三档，配权限时心里有数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;等级&lt;/th&gt;
&lt;th&gt;例子&lt;/th&gt;
&lt;th&gt;配权策略&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L1 只读&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Read&lt;/code&gt;、&lt;code&gt;Grep&lt;/code&gt;、&lt;code&gt;Glob&lt;/code&gt;、&lt;code&gt;Bash(ls *)&lt;/code&gt;、&lt;code&gt;Bash(git status)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;几乎无副作用，可以默认开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L2 局部写&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Edit&lt;/code&gt;、&lt;code&gt;Write&lt;/code&gt;、&lt;code&gt;Bash(npm test)&lt;/code&gt;、&lt;code&gt;Bash(git commit *)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;影响工作区，需要按任务白名单&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L3 系统级&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bash(rm -rf *)&lt;/code&gt;、&lt;code&gt;Bash(curl *)&lt;/code&gt;、&lt;code&gt;Bash(git push *)&lt;/code&gt;、&lt;code&gt;Bash(sudo *)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;必须显式拒绝或每次确认&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;4.2 deny 永远优先于 allow&lt;/h3&gt;
&lt;p&gt;这是 Claude Code / Codex 共同的关键规则（Anthropic 原话）：&quot;Rules are evaluated in order: deny, then ask, then allow. The first match in that order determines the outcome, and rule specificity does not change the order.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心警告&lt;/strong&gt;：&quot;提示中的指令或 &lt;code&gt;CLAUDE.md&lt;/code&gt; 只能&apos;影响 Claude 尝试做什么&apos;，不能改变 Claude Code 实际允许的内容。&quot;&lt;/p&gt;
&lt;p&gt;也就是说，prompt 写&quot;请不要 rm -rf&quot;是&lt;strong&gt;无效&lt;/strong&gt;的；必须在 &lt;code&gt;settings.json&lt;/code&gt; 里 deny。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;permissions&quot;: {
    &quot;allow&quot;: [
      &quot;Bash(npm run *)&quot;,
      &quot;Bash(git commit *)&quot;,
      &quot;Bash(* --version)&quot;
    ],
    &quot;deny&quot;: [
      &quot;Bash(git push *)&quot;,
      &quot;Bash(rm -rf *)&quot;,
      &quot;Bash(curl *)&quot;,
      &quot;Bash(sudo *)&quot;
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;复合命令陷阱&lt;/strong&gt;：&quot;Claude Code 识别 shell 操作符，因此 &lt;code&gt;Bash(safe-cmd *)&lt;/code&gt; &lt;strong&gt;不会&lt;/strong&gt;授权执行 &lt;code&gt;safe-cmd &amp;amp;&amp;amp; other-cmd&lt;/code&gt;。&quot; —— 不会被通配符绕过。&lt;/p&gt;
&lt;h3&gt;4.3 三家权限机制对照&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Copilot CLI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;配置文件&lt;/td&gt;
&lt;td&gt;&lt;code&gt;settings.json&lt;/code&gt;（4 层优先级）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.codex/config.toml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CLI 启动参数 + &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写规则语法&lt;/td&gt;
&lt;td&gt;&lt;code&gt;permissions.allow/deny/ask&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Approval mode&lt;/strong&gt;（Auto / Read-only / Full Access）+ &lt;code&gt;sandbox&lt;/code&gt; 配置&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--allow-tool&lt;/code&gt; / &lt;code&gt;--deny-tool&lt;/code&gt; / &lt;code&gt;--available-tools&lt;/code&gt; / &lt;code&gt;--excluded-tools&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工具模式匹配&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bash(git:*)&lt;/code&gt;（冒号）&lt;/td&gt;
&lt;td&gt;基于前缀 + shell 元字符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shell(git:*)&lt;/code&gt;（冒号）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;优先级&lt;/td&gt;
&lt;td&gt;deny &amp;gt; ask &amp;gt; allow（按列表顺序）&lt;/td&gt;
&lt;td&gt;Approval mode 是边界 + 是否提示&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--available-tools&lt;/code&gt; 覆盖 &lt;code&gt;--excluded-tools&lt;/code&gt;；&lt;code&gt;--deny-tool&lt;/code&gt; 覆盖 &lt;code&gt;--allow-tool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;沙箱能力&lt;/td&gt;
&lt;td&gt;OS 级 sandbox（&lt;code&gt;/sandbox&lt;/code&gt;，仅 Bash）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sandbox&lt;/code&gt; 配置（结合 Approval mode）&lt;/td&gt;
&lt;td&gt;仅基于规则的 allow/deny&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;推荐默认&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt; 模式 + 编辑过的 &lt;code&gt;settings.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Approval = &lt;code&gt;Auto&lt;/code&gt;，按任务加白名单&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--available-tools=&apos;bash,edit,view,grep,glob&apos; --allow-tool=&apos;shell(git:*)&apos; --deny-tool=&apos;shell(git push)&apos;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;4.4 Approval mode 三档&lt;/h3&gt;
&lt;p&gt;Codex 把审批做成了三档显式状态（通过 &lt;code&gt;/permissions&lt;/code&gt; 切换，配置在 &lt;code&gt;config.toml&lt;/code&gt; 的 &lt;code&gt;approval_policy&lt;/code&gt;）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;能力&lt;/th&gt;
&lt;th&gt;何时用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auto&lt;/strong&gt;（默认）&lt;/td&gt;
&lt;td&gt;当前工作目录内读/写/执行；越界（目录外、网络）需确认&lt;/td&gt;
&lt;td&gt;日常开发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Read-only&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;只浏览文件，不做修改，需先批准 plan&lt;/td&gt;
&lt;td&gt;审 PR、看代码、咨询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Full Access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;跨整台机器 + 网络，不再询问&lt;/td&gt;
&lt;td&gt;可信仓库的自动化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Claude Code 有 &lt;strong&gt;OS 级 sandbox&lt;/strong&gt;（&lt;code&gt;/sandbox&lt;/code&gt; 启用，限制 Bash 文件系统和网络访问）。Copilot CLI 没有真沙箱，只能靠 &lt;code&gt;--deny-tool&lt;/code&gt; 黑名单 + &lt;code&gt;--available-tools&lt;/code&gt; 白名单缩小可选工具集。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Codex 的 4 层防御&lt;/strong&gt;（配合使用）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Approval mode（&lt;code&gt;Auto&lt;/code&gt; / &lt;code&gt;Read-only&lt;/code&gt; / &lt;code&gt;Full Access&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sandbox&lt;/code&gt; 配置（文件系统和网络隔离）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config.toml&lt;/code&gt; 的 &lt;code&gt;[permissions]&lt;/code&gt; 段（具体规则白/黑名单）&lt;/li&gt;
&lt;li&gt;启动旗标 &lt;code&gt;--yolo&lt;/code&gt; 完全绕过（最后手段）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4.5 &lt;code&gt;--yolo&lt;/code&gt; / &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; 是最后手段&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;旗标&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--dangerously-skip-permissions&lt;/code&gt;（&lt;code&gt;bypassPermissions&lt;/code&gt; 模式）&lt;/td&gt;
&lt;td&gt;跳过权限提示，但&lt;strong&gt;强制 ask 规则仍然提示&lt;/strong&gt;；针对根目录与家目录的删除仍会作为熔断机制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--yolo&lt;/code&gt; = &lt;code&gt;--dangerously-bypass-approvals-and-sandbox&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&quot;在生产或共享环境使用 &lt;code&gt;--yolo&lt;/code&gt;（除非处于专用沙箱 VM）&quot;是被明令禁止的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--yolo&lt;/code&gt; / &lt;code&gt;--allow-all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;等同于同时启用 &lt;code&gt;--allow-all-tools&lt;/code&gt;、&lt;code&gt;--allow-all-paths&lt;/code&gt;、&lt;code&gt;--allow-all-urls&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;官方一致警告&lt;/strong&gt;：&quot;Only use this mode in isolated environments like containers or VMs&quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;替代方案&lt;/strong&gt;：别用 &lt;code&gt;--yolo&lt;/code&gt;，用 &lt;code&gt;--allowedTools &quot;Edit,Bash(git commit *)&quot;&lt;/code&gt; 显式限定权限范围——Anthropic 原话：&quot;The &lt;code&gt;--allowedTools&lt;/code&gt; flag restricts what Claude can do, which matters when you&apos;re running unattended.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Code 还有一个新选项&lt;/strong&gt;：&lt;code&gt;--permission-mode auto&lt;/code&gt;（Auto mode）—— 单独的分类器模型审核命令，拦截范围升级、未知基础设施、恶意驱动操作。比 &lt;code&gt;bypassPermissions&lt;/code&gt; 更安全：&lt;strong&gt;只跳过&quot;低风险&quot;命令的提示，高风险仍询问&lt;/strong&gt;。注意：Auto mode 在非交互 &lt;code&gt;claude -p&lt;/code&gt; 模式下，若分类器反复阻止会&lt;strong&gt;中止&lt;/strong&gt;（无用户兜底）。&lt;/p&gt;
&lt;h2&gt;5. 做 Agent 的产品经理&lt;/h2&gt;
&lt;p&gt;Agent 时代编程的本质是&quot;做 Agent 的清晰操作员&quot;。Simon Willison 的原话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The &apos;agentic&apos; coding tools we have right now work like this: A skilled individual with both deep domain understanding and deep understanding of the capabilities of the agent...&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;价值 = 领域知识 × 工具熟练度。下面 7 条心法按&quot;反人性&quot;程度排序。&lt;/p&gt;
&lt;h3&gt;5.1 提问要像产品需求文档&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;反例&lt;/strong&gt;：&quot;修一下这个 bug&quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正例&lt;/strong&gt;：&quot;登录在 session timeout 后失败，错误堆栈是 [粘贴]。先写一个失败测试复现，再修根因，不要只 suppress 错误。修改后跑 &lt;code&gt;pytest tests/auth/test_session.py&lt;/code&gt; 全部用例验证。&quot;&lt;/p&gt;
&lt;p&gt;黄金模板：&lt;strong&gt;[症状] + [期望行为] + [验证标准] + [禁止做法]&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Anthropic 原话：&quot;Give Claude a way to verify its work. &apos;Looks done&apos; is not a good signal.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;给 Agent 配 4 种&quot;验证门控&quot;（按设置成本从低到高）&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;同一个 prompt 内&lt;/strong&gt; —— 在指令里直接要求&quot;运行测试并迭代直到通过&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/goal&lt;/code&gt;&lt;/strong&gt;（Claude Code） —— 跨会话条件门控，评估器每轮重新检查。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stop hook&lt;/strong&gt;（Claude Code） —— 脚本执行检查，&lt;strong&gt;阻止 turn 结束&lt;/strong&gt;直到条件满足（注意：连续 8 次阻止后 Claude Code 会强制结束 turn，避免死循环）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification subagent&lt;/strong&gt;（独立 context）—— 在新上下文中反驳/审阅结论，避免自评偏差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browser screenshot&lt;/strong&gt;（Claude Code + Chrome）—— 截图对比设计稿，验证 UI 改动。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.2 增量验证：每改 10 行看一次&lt;/h3&gt;
&lt;p&gt;不要 100 行一次性 commit。每次让 Agent 改完：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;git diff&lt;/code&gt; 确认范围正确&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;/diff&lt;/code&gt;（Claude Code）看每 turn 的具体改动&lt;/li&gt;
&lt;li&gt;让 Agent 跑测试&lt;/li&gt;
&lt;li&gt;每次提交独立一个 commit（&lt;code&gt;git add -p&lt;/code&gt;），便于回退和 review&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;反模式&lt;/strong&gt;：&quot;把整个功能一次写完再 review&quot;——一旦 review 出问题，&lt;strong&gt;不知道哪 50 行引入的&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;5.3 任务拆分：一个会话 = 一件事&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;反模式&lt;/strong&gt;：一个会话里做&quot;加 OAuth + 修首页 bug + 重构 utils&quot;，&lt;strong&gt;混完后清理 context 极痛苦&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正例&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;会话 1 = &quot;实现 password reset&quot;&lt;/li&gt;
&lt;li&gt;会话 2 = &quot;加 metrics middleware&quot;&lt;/li&gt;
&lt;li&gt;用 git 分支隔离&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官方警告：&quot;The kitchen sink session: One session with unrelated tasks. Use &lt;code&gt;/clear&lt;/code&gt; between unrelated tasks.&quot;&lt;/p&gt;
&lt;h3&gt;5.4 两次失败就 &lt;code&gt;/clear&lt;/code&gt; 重来&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Anthropic 原话：&quot;If you&apos;ve corrected Claude more than twice on the same issue in one session, the context is cluttered with failed approaches. Run &lt;code&gt;/clear&lt;/code&gt; and start fresh with a more specific prompt that incorporates what you learned.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;干净会话 + 更好的 prompt 永远胜过&quot;长会话 + 累积修正&quot;。&lt;/p&gt;
&lt;h3&gt;5.5 子代理是 context 的瑞士军刀&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Since context is your fundamental constraint, subagents are one of the most powerful tools available.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;子代理在&lt;strong&gt;独立 context&lt;/strong&gt; 中运行、返回摘要，主对话不被&quot;读了 200 个文件&quot;污染。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;典型用法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;让 &lt;code&gt;explore&lt;/code&gt; 子代理查 &quot;auth 系统怎么管 token refresh&quot; → 返回一段摘要。&lt;/li&gt;
&lt;li&gt;让审查子代理用&lt;strong&gt;新 context&lt;/strong&gt; 复审刚改的 diff —— &lt;strong&gt;关键洞察&lt;/strong&gt;：&quot;A reviewer running in a fresh subagent context sees only the diff and the criteria you give it, not the reasoning that produced the change, so it evaluates the result on its own terms.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是&lt;strong&gt;对抗性验证&lt;/strong&gt;的标准模式：让一个干净的 context 独立判断结果好坏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Code 的子代理体系&lt;/strong&gt;（最完整）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;claude agents&lt;/code&gt;：内置多代理视图，监控并行后台会话&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent teams&lt;/strong&gt;（&lt;code&gt;/agent-teams&lt;/code&gt;）：多 session 自动协调 + 共享任务列表&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code intelligence plugin&lt;/strong&gt;：给 typed language 提供精确符号导航和自动错误检测&lt;/li&gt;
&lt;li&gt;子代理配置在 &lt;code&gt;.claude/agents/*.md&lt;/code&gt;，frontmatter 指定 &lt;code&gt;tools&lt;/code&gt; 和 &lt;code&gt;model&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Codex 的子代理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;codex agents&lt;/code&gt; 视图&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/fork&lt;/code&gt;&lt;/strong&gt; 和 &lt;strong&gt;&lt;code&gt;/side&lt;/code&gt;&lt;/strong&gt;：在历史消息节点分叉/并行会话&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Codex 关键差异&lt;/strong&gt;：&quot;Codex only spawns subagents when you &lt;strong&gt;explicitly ask it to&lt;/strong&gt;&quot; —— 不像 Claude Code 那样按需自动派生，token 消耗更可控&lt;/li&gt;
&lt;li&gt;子代理配置在 &lt;code&gt;config.toml&lt;/code&gt; 的 &lt;code&gt;[agents]&lt;/code&gt; 段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Copilot CLI 的子代理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/fleet&lt;/code&gt;：把大任务&lt;strong&gt;分解为并行子任务&lt;/strong&gt;由子代理执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/delegate&lt;/code&gt;：把整个工作转移到 &lt;strong&gt;GitHub 云端 Copilot&lt;/strong&gt;，云端代理会创建 PR&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/task&lt;/code&gt;、&lt;code&gt;/review&lt;/code&gt;：显式触发子代理（&lt;code&gt;/review&lt;/code&gt; 有 4 种预设：base branch / uncommitted / commit / custom）&lt;/li&gt;
&lt;li&gt;内置子代理（&lt;code&gt;/review&lt;/code&gt;、&lt;code&gt;/task&lt;/code&gt;、explore、&lt;code&gt;/fleet&lt;/code&gt;）&lt;strong&gt;自动继承&lt;/strong&gt; provider 配置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Writer/Reviewer 模式&lt;/strong&gt;（Claude Code 强调）：用全新上下文的 Reviewer 子代理审 Writer 子代理的产出，避免自评偏差。&lt;/p&gt;
&lt;h3&gt;5.6 测试是 Agent 时代的&quot;免费午餐&quot;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Simon Willison 原话：&quot;Good automated tests which the coding agent can run ... pytest ... 1500 tests ... Claude Code is great at selectively running just the relevant tests for a change, and running the full suite at the end.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键技巧&lt;/strong&gt;：&quot;detailed error messages! If a manual or automated test fails the more information you can return back to the model the better&quot; —— &lt;strong&gt;把 assertion 写详细&lt;/strong&gt;，让 Agent 能&quot;自己读错误自己修&quot;。&lt;/p&gt;
&lt;p&gt;差的 assertion：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;assert user.is_authenticated()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好的 assertion（Agent 能直接定位问题）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;assert user.is_authenticated(), f&quot;Expected authenticated user, got state={user.state}, session_age={user.session_age}s, token={user.token[:8]}...&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copilot CLI 的 Plan 流程标准最后一步就是 &quot;Run the tests and fix any failures&quot;——把&quot;跑测试&quot;嵌进标准工作流。&lt;/p&gt;
&lt;h3&gt;5.7 给 Agent 一个工具齐全的开发环境&lt;/h3&gt;
&lt;p&gt;Simon Willison 的 HN 回复清单（值得全文照搬）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;好测试&lt;/strong&gt;（pytest + 详细 assertion message）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开发服务器启动说明&lt;/strong&gt;（让 Agent 用 Playwright / curl 交互式验证 UI 改动）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lint + type check + formatter&lt;/strong&gt;（Agent 会自己用）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub issues 列表&lt;/strong&gt;（把 issue URL 直接贴进 prompt，&quot;having great results&quot;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;反直觉点&lt;/strong&gt;：&quot;I have extensive documentation in all of my projects, but I don&apos;t think it&apos;s particularly useful for coding agents. LLMs can read the code a lot faster than you to figure out how to use it.&quot;&lt;/p&gt;
&lt;p&gt;文档对 Agent 没用，&lt;strong&gt;Agent 读代码比人快&lt;/strong&gt;；文档的作用反而是&quot;让 Agent 检查文档是否需要更新&quot;。&lt;/p&gt;
&lt;h3&gt;5.8 5 条必避反模式&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;反模式&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;修复&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The kitchen sink session&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一个会话混入不相关任务&lt;/td&gt;
&lt;td&gt;任务间用 &lt;code&gt;/clear&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Correcting over and over&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;同一问题多次纠正&lt;/td&gt;
&lt;td&gt;2 次失败就 &lt;code&gt;/clear&lt;/code&gt; 改 prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The over-specified CLAUDE.md&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;规则太多淹没有效指令&lt;/td&gt;
&lt;td&gt;严格剪枝（200 行内），复杂规则改 hook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The infinite exploration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&quot;调查&quot;无边界，Agent 读数百文件&lt;/td&gt;
&lt;td&gt;缩小范围或用 subagent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The trust-then-verify gap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;实现看起来合理但缺边缘场景&lt;/td&gt;
&lt;td&gt;始终要求提供验证（测试 / 脚本 / 截图）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Claude Code: &lt;a href=&quot;https://code.claude.com/docs/en/best-practices&quot;&gt;https://code.claude.com/docs/en/best-practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Codex CLI: &lt;a href=&quot;https://developers.openai.com/codex/cli/features&quot;&gt;https://developers.openai.com/codex/cli/features&lt;/a&gt; + &lt;a href=&quot;https://developers.openai.com/codex/cli/reference&quot;&gt;https://developers.openai.com/codex/cli/reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Copilot CLI: &lt;a href=&quot;https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-best-practices&quot;&gt;https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-best-practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Simon Willison &quot;Setting up a codebase for working with coding agents&quot;（2025-10-25）: &lt;a href=&quot;https://simonwillison.net/2025/Oct/25/coding-agent-tips/&quot;&gt;https://simonwillison.net/2025/Oct/25/coding-agent-tips/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>VSCode + SSH + Docker 完整配置流程</title><link>https://youki.bbroot.com/posts/tools/vscode-ssh-docker/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/tools/vscode-ssh-docker/</guid><description>从全新 Linux 机器到 VSCode 免密管理 Docker 的完整可复用配置流程</description><pubDate>Sat, 25 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;以下是一份&lt;strong&gt;完整的、可复用的配置流程&lt;/strong&gt;，从一台全新的 Linux 机器到 VSCode 免密管理 Docker，按顺序执行即可。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;一、Linux 端准备&lt;/h2&gt;
&lt;h3&gt;1.1 开启 SSH 服务&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kali Linux：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 启动 SSH
sudo /etc/init.d/ssh start

# 开机自启
sudo systemctl enable ssh.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Ubuntu/Debian：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装并启动
sudo apt-get update
sudo apt-get install -y openssh-server
sudo service ssh start
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 查看 IP 地址&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ip a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记下 &lt;code&gt;inet&lt;/code&gt; 后面的局域网 IP（如 &lt;code&gt;192.168.17.129&lt;/code&gt;）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;二、Windows 端准备&lt;/h2&gt;
&lt;h3&gt;2.1 安装 VSCode 插件&lt;/h3&gt;
&lt;p&gt;打开 VSCode，安装以下扩展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Remote - SSH&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 生成 SSH 密钥对&lt;/h3&gt;
&lt;p&gt;打开 &lt;strong&gt;PowerShell&lt;/strong&gt;，执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;保存路径：直接回车（默认 &lt;code&gt;C:\Users\你的用户名\.ssh\id_ed25519&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;密码：直接回车（不设密码，实现完全免密）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生成后确认文件存在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls $env:USERPROFILE\.ssh\
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应看到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id_ed25519&lt;/code&gt; —— &lt;strong&gt;私钥&lt;/strong&gt;（留在 Windows）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;id_ed25519.pub&lt;/code&gt; —— &lt;strong&gt;公钥&lt;/strong&gt;（放到 Linux）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;三、配置免密登录（公私钥）&lt;/h2&gt;
&lt;h3&gt;3.1 将公钥上传到 Linux&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方法一：命令自动上传（推荐）&lt;/strong&gt;
在 Windows PowerShell 执行（替换 IP 和用户名）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh kali@192.168.17.129 &quot;mkdir -p ~/.ssh &amp;amp;&amp;amp; cat &amp;gt;&amp;gt; ~/.ssh/authorized_keys &amp;amp;&amp;amp; chmod 600 ~/.ssh/authorized_keys &amp;amp;&amp;amp; chmod 700 ~/.ssh&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入一次密码后，公钥即上传完成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法二：手动复制&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用记事本打开 &lt;code&gt;C:\Users\你的用户名\.ssh\id_ed25519.pub&lt;/code&gt;，复制全部内容&lt;/li&gt;
&lt;li&gt;在 VSCode 连上的 Linux 终端中：&lt;pre&gt;&lt;code&gt;nano ~/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;粘贴 → &lt;code&gt;Ctrl+O&lt;/code&gt; 回车保存 → &lt;code&gt;Ctrl+X&lt;/code&gt; 退出&lt;/li&gt;
&lt;li&gt;设置权限：&lt;pre&gt;&lt;code&gt;chmod 600 ~/.ssh/authorized_keys
chmod 700 ~/.ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.2 配置 VSCode SSH 使用私钥&lt;/h3&gt;
&lt;p&gt;按 &lt;code&gt;Ctrl+Shift+P&lt;/code&gt; → &lt;code&gt;Remote-SSH: Open SSH Configuration File&lt;/code&gt; → 选择第一个。&lt;/p&gt;
&lt;p&gt;添加或修改配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host kali
    HostName 192.168.17.129
    User kali
    IdentityFile C:\Users\你的用户名\.ssh\id_ed25519
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;IdentityFile&lt;/code&gt; 指向的是&lt;strong&gt;私钥&lt;/strong&gt;（&lt;code&gt;id_ed25519&lt;/code&gt;），不是 &lt;code&gt;.pub&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.3 修复 Windows 私钥权限（关键！）&lt;/h3&gt;
&lt;p&gt;在 PowerShell 执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd $env:USERPROFILE\.ssh

# 移除继承权限，仅保留当前用户读取
icacls id_ed25519 /inheritance:r
icacls id_ed25519 /grant:r &quot;$($env:USERNAME):(R)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4 测试免密连接&lt;/h3&gt;
&lt;p&gt;在 PowerShell 测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -i $env:USERPROFILE\.ssh\id_ed25519 kali@192.168.17.129
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果直接连上，说明配置成功。&lt;/p&gt;
&lt;p&gt;然后在 VSCode 中：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;点击左下角绿色 &lt;code&gt;&amp;gt;&amp;lt;&lt;/code&gt; 图标&lt;/li&gt;
&lt;li&gt;选择 &lt;code&gt;Connect to Host...&lt;/code&gt; → &lt;code&gt;kali&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;应直接连上，不再提示密码&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;四、Linux 端安装 Docker&lt;/h2&gt;
&lt;h3&gt;4.1 安装 Docker&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;推荐方式（Ubuntu/Debian）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget -qO- https://get.docker.com/ | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Kali Linux（Docker 官方不支持 Kali，需手动）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# 添加 GPG key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# 使用 Debian 源（Kali 基于 Debian）
echo &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian buster stable&quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null

# 安装
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 验证安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo docker run hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到 &lt;code&gt;Hello from Docker!&lt;/code&gt; 即成功。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、解决 Docker 权限问题&lt;/h2&gt;
&lt;p&gt;VSCode 连上后，Docker 插件可能提示无法访问 &lt;code&gt;/var/run/docker.sock&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一劳永逸的解法：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Linux 终端执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将当前用户加入 docker 组
sudo usermod -aG docker $USER

# 重启 Docker 服务
sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后&lt;strong&gt;断开 VSCode SSH 连接，重新连接&lt;/strong&gt;（或重启 Linux）。&lt;/p&gt;
&lt;p&gt;验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不再提示权限错误即成功。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;六、VSCode Docker 插件使用&lt;/h2&gt;
&lt;p&gt;连接成功后，左侧边栏出现 Docker 图标，常用功能：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;拉取镜像&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Images → 右键 &lt;code&gt;Pull&lt;/code&gt; 或终端执行 &lt;code&gt;docker pull&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;运行容器&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;镜像右键 → &lt;code&gt;Run&lt;/code&gt;（后台）/ &lt;code&gt;Run Interactive&lt;/code&gt;（带日志）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;进入容器终端&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;容器右键 → &lt;code&gt;Attach Shell&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;查看日志&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;容器右键 → &lt;code&gt;View Logs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;编辑容器文件&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;展开容器 &lt;code&gt;Files&lt;/code&gt; 树，直接双击修改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;端口转发&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;容器右键 → &lt;code&gt;Forward Port&lt;/code&gt;，自动映射到 Windows 本地&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;浏览器打开&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;端口转发后，右键 &lt;code&gt;Open in Browser&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;七、快速检查清单&lt;/h2&gt;
&lt;p&gt;每次复用或排查问题时，按以下顺序检查：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;检查项&lt;/th&gt;
&lt;th&gt;命令/操作&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linux SSH 是否运行&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo service ssh status&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows 私钥权限&lt;/td&gt;
&lt;td&gt;&lt;code&gt;icacls C:\Users\用户名\.ssh\id_ed25519&lt;/code&gt;（应只有当前用户）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux 公钥是否正确&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cat ~/.ssh/authorized_keys&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux .ssh 权限&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ls -la ~/.ssh&lt;/code&gt;（700 + 600）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用户是否在 docker 组&lt;/td&gt;
&lt;td&gt;&lt;code&gt;groups&lt;/code&gt;（应看到 &lt;code&gt;docker&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VSCode 配置路径&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IdentityFile&lt;/code&gt; 指向私钥，不是 &lt;code&gt;.pub&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;八、配置文件模板&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Windows SSH 配置&lt;/strong&gt;（&lt;code&gt;C:\Users\用户名\.ssh\config&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host kali
    HostName 192.168.17.129
    User kali
    IdentityFile C:\Users\你的用户名\.ssh\id_ed25519
    ForwardAgent yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Linux SSH 配置&lt;/strong&gt;（&lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;，可选加固）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PubkeyAuthentication yes
PasswordAuthentication no    # 确认密钥可用后再开启，禁用密码登录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后重启 SSH：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo service ssh restart
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;按这个流程走，任何新的 Kali/Ubuntu/Debian 机器都可以在 5 分钟内配置好完整的远程 Docker 开发环境。&lt;/p&gt;
</content:encoded></item><item><title>Word 排版技巧</title><link>https://youki.bbroot.com/posts/tools/word-layout/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/tools/word-layout/</guid><description>图片题注、交叉引用、三线表制作、PDF编辑等Word排版实用技巧</description><pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文整理 Word 排版中的实用技巧，包括图片题注、交叉引用、三线表制作等。&lt;/p&gt;
&lt;h2&gt;1. 图片排版&lt;/h2&gt;
&lt;h3&gt;1.1 粘贴图片显示不全&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：粘贴图片后只显示一行，图片无法完整显示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-01.png&quot; alt=&quot;图片显示问题&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方法&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选中图片所在段落&lt;/li&gt;
&lt;li&gt;右键 → 段落&lt;/li&gt;
&lt;li&gt;将行间距改为&lt;strong&gt;单倍行距&lt;/strong&gt;或&lt;strong&gt;固定值&lt;/strong&gt;（如 20 磅）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.2 添加题注&lt;/h3&gt;
&lt;p&gt;给图片添加题注，方便引用和管理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-02.png&quot; alt=&quot;添加题注&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;右键图片 → &lt;strong&gt;插入题注&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;点击&lt;strong&gt;新建标签&lt;/strong&gt;，输入&quot;图&quot;&lt;/li&gt;
&lt;li&gt;位置选择&quot;所选项目下方&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;设置编号格式&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-03.png&quot; alt=&quot;编号设置&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;点击&lt;strong&gt;编号&lt;/strong&gt;按钮&lt;/li&gt;
&lt;li&gt;勾选&quot;包含章节号&quot;&lt;/li&gt;
&lt;li&gt;选择章节样式（如&quot;标题 1&quot;）&lt;/li&gt;
&lt;li&gt;选择分隔符（如&quot;-&quot;或&quot;.&quot;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3 交叉引用&lt;/h3&gt;
&lt;p&gt;使用交叉引用，可以在图片增删后&lt;strong&gt;自动更新编号&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-04.png&quot; alt=&quot;交叉引用&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;光标定位到引用位置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;引用&lt;/strong&gt; → &lt;strong&gt;交叉引用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;引用类型选择&quot;图&quot;&lt;/li&gt;
&lt;li&gt;选择要引用的题注&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.4 修改题注样式&lt;/h3&gt;
&lt;p&gt;修改题注的字体、字号、对齐方式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-05.png&quot; alt=&quot;修改样式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-06.png&quot; alt=&quot;样式设置&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;开始&lt;/strong&gt; → &lt;strong&gt;样式&lt;/strong&gt; 右下角展开&lt;/li&gt;
&lt;li&gt;找到&quot;题注&quot;样式，右键 → &lt;strong&gt;修改&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;设置字体（如宋体、五号）&lt;/li&gt;
&lt;li&gt;设置对齐方式（如居中）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;段落居中设置&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-07.png&quot; alt=&quot;居中设置&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;右键题注 → 段落&lt;/li&gt;
&lt;li&gt;对齐方式选择&quot;居中&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.5 批量更新编号&lt;/h3&gt;
&lt;p&gt;当图片增删后，需要批量更新所有编号。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-08.png&quot; alt=&quot;更新编号&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ctrl + A&lt;/strong&gt; 全选文档&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;F9&lt;/strong&gt; 更新域&lt;/li&gt;
&lt;li&gt;所有题注编号自动更新&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. 三线表制作&lt;/h2&gt;
&lt;p&gt;学术论文中常用的三线表样式。&lt;/p&gt;
&lt;h3&gt;2.1 新建表格样式&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-09.png&quot; alt=&quot;表格样式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;表格设计&lt;/strong&gt; → &lt;strong&gt;新建表格样式&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;设置样式名称（如&quot;三线表&quot;）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./remote-10.png&quot; alt=&quot;样式设置&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设置边框：
&lt;ul&gt;
&lt;li&gt;顶线：1.5 磅，黑色&lt;/li&gt;
&lt;li&gt;中间线：0.5 磅，黑色&lt;/li&gt;
&lt;li&gt;底线：1.5 磅，黑色&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./remote-11.png&quot; alt=&quot;边框设置&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;应用范围选择&quot;整张表格&quot;&lt;/li&gt;
&lt;li&gt;点击确定保存&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.2 应用样式&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;选中表格&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;表格设计&lt;/strong&gt; → 找到自定义的&quot;三线表&quot;样式&lt;/li&gt;
&lt;li&gt;点击应用&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. PDF 编辑&lt;/h2&gt;
&lt;p&gt;如果需要编辑 PDF 文件，可以使用 Adobe Acrobat。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-12.png&quot; alt=&quot;Acrobat&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常用功能&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PDF 转 Word&lt;/li&gt;
&lt;li&gt;合并/拆分 PDF&lt;/li&gt;
&lt;li&gt;添加水印&lt;/li&gt;
&lt;li&gt;表单填写&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;h3&gt;技巧速查表&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;技巧&lt;/th&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;图片显示不全&lt;/td&gt;
&lt;td&gt;段落 → 行间距改为单倍行距&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;添加题注&lt;/td&gt;
&lt;td&gt;右键 → 插入题注 → 新建标签&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自动编号&lt;/td&gt;
&lt;td&gt;插入题注时勾选&quot;包含章节号&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;交叉引用&lt;/td&gt;
&lt;td&gt;引用 → 交叉引用 → 选择题注&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;批量更新&lt;/td&gt;
&lt;td&gt;Ctrl+A → F9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;三线表&lt;/td&gt;
&lt;td&gt;新建表格样式 → 设置顶线/底线 1.5 磅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF 编辑&lt;/td&gt;
&lt;td&gt;使用 Adobe Acrobat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;注意事项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;题注样式修改后，所有题注会自动更新&lt;/li&gt;
&lt;li&gt;交叉引用需要手动更新（F9）&lt;/li&gt;
&lt;li&gt;三线表的中间线通常比顶底线细&lt;/li&gt;
&lt;li&gt;建议在排版前先设置好样式，避免后期大量修改&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Cloudflare 边缘应用：一个 IP 承载数千网站</title><link>https://youki.bbroot.com/posts/network/cloudflare-ip-edge-apps/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/cloudflare-ip-edge-apps/</guid><description>Cloudflare 一个 IP 承载数千网站的原理：SNI/Host 路由、Anycast、边缘计算</description><pubDate>Fri, 10 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;参考讨论：&lt;a href=&quot;https://github.com/XTLS/BBS/issues/23&quot;&gt;XTLS/BBS#23&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Cloudflare 一个 IP（如 &lt;code&gt;104.16.x.x&lt;/code&gt;）能同时承载数千甚至上万个网站，背后是 CDN 的几项核心技术叠加：反向代理 + SNI/Host 路由、Anycast、边缘计算。&lt;/p&gt;
&lt;h2&gt;1. 反向代理（Reverse Proxy）+ SNI 路由&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;用户访问 site1.com ──┐
用户访问 site2.com ──┼──→ Cloudflare IP (104.16.x.x) ──→ 根据信息分发到不同源站
用户访问 site3.com ──┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键技术：SNI + Host 头双重路由。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;第一步：TLS 握手（SNI 决定证书）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;你的浏览器 → Cloudflare:443

Client Hello:
  - SNI: &quot;site1.com&quot;  ← 明文传输

Cloudflare 根据 SNI：
  - 找到 site1.com 的证书
  - 返回 site1.com 的 TLS 证书
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第二步：HTTP 请求（Host 头决定源站）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HTTP GET / HTTP/1.1
Host: site1.com  ← 决定请求转发给哪个源服务器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;结果：&lt;/strong&gt; 同一个 IP，根据 SNI 和 Host 头的不同，可以服务无限个域名。&lt;/p&gt;
&lt;h2&gt;2. Anycast 技术（全球共享 IP）&lt;/h2&gt;
&lt;p&gt;这是 Cloudflare 的独特之处：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;传统 DNS：
- site.com → 1.2.3.4（美国服务器）
- 中国用户访问慢

Cloudflare Anycast：
- 104.16.249.249 同时在全球广播：
  - 北京用户访问 → 被路由到北京机房
  - 纽约用户访问 → 被路由到纽约机房
  - 伦敦用户访问 → 被路由到伦敦机房
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;同一个 IP 地址，物理上对应全球数百个数据中心。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;3. 边缘计算（Edge Computing）&lt;/h2&gt;
&lt;p&gt;对于 Workers/Pages：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;用户请求 example.worker.dev
           ↓
到达最近的 Cloudflare 边缘节点
           ↓
不转发到源站，直接在边缘节点执行 JavaScript 代码
           ↓
返回结果
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;一个 IP 既是反向代理入口，又是代码执行环境。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;4. 边缘应用为什么几乎不占资源&lt;/h2&gt;
&lt;p&gt;边缘应用（Worker）是 &lt;strong&gt;Serverless 无服务器架构&lt;/strong&gt;，特点是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不是一直开着一台服务器等请求&lt;/li&gt;
&lt;li&gt;来一个请求，临时启动一段代码&lt;/li&gt;
&lt;li&gt;执行完（通常几毫秒～几十毫秒）直接销毁&lt;/li&gt;
&lt;li&gt;不请求时，&lt;strong&gt;完全不占资源&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个节点上可以同时跑几万个用户的边缘函数，资源是&lt;strong&gt;极度共享、极度轻量化&lt;/strong&gt;的。对 Cloudflare 来说，这点算力几乎可以忽略不计。&lt;/p&gt;
&lt;h2&gt;5. 写 API、鉴权、查数据库会不会很耗资源？&lt;/h2&gt;
&lt;p&gt;并不会，而且架构是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;边缘节点：只做&lt;strong&gt;轻逻辑&lt;/strong&gt;（校验、路由、拼接、简单计算）&lt;/li&gt;
&lt;li&gt;数据库：依然放在&lt;strong&gt;中心服务器 / 云数据库&lt;/strong&gt;（比如阿里云、腾讯云、PlanetScale）&lt;/li&gt;
&lt;li&gt;边缘节点 → 发请求 → 中心数据库拿数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不是每个边缘节点都装数据库。只是&lt;strong&gt;用户到边缘近&lt;/strong&gt;，边缘到数据库那一步是厂商优化过的内网专线，整体依然很快。&lt;/p&gt;
&lt;h2&gt;6. 对比传统后端&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;传统后端&lt;/strong&gt;：买 1 台 / 3 台 / 10 台服务器，自己扛所有流量，崩了要自己重启扩容&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;边缘后端&lt;/strong&gt;：厂商全球几百个节点自动兜底，流量再大也自动分配，不用管服务器数量&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>VMware Workstation 下载指南</title><link>https://youki.bbroot.com/posts/tools/vmware-download/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/tools/vmware-download/</guid><description>VMware Workstation Pro 下载与中文界面配置指南，个人使用免费</description><pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;VMware 于 2023 年 11 月被 Broadcom 收购。2024 年 5 月起，&lt;strong&gt;VMware Workstation Pro 个人使用免费&lt;/strong&gt;（商业用途仍需付费）。原 VMware Workstation Player 已合并进 Pro 版本。&lt;/p&gt;
&lt;h2&gt;下载步骤&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;访问 &lt;a href=&quot;https://support.broadcom.com&quot;&gt;Broadcom 支持门户&lt;/a&gt;，注册并登录账号&lt;/li&gt;
&lt;li&gt;进入产品页面，找到 &lt;strong&gt;VMware Workstation Pro&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;选择版本（当前最新为 17.6.x）和操作系统（Windows / Linux）&lt;/li&gt;
&lt;li&gt;下载安装包&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://knowledge.broadcom.com/external/article/368734&quot;&gt;Broadcom 官方说明&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;运行下载的安装程序，按向导完成安装即可。建议使用默认安装路径，安装过程中勾选&quot;增强型键盘驱动&quot;以获得更好的输入体验。&lt;/p&gt;
&lt;h2&gt;配置中文界面&lt;/h2&gt;
&lt;p&gt;安装完成后如需切换为中文界面：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开 VMware Workstation&lt;/li&gt;
&lt;li&gt;菜单栏 → &lt;strong&gt;Edit&lt;/strong&gt; → &lt;strong&gt;Preferences&lt;/strong&gt; → &lt;strong&gt;Language&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;选择 &lt;strong&gt;简体中文&lt;/strong&gt;，重启软件生效&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;详细图文教程参考：&lt;a href=&quot;https://blog.timxs.com/archives/ebcamx05&quot;&gt;VMware 中文语言配置&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;务必从 &lt;a href=&quot;https://support.broadcom.com&quot;&gt;Broadcom 官网&lt;/a&gt; 下载，避免第三方来源的恶意软件&lt;/li&gt;
&lt;li&gt;仅限个人非商业用途免费，商业环境需购买许可&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>校园网 IP 切换原理</title><link>https://youki.bbroot.com/posts/network/campus-network-ip/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/campus-network-ip/</guid><description>校园网 NAT 出口会频繁切换公网 IP，但日常上网几乎无感知——已建立的连接不受影响，只有新连接才使用新 IP</description><pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;很多人会注意到一个现象：在校园网里隔一段时间查一下自己的公网 IP，会发现它在悄悄变化，但自己刷网页、登微信、打游戏却完全没有感知。这篇笔记拆解这背后的原理：NAT 出口为什么频繁换 IP，以及为什么用户感觉不到。&lt;/p&gt;
&lt;h2&gt;1. 校园网为什么用 NAT&lt;/h2&gt;
&lt;p&gt;校园网通常只有一个或少数几个&lt;strong&gt;公网 IP&lt;/strong&gt;，但网内可能有成千上万台设备（手机、电脑、平板）。要把这么多设备接上互联网，靠的是 &lt;strong&gt;NAT（网络地址转换）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每台设备拿到的是一个&lt;strong&gt;内网 IP&lt;/strong&gt;（如 &lt;code&gt;10.x.x.x&lt;/code&gt;、&lt;code&gt;172.16.x.x&lt;/code&gt;、&lt;code&gt;192.168.x.x&lt;/code&gt;），这些地址在公网上无法路由。&lt;/li&gt;
&lt;li&gt;设备访问外网时，出口的 NAT 设备（路由器/网关）把&lt;strong&gt;源地址从内网 IP 改写成公网 IP&lt;/strong&gt;，并记录一条&quot;内网 IP+端口 ↔ 公网 IP+端口&quot;的映射。&lt;/li&gt;
&lt;li&gt;对端回包时，NAT 再根据映射表把目的地址翻译回内网 IP，送到对应设备。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，所有设备的流量对外都&quot;伪装&quot;成从那几个公网 IP 发出，&lt;strong&gt;对外呈现的公网 IP 是 NAT 出口决定的&lt;/strong&gt;，而不是你的设备决定的。&lt;/p&gt;
&lt;h2&gt;2. 为什么校园网的公网 IP 会频繁切换&lt;/h2&gt;
&lt;p&gt;公网 IP 的频繁变化，通常由这几个机制叠加造成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DHCP 短租约&lt;/strong&gt;：出口公网 IP 本身可能也是向运营商 DHCP 租来的，租约一过期就可能续到另一个 IP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多出口负载均衡&lt;/strong&gt;：大型校园网往往接了多条运营商线路（电信、联通、教育网等），网关根据负载把流量分到不同出口，于是你看到的公网 IP 在不同出口之间跳。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;故障/拥塞切换&lt;/strong&gt;：某条出口线路抖动或拥塞时，网关会自动把流量切到健康线路，公网 IP 跟着变。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以&quot;公网 IP 变了&quot;在校园网里是常态，而非异常。&lt;/p&gt;
&lt;h2&gt;3. 为什么已建立的连接不受影响&lt;/h2&gt;
&lt;p&gt;关键在于：NAT 设备对&lt;strong&gt;已经打开的应用、已经建立的 TCP 连接&lt;/strong&gt;（比如正在刷的网页、登着的微信、打着的游戏），会保留这个会话的映射关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪怕公网 IP 变了，已经建立的连接不会断，应用完全不受影响，用着和平时一模一样。&lt;/li&gt;
&lt;li&gt;只有当发起&lt;strong&gt;全新的连接&lt;/strong&gt;（比如新开一个 SSH、重新登录某个服务、新开浏览器标签页），才会使用新的公网 IP。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 为什么应用本身不关心源 IP&lt;/h2&gt;
&lt;p&gt;现在的所有互联网应用（微信、浏览器、视频软件等），只需要「网络通」，完全不依赖固定的源公网 IP：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源 IP 变了，应用不会报错、不会掉线，用户察觉不到任何异常。&lt;/li&gt;
&lt;li&gt;只有&lt;strong&gt;需要「源 IP 白名单」的服务&lt;/strong&gt;（比如服务器的安全组、企业 VPN、特定后台系统），才会因为源 IP 变了而被拦截——这就是在校园网里 SSH 突然连不上服务器的一个常见根因。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 链路切换是毫秒级的&lt;/h2&gt;
&lt;p&gt;校园网的负载均衡、故障切换都是毫秒级完成的，几乎感觉不到断网，自然也不会意识到 IP 变了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;小结&lt;/strong&gt;：校园网公网 IP 频繁变化是 NAT + 多出口的正常行为；已建立的连接靠 NAT 映射表维持不断；普通应用不依赖固定源 IP 所以无感；只有依赖源 IP 白名单的服务（典型如 SSH 白名单、VPN）才会被影响。&lt;/p&gt;
</content:encoded></item><item><title>虚拟机网络配置</title><link>https://youki.bbroot.com/posts/network/vm-network-config/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/vm-network-config/</guid><description>从 IP 规划到故障修复，一文搞定 VMware 虚拟机固定 IP，覆盖 nmcli 与 network.service 两种方案</description><pubDate>Sat, 07 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、为什么需要固定 IP？最稳妥的规划策略&lt;/h2&gt;
&lt;p&gt;VMware NAT 默认通过 DHCP 分配 IP，地址会浮动，多台虚拟机同时开机还可能撞 IP，导致 SSH 频繁断连。最彻底的解决办法是：&lt;strong&gt;设置静态 IP，并故意选在 DHCP 分配池之外&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;以常见的 NAT 网段 &lt;code&gt;192.168.17.0/24&lt;/code&gt; 为例，VMware 的 DHCP 通常分配 &lt;code&gt;128-254&lt;/code&gt; 这个区间。因此安全可用的静态 IP 范围为 &lt;strong&gt;&lt;code&gt;192.168.17.3&lt;/code&gt; ~ &lt;code&gt;192.168.17.127&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;规划示例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;宿主机：&lt;code&gt;192.168.17.1&lt;/code&gt;（系统固定）&lt;/li&gt;
&lt;li&gt;NAT 网关：&lt;code&gt;192.168.17.2&lt;/code&gt;（系统固定）&lt;/li&gt;
&lt;li&gt;虚拟机 A：&lt;code&gt;192.168.17.10&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;虚拟机 B：&lt;code&gt;192.168.17.11&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样无论 DHCP 怎么分配，都不会跟你手动的地址冲突。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文示例均以目标 IP &lt;code&gt;192.168.17.10&lt;/code&gt;、网卡名 &lt;code&gt;ens160&lt;/code&gt; 为例。&lt;strong&gt;请务必将命令中的 IP 和网卡名替换为你实际规划的值。&lt;/strong&gt;
不确定网卡名？执行 &lt;code&gt;ip addr&lt;/code&gt; 或 &lt;code&gt;nmcli connection show&lt;/code&gt; 查看，常见名有 &lt;code&gt;ens33&lt;/code&gt;、&lt;code&gt;eth0&lt;/code&gt; 等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;二、前置检查：确保虚拟链路与 VMware 服务正常&lt;/h2&gt;
&lt;p&gt;很多“配置完不通”的问题，根源不在 Linux 配置，而在虚拟机硬件或 Windows 服务。正式配 IP 之前，先过一遍下面几点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 虚拟机网卡硬件设置&lt;/strong&gt;（所有方案通用）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关闭虚拟机（不是暂停），打开「虚拟机设置」→「网络适配器」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;必须勾选&lt;/strong&gt; ✅「已连接」、✅「启动时连接」。&lt;/li&gt;
&lt;li&gt;确认网络连接方式为 &lt;strong&gt;「NAT 模式」&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 检查宿主机 VMware 核心服务&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Windows 中 &lt;code&gt;Win+R&lt;/code&gt; 输入 &lt;code&gt;services.msc&lt;/code&gt;，找到下面两个服务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VMware NAT Service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VMware DHCP Service&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;确保它们都是「正在运行」状态。如有异常，右键重新启动。&lt;br /&gt;
如果之前修改过虚拟网络编辑器（如恢复了默认设置），可能需要重启这两个服务甚至宿主机。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 启动虚拟机，快速确认网卡链路&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip addr show ens160   # 记得替换网卡名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果输出中包含 &lt;code&gt;state UP&lt;/code&gt;，说明链路已通，可以进入下一步。&lt;br /&gt;
如果始终是 &lt;code&gt;state DOWN&lt;/code&gt;，回第一步重新确认 VMware 设置与服务。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;三、方案 A：NetworkManager 正常时的推荐做法（nmcli 一键搞定）&lt;/h2&gt;
&lt;p&gt;大多数现代 Linux 发行版（CentOS 7+、Fedora、Ubuntu 等）默认使用 NetworkManager 管理网络。这种情况下 &lt;code&gt;nmcli&lt;/code&gt; 是最安全、最不容易产生配置冲突的方式。&lt;/p&gt;
&lt;p&gt;直接把下面命令里的 &lt;code&gt;192.168.17.10&lt;/code&gt; 和 &lt;code&gt;ens160&lt;/code&gt; 替换成你的实际值，整个复制执行即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CONN=&quot;ens160&quot;  # 先确认网卡名

# 1. 绑定静态 IP 和子网掩码
sudo nmcli connection modify &quot;$CONN&quot; ipv4.addresses 192.168.17.10/24

# 2. 设置网关
sudo nmcli connection modify &quot;$CONN&quot; ipv4.gateway 192.168.17.2

# 3. 设置 DNS（推荐公共 DNS）
sudo nmcli connection modify &quot;$CONN&quot; ipv4.dns &quot;8.8.8.8,114.114.114.114&quot;

# 4. 改获取方式为手动
sudo nmcli connection modify &quot;$CONN&quot; ipv4.method manual

# 5. 可选：禁用 IPv6，避免其干扰连通性测试
sudo nmcli connection modify &quot;$CONN&quot; ipv6.method disabled

# 6. 重启网卡
sudo nmcli connection down &quot;$CONN&quot; &amp;amp;&amp;amp; sudo nmcli connection up &quot;$CONN&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行后验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip addr show ens160 | grep 192.168.17.10   # 确认 IP 已绑定
ping -c 3 baidu.com                        # 确认外网域名解析正常
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此后即便重启虚拟机，IP 也不会变，MobaXterm 之类的工具直接用 &lt;code&gt;192.168.17.10&lt;/code&gt; 连接即可。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、方案 B：当 NetworkManager 罢工时（未托管、device not found）&lt;/h2&gt;
&lt;p&gt;有时候系统会出现以下症状：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nmcli device status&lt;/code&gt; 显示 &lt;code&gt;ens160&lt;/code&gt; 为 &lt;code&gt;unmanaged&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;尝试 &lt;code&gt;nmcli connection up&lt;/code&gt; 提示 &lt;code&gt;No suitable device found&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;修改 &lt;code&gt;/etc/NetworkManager/...&lt;/code&gt; 的托管配置无效；&lt;/li&gt;
&lt;li&gt;甚至 VMware 中「已连接」都勾不上，网卡始终 &lt;code&gt;DOWN&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是典型的 &lt;strong&gt;NetworkManager 未托管 + 虚拟链路异常&lt;/strong&gt;，二合一故障。此时最直接的办法是&lt;strong&gt;跳过 NetworkManager，用传统 &lt;code&gt;network.service&lt;/code&gt; 接管&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;兼容性警告&lt;/strong&gt;&lt;br /&gt;
方案 B 依赖传统 &lt;code&gt;network-scripts&lt;/code&gt; 包，在 &lt;strong&gt;CentOS 7.x&lt;/strong&gt; 上适用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你的系统是&lt;strong&gt;最小化安装&lt;/strong&gt;，可能未包含此包，需先执行 &lt;code&gt;sudo yum install network-scripts&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果你使用的是 &lt;strong&gt;RHEL 9 / Rocky Linux 9 / CentOS Stream 9&lt;/strong&gt; 及更新版本，该包已被官方移除，方案 B 不可用。请返回方案 A 排查 NM 的配置问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4.1 确保链路恢复（若仍 DOWN）&lt;/h3&gt;
&lt;p&gt;如果第一节的前置检查已通过，网卡已是 &lt;code&gt;UP&lt;/code&gt; 状态可略过。否则先在虚拟机设置中确认「已连接」已勾，并重启 VMware NAT/DHCP 服务，直至 &lt;code&gt;ip addr show&lt;/code&gt; 看到 &lt;code&gt;state UP&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;4.2 停用 NetworkManager，手动固定 IP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1. 彻底停用 NetworkManager
sudo systemctl stop NetworkManager
sudo systemctl disable NetworkManager

# 2. 临时配通 IP（立即生效）
sudo ip addr add 192.168.17.10/24 dev ens160
sudo ip link set ens160 up
sudo ip route add default via 192.168.17.2 dev ens160
echo -e &quot;nameserver 8.8.8.8\nnameserver 114.114.114.114&quot; | sudo tee /etc/resolv.conf

# 3. 排除 IPv6 干扰：若 ping 外网失败，先临时禁用 IPv6 再测
sudo sysctl -w net.ipv6.conf.ens160.disable_ipv6=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在测试 &lt;code&gt;ping 192.168.17.2&lt;/code&gt; 和 &lt;code&gt;ping baidu.com&lt;/code&gt;，应该都能通。&lt;/p&gt;
&lt;h3&gt;4.3 让配置永久生效（重启后 IP 依旧）&lt;/h3&gt;
&lt;p&gt;停止 NetworkManager 后，系统会转而使用传统 &lt;code&gt;network&lt;/code&gt; 服务读取 &lt;code&gt;/etc/sysconfig/network-scripts/ifcfg-*&lt;/code&gt; 文件。我们直接写入正确配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo tee /etc/sysconfig/network-scripts/ifcfg-ens160 &amp;lt;&amp;lt;&apos;EOF&apos;
TYPE=Ethernet
BOOTPROTO=static
NAME=ens160
DEVICE=ens160
ONBOOT=yes
IPADDR=192.168.17.10
PREFIX=24
GATEWAY=192.168.17.2
DNS1=8.8.8.8
DNS2=114.114.114.114
IPV6INIT=no
PEERDNS=no
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关于 &lt;code&gt;PEERDNS=no&lt;/code&gt; 的说明&lt;/strong&gt;&lt;br /&gt;
加上此参数后，网络服务启动时不会自动修改 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;，DNS 将严格使用此文件中定义的 &lt;code&gt;DNS1&lt;/code&gt; 和 &lt;code&gt;DNS2&lt;/code&gt;。这能确保你手动设定的 DNS 不被其他服务覆盖。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后启用并启动传统网络服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable network
sudo systemctl restart network
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，如果之前禁用了 IPv6，可将其设为永久生效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo sysctl -w net.ipv6.conf.ens160.disable_ipv6=1 | sudo tee -a /etc/sysctl.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，无论是否重启，虚拟机都会固定使用 &lt;code&gt;192.168.17.10&lt;/code&gt;，且不会再被 NetworkManager 干扰。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、快速验证清单&lt;/h2&gt;
&lt;p&gt;无论用哪个方案，最后都跑一遍：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip addr show ens160          # 看到 192.168.17.10/24
ping 192.168.17.2 -c 2       # 网关通
ping www.baidu.com -c 4      # 外网域名解析通
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全通则成功。然后就可以打开 MobaXterm，新建 SSH Session，Host 填入 &lt;code&gt;192.168.17.10&lt;/code&gt;，享受稳定的连接。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;六、故障速查表&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;可能原因&lt;/th&gt;
&lt;th&gt;快速处置&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;网卡始终 &lt;code&gt;DOWN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;虚拟机设置中未勾选「已连接」或 VMware 服务未运行&lt;/td&gt;
&lt;td&gt;重启 VMware NAT/DHCP 服务，确认勾选已打&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nmcli&lt;/code&gt; 报 &lt;code&gt;unmanaged&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NetworkManager 配置禁止管理该网卡&lt;/td&gt;
&lt;td&gt;若系统版本支持，直接用方案 B；否则排查 NM 配置文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ping&lt;/code&gt; 网关通但域名解析失败&lt;/td&gt;
&lt;td&gt;DNS 配置错误或被覆盖&lt;/td&gt;
&lt;td&gt;确认 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 内容；使用方案 B 时检查 &lt;code&gt;ifcfg&lt;/code&gt; 中 &lt;code&gt;PEERDNS&lt;/code&gt; 设置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ping&lt;/code&gt; IP 通但域名不通&lt;/td&gt;
&lt;td&gt;IPv6 路由异常，请求优先走了无响应的 IPv6&lt;/td&gt;
&lt;td&gt;尝试 &lt;code&gt;ping -4 baidu.com&lt;/code&gt; 确认，然后禁用 IPv6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重启后 IP 丢失&lt;/td&gt;
&lt;td&gt;配置未持久化或 NM 重新接管&lt;/td&gt;
&lt;td&gt;检查 &lt;code&gt;/etc/sysconfig/network-scripts/ifcfg-ens160&lt;/code&gt; 是否存在且内容正确；若用方案 A，确保 &lt;code&gt;ipv4.method&lt;/code&gt; 为 &lt;code&gt;manual&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多虚拟机互相抢 IP&lt;/td&gt;
&lt;td&gt;静态 IP 落在了 DHCP 池内&lt;/td&gt;
&lt;td&gt;将静态 IP 调整至 DHCP 池外（如 &lt;code&gt;3~127&lt;/code&gt;）；除非所有虚拟机均已手工配置静态 IP，否则不要关闭 VMware DHCP 服务&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>让手机连上热点直接使用主机的 Clash</title><link>https://youki.bbroot.com/posts/network/vm-use-host-clash/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/vm-use-host-clash/</guid><description>让手机/虚拟机连 Windows 热点直接走 Clash TUN</description><pubDate>Sat, 07 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Windows 上实现&lt;strong&gt;透明网关/全局路由&lt;/strong&gt;的方案，核心思路是：&lt;strong&gt;让 Windows 热点的上游不是物理网卡，而是 Clash 的 TUN 虚拟网卡&lt;/strong&gt;。这样手机连上热点后无需任何代理设置，所有流量强制经过 TUN。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;p&gt;Windows 自带热点的默认逻辑是：热点网卡 → 物理网卡（WiFi/以太网）→ 互联网。&lt;br /&gt;
我们要改成：热点网卡 → &lt;strong&gt;Clash TUN 虚拟网卡&lt;/strong&gt; → 互联网。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;已知限制&lt;/strong&gt;：Clash Verge Rev 的 TUN 与 Windows ICS（网络共享）存在兼容性冲突，部分版本下同时开启会导致本机和热点设备都无法上网。以下方案在 Clash Verge Rev 2.5.1上可行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;操作步骤&lt;/h3&gt;
&lt;h4&gt;1. 先开启 Clash Verge 的 TUN 模式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;打开 Clash Verge → 设置 → 开启 &lt;strong&gt;TUN 模式&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保已安装服务模式（Service Mode），TUN 网卡正常创建（设备管理器中能看到如 &lt;code&gt;Meta Tunnel&lt;/code&gt; 或 &lt;code&gt;Clash&lt;/code&gt; 字样的虚拟网卡）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确认电脑本机可以正常翻墙&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 再开启 Windows 移动热点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;设置 → 网络和 Internet → 移动热点 → 开启&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;记住热点名称和密码，&lt;strong&gt;此时先不要连接手机&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 关键：在 TUN 网卡上配置 ICS 共享&lt;/h4&gt;
&lt;p&gt;按 &lt;code&gt;Win + R&lt;/code&gt;，输入 &lt;code&gt;ncpa.cpl&lt;/code&gt; 回车，打开网络连接面板：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;找到 &lt;strong&gt;Clash 的 TUN 虚拟网卡&lt;/strong&gt;（名称通常包含 &lt;code&gt;Meta Tunnel&lt;/code&gt;、&lt;code&gt;Clash&lt;/code&gt;、&lt;code&gt;mihomo&lt;/code&gt; 或 &lt;code&gt;Wintun&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右键 → &lt;strong&gt;属性&lt;/strong&gt; → 切换到 &lt;strong&gt;共享&lt;/strong&gt; 选项卡&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;勾选 &lt;strong&gt;&quot;允许其他网络用户通过此计算机的 Internet 连接来连接&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;strong&gt;&quot;家庭网络连接&quot;&lt;/strong&gt; 下拉框中，选择&lt;strong&gt;刚才开启热点生成的网卡&lt;/strong&gt;（通常叫 &lt;code&gt;本地连接*&lt;/code&gt;、&lt;code&gt;Microsoft Wi-Fi Direct Virtual Adapter&lt;/code&gt; 或 &lt;code&gt;Local Area Connection&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定保存&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;顺序很重要&lt;/strong&gt;：必须是 &lt;strong&gt;先开 TUN → 再开热点 → 最后设置共享&lt;/strong&gt;。如果反过来，Windows 的 ICS 会与 TUN 冲突。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;4. 手机连接热点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;手机直接连接电脑热点，&lt;strong&gt;不需要设置任何代理&lt;/strong&gt;（不要填 IP 和端口）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开浏览器测试，所有流量应已被 TUN 透明接管&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Trap、中断与系统调用详解</title><link>https://youki.bbroot.com/posts/cs/trap-interrupt-syscall/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/cs/trap-interrupt-syscall/</guid><description>Trap 与中断的区别、系统调用完整流程（从库函数到内核态返回）</description><pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;陷入 (Trap)&lt;/h3&gt;
&lt;p&gt;陷入是由&lt;strong&gt;程序主动执行&lt;/strong&gt;特定指令（如系统调用指令 &lt;code&gt;int 0x80&lt;/code&gt; 或 &lt;code&gt;syscall&lt;/code&gt;）触发的&lt;strong&gt;同步&lt;/strong&gt;事件。它让CPU从用户态切换到内核态，执行内核代码。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：&lt;strong&gt;同步&lt;/strong&gt;（由程序主动发起）、&lt;strong&gt;软件驱动&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的&lt;/strong&gt;：实现用户程序向操作系统请求服务（如读写文件、创建进程）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;特权指令 (Privileged Instruction)&lt;/h3&gt;
&lt;p&gt;特权指令是只能在&lt;strong&gt;内核态&lt;/strong&gt;（最高权限）下执行的指令，如修改页表基址寄存器、开关中断等。如果在用户态执行这些指令，CPU会触发一个&lt;strong&gt;保护异常&lt;/strong&gt;（属于中断的一种），阻止操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：&lt;strong&gt;权限限制&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的&lt;/strong&gt;：保护系统资源不被用户程序随意修改，保证系统安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;区别总结&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;中断&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;陷入&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;特权指令&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;本质&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;事件（Event）&lt;/td&gt;
&lt;td&gt;事件（Event）&lt;/td&gt;
&lt;td&gt;指令（Instruction）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;触发源&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;硬件/异常&lt;/td&gt;
&lt;td&gt;软件（程序）&lt;/td&gt;
&lt;td&gt;程序（试图执行）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;同步性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;异步&lt;/td&gt;
&lt;td&gt;同步&lt;/td&gt;
&lt;td&gt;同步&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;作用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;响应外部变化&lt;/td&gt;
&lt;td&gt;请求系统服务&lt;/td&gt;
&lt;td&gt;管理硬件资源&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;关系与交互&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;当用户程序需要执行特权操作（如读写磁盘）时，它不能直接执行特权指令，而是通过&lt;strong&gt;陷入&lt;/strong&gt;（系统调用）进入内核，由内核代为执行。&lt;/li&gt;
&lt;li&gt;如果用户程序&lt;strong&gt;非法执行&lt;/strong&gt;特权指令，会触发一个&lt;strong&gt;中断&lt;/strong&gt;（保护异常），导致程序被终止。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;触发陷入（进入内核）只需要执行一条特定的指令（如 &lt;code&gt;int 0x80&lt;/code&gt;, &lt;code&gt;syscall&lt;/code&gt;）。&lt;strong&gt;但&lt;/strong&gt;，这条指令只是一个“&lt;strong&gt;门铃&lt;/strong&gt;”，按下门铃后，执行的是操作系统内核中&lt;strong&gt;预先写好的、功能完整且复杂&lt;/strong&gt;的服务例程&lt;/h3&gt;
&lt;p&gt;这是一个经典的交互流程，以“写文件”这个系统调用为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 用户程序
int main() {
    write(fd, buffer, size); // 1. 调用库函数
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;发生的步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;库函数包装&lt;/strong&gt;：&lt;code&gt;write&lt;/code&gt; 是C库函数，它的&lt;strong&gt;核心&lt;/strong&gt;工作是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将系统调用号（标识是&lt;code&gt;write&lt;/code&gt;）、参数（&lt;code&gt;fd&lt;/code&gt;, &lt;code&gt;buffer&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt;）放到约定好的寄存器或栈中。&lt;/li&gt;
&lt;li&gt;执行一条 &lt;strong&gt;&lt;code&gt;trap&lt;/code&gt; 指令&lt;/strong&gt;（如 &lt;code&gt;syscall&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;这条指令是用户态最后一条指令&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;陷入与切换&lt;/strong&gt;：CPU执行 &lt;code&gt;syscall&lt;/code&gt; 指令，硬件自动完成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;切换到内核态。&lt;/li&gt;
&lt;li&gt;保存用户程序现场（寄存器、程序计数器等）。&lt;/li&gt;
&lt;li&gt;跳转到&lt;strong&gt;操作系统内核预先设定好的统一入口&lt;/strong&gt;（系统调用处理程序）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内核执行复杂逻辑&lt;/strong&gt;：现在完全在内核态运行，操作系统开始工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分发&lt;/strong&gt;：根据放在寄存器里的系统调用号，查表找到对应的服务函数，比如 &lt;code&gt;sys_write&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行核心服务&lt;/strong&gt;：&lt;code&gt;sys_write&lt;/code&gt; 是操作系统内核里一个复杂的函数，它可能要做：
&lt;ul&gt;
&lt;li&gt;参数检查（缓冲区是否有效？文件描述符是否合法？）。&lt;/li&gt;
&lt;li&gt;从用户态缓冲区拷贝数据到内核（因为内核不能直接操作用户内存）。&lt;/li&gt;
&lt;li&gt;文件系统操作：找到文件的inode，检查权限。&lt;/li&gt;
&lt;li&gt;驱动交互：调用硬盘驱动程序，将数据写入缓存或直接写入磁盘。&lt;/li&gt;
&lt;li&gt;进程调度：如果需要等待IO，可能会挂起当前进程，切换到其他进程运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所有这些，都是在内核态执行的特权代码，功能可以非常复杂。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;返回结果&lt;/strong&gt;：内核函数执行完毕后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将返回值（成功写入的字节数或错误码）放入约定寄存器。&lt;/li&gt;
&lt;li&gt;执行一条特殊的&lt;strong&gt;返回指令&lt;/strong&gt;（如 &lt;code&gt;sysret&lt;/code&gt; 或 &lt;code&gt;iret&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;CPU硬件恢复用户态现场，跳回用户程序 &lt;code&gt;syscall&lt;/code&gt; 指令之后继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;所以，核心思想是：&lt;/strong&gt;
&lt;strong&gt;&lt;code&gt;trap&lt;/code&gt; 指令只是一把钥匙，打开了通往内核的大门。门后是一个由操作系统构建的、拥有完整功能和最高权限的“新世界”。用户程序通过“钥匙”请求这个世界里的“居民”（内核函数）来为自己完成复杂的特权工作。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>计算机组成原理笔记</title><link>https://youki.bbroot.com/posts/cs/computer-organization/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/cs/computer-organization/</guid><description>计算机组成原理核心知识点总结：冯诺依曼架构、CPU组成、指令系统、补码运算、存储系统</description><pubDate>Fri, 20 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 计算机系统概述&lt;/h2&gt;
&lt;h3&gt;1.1 冯·诺依曼架构特点&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-08.png&quot; alt=&quot;冯诺依曼架构&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：存储程序，即将程序和数据存储在存储器中，CPU 按地址依次取出并执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;五大部件&lt;/strong&gt;：运算器、控制器、存储器、输入设备、输出设备&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;以存储器为中心&lt;/strong&gt;：早期冯·诺依曼机以运算器为中心，现代计算机以存储器为中心&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指令和数据均用二进制表示&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指令和数据存储在同一存储器中&lt;/strong&gt;（区别于哈佛架构）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.2 软件与硬件的逻辑等价性&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-05.png&quot; alt=&quot;计算机软件分类&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三个级别的语言&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语言类型&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;翻译工具&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;高级语言&lt;/td&gt;
&lt;td&gt;接近自然语言，如 C、Python&lt;/td&gt;
&lt;td&gt;编译程序（编译器）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;汇编语言&lt;/td&gt;
&lt;td&gt;用助记符表示指令&lt;/td&gt;
&lt;td&gt;汇编程序（汇编器）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;机器语言&lt;/td&gt;
&lt;td&gt;二进制代码，CPU 直接执行&lt;/td&gt;
&lt;td&gt;无需翻译&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编译程序&lt;/strong&gt;：将高级语言一次性全部翻译成汇编语言或机器语言&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解释程序&lt;/strong&gt;：将高级语言翻译成机器语言（翻译一句执行一句）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISA（指令集体系结构）&lt;/strong&gt;：定义一台计算机可以支持的指令，以及每条指令的作用&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 数据表示与运算&lt;/h2&gt;
&lt;h3&gt;2.1 原码、反码、补码&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;定点整数的编码方式&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编码方式&lt;/th&gt;
&lt;th&gt;正数&lt;/th&gt;
&lt;th&gt;负数&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;原码&lt;/td&gt;
&lt;td&gt;符号位0 + 真值绝对值&lt;/td&gt;
&lt;td&gt;符号位1 + 真值绝对值&lt;/td&gt;
&lt;td&gt;有+0和-0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;反码&lt;/td&gt;
&lt;td&gt;与原码相同&lt;/td&gt;
&lt;td&gt;符号位不变，数值位取反&lt;/td&gt;
&lt;td&gt;有+0和-0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;补码&lt;/td&gt;
&lt;td&gt;与原码相同&lt;/td&gt;
&lt;td&gt;反码+1&lt;/td&gt;
&lt;td&gt;只有一个0，范围更大&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;补码的优势&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;消除正负零&lt;/strong&gt;：原码和反码中+0和-0表示不同，补码中只有一个0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加减法统一&lt;/strong&gt;：减法可以转化为加法运算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;表示范围扩大&lt;/strong&gt;：8位补码可表示 -128 ~ +127&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;符号扩展&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正数扩展：高位补0&lt;/li&gt;
&lt;li&gt;负数扩展：高位补1&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 溢出判断&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;溢出检测方法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单符号位法&lt;/strong&gt;：操作数符号相同，结果符号相反则溢出&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双符号位法&lt;/strong&gt;：结果的两个符号位不同则溢出&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. CPU 组成与功能&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./remote-04.png&quot; alt=&quot;各硬件部件&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.1 运算器&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;部件&lt;/th&gt;
&lt;th&gt;全称&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ACC&lt;/td&gt;
&lt;td&gt;累加计数器&lt;/td&gt;
&lt;td&gt;存放操作数、运算结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MQ&lt;/td&gt;
&lt;td&gt;乘商寄存器&lt;/td&gt;
&lt;td&gt;乘法运算时存放乘数，除法时存放商&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;通用寄存器&lt;/td&gt;
&lt;td&gt;存放操作数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALU&lt;/td&gt;
&lt;td&gt;算术逻辑单元&lt;/td&gt;
&lt;td&gt;实现各种算术运算、逻辑运算&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3.2 控制器&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;部件&lt;/th&gt;
&lt;th&gt;全称&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PC&lt;/td&gt;
&lt;td&gt;程序计数器&lt;/td&gt;
&lt;td&gt;存放下一条指令的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IR&lt;/td&gt;
&lt;td&gt;指令寄存器&lt;/td&gt;
&lt;td&gt;存放当前正在执行的指令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CU&lt;/td&gt;
&lt;td&gt;控制单元&lt;/td&gt;
&lt;td&gt;分析指令，给出控制信号&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;重要说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PC&lt;/strong&gt; 自动指向下一条指令的地址（取指后 PC+1）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IR&lt;/strong&gt; 保存当前正在执行的指令&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CU&lt;/strong&gt; 是控制器的核心，负责分析指令并产生控制信号&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 主存储器&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./remote-06.png&quot; alt=&quot;主存储器基本组成&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4.1 基本组成&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;存储体&lt;/strong&gt;：数据在存储体内按地址存储&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键寄存器&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MAR（主存地址寄存器）&lt;/strong&gt;：用于存放要访问的存储单元的地址，位数反映存储单元的个数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MDR（主存数据寄存器）&lt;/strong&gt;：用于暂存从存储体读出或写入的数据，位数 = 存储字长&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;基本概念&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;定义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;存储单元&lt;/td&gt;
&lt;td&gt;每个存储单元存放一串二进制代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储字&lt;/td&gt;
&lt;td&gt;存储单元中二进制代码的组合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储字长&lt;/td&gt;
&lt;td&gt;存储单元中二进制代码的位数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储元&lt;/td&gt;
&lt;td&gt;存储二进制的电子元件，每个存储元可存 1bit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MAR = 4位 → 共有 2^4 = 16 个存储单元&lt;/li&gt;
&lt;li&gt;MDR = 16位 → 每个存储单元可存放 16bit&lt;/li&gt;
&lt;li&gt;1个字（word）= 16bit&lt;/li&gt;
&lt;li&gt;1字节（Byte）= 8bit，1B = 1Byte，1b = 1bit&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 指令系统&lt;/h2&gt;
&lt;h3&gt;5.1 指令格式&lt;/h3&gt;
&lt;p&gt;指令由&lt;strong&gt;操作码&lt;/strong&gt;和&lt;strong&gt;地址码&lt;/strong&gt;组成：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;操作码（OP）&lt;/td&gt;
&lt;td&gt;指明指令的操作类型（如加法、传送）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;地址码（Ad）&lt;/td&gt;
&lt;td&gt;指明操作数的地址或下一条指令的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;地址码的结构&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| 操作码 | 地址码1 | 地址码2 | 地址码3 | 地址码4 |
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 寻址方式&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;寻址方式&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;立即寻址&lt;/td&gt;
&lt;td&gt;地址码即为操作数&lt;/td&gt;
&lt;td&gt;速度快&lt;/td&gt;
&lt;td&gt;地址码位数限制操作数范围&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;直接寻址&lt;/td&gt;
&lt;td&gt;地址码给出操作数的内存地址&lt;/td&gt;
&lt;td&gt;简单&lt;/td&gt;
&lt;td&gt;地址码位数限制寻址范围&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;间接寻址&lt;/td&gt;
&lt;td&gt;地址码给出的内存地址中存放的是操作数的地址&lt;/td&gt;
&lt;td&gt;寻址范围大&lt;/td&gt;
&lt;td&gt;访存次数多，速度慢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;寄存器寻址&lt;/td&gt;
&lt;td&gt;地址码给出寄存器编号&lt;/td&gt;
&lt;td&gt;速度快&lt;/td&gt;
&lt;td&gt;寄存器数量有限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;寄存器间接寻址&lt;/td&gt;
&lt;td&gt;地址码给出的寄存器中存放的是操作数的地址&lt;/td&gt;
&lt;td&gt;寻址范围大&lt;/td&gt;
&lt;td&gt;需要访存&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;6. CPU 工作过程&lt;/h2&gt;
&lt;h3&gt;6.1 指令执行流程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-01.png&quot; alt=&quot;CPU工作过程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;指令执行的基本步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;取指周期&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(PC) → MAR：将程序计数器的内容送到主存地址寄存器&lt;/li&gt;
&lt;li&gt;M(MAR) → MDR：从存储体读取指令到主存数据寄存器&lt;/li&gt;
&lt;li&gt;(MDR) → IR：将指令送到指令寄存器&lt;/li&gt;
&lt;li&gt;OP(IR) → CU：将操作码送到控制单元分析&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;执行周期&lt;/strong&gt;（以取数指令为例）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ad(IR) → MAR：将地址码送到主存地址寄存器&lt;/li&gt;
&lt;li&gt;M(MAR) → MDR：从存储体读取数据&lt;/li&gt;
&lt;li&gt;(MDR) → ACC：将数据送到累加器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.2 取数指令详解&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-03.png&quot; alt=&quot;取数指令执行&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;取数&quot;指令的执行&lt;/strong&gt;（从主存指定地址处取数）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;取指周期（必经步骤）：
    (PC) → MAR
    M(MAR) → MDR
    (MDR) → IR
    (PC) + 1 → PC
    OP(IR) → CU

执行周期（不同指令具体步骤不同）：
    Ad(IR) → MAR
    M(MAR) → MDR
    (MDR) → ACC
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;取指周期&lt;/strong&gt;：所有指令都必须经历的阶段&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行周期&lt;/strong&gt;：根据指令类型不同，具体操作不同&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PC+1&lt;/strong&gt;：在取指周期结束时自动执行，指向下一条指令&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3 乘法指令执行&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-02.png&quot; alt=&quot;乘法指令执行&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;乘法指令的执行过程&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;取指周期：
    (PC) → MAR → MDR → IR → CU

执行周期：
    Ad(IR) → MAR → MDR → MQ    # 取乘数到MQ
    (ACC) × (MQ) → ACC          # 乘法运算
    结果存入主存单元
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：乘法运算结果可能超出 ACC 的位数，需要 MQ 辅助存储。&lt;/p&gt;
&lt;h3&gt;6.4 指令周期&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;CPU 区分指令和数据的依据&lt;/strong&gt;：指令周期的不同阶段&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;取出的内容&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;取指周期&lt;/td&gt;
&lt;td&gt;指令&lt;/td&gt;
&lt;td&gt;送到 IR 分析&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行周期&lt;/td&gt;
&lt;td&gt;数据&lt;/td&gt;
&lt;td&gt;送到 ACC 或其他寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;指令周期的组成&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;取指周期&lt;/strong&gt;：从存储器取出指令&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;间址周期&lt;/strong&gt;（可选）：如果指令使用间接寻址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行周期&lt;/strong&gt;：执行指令的操作&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中断周期&lt;/strong&gt;（可选）：处理中断请求&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 总结&lt;/h2&gt;
&lt;h3&gt;7.1 核心知识点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;冯·诺依曼架构&lt;/strong&gt;：存储程序，五大部件，以存储器为中心&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU 组成&lt;/strong&gt;：运算器（ACC、MQ、X、ALU）+ 控制器（PC、IR、CU）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储系统&lt;/strong&gt;：MAR（地址）、MDR（数据）、存储体&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指令系统&lt;/strong&gt;：操作码 + 地址码，多种寻址方式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指令周期&lt;/strong&gt;：取指周期 → 执行周期，PC 自动+1&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;7.2 易错点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PC&lt;/strong&gt; 存放下一条指令的地址，不是当前指令的地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IR&lt;/strong&gt; 保存当前正在执行的指令&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MAR&lt;/strong&gt; 的位数决定存储单元的个数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MDR&lt;/strong&gt; 的位数等于存储字长&lt;/li&gt;
&lt;li&gt;取指周期取出的是&lt;strong&gt;指令&lt;/strong&gt;，执行周期取出的是&lt;strong&gt;数据&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>李宏毅机器学习笔记：线性模型与梯度下降</title><link>https://youki.bbroot.com/posts/ai/linear-model-basics/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/ai/linear-model-basics/</guid><description>李宏毅 ML/DL 课程第一节课笔记：线性模型、Loss、梯度下降、Hard Sigmoid、激活函数到深度学习</description><pubDate>Sun, 28 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文是&lt;a href=&quot;https://www.bilibili.com/video/BV1TAtwzTE1S?p=2&quot;&gt;李宏毅机器学习深度学习系列课程&lt;/a&gt;第一节课的笔记，从最简单的线性模型一路串到深度学习。&lt;/p&gt;
&lt;h2&gt;第一节课 基本概念&lt;/h2&gt;
&lt;h3&gt;1. 最简单的模型：线性模型&lt;/h3&gt;
&lt;p&gt;我们要预测的值记为 &lt;code&gt;y&lt;/code&gt;，最简单的预测方式是假设它和某个特征 &lt;code&gt;x&lt;/code&gt; 成线性关系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;y = b + w * x
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;b&lt;/code&gt;：偏置（bias）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;w&lt;/code&gt;：权重（weight）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt;：输入特征（下标 &lt;code&gt;x_1&lt;/code&gt; 表示第一个特征）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Loss：衡量一组参数好不好&lt;/h3&gt;
&lt;p&gt;有了模型，还需要一个标准来评判当前的 &lt;code&gt;w&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 好不好，这个标准就是 &lt;strong&gt;Loss（损失函数）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;label&lt;/strong&gt;：正确的值（真实标签）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算误差的方式&lt;/strong&gt;：把模型预测值和 label 做差，累加起来（例如 MAE：&lt;code&gt;e = |ŷ - y|&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./remote-01.png&quot; alt=&quot;计算误差的方式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Loss 越小，说明这组 &lt;code&gt;w&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt; 越好。&lt;strong&gt;要找到最小的 Loss，就是找到最优的 &lt;code&gt;w&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt;，方法是梯度下降（Gradient Descent）。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;3. 梯度下降：怎么更新参数&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-02.png&quot; alt=&quot;梯度下降示意&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-03.png&quot; alt=&quot;参数更新过程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;梯度下降的核心思路：算出 Loss 对每个参数的梯度（偏导），然后让参数&lt;strong&gt;沿着梯度的反方向&lt;/strong&gt;走一小步，Loss 就会下降一点。反复迭代，直到 Loss 不再明显下降。上图展示的就是这个&quot;更新参数&quot;的过程。&lt;/p&gt;
&lt;h3&gt;4. 用更复杂的模型拟合真实数据&lt;/h3&gt;
&lt;p&gt;线性模型（一条直线）往往不足以描述真实数据。结合真实情况，需要改进模型，让它具备表达非线性关系的能力。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-04.png&quot; alt=&quot;结合真实情况改进模型&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这就引出了下面要讲的 &lt;strong&gt;linear model 的扩展&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;5. Linear Model 的扩展：用蓝色折线拼出红色曲线&lt;/h3&gt;
&lt;p&gt;模型名仍叫 &lt;strong&gt;linear model&lt;/strong&gt;，但思路变了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-05.png&quot; alt=&quot;线性模型扩展&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-06.png&quot; alt=&quot;蓝色折线拼红色曲线&quot; /&gt;&lt;/p&gt;
&lt;p&gt;核心想法是：&lt;strong&gt;用多段蓝色的折线（分段线性函数）组合起来，逼近任意形状的红色曲线&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-07.png&quot; alt=&quot;蓝色折线逼近&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这种蓝色折线段的数学名字叫 &lt;strong&gt;Hard Sigmoid&lt;/strong&gt;——形状像一个拉长的 S。&lt;/p&gt;
&lt;h3&gt;6. Hard Sigmoid 与激活函数&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-08.png&quot; alt=&quot;Hard Sigmoid&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Hard Sigmoid 可以用一个连续函数 &lt;strong&gt;Sigmoid&lt;/strong&gt; 来近似：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-09.png&quot; alt=&quot;Sigmoid 近似&quot; /&gt;&lt;/p&gt;
&lt;p&gt;于是每一小段蓝色曲线可以写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;y = c / (1 + exp(-(b + w * x)))
    = c * sigmoid(b + w * x)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./remote-10.png&quot; alt=&quot;Sigmoid 公式&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;问：为什么这里 &lt;code&gt;c&lt;/code&gt; 要转置？&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;公式里的 &lt;code&gt;c&lt;/code&gt;（以及 &lt;code&gt;b&lt;/code&gt;、&lt;code&gt;w&lt;/code&gt;）在多个特征/多段曲线堆叠时会变成向量/矩阵，写成 &lt;code&gt;c^T&lt;/code&gt; 是为了&lt;strong&gt;把行向量转置成列向量&lt;/strong&gt;，方便和后面的未知向量做矩阵乘法、合并起来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./remote-11.png&quot; alt=&quot;转置与合并&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这一步是在&lt;strong&gt;合并未知向量&lt;/strong&gt;：把多段曲线的参数统一写成一个矩阵运算 &lt;code&gt;y = b + c^T * sigmoid(b + w * x)&lt;/code&gt;，形式更紧凑，也便于后续推广到多层网络。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;7. 多段曲线堆叠：还是用梯度下降更新参数&lt;/h3&gt;
&lt;p&gt;把多个 Hard Sigmoid 加起来后，未知参数变多了（每段都有自己的 &lt;code&gt;c&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;w&lt;/code&gt;），但&lt;strong&gt;更新方法不变&lt;/strong&gt;——还是梯度下降，只是参数更多。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-12.png&quot; alt=&quot;多段曲线更新参数&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;8. Batch 与 Epoch：分段处理数据&lt;/h3&gt;
&lt;p&gt;把一整份训练数据&lt;strong&gt;分成多段（batch）逐段计算并更新参数&lt;/strong&gt;。相关的两个概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;batch&lt;/strong&gt;：一次取一小批样本算梯度、更新一次参数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;epoch&lt;/strong&gt;：把所有训练样本都过一遍叫一个 epoch。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;这个概念在 YOLO 等模型的训练里也会用到，是深度学习训练的通用约定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./remote-13.png&quot; alt=&quot;Batch 与 Epoch&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;9. 两种激活函数&lt;/h3&gt;
&lt;p&gt;除了 Sigmoid，还有另一种常用的激活函数。课程里对比了两种：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-14.png&quot; alt=&quot;两种激活函数&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（Sigmoid 和 ReLU，后者在现代网络里更常用，因为计算简单、收敛快。）&lt;/p&gt;
&lt;h3&gt;10. 把结构叠多层：Neural Network → Deep Learning&lt;/h3&gt;
&lt;p&gt;上面这种&quot;输入 → 一组激活函数 → 输出&quot;的结构可以&lt;strong&gt;多次重复堆叠（layers）&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-15.png&quot; alt=&quot;多层堆叠&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单个这种计算单元叫 &lt;strong&gt;neuron（神经元）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;把很多 neuron 组织起来就叫 &lt;strong&gt;neural network（神经网络）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;当层数很多时，换了个名字：&lt;strong&gt;Many layers means Deep → Deep Learning（深度学习）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;这节课从 &lt;code&gt;y = b + w*x&lt;/code&gt; 出发，一步步展示了：怎么用 Loss 衡量模型好坏、怎么用梯度下降找最优参数、怎么用 Hard Sigmoid/Sigmoid 把线性模型扩展成能拟合复杂曲线的模型，最后通过堆叠 layers 自然引出了神经网络和深度学习。后续课程会在这个基础上讲网络结构、反向传播和具体任务。&lt;/p&gt;
</content:encoded></item><item><title>二叉树基础笔记</title><link>https://youki.bbroot.com/posts/cs/binary-tree/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/cs/binary-tree/</guid><description>二叉树的顺序存储、链式存储、线索二叉树详解</description><pubDate>Tue, 16 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文整理二叉树的存储方式和线索二叉树的核心知识点。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;1. 二叉树的存储&lt;/h2&gt;
&lt;h3&gt;1.1 顺序存储&lt;/h3&gt;
&lt;p&gt;顺序存储使用&lt;strong&gt;数组&lt;/strong&gt;存放完全二叉树，通过下标表示结点关系。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./01.png&quot; alt=&quot;顺序存储&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;编号规则&lt;/strong&gt;（从 1 开始）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;结点&lt;/th&gt;
&lt;th&gt;公式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;左孩子&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2i&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;右孩子&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2i + 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;父亲&lt;/td&gt;
&lt;td&gt;&lt;code&gt;⌊i/2⌋&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：适合&lt;strong&gt;完全二叉树&lt;/strong&gt;，空间利用率高；非完全二叉树会浪费空间。&lt;/p&gt;
&lt;h3&gt;1.2 链式存储（二叉链表）&lt;/h3&gt;
&lt;p&gt;链式存储使用&lt;strong&gt;指针&lt;/strong&gt;连接结点，每个结点包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt;：数据域&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lchild&lt;/code&gt;：左孩子指针&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rchild&lt;/code&gt;：右孩子指针&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./02.png&quot; alt=&quot;链式存储&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重要规律&lt;/strong&gt;：空指针个数 = 结点数 + 1&lt;/p&gt;
&lt;h3&gt;1.3 三叉链表&lt;/h3&gt;
&lt;p&gt;在二叉链表基础上增加 &lt;code&gt;parent&lt;/code&gt; 指针，方便向上查找。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./06.png&quot; alt=&quot;三叉链表&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;指针&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;二叉链表&lt;/td&gt;
&lt;td&gt;lchild, rchild&lt;/td&gt;
&lt;td&gt;只能向下查找&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;三叉链表&lt;/td&gt;
&lt;td&gt;lchild, rchild, parent&lt;/td&gt;
&lt;td&gt;可双向查找&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;2. 链式存储代码实现&lt;/h2&gt;
&lt;h3&gt;2.1 C++ 写法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct BTNode {
    char data;
    BTNode *lchild;
    BTNode *rchild;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./03.png&quot; alt=&quot;C++写法&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.2 C 语言写法（typedef）&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;typedef&lt;/code&gt; 给结构体起别名，简化声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct BTNode {
    char data;
    struct BTNode *lchild, *rchild;
} BTNode;

// 使用时不需要加 struct 前缀
BTNode a, *d;
BTNode *b = (BTNode*)malloc(sizeof(BTNode));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./04.png&quot; alt=&quot;C写法&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./05.png&quot; alt=&quot;typedef详解&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3. 线索二叉树&lt;/h2&gt;
&lt;h3&gt;3.1 为什么需要线索化&lt;/h3&gt;
&lt;p&gt;二叉链表中存在大量空指针（结点数+1 个），造成浪费。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;线索化&lt;/strong&gt;：利用空指针存储&lt;strong&gt;前驱/后继&lt;/strong&gt;信息，提高遍历效率。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./07.png&quot; alt=&quot;线索化原理&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.2 线索化规则&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;空指针&lt;/th&gt;
&lt;th&gt;指向&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;左空指针&lt;/td&gt;
&lt;td&gt;前驱结点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;右空指针&lt;/td&gt;
&lt;td&gt;后继结点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3.3 线索标志位&lt;/h3&gt;
&lt;p&gt;如何区分指针指向的是&lt;strong&gt;孩子&lt;/strong&gt;还是&lt;strong&gt;线索&lt;/strong&gt;？增加 &lt;code&gt;ltag&lt;/code&gt; 和 &lt;code&gt;rtag&lt;/code&gt; 标志位：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./09.png&quot; alt=&quot;线索标志&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;标志位&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ltag/rtag&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;指向左/右孩子&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ltag/rtag&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;指向前驱/后继（线索）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;结点结构&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct BTNode {
    char data;
    struct BTNode *lchild, *rchild;
    int ltag, rtag;  // 线索标志
} BTNode;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4 中序线索二叉树示例&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./08.png&quot; alt=&quot;中序线索二叉树&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对二叉树进行中序线索化后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中序序列：B → D → A → C&lt;/li&gt;
&lt;li&gt;空指针被利用为前驱/后继线索&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;存储方式&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;顺序存储&lt;/td&gt;
&lt;td&gt;访问简单，适合完全二叉树&lt;/td&gt;
&lt;td&gt;非完全树浪费空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二叉链表&lt;/td&gt;
&lt;td&gt;灵活，节省空间&lt;/td&gt;
&lt;td&gt;空指针浪费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;三叉链表&lt;/td&gt;
&lt;td&gt;可双向查找&lt;/td&gt;
&lt;td&gt;多一个指针开销&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;线索二叉树&lt;/td&gt;
&lt;td&gt;利用空指针，遍历高效&lt;/td&gt;
&lt;td&gt;结构复杂&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;空指针个数 = 结点数 + 1&lt;/li&gt;
&lt;li&gt;线索化利用空指针存储前驱/后继&lt;/li&gt;
&lt;li&gt;ltag/rtag 用于区分孩子和线索&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>链队列实现</title><link>https://youki.bbroot.com/posts/cs/queue-and-array/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/cs/queue-and-array/</guid><description>带头结点的链队列 C 语言实现：初始化、入队、出队、判空与释放，附二级指针用法详解</description><pubDate>Fri, 14 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;队列是一种**先进先出（FIFO）**的线性数据结构，只允许在一端（队尾）插入、另一端（队头）删除。链队列用带头结点的单链表实现，相比顺序队列（循环数组）不会存在假溢出问题，空间按需分配。&lt;/p&gt;
&lt;h3&gt;数据结构定义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdbool.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

typedef char DataType;

typedef struct LQNode {
    DataType data;
    struct LQNode *next;
} LQNode, *LinkedQNode; // 链队列结点的类型

typedef struct {
    struct LQNode *front, *rear; // 头指针和尾指针
} LQueue, *LinkedQueue; // 将头、尾指针封装在一起的链队列

// [算法 3.21]    创建一个带头结点的空队列
void QueueInit(LinkedQueue *Q) {  //构造一个空队列 Q
    *Q = (LinkedQueue)malloc(sizeof(LQueue)); // 分配队列结构体空间
    if (!(*Q)) return; // 分配失败
    (*Q)-&amp;gt;front = (LQNode*)malloc(sizeof(LQNode)); // 分配头结点
    if (!((*Q)-&amp;gt;front)) {
        free(*Q);
        *Q = NULL;
        return;
    }
    (*Q)-&amp;gt;front-&amp;gt;next = NULL; // 头结点next置空
    (*Q)-&amp;gt;rear = (*Q)-&amp;gt;front; // front和rear都指向头结点，表示队列为空
}

// [算法 3.22]    入队列
bool QueueIn(LinkedQueue Q, DataType x) {  //插入元素 x 为 Q 的队尾元素
    if (!Q) return false;
    LQNode *node = (LQNode*)malloc(sizeof(LQNode)); // 新建结点
    if (!node) return false;
    node-&amp;gt;data = x;
    node-&amp;gt;next = NULL;
    Q-&amp;gt;rear-&amp;gt;next = node; // 原队尾结点next指向新结点
    Q-&amp;gt;rear = node; // rear指向新结点
    return true;
}

// [算法 3.23]    出队列
bool QueueOut (LinkedQueue Q, DataType *x){  //若队列不空 ,则删除 Q 的队头元素
    if (!Q || Q-&amp;gt;front == Q-&amp;gt;rear) return false; // 队空
    LQNode *p = Q-&amp;gt;front-&amp;gt;next; // 队头元素
    *x = p-&amp;gt;data; // 取数据
    Q-&amp;gt;front-&amp;gt;next = p-&amp;gt;next; // 头结点next指向下一个结点
    if (Q-&amp;gt;rear == p) Q-&amp;gt;rear = Q-&amp;gt;front; // 若出队后队空，rear回到头结点
    free(p);
    return true;
}

// [算法 3.24]    判断队列是否为空
bool QueueEmpty(LinkedQueue Q) {  //判断队列 Q 是否为空
    if (!Q) return true;            // 队列指针为空
    return Q-&amp;gt;front == Q-&amp;gt;rear;     // 头尾指针相遇即为空
}

//清空队列
void Empty(LinkedQueue Q)
{
    while (Q-&amp;gt;front != Q-&amp;gt;rear)
    {
        LQNode * p = Q-&amp;gt;front-&amp;gt;next;    //取得队列第一个元素指针
        Q-&amp;gt;front-&amp;gt;next = p-&amp;gt;next;   //修改队头指针指向
        free(p);
        if(Q-&amp;gt;front-&amp;gt;next == NULL) //只有一个元素时 ,出队后队空 ,此时还要修改队尾指针
            Q-&amp;gt;rear = Q-&amp;gt;front;
    }
}

// 释放队列
void FreeQueue(LinkedQueue *Q) {
    if (*Q == NULL) return; // 如果队列为空，直接返回

    LQNode * current = (*Q)-&amp;gt;front; // 从头节点开始
    LQNode * temp;

    while (current != NULL) {
        temp = current; // 保存当前节点
        current = current-&amp;gt;next; // 移动到下一个节点
        free(temp); // 释放当前节点
    }

    free(*Q); // 最后释放队列结构体本身
    *Q = NULL; // 将队列指针设为NULL，防止悬挂指针
}

//获取队首元素
void GetQueue(LinkedQueue Q)
{
    if (Q-&amp;gt;front == NULL)
    {
        printf(&quot;None\n&quot;);
        return;
    }
    else
       printf(&quot;%c\n&quot;, Q-&amp;gt;front-&amp;gt;next-&amp;gt;data);

}

int main()
{
    DataType c,x;
    LinkedQueue Q = NULL;
    QueueInit(&amp;amp;Q);
    while (1)
    {
        scanf(&quot; %c&quot;, &amp;amp;c);
        if (c == &apos;E&apos;)
        {
            scanf(&quot; %c&quot;, &amp;amp;x);
            QueueIn(Q, x);
        }
        else if (c == &apos;C&apos;)
        {
            Empty(Q);
        }
        else if (c == &apos;G&apos;)
        {
            GetQueue(Q);
        }
        else if (c == &apos;D&apos;)
        {
            //dequeue(m);
            if(QueueOut(Q, &amp;amp;x))
             printf(&quot;%c\n&quot;, x);
          else
             printf(&quot;None\n&quot;);
        }
        else if (c == &apos;Q&apos;)
        {
            break;
        }
    }
    FreeQueue(&amp;amp;Q);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里详细讲一下 &lt;code&gt;(*Q)-&amp;gt;&lt;/code&gt; 的含义和作用：&lt;/p&gt;
&lt;h3&gt;二级指针解引用 &lt;code&gt;(*Q)-&amp;gt;&lt;/code&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;参数类型&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;QueueInit(LinkedQueue *Q)&lt;/code&gt; 里的 &lt;code&gt;Q&lt;/code&gt; 是“指向队列指针的指针”。&lt;br /&gt;
也就是：&lt;code&gt;LinkedQueue&lt;/code&gt; 是 &lt;code&gt;LQueue *&lt;/code&gt;，&lt;code&gt;LinkedQueue *Q&lt;/code&gt; 就是 &lt;code&gt;LQueue **Q&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;解引用&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;*Q&lt;/code&gt; 表示“取出 Q 指向的内容”，也就是实际的队列指针（&lt;code&gt;LQueue *&lt;/code&gt;）。&lt;br /&gt;
例如：&lt;code&gt;*Q = malloc(...)&lt;/code&gt;，就是让外部的队列指针指向新分配的队列结构体。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;访问结构体成员&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;(*Q)-&amp;gt;front&lt;/code&gt; 表示“队列的头指针”。&lt;br /&gt;
因为 &lt;code&gt;*Q&lt;/code&gt; 是 &lt;code&gt;LQueue *&lt;/code&gt;，所以用 &lt;code&gt;-&amp;gt;&lt;/code&gt; 访问结构体成员。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;为什么不用 Q-&amp;gt;front&lt;/strong&gt;&lt;br /&gt;
如果直接用 &lt;code&gt;Q-&amp;gt;front&lt;/code&gt;，那 Q 必须是 &lt;code&gt;LQueue *&lt;/code&gt;，而不是 &lt;code&gt;LQueue **&lt;/code&gt;。&lt;br /&gt;
但这里 Q 是指向指针的指针，所以要先解引用，再访问成员。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;举例说明：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void QueueInit(LinkedQueue *Q) {
    *Q = (LinkedQueue)malloc(sizeof(LQueue)); // 分配队列结构体
    (*Q)-&amp;gt;front = (*Q)-&amp;gt;rear = (LQNode *)malloc(sizeof(LQNode)); // 分配头结点
    (*Q)-&amp;gt;front-&amp;gt;next = NULL; // 头结点next置空
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*Q&lt;/code&gt;：拿到实际的队列指针&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(*Q)-&amp;gt;front&lt;/code&gt;：访问队列的头指针&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(*Q)-&amp;gt;rear&lt;/code&gt;：访问队列的尾指针&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;：&lt;code&gt;(*Q)-&amp;gt;&lt;/code&gt; 是先解引用拿到队列指针，再用 &lt;code&gt;-&amp;gt;&lt;/code&gt; 访问结构体成员。这样才能正确初始化和操作队列。&lt;/p&gt;
&lt;h3&gt;用指针返回出队值 &lt;code&gt;*x = p-&amp;gt;data&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;出队函数签名是 &lt;code&gt;QueueOut(LinkedQueue Q, DataType *x)&lt;/code&gt;，其中 &lt;code&gt;*x = p-&amp;gt;data;&lt;/code&gt; 这一行&lt;strong&gt;不带 &lt;code&gt;*&lt;/code&gt; 不行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;原因是：要把出队的元素值“带出来”给主程序用，必须用&lt;strong&gt;指针参数&lt;/strong&gt;（&lt;code&gt;DataType *x&lt;/code&gt;），在函数里通过 &lt;code&gt;*x = p-&amp;gt;data;&lt;/code&gt; 赋值。如果只用值参数（如 &lt;code&gt;DataType x&lt;/code&gt;），函数内部的赋值不会影响主程序变量，主程序拿不到出队的值。这就是 C 语言“用指针返回结果”的常规做法——和 &lt;code&gt;scanf(&quot;%d&quot;, &amp;amp;n)&lt;/code&gt; 传 &lt;code&gt;&amp;amp;n&lt;/code&gt; 一个道理。&lt;/p&gt;
</content:encoded></item><item><title>Git 使用指南</title><link>https://youki.bbroot.com/posts/tools/git-usage-guide/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/tools/git-usage-guide/</guid><description>Git 安装配置、基础工作流、分支管理、常用技巧速查</description><pubDate>Fri, 14 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;安装与配置&lt;/h2&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Windows：&lt;a href=&quot;https://git-scm.com/download/win&quot;&gt;git-scm.com&lt;/a&gt; 下载安装包，按向导完成&lt;/li&gt;
&lt;li&gt;macOS：&lt;code&gt;brew install git&lt;/code&gt; 或安装 Xcode Command Line Tools&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;用户信息配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git config --global user.name &quot;你的名字&quot;
git config --global user.email &quot;你的邮箱&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;SSH Key 配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 生成密钥对
ssh-keygen -t ed25519 -C &quot;你的邮箱&quot;

# 查看公钥，复制到 GitHub/GitLab 的 SSH Keys 设置中
cat ~/.ssh/id_ed25519.pub

# 测试连接
ssh -T git@github.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基础工作流&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 初始化仓库
git init

# 克隆远程仓库
git clone git@github.com:user/repo.git

# 查看状态
git status

# 添加到暂存区
git add .                # 所有文件
git add file.txt         # 指定文件

# 提交
git commit -m &quot;提交说明&quot;

# 推送到远程
git push origin main

# 拉取远程更新
git pull origin main

# 查看提交历史
git log --oneline
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分支管理&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 查看分支
git branch

# 创建并切换分支
git checkout -b feature/xxx
# 或用新版命令
git switch -c feature/xxx

# 切换分支
git checkout main

# 合并分支（在 main 上合并 feature）
git merge feature/xxx

# 删除分支
git branch -d feature/xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决合并冲突&lt;/h3&gt;
&lt;p&gt;合并时如果出现冲突，手动编辑冲突文件，保留需要的内容，然后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add .
git commit -m &quot;解决合并冲突&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用技巧&lt;/h2&gt;
&lt;h3&gt;.gitignore&lt;/h3&gt;
&lt;p&gt;在仓库根目录创建 &lt;code&gt;.gitignore&lt;/code&gt; 文件，列出不需要跟踪的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 依赖目录
node_modules/
__pycache__/

# 编辑器配置
.vscode/
.idea/

# 系统文件
.DS_Store
Thumbs.db

# 构建产物
dist/
build/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;移除跟踪但保留本地文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 从 Git 跟踪中移除（不会删除本地文件）
git rm --cached .obsidian/app.json

# 或移除整个目录
git rm --cached -r .obsidian/

# 加入 .gitignore 防止再次跟踪
echo &quot;.obsidian/&quot; &amp;gt;&amp;gt; .gitignore
git add .gitignore
git commit -m &quot;移除配置文件跟踪&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;stash 暂存工作区&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 暂存当前修改
git stash

# 恢复暂存
git stash pop

# 查看暂存列表
git stash list
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;撤销操作&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 撤销工作区修改（未 add）
git checkout -- file.txt

# 撤销暂存（已 add 未 commit）
git reset HEAD file.txt

# 撤销最近一次提交（保留修改）
git reset --soft HEAD~1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;图形化工具&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PyCharm 集成&lt;/strong&gt;：内置 Git 支持，VCS 菜单直接操作&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VSCode 集成&lt;/strong&gt;：左侧源代码管理面板，支持 diff、提交、推送&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>二进制表示与数制转换</title><link>https://youki.bbroot.com/posts/network/binary-representation/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/binary-representation/</guid><description>数制转换、原码反码补码移码详解</description><pubDate>Thu, 11 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文整理二进制表示、数制转换以及原码/反码/补码/移码的核心知识点。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;1. 数制基础&lt;/h2&gt;
&lt;h3&gt;1.1 常用数制&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;数制&lt;/th&gt;
&lt;th&gt;基数&lt;/th&gt;
&lt;th&gt;数码&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;二进制&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0, 1&lt;/td&gt;
&lt;td&gt;1101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;八进制&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0-7&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;十进制&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0-9&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;十六进制&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0-9, A-F&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;1.2 数制转换&lt;/h3&gt;
&lt;h4&gt;十进制 → 二进制（除2取余法）&lt;/h4&gt;
&lt;p&gt;对整数部分&lt;strong&gt;除2取余，逆序排列&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./02.png&quot; alt=&quot;十进制转二进制&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：256 转二进制&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;除法&lt;/th&gt;
&lt;th&gt;商&lt;/th&gt;
&lt;th&gt;余数&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;256 ÷ 2&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128 ÷ 2&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 ÷ 2&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32 ÷ 2&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 ÷ 2&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 ÷ 2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 ÷ 2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 ÷ 2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 ÷ 2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;结果：&lt;code&gt;100000000&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;二进制 → 十进制（按权展开）&lt;/h4&gt;
&lt;p&gt;将每一位乘以 2 的对应次幂，求和。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;二进制&lt;/th&gt;
&lt;th&gt;计算&lt;/th&gt;
&lt;th&gt;十进制&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11101100&lt;/td&gt;
&lt;td&gt;128+64+32+8+4&lt;/td&gt;
&lt;td&gt;236&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11111111&lt;/td&gt;
&lt;td&gt;128+64+32+16+8+4+2+1&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;二进制 ↔ 八进制（三位一组）&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./01.png&quot; alt=&quot;数制转换规则&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;规则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;二进制 → 八进制&lt;/strong&gt;：从右往左每&lt;strong&gt;三位&lt;/strong&gt;一组，转为一位八进制，不足三位左边补零&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;八进制 → 二进制&lt;/strong&gt;：每一位八进制转为三位二进制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;code&gt;1110 0101 1101&lt;/code&gt; 转八进制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;分组：111 001 011 101
转八进制：7  1  3  5
结果：7135
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;二进制 ↔ 十六进制（四位一组）&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;规则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;二进制 → 十六进制&lt;/strong&gt;：从右往左每&lt;strong&gt;四位&lt;/strong&gt;一组，转为一位十六进制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;十六进制 → 二进制&lt;/strong&gt;：每一位十六进制转为四位二进制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;code&gt;1110 0101 1101&lt;/code&gt; 转十六进制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;分组：1110 0101 1101
转十六进制：E  5  D
结果：E5D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./03.png&quot; alt=&quot;数制转换示例&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2. 原码、反码、补码&lt;/h2&gt;
&lt;h3&gt;2.1 原码&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;：最高位为符号位（0正1负），其余位为数值位。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;（8位）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;十进制&lt;/th&gt;
&lt;th&gt;原码&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+1&lt;/td&gt;
&lt;td&gt;00000001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-1&lt;/td&gt;
&lt;td&gt;10000001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+127&lt;/td&gt;
&lt;td&gt;01111111&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-127&lt;/td&gt;
&lt;td&gt;11111111&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2.2 反码&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正数：与原码相同&lt;/li&gt;
&lt;li&gt;负数：符号位不变，数值位按位取反&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;十进制&lt;/th&gt;
&lt;th&gt;原码&lt;/th&gt;
&lt;th&gt;反码&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+1&lt;/td&gt;
&lt;td&gt;00000001&lt;/td&gt;
&lt;td&gt;00000001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-1&lt;/td&gt;
&lt;td&gt;10000001&lt;/td&gt;
&lt;td&gt;11111110&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2.3 补码&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正数：与原码相同&lt;/li&gt;
&lt;li&gt;负数：反码 + 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;十进制&lt;/th&gt;
&lt;th&gt;原码&lt;/th&gt;
&lt;th&gt;反码&lt;/th&gt;
&lt;th&gt;补码&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+1&lt;/td&gt;
&lt;td&gt;00000001&lt;/td&gt;
&lt;td&gt;00000001&lt;/td&gt;
&lt;td&gt;00000001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-1&lt;/td&gt;
&lt;td&gt;10000001&lt;/td&gt;
&lt;td&gt;11111110&lt;/td&gt;
&lt;td&gt;11111111&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;为什么计算机用补码？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加减法统一处理，无需区分符号&lt;/li&gt;
&lt;li&gt;0 的表示唯一（原码有 +0 和 -0 两种）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4 移码&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;：补码的符号位取反。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用途&lt;/strong&gt;：浮点数的阶码表示。&lt;/p&gt;
&lt;h3&gt;2.5 对比表格&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./04.png&quot; alt=&quot;原反补移码对比&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编码&lt;/th&gt;
&lt;th&gt;+0&lt;/th&gt;
&lt;th&gt;-0&lt;/th&gt;
&lt;th&gt;范围（8位）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;原码&lt;/td&gt;
&lt;td&gt;00000000&lt;/td&gt;
&lt;td&gt;10000000&lt;/td&gt;
&lt;td&gt;-127 ~ +127&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;反码&lt;/td&gt;
&lt;td&gt;00000000&lt;/td&gt;
&lt;td&gt;11111111&lt;/td&gt;
&lt;td&gt;-127 ~ +127&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;补码&lt;/td&gt;
&lt;td&gt;00000000&lt;/td&gt;
&lt;td&gt;00000000（唯一）&lt;/td&gt;
&lt;td&gt;-128 ~ +127&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;移码&lt;/td&gt;
&lt;td&gt;10000000&lt;/td&gt;
&lt;td&gt;10000000（唯一）&lt;/td&gt;
&lt;td&gt;-127 ~ +128&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;3. 定点数表示&lt;/h2&gt;
&lt;h3&gt;3.1 定点整数&lt;/h3&gt;
&lt;p&gt;小数点固定在最低位之后，表示整数。&lt;/p&gt;
&lt;h3&gt;3.2 定点小数&lt;/h3&gt;
&lt;p&gt;小数点固定在符号位之后，表示纯小数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./05.png&quot; alt=&quot;定点小数习题&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;习题&lt;/strong&gt;：已知 x = -53/64，用 8 位定点机器码表示原码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解析&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;计算 -53/64 的二进制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;53 = 32 + 16 + 4 + 1 = &lt;code&gt;110101&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;53/64 = 0.110101（二进制）&lt;/li&gt;
&lt;li&gt;-53/64 = -0.110101&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;8 位定点小数原码：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;符号位：1（负数）&lt;/li&gt;
&lt;li&gt;数值位：11010100（补零到 7 位）&lt;/li&gt;
&lt;li&gt;结果：&lt;code&gt;1.11010100&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;答案&lt;/strong&gt;：A（1.11010100）&lt;/p&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;h3&gt;数制转换速查&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;转换&lt;/th&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;十进制 → 二进制&lt;/td&gt;
&lt;td&gt;除2取余，逆序排列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二进制 → 十进制&lt;/td&gt;
&lt;td&gt;按权展开求和&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二进制 → 八进制&lt;/td&gt;
&lt;td&gt;三位一组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二进制 → 十六进制&lt;/td&gt;
&lt;td&gt;四位一组&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;原反补移码对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编码&lt;/th&gt;
&lt;th&gt;正数&lt;/th&gt;
&lt;th&gt;负数&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;原码&lt;/td&gt;
&lt;td&gt;不变&lt;/td&gt;
&lt;td&gt;符号位+数值位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;反码&lt;/td&gt;
&lt;td&gt;不变&lt;/td&gt;
&lt;td&gt;符号位不变，数值位取反&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;补码&lt;/td&gt;
&lt;td&gt;不变&lt;/td&gt;
&lt;td&gt;反码+1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;移码&lt;/td&gt;
&lt;td&gt;补码符号位取反&lt;/td&gt;
&lt;td&gt;补码符号位取反&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算机内部使用&lt;strong&gt;补码&lt;/strong&gt;存储整数&lt;/li&gt;
&lt;li&gt;0 的补码表示&lt;strong&gt;唯一&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;定点小数原码要注意小数点位置&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>机器学习入门：从 ML 概述到 KNN 分类</title><link>https://youki.bbroot.com/posts/ai/iris-knn/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/ai/iris-knn/</guid><description>机器学习基本概念、KNN 分类算法原理与 scikit-learn 实现、参数调优实战</description><pubDate>Fri, 15 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;人工智能、机器学习与深度学习的关系&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;人工智能（AI）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;概念起源：1950年代&lt;/li&gt;
&lt;li&gt;目标：创建能够达到人类智慧水平的机器&lt;/li&gt;
&lt;li&gt;实现方式：有多种方法，机器学习是其中之一&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;机器学习（ML）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发展时间：1980年代开始兴起&lt;/li&gt;
&lt;li&gt;地位：实现人工智能的一种重要方法&lt;/li&gt;
&lt;li&gt;发展推动因素：数据量增长、硬件设施发展、算法进步&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;深度学习（DL）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发展时间：2010年左右开始热门&lt;/li&gt;
&lt;li&gt;地位：机器学习的一个重要分支&lt;/li&gt;
&lt;li&gt;包含算法：CNN、RNN等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关系总结：&lt;strong&gt;深度学习 ⊂ 机器学习 ⊂ 人工智能&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;机器学习基本概念&lt;/h2&gt;
&lt;h3&gt;机器学习解决问题的基本流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;训练样本&lt;/strong&gt;：N个已知样本数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特征提取&lt;/strong&gt;：从样本中提取有用属性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;算法学习&lt;/strong&gt;：使用算法学习特征规律&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型构建&lt;/strong&gt;：形成可预测的模型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预测未知&lt;/strong&gt;：对新数据进行预测&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;机器学习分类&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;监督学习&lt;/strong&gt;：训练样本包含标签，应用：分类、回归问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无监督学习&lt;/strong&gt;：训练样本不包含标签，应用：聚类分析&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;鸢尾花分类项目实践&lt;/h2&gt;
&lt;h3&gt;数据集&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;样本数量：150个&lt;/li&gt;
&lt;li&gt;特征数量：4个（花萼长度、花萼宽度、花瓣长度、花瓣宽度）&lt;/li&gt;
&lt;li&gt;分类类别：3种（山鸢尾、变色鸢尾、维吉尼亚鸢尾）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;KNN 分类算法基础&lt;/h2&gt;
&lt;h3&gt;scikit-learn 基础&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据格式要求&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特征X必须是数值矩阵（如150×4）&lt;/li&gt;
&lt;li&gt;标签y必须是数值向量（如150个0/1/2）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据预处理&lt;/strong&gt;：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;iris_data[&apos;Label&apos;] = iris_data[&apos;Species&apos;].map({
    &apos;Iris-setosa&apos;: 0,
    &apos;Iris-versicolor&apos;: 1,
    &apos;Iris-virginica&apos;: 2
})
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据划分&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.33, random_state=10)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;KNN 算法原理&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算测试样本与所有训练样本的距离&lt;/li&gt;
&lt;li&gt;找出距离最近的K个邻居&lt;/li&gt;
&lt;li&gt;根据邻居的多数类别确定预测结果&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;K值影响&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;K=1：只考虑最近的一个样本（容易受噪声影响）&lt;/li&gt;
&lt;li&gt;K增大：决策边界更平滑（可能忽略局部特征）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;scikit-learn 实现流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.neighbors import KNeighborsClassifier

# 创建模型（默认K=5）
knn = KNeighborsClassifier()

# 训练模型
knn.fit(X_train, y_train)

# 评估与预测
accuracy = knn.score(X_test, y_test)
y_pred = knn.predict(X_new)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;KNN 算法深入与参数调优&lt;/h2&gt;
&lt;h3&gt;相似性度量（距离计算）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;明可夫斯基距离&lt;/strong&gt;：&lt;code&gt;distance = (∑|x_i - y_i|^p)^(1/p)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;p=2：欧式距离（默认）&lt;/li&gt;
&lt;li&gt;p=1：曼哈顿距离&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;KNeighborsClassifier(metric=&apos;minkowski&apos;, p=2)  # 欧式距离
KNeighborsClassifier(metric=&apos;minkowski&apos;, p=1)  # 曼哈顿距离
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;模型参数分类&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;超参数&lt;/strong&gt;：需要人工预先设定（如K值、距离度量方式）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型参数&lt;/strong&gt;：通过训练数据自动学习得到（如线性回归系数）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;KNN 优缺点&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;原理简单直观，易于实现&lt;/li&gt;
&lt;li&gt;无需训练过程（惰性学习）&lt;/li&gt;
&lt;li&gt;对数据分布没有假设&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算量大（需计算与所有训练样本的距离）&lt;/li&gt;
&lt;li&gt;对高维数据效果差（维度灾难）&lt;/li&gt;
&lt;li&gt;需要合理选择K值和距离度量&lt;/li&gt;
&lt;li&gt;对不平衡数据敏感&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;实践：不同 K 值的分类效果&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;from sklearn.neighbors import KNeighborsClassifier

k_values = [3, 5, 10]
selected_features = [&apos;SepalLengthCm&apos;, &apos;SepalWidthCm&apos;]

for k in k_values:
    knn = KNeighborsClassifier(n_neighbors=k)
    knn.fit(X_train[selected_features], y_train)
    accuracy = knn.score(X_test[selected_features], y_test)
    print(f&apos;K={k}时准确率: {accuracy:.2%}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实验结果（仅使用花萼长宽两个特征）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;K=3：准确率66%&lt;/li&gt;
&lt;li&gt;K=5：准确率68%&lt;/li&gt;
&lt;li&gt;K=10：准确率78%&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;关键知识点总结&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;距离度量选择&lt;/strong&gt;：欧式距离适合大多数情况，曼哈顿距离对异常值更鲁棒&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;K值选择原则&lt;/strong&gt;：一般取3-20之间的奇数（避免平票），通过交叉验证确定最优值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特征选择影响&lt;/strong&gt;：不同特征组合会影响分类效果&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>CDMA 与 CSMA 协议笔记</title><link>https://youki.bbroot.com/posts/network/cdma-csma/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/cdma-csma/</guid><description>码分多址复用、CSMA/CD 碰撞检测、CSMA/CA 碰撞避免、介质访问控制协议</description><pubDate>Fri, 01 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 信道复用技术&lt;/h2&gt;
&lt;h3&gt;1.1 码分复用 CDM&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-01.png&quot; alt=&quot;码分复用CDM&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CDMA 原理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个站点被分配唯一的 &lt;strong&gt;m bit 码片序列&lt;/strong&gt;（Chip Sequence）&lt;/li&gt;
&lt;li&gt;码片序列相互正交：不同站点的码片内积为 0&lt;/li&gt;
&lt;li&gt;发送数据时：发送 1 → 发送码片序列；发送 0 → 发送码片序列的反码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 站码片序列：(-1, -1, +1, +1, -1, +1, +1, +1)&lt;/li&gt;
&lt;li&gt;B 站码片序列：(-1, +1, -1, +1, +1, +1, -1, -1)&lt;/li&gt;
&lt;li&gt;A 发送 1，B 发送 0，C 未发送&lt;/li&gt;
&lt;li&gt;接收端收到的信号 = A 的码片 - B 的码片&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 CDMA 例题解析&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-02.png&quot; alt=&quot;CDMA例题&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;判断方法&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将接收到的序列与各站点码片进行&lt;strong&gt;内积运算&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;结果为 +1：发送了比特 1&lt;/li&gt;
&lt;li&gt;结果为 -1：发送了比特 0&lt;/li&gt;
&lt;li&gt;结果为 0：未发送数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;计算示例&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;收到的序列：(-1, +1, +3, -1, -3, -1, +3, +1)

与 A 站码片内积：
(-1)×(-1) + (+1)×(-1) + (+3)×(+1) + (-1)×(+1) + 
(-3)×(-1) + (-1)×(+1) + (+3)×(+1) + (+1)×(+1)
= 1 - 1 + 3 - 1 + 3 - 1 + 3 + 1 = 8 → 结果为 +1 → A 发送了 1
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. CSMA/CD 协议（有线以太网）&lt;/h2&gt;
&lt;h3&gt;2.1 帧发送流程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-03.png&quot; alt=&quot;CSMA/CD帧发送流程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发送流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;封装成帧&lt;/strong&gt;，重传计数器 c = 0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;载波监听&lt;/strong&gt;：信道空闲 96 比特时间？
&lt;ul&gt;
&lt;li&gt;是：发送帧的第一比特&lt;/li&gt;
&lt;li&gt;否：继续监听&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;碰撞检测&lt;/strong&gt;：边发送边监听
&lt;ul&gt;
&lt;li&gt;有碰撞：发送 32 或 48 比特干扰信号&lt;/li&gt;
&lt;li&gt;无碰撞：继续发送下一比特&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检测到碰撞后的处理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;发送干扰信号&lt;/li&gt;
&lt;li&gt;等待随机时间 T&lt;/li&gt;
&lt;li&gt;使用&lt;strong&gt;二进制指数退避算法&lt;/strong&gt;计算退避时间&lt;/li&gt;
&lt;li&gt;c = c + 1&lt;/li&gt;
&lt;li&gt;若 c &amp;lt; 15：返回重传&lt;/li&gt;
&lt;li&gt;若 c ≥ 15：报告信道繁忙&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.2 帧接收流程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-04.png&quot; alt=&quot;CSMA/CD帧接收流程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接收流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;监听信道&lt;/strong&gt;：信道活跃？
&lt;ul&gt;
&lt;li&gt;否：继续监听&lt;/li&gt;
&lt;li&gt;是：开始接收帧&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接收完成&lt;/strong&gt;后检查：
&lt;ul&gt;
&lt;li&gt;帧太小（&amp;lt; 64 字节）→ 认为遭遇碰撞，丢弃&lt;/li&gt;
&lt;li&gt;帧的目的 MAC 地址与本机相同或是广播地址？&lt;/li&gt;
&lt;li&gt;使用 &lt;strong&gt;CRC&lt;/strong&gt; 检查帧是否出现误码&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校验正确&lt;/strong&gt; → 接收该帧；&lt;strong&gt;校验错误&lt;/strong&gt; → 丢弃&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.3 CSMA/CD 知识点总结&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-05.png&quot; alt=&quot;CSMA/CD知识点&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键概念&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;争用期&lt;/td&gt;
&lt;td&gt;碰撞检测的最长时间 = 2τ（端到端往返时间）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最小帧长&lt;/td&gt;
&lt;td&gt;64 字节（512 bit），保证能检测到碰撞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最大帧长&lt;/td&gt;
&lt;td&gt;1518 字节（1500 数据 + 18 首尾）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二进制指数退避&lt;/td&gt;
&lt;td&gt;第 k 次重传，从 {0, 1, ..., 2^k - 1} 随机选择等待时间&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;早期共享式以太网（集线器连接）&lt;/li&gt;
&lt;li&gt;现在的交换式以太网已很少使用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4 CSMA/CD 考研真题&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-06.png&quot; alt=&quot;CSMA/CD真题&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;题目&lt;/strong&gt;：在采用 CSMA/CD 协议的网络中，传输速率为 1Gbps，信号传播速率为 200000km/s，若最小帧长减少 800 比特，则最远的两个站点之间的距离至少需要如何变化？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解题思路&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;最小帧长 = 争用期 × 数据传输速率&lt;/li&gt;
&lt;li&gt;争用期 = 2 × 距离 / 传播速率&lt;/li&gt;
&lt;li&gt;最小帧长减少 → 争用期减小 → 距离减小&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;3. CSMA/CA 协议（无线局域网）&lt;/h2&gt;
&lt;h3&gt;3.1 退避算法时序&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-07.png&quot; alt=&quot;CSMA/CA退避算法&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CSMA/CA 工作原理&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DIFS（DCF 帧间间隔）&lt;/strong&gt;：发送前等待 DIFS 时间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;退避算法&lt;/strong&gt;：信道忙时，冻结退避计时器；信道空闲时，继续倒计时&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发送帧&lt;/strong&gt;：退避计时器归零后发送&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等待确认&lt;/strong&gt;：接收方收到帧后发送 ACK&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;与 CSMA/CD 的区别&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CSMA/CD：边发送边检测碰撞&lt;/li&gt;
&lt;li&gt;CSMA/CA：发送前尽量避免碰撞（无线环境无法有效检测碰撞）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 虚拟载波监听&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-08.png&quot; alt=&quot;虚拟载波监听&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RTS/CTS 机制&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发送方先发送 &lt;strong&gt;RTS&lt;/strong&gt;（Request to Send）帧&lt;/li&gt;
&lt;li&gt;接收方回应 &lt;strong&gt;CTS&lt;/strong&gt;（Clear to Send）帧&lt;/li&gt;
&lt;li&gt;RTS/CTS 帧中包含数据帧的持续时间&lt;/li&gt;
&lt;li&gt;其他站点监听到 RTS/CTS 后，在该时间段内保持静默&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解决&lt;strong&gt;隐藏站问题&lt;/strong&gt;（A 和 C 都能与 B 通信，但 A 和 C 互相听不到）&lt;/li&gt;
&lt;li&gt;减少碰撞概率&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 CSMA/CA 考研真题&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-09.png&quot; alt=&quot;CSMA/CA真题&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;题目&lt;/strong&gt;：以下哪种协议用于无线局域网的碰撞避免？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案&lt;/strong&gt;：CSMA/CA&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对比&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;协议&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;th&gt;碰撞处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CSMA/CD&lt;/td&gt;
&lt;td&gt;有线以太网&lt;/td&gt;
&lt;td&gt;检测碰撞后重传&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSMA/CA&lt;/td&gt;
&lt;td&gt;无线局域网&lt;/td&gt;
&lt;td&gt;发送前避免碰撞&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3.4 介质访问控制总结&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-10.png&quot; alt=&quot;介质访问控制总结&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;静态划分信道&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FDM（频分复用）&lt;/li&gt;
&lt;li&gt;TDM（时分复用）&lt;/li&gt;
&lt;li&gt;WDM（波分复用）&lt;/li&gt;
&lt;li&gt;CDM（码分复用）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;动态接入控制&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;争用型&lt;/strong&gt;：CSMA/CD、CSMA/CA（可能发生冲突）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;受控型&lt;/strong&gt;：令牌传递（不会发生冲突）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;h3&gt;核心知识点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CDMA&lt;/strong&gt;：码片序列正交，内积判断发送数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSMA/CD&lt;/strong&gt;：边发送边检测碰撞，二进制指数退避&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSMA/CA&lt;/strong&gt;：发送前避免碰撞，RTS/CTS 解决隐藏站&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最小帧长&lt;/strong&gt;：64 字节（争用期 × 传输速率）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：CSMA/CD 用于有线，CSMA/CA 用于无线&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>物理层与数据链路层笔记</title><link>https://youki.bbroot.com/posts/network/physical-datalink/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/physical-datalink/</guid><description>编码与调制、信道特性（奈氏准则、香农定理）、封装成帧、透明传输</description><pubDate>Fri, 01 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 编码与调制&lt;/h2&gt;
&lt;h3&gt;1.1 编码与调制概述&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-01.png&quot; alt=&quot;编码与调制&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基本概念&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基带信号&lt;/strong&gt;：数字设备产生的原始信号（如计算机输出的矩形波）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;宽带信号&lt;/strong&gt;：经过调制后的信号（适合远距离传输）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;编码方式&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数字数据 → 数字信号&lt;/strong&gt;：使用数字编码（如 NRZ、曼彻斯特编码）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数字数据 → 模拟信号&lt;/strong&gt;：需要调制（ASK、FSK、PSK、QAM）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟数据 → 数字信号&lt;/strong&gt;：需要采样、量化、编码（PCM）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟数据 → 模拟信号&lt;/strong&gt;：可以使用调制放大&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 调制方式&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-02.png&quot; alt=&quot;调制方式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基本调制方式&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;调制方式&lt;/th&gt;
&lt;th&gt;英文&lt;/th&gt;
&lt;th&gt;原理&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;调幅 AM&lt;/td&gt;
&lt;td&gt;Amplitude Shift Keying&lt;/td&gt;
&lt;td&gt;改变载波振幅&lt;/td&gt;
&lt;td&gt;实现简单，抗干扰差&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调频 FM&lt;/td&gt;
&lt;td&gt;Frequency Shift Keying&lt;/td&gt;
&lt;td&gt;改变载波频率&lt;/td&gt;
&lt;td&gt;抗干扰好，带宽大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调相 PM&lt;/td&gt;
&lt;td&gt;Phase Shift Keying&lt;/td&gt;
&lt;td&gt;改变载波相位&lt;/td&gt;
&lt;td&gt;抗干扰好，实现复杂&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;混合调制&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;QAM（正交振幅调制）&lt;/strong&gt;：结合调幅和调相&lt;/li&gt;
&lt;li&gt;QAM-16：16 种状态，每个码元携带 4 bit&lt;/li&gt;
&lt;li&gt;QAM-64：64 种状态，每个码元携带 6 bit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;码元&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在使用时间域的波形表示数字信号时，代表不同离散数值的基本波形&lt;/li&gt;
&lt;li&gt;一个码元可以携带多个 bit 的信息&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 信道特性&lt;/h2&gt;
&lt;h3&gt;2.1 奈氏准则（理想低通信道）&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-03.png&quot; alt=&quot;奈氏准则&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;奈氏准则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;理想低通信道的最高码元传输速率 = &lt;strong&gt;2W Baud&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;其中 W 是信道带宽（Hz）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;含义&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在带宽为 W Hz 的理想低通信道中，码元传输速率为 2W Baud&lt;/li&gt;
&lt;li&gt;每个码元可以携带 n bit 信息（n = log2(V)，V 为离散等级数）&lt;/li&gt;
&lt;li&gt;最大数据传输速率 = 2W × log2(V) bps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信道带宽 W = 3000 Hz&lt;/li&gt;
&lt;li&gt;最高码元传输速率 = 2 × 3000 = 6000 Baud&lt;/li&gt;
&lt;li&gt;若使用 4 级量化（V = 4），则最高数据速率 = 6000 × log2(4) = 12000 bps&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 香农定理（有噪声信道）&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-04.png&quot; alt=&quot;香农定理&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;香农定理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信道的极限数据传输速率 = &lt;strong&gt;W × log2(1 + S/N) bps&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;其中 W 是带宽（Hz），S/N 是信噪比&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;信噪比&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通常用分贝（dB）表示：信噪比(dB) = 10 × log10(S/N)&lt;/li&gt;
&lt;li&gt;例如：信噪比 30dB → S/N = 1000&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;含义&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信道带宽和信噪比决定了信道的极限传输速率&lt;/li&gt;
&lt;li&gt;实际传输速率不可能超过这个极限值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信道带宽 W = 3000 Hz，信噪比 = 30dB（S/N = 1000）&lt;/li&gt;
&lt;li&gt;极限数据速率 = 3000 × log2(1 + 1000) ≈ 3000 × 10 = 30000 bps&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 奈氏准则与香农定理的关系&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;准则&lt;/th&gt;
&lt;th&gt;适用条件&lt;/th&gt;
&lt;th&gt;决定因素&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;奈氏准则&lt;/td&gt;
&lt;td&gt;理想无噪声信道&lt;/td&gt;
&lt;td&gt;带宽 W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;香农定理&lt;/td&gt;
&lt;td&gt;有噪声信道&lt;/td&gt;
&lt;td&gt;带宽 W + 信噪比 S/N&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;实际应用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通常同时考虑两个准则&lt;/li&gt;
&lt;li&gt;取两者中较小的值作为极限数据速率&lt;/li&gt;
&lt;li&gt;若题目未指明信道类型，默认按&lt;strong&gt;低通信道&lt;/strong&gt;处理&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 常用编码方式&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./remote-05.png&quot; alt=&quot;常用编码&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.1 编码类型对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;编码&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;不归零编码 NRZ&lt;/td&gt;
&lt;td&gt;非自同步&lt;/td&gt;
&lt;td&gt;高电平=1，低电平=0；需要额外时钟同步&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;归零编码 RZ&lt;/td&gt;
&lt;td&gt;自同步&lt;/td&gt;
&lt;td&gt;每个码元中间跳变到零电平；编码效率低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;反向不归零编码 NRZI&lt;/td&gt;
&lt;td&gt;自同步&lt;/td&gt;
&lt;td&gt;遇 1 翻转，遇 0 不变；USB 使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;差分曼彻斯特编码&lt;/td&gt;
&lt;td&gt;自同步&lt;/td&gt;
&lt;td&gt;位中间跳变=0，位开始跳变=1；局域网使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;曼彻斯特编码&lt;/td&gt;
&lt;td&gt;自同步&lt;/td&gt;
&lt;td&gt;位中间跳变：低→高=1，高→低=0；以太网使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3.2 曼彻斯特编码详解&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;编码规则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个码元中间都有跳变（用于时钟同步）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高→低跳变&lt;/strong&gt;：表示 1&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低→高跳变&lt;/strong&gt;：表示 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自带时钟同步信号&lt;/li&gt;
&lt;li&gt;抗干扰能力强&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编码效率只有 50%（每个码元只携带 1 bit）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 数据链路层&lt;/h2&gt;
&lt;h3&gt;4.1 封装成帧&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-06.png&quot; alt=&quot;封装成帧&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;封装成帧&lt;/strong&gt;：在数据链路层，将网络层交付的协议数据单元添加帧头和帧尾，形成帧。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;帧的结构&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;帧首部 | 数据部分 | 帧尾部
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;透明传输&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据链路层对数据部分的内容没有任何限制&lt;/li&gt;
&lt;li&gt;就像数据链路层不存在一样&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;字节填充&lt;/strong&gt;（面向字节的物理链路）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用转义字符实现透明传输&lt;/li&gt;
&lt;li&gt;数据中出现与定界符相同的字节时，在前面插入转义字符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;比特填充&lt;/strong&gt;（面向比特的物理链路）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用标志字段（01111110）作为帧定界符&lt;/li&gt;
&lt;li&gt;数据中出现连续 5 个 1 时，自动插入一个 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;最大传送单元 MTU&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一种链路层协议都规定了数据部分的最大长度&lt;/li&gt;
&lt;li&gt;以太网的 MTU = 1500 字节&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 差错检测&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-07.png&quot; alt=&quot;差错检测&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;XOR 运算&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据链路层使用异或运算进行简单的差错检测&lt;/li&gt;
&lt;li&gt;发送方：将数据按位异或，得到校验码&lt;/li&gt;
&lt;li&gt;接收方：将数据和校验码一起异或，结果为 0 表示无差错&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;CRC（循环冗余校验）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用生成多项式进行模 2 除法&lt;/li&gt;
&lt;li&gt;发送方：数据 + FCS（CRC 校验码）&lt;/li&gt;
&lt;li&gt;接收方：用同样的生成多项式除，余数为 0 表示无差错&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只能检测错误，不能纠正错误&lt;/li&gt;
&lt;li&gt;检错能力强，实现简单&lt;/li&gt;
&lt;li&gt;广泛应用于以太网、WiFi 等&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 总结&lt;/h2&gt;
&lt;h3&gt;核心知识点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编码调制&lt;/strong&gt;：数字数据→数字信号用编码，数字数据→模拟信号用调制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;奈氏准则&lt;/strong&gt;：理想低通信道最高码元速率 = 2W Baud&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;香农定理&lt;/strong&gt;：有噪声信道极限速率 = W × log2(1 + S/N)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;曼彻斯特编码&lt;/strong&gt;：位中间跳变表示 0/1，自带时钟同步&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封装成帧&lt;/strong&gt;：添加帧头帧尾，透明传输，MTU 限制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;差错检测&lt;/strong&gt;：CRC 校验，只能检错不能纠错&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>可靠传输协议笔记</title><link>https://youki.bbroot.com/posts/network/reliable-transmission/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/reliable-transmission/</guid><description>CRC 差错检测、停止-等待协议、回退N帧协议、选择重传协议、PPP协议</description><pubDate>Fri, 01 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 可靠传输基本概念&lt;/h2&gt;
&lt;h3&gt;1.1 传输差错类型&lt;/h3&gt;
&lt;p&gt;使用差错检测技术（如 CRC），接收方可以检测出帧在传输过程中是否产生误码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传输差错类型&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;比特差错&lt;/strong&gt;：数据中某些比特发生了翻转&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分组丢失&lt;/strong&gt;：数据包在网络中丢失&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分组失序&lt;/strong&gt;：数据包到达顺序与发送顺序不一致&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分组重复&lt;/strong&gt;：同一数据包被重复传输&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;可靠传输要求&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有线链路：误码率低，通常不要求可靠传输（上层处理）&lt;/li&gt;
&lt;li&gt;无线链路：误码率高，必须提供可靠传输服务&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 停止-等待协议（Stop-and-Wait）&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./remote-01.png&quot; alt=&quot;停止-等待协议&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.1 工作原理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;发送方&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发送一个数据分组&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;暂停发送&lt;/strong&gt;，等待接收方的确认&lt;/li&gt;
&lt;li&gt;收到 ACK 后，发送下一个分组&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;接收方&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;收到数据分组后，进行差错检测&lt;/li&gt;
&lt;li&gt;若正确：发送 ACK 确认&lt;/li&gt;
&lt;li&gt;若错误：丢弃该分组，不发送任何响应&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.2 异常处理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;确认丢失&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发送方超时未收到 ACK → 重新发送数据分组&lt;/li&gt;
&lt;li&gt;接收方收到重复分组 → 丢弃但仍发送 ACK&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;确认迟到&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发送方超时重传 → 收到迟到的 ACK&lt;/li&gt;
&lt;li&gt;发送方忽略迟到的 ACK，继续发送下一个分组&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 信道利用率&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;公式&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;U = T_D / (T_D + RTT + T_A)

其中：
- T_D：发送数据分组的时间
- RTT：往返时延
- T_A：发送确认分组的时间
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信道利用率低（大部分时间在等待确认）&lt;/li&gt;
&lt;li&gt;适用于数据量小、实时性要求不高的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 回退N帧协议（Go-Back-N）&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./remote-02.png&quot; alt=&quot;回退N帧协议&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.1 发送方工作原理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;窗口机制&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发送窗口大小 W_T：1 &amp;lt; W_T ≤ 2^n - 1（n 为序号位数）&lt;/li&gt;
&lt;li&gt;发送方可以在未收到确认的情况下，连续发送 W_T 个分组&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;超时重传&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若某个分组超时，&lt;strong&gt;从该分组开始的所有已发送但未确认的分组都需要重传&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;这就是&quot;回退N帧&quot;名称的由来&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 接收方工作原理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;接收窗口&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收窗口大小 W_R = 1（只能按序接收）&lt;/li&gt;
&lt;li&gt;只接受序号在接收窗口内且无误码的分组&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;累计确认&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收方不必每个分组都发送 ACK&lt;/li&gt;
&lt;li&gt;可以在收到多个正确分组后，发送一个累计确认&lt;/li&gt;
&lt;li&gt;例如：收到 0、1、2 后，发送 ACK 2 表示 0、1、2 都已正确接收&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 优缺点&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现简单&lt;/li&gt;
&lt;li&gt;信道利用率比停止-等待协议高&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;超时重传时，需要回退重传多个分组（即使后续分组已正确接收）&lt;/li&gt;
&lt;li&gt;适用于误码率低的网络&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 选择重传协议（Selective Repeat）&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./remote-03.png&quot; alt=&quot;选择重传协议&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4.1 发送方工作原理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;窗口机制&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发送窗口大小 W_T：1 &amp;lt; W_T ≤ 2^(n-1)&lt;/li&gt;
&lt;li&gt;可以在未收到确认的情况下，连续发送 W_T 个分组&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;超时重传&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只重传超时的分组（不回退）&lt;/li&gt;
&lt;li&gt;对已正确接收的分组发送确认&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 接收方工作原理&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;接收窗口&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收窗口大小 W_R &amp;gt; 1（可以乱序接收）&lt;/li&gt;
&lt;li&gt;接收方可以接受未按序到达但无误码的分组&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;逐个确认&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收方对每个正确接收的分组单独发送 ACK&lt;/li&gt;
&lt;li&gt;不使用累计确认&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 优缺点&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只重传出错的分组，效率更高&lt;/li&gt;
&lt;li&gt;适用于误码率较高的网络&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现复杂&lt;/li&gt;
&lt;li&gt;接收方需要缓存乱序到达的分组&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.4 三种协议对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;停止-等待&lt;/th&gt;
&lt;th&gt;回退N帧&lt;/th&gt;
&lt;th&gt;选择重传&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;发送窗口&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1 &amp;lt; W_T ≤ 2^n - 1&lt;/td&gt;
&lt;td&gt;1 &amp;lt; W_T ≤ 2^(n-1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;接收窗口&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;W_R &amp;gt; 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;确认方式&lt;/td&gt;
&lt;td&gt;逐个确认&lt;/td&gt;
&lt;td&gt;累计确认&lt;/td&gt;
&lt;td&gt;逐个确认&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重传方式&lt;/td&gt;
&lt;td&gt;重传单个&lt;/td&gt;
&lt;td&gt;回退重传&lt;/td&gt;
&lt;td&gt;选择重传&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实现复杂度&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;5. PPP 协议&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./remote-04.png&quot; alt=&quot;PPP协议&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;5.1 PPP 帧格式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;F(7E) | 地址(FF) | 控制(03) | 协议 | 信息 | FCS | F(7E)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;字段说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;F（标志字段）&lt;/strong&gt;：0x7E，帧定界符&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地址字段&lt;/strong&gt;：0xFF（广播地址）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;控制字段&lt;/strong&gt;：0x03&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议字段&lt;/strong&gt;：标识信息字段的内容类型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FCS&lt;/strong&gt;：帧校验序列（CRC 校验）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 PPP 工作流程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;建立阶段&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;LCP（链路控制协议）协商链路参数&lt;/li&gt;
&lt;li&gt;认证阶段（可选）：PAP 或 CHAP&lt;/li&gt;
&lt;li&gt;NCP（网络控制协议）协商网络层参数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;数据传输阶段&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;封装数据帧&lt;/li&gt;
&lt;li&gt;透明传输（字节填充或比特填充）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;终止阶段&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LCP 关闭链路&lt;/li&gt;
&lt;li&gt;释放连接&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.3 PPP 特点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;简单可靠&lt;/li&gt;
&lt;li&gt;支持多种网络层协议&lt;/li&gt;
&lt;li&gt;适用于点对点链路（如拨号上网、PPPoE）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 总结&lt;/h2&gt;
&lt;h3&gt;核心知识点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;停止-等待&lt;/strong&gt;：简单但效率低，信道利用率低&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回退N帧&lt;/strong&gt;：连续发送，累计确认，超时回退重传&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择重传&lt;/strong&gt;：连续发送，逐个确认，选择性重传&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PPP&lt;/strong&gt;：点对点协议，简单可靠，支持多种网络层协议&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;选择依据&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;数据量小、实时性要求不高 → 停止-等待&lt;/li&gt;
&lt;li&gt;误码率低的网络 → 回退N帧&lt;/li&gt;
&lt;li&gt;误码率高的网络 → 选择重传&lt;/li&gt;
&lt;li&gt;点对点链路 → PPP&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>路由聚合与路由选择协议</title><link>https://youki.bbroot.com/posts/network/routing/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/routing/</guid><description>路由聚合、变长子网掩码、常见路由选择协议详解</description><pubDate>Fri, 01 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文整理路由聚合、子网掩码以及常见路由选择协议的核心知识点。&lt;/p&gt;
&lt;h2&gt;1. 路由聚合（构造超网）&lt;/h2&gt;
&lt;h3&gt;1.1 什么是路由聚合&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;路由聚合&lt;/strong&gt;是将多条路由记录合并为一条的过程，目的是&lt;strong&gt;减少路由表条目&lt;/strong&gt;，提高路由效率。&lt;/p&gt;
&lt;h3&gt;1.2 聚合过程详解&lt;/h3&gt;
&lt;p&gt;以 5 条路由记录为例：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;原始路由&lt;/th&gt;
&lt;th&gt;地址段&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;172.1.4.0/25&lt;/td&gt;
&lt;td&gt;172.1.4.0 ~ 172.1.4.127&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;172.1.4.128/25&lt;/td&gt;
&lt;td&gt;172.1.4.128 ~ 172.1.4.255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;172.1.5.0/24&lt;/td&gt;
&lt;td&gt;172.1.5.0 ~ 172.1.5.255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;172.1.6.0/24&lt;/td&gt;
&lt;td&gt;172.1.6.0 ~ 172.1.6.255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;172.1.7.0/24&lt;/td&gt;
&lt;td&gt;172.1.7.0 ~ 172.1.7.255&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;聚合步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找共同前缀：所有地址的前 22 位相同（172.1.4.0）&lt;/li&gt;
&lt;li&gt;计算聚合地址块：&lt;code&gt;172.1.4.0/22&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;5 条路由 → 1 条路由&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./remote-01.png&quot; alt=&quot;路由聚合示例&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1.3 最长前缀匹配原则&lt;/h3&gt;
&lt;p&gt;当路由器转发数据包时，如果存在多条匹配的路由，&lt;strong&gt;选择前缀最长的那条&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;网络前缀越长，地址块越小，路由越具体。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：更精确的路由能确保数据包到达正确的目的地。&lt;/p&gt;
&lt;h2&gt;2. 子网掩码：FLSM vs VLSM&lt;/h2&gt;
&lt;h3&gt;2.1 定长子网掩码（FLSM）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;定义&lt;/td&gt;
&lt;td&gt;使用&lt;strong&gt;同一子网掩码&lt;/strong&gt;划分所有子网&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;划分方式&lt;/td&gt;
&lt;td&gt;不灵活，只能划分 2^n 个子网&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP 分配&lt;/td&gt;
&lt;td&gt;每个子网 IP 数量相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缺点&lt;/td&gt;
&lt;td&gt;容易造成 IP 地址浪费&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2.2 变长子网掩码（VLSM）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;定义&lt;/td&gt;
&lt;td&gt;使用&lt;strong&gt;不同子网掩码&lt;/strong&gt;划分不同子网&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;划分方式&lt;/td&gt;
&lt;td&gt;灵活，可按需分配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP 分配&lt;/td&gt;
&lt;td&gt;每个子网 IP 数量可以不同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;优势&lt;/td&gt;
&lt;td&gt;减少 IP 地址浪费&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2.3 对比总结&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对比项&lt;/th&gt;
&lt;th&gt;FLSM&lt;/th&gt;
&lt;th&gt;VLSM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;子网掩码&lt;/td&gt;
&lt;td&gt;统一&lt;/td&gt;
&lt;td&gt;可变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;灵活性&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP 利用率&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适用场景&lt;/td&gt;
&lt;td&gt;简单网络&lt;/td&gt;
&lt;td&gt;复杂网络&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;./remote-02.png&quot; alt=&quot;FLSM vs VLSM&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3. 常见路由选择协议&lt;/h2&gt;
&lt;h3&gt;3.1 协议分类&lt;/h3&gt;
&lt;p&gt;路由选择协议分为两大类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内部网关协议（IGP）&lt;/strong&gt;：用于自治系统内部&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;外部网关协议（EGP）&lt;/strong&gt;：用于自治系统之间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./remote-03.png&quot; alt=&quot;路由协议分类&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.2 内部网关协议（IGP）&lt;/h3&gt;
&lt;h4&gt;RIP（路由信息协议）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;基于&lt;strong&gt;距离向量&lt;/strong&gt;算法&lt;/li&gt;
&lt;li&gt;因特网上&lt;strong&gt;最早使用&lt;/strong&gt;的路由协议&lt;/li&gt;
&lt;li&gt;简单但收敛慢，适用于小型网络&lt;/li&gt;
&lt;li&gt;最大跳数限制为 &lt;strong&gt;15 跳&lt;/strong&gt;（16 跳视为不可达）&lt;/li&gt;
&lt;li&gt;每 &lt;strong&gt;30 秒&lt;/strong&gt;向邻居广播整个路由表&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;IGRP（内部网关路由协议）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;思科&lt;strong&gt;私有&lt;/strong&gt;协议&lt;/li&gt;
&lt;li&gt;已被 EIGRP 替代&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;EIGRP（增强型内部网关路由协议）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;思科&lt;strong&gt;私有&lt;/strong&gt;的&lt;strong&gt;混合型&lt;/strong&gt;协议&lt;/li&gt;
&lt;li&gt;结合距离向量和链路状态的优点&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;OSPF（开放最短路径优先）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;基于&lt;strong&gt;链路状态&lt;/strong&gt;算法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;广泛使用&lt;/strong&gt;的 IGP 协议&lt;/li&gt;
&lt;li&gt;收敛快，支持大型网络&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;IS-IS（中间系统到中间系统）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;基于链路状态算法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISP 骨干网最常用&lt;/strong&gt;的 IGP 协议&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 外部网关协议（EGP）&lt;/h3&gt;
&lt;h4&gt;BGP（边界网关协议）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;自治系统间的&lt;strong&gt;域间路由&lt;/strong&gt;协议&lt;/li&gt;
&lt;li&gt;互联网骨干网的核心协议&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;关键点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;路由聚合&lt;/td&gt;
&lt;td&gt;合并多条路由为一条，减少路由表条目&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最长前缀匹配&lt;/td&gt;
&lt;td&gt;选择前缀最长的路由，更精确&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VLSM&lt;/td&gt;
&lt;td&gt;按需分配 IP，减少浪费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSPF&lt;/td&gt;
&lt;td&gt;链路状态协议，广泛使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BGP&lt;/td&gt;
&lt;td&gt;自治系统间路由协议&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;学习建议&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;理解路由聚合的计算过程&lt;/li&gt;
&lt;li&gt;掌握 VLSM 子网划分方法&lt;/li&gt;
&lt;li&gt;熟悉常见协议的特点和适用场景&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>VLAN 与 IPv4 地址笔记</title><link>https://youki.bbroot.com/posts/network/vlan-ipv4/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/vlan-ipv4/</guid><description>MAC 多播地址、IEEE 802.1Q VLAN 实现、交换机端口类型、IPv4 地址分类与子网划分</description><pubDate>Fri, 01 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. MAC 地址与多播&lt;/h2&gt;
&lt;h3&gt;1.1 MAC 多播地址判断&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-01.png&quot; alt=&quot;MAC多播地址判断&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;判断方法&lt;/strong&gt;：检查 MAC 地址第一字节的最低位（b0 位）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;b0 = 0&lt;/strong&gt;：单播地址（一对一通信）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;b0 = 1&lt;/strong&gt;：多播地址（一对多通信）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;code&gt;07-E0-12-F6-2A-D8&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一字节 &lt;code&gt;07&lt;/code&gt; 的二进制为 &lt;code&gt;0000 0111&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最低位 b0 = 1 → 这是&lt;strong&gt;多播地址&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 IEEE 802 局域网 MAC 地址格式&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-02.png&quot; alt=&quot;MAC地址格式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EUI-48 格式&lt;/strong&gt;（48 位 = 6 字节）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;长度&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OUI（组织唯一标识符）&lt;/td&gt;
&lt;td&gt;3 字节&lt;/td&gt;
&lt;td&gt;由 IEEE 分配给厂商&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;网络接口标识符&lt;/td&gt;
&lt;td&gt;3 字节&lt;/td&gt;
&lt;td&gt;厂商自行分配&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;第一字节的位含义&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;b0 位&lt;/strong&gt;：0 = 单播，1 = 多播&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;b1 位&lt;/strong&gt;：0 = 全球管理（厂商分配），1 = 本地管理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;地址数量&lt;/strong&gt;：2^47 ≈ 281 万亿个全球单播地址&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. VLAN 技术&lt;/h2&gt;
&lt;h3&gt;2.1 IEEE 802.1Q 帧格式&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-03.png&quot; alt=&quot;802.1Q帧格式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;VLAN 标记结构&lt;/strong&gt;（插入 4 字节 VLAN 标签）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;目的MAC(6B) | 源MAC(6B) | VLAN标签(4B) | 类型(2B) | 数据(46-1500B) | FCS(4B)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;VLAN 标签字段&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TPID&lt;/strong&gt;（2 字节）：0x8100，表示 802.1Q 帧&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCI&lt;/strong&gt;（2 字节）：包含优先级、CFI、VID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;VID（VLAN ID）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;取值范围：0 ~ 4095（12 位）&lt;/li&gt;
&lt;li&gt;有效范围：1 ~ 4094（0 和 4095 保留）&lt;/li&gt;
&lt;li&gt;唯一标识以太网帧属于哪个 VLAN&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;交换机处理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;入向&lt;/strong&gt;：收到普通以太网帧 → 打标签（插入 4 字节 VLAN 标记）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;出向&lt;/strong&gt;：转发前 → 去标签（删除 4 字节 VLAN 标记）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 交换机端口类型&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;三种端口类型&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;PVID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Access&lt;/td&gt;
&lt;td&gt;连接终端设备，只属于一个 VLAN&lt;/td&gt;
&lt;td&gt;通常为 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trunk&lt;/td&gt;
&lt;td&gt;连接交换机/路由器，可属于多个 VLAN&lt;/td&gt;
&lt;td&gt;默认为 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid&lt;/td&gt;
&lt;td&gt;华为特有，灵活配置&lt;/td&gt;
&lt;td&gt;可自定义&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2.3 Tagged 与 Untagged 帧&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Untagged Frame（普通数据帧）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;终端设备（电脑、打印机）发出的原始以太网帧&lt;/li&gt;
&lt;li&gt;不带任何 VLAN 标签，终端设备不认识 VLAN&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Tagged Frame（带标签的数据帧）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;交换机之间传输时，在帧中插入 4 字节 VLAN Tag&lt;/li&gt;
&lt;li&gt;明确标明该帧属于哪个 VLAN（VID）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4 PVID（Port VLAN ID）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;PVID 的核心作用&lt;/strong&gt;：给&quot;无标签&quot;的数据帧&quot;贴标签&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;术语对照&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思科&lt;/strong&gt;：称为 Native VLAN（本征 VLAN）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;华为&lt;/strong&gt;：称为 Port VLAN ID（端口 VLAN ID）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;工作流程&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据进入交换机（Ingress）&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;收到 Untagged 数据帧&lt;/li&gt;
&lt;li&gt;查看端口 PVID 设置（假设 PVID=10）&lt;/li&gt;
&lt;li&gt;为数据帧打上 VLAN Tag，标记为 VLAN 10&lt;/li&gt;
&lt;li&gt;后续按 VLAN 10 成员处理和转发&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;数据离开交换机（Egress）&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查端口设置&lt;/li&gt;
&lt;li&gt;若端口允许该 VLAN 以 Untagged 方式送出 → &lt;strong&gt;剥离 VLAN Tag&lt;/strong&gt; 再发送&lt;/li&gt;
&lt;li&gt;若端口允许该 VLAN 以 Tagged 方式送出 → &lt;strong&gt;保留 VLAN Tag&lt;/strong&gt; 直接发送&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.5 Trunk 端口工作原理&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-04.png&quot; alt=&quot;Trunk端口&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trunk 端口特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用于交换机之间或交换机与路由器互连&lt;/li&gt;
&lt;li&gt;可以承载多个 VLAN 的流量&lt;/li&gt;
&lt;li&gt;默认 PVID = 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;发送处理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对 VID = PVID 的帧：&lt;strong&gt;去标签&lt;/strong&gt;后转发&lt;/li&gt;
&lt;li&gt;对 VID ≠ PVID 的帧：保留标签转发&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;接收处理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;收到未打标签的帧：根据端口 PVID 打标签&lt;/li&gt;
&lt;li&gt;收到已打标签的帧：直接转发&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. IPv4 地址&lt;/h2&gt;
&lt;h3&gt;3.1 IPv4 地址分类&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-05.png&quot; alt=&quot;IPv4地址分类练习&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;地址分类规则&lt;/strong&gt;（根据第一个十进制数）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类别&lt;/th&gt;
&lt;th&gt;第一字节范围&lt;/th&gt;
&lt;th&gt;网络号长度&lt;/th&gt;
&lt;th&gt;主机号长度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A 类&lt;/td&gt;
&lt;td&gt;1 ~ 126&lt;/td&gt;
&lt;td&gt;8 位&lt;/td&gt;
&lt;td&gt;24 位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B 类&lt;/td&gt;
&lt;td&gt;128 ~ 191&lt;/td&gt;
&lt;td&gt;16 位&lt;/td&gt;
&lt;td&gt;16 位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C 类&lt;/td&gt;
&lt;td&gt;192 ~ 223&lt;/td&gt;
&lt;td&gt;24 位&lt;/td&gt;
&lt;td&gt;8 位&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;判断要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网络号全 0：本网络本主机（DHCP）&lt;/li&gt;
&lt;li&gt;主机号全 0：网络地址（不可指派）&lt;/li&gt;
&lt;li&gt;主机号全 1：广播地址（不可指派）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 特殊 IPv4 地址&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-06.png&quot; alt=&quot;特殊IPv4地址&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;地址类型&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;网络号全 0&lt;/td&gt;
&lt;td&gt;0.x.x.x&lt;/td&gt;
&lt;td&gt;本网络本主机（DHCP）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主机号全 1&lt;/td&gt;
&lt;td&gt;x.x.x.255&lt;/td&gt;
&lt;td&gt;本网络广播&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;网络号全 1&lt;/td&gt;
&lt;td&gt;255.255.255.255&lt;/td&gt;
&lt;td&gt;受限广播（路由器不转发）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;127.x.x.x&lt;/td&gt;
&lt;td&gt;127.0.0.1&lt;/td&gt;
&lt;td&gt;本地环回测试&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;环回测试&lt;/strong&gt;：通过&quot;自发自收&quot;方式验证本地协议栈，不经过网卡。&lt;/p&gt;
&lt;h3&gt;3.3 子网划分&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./remote-07.png&quot; alt=&quot;子网划分计算&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;子网划分步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;确定子网掩码&lt;/strong&gt;：将 IP 地址与子网掩码进行 AND 运算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算网络地址&lt;/strong&gt;：IP &amp;amp; 子网掩码 = 网络地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算广播地址&lt;/strong&gt;：网络地址 | (NOT 子网掩码) = 广播地址&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IP：180.80.77.55&lt;/li&gt;
&lt;li&gt;子网掩码：255.255.252.0（/22）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;计算过程&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IP：      180.80.0100 1101.0011 0111
掩码：    255.255.1111 1100.0000 0000
网络地址：180.80.0100 1100.0000 0000 = 180.80.76.0
广播地址：180.80.0100 1111.1111 1111 = 180.80.79.255
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 总结&lt;/h2&gt;
&lt;h3&gt;核心知识点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;MAC 多播&lt;/strong&gt;：第一字节最低位为 1&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VLAN&lt;/strong&gt;：802.1Q 帧插入 4 字节标签，VID 范围 1-4094&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端口类型&lt;/strong&gt;：Access（终端）、Trunk（交换机间）、Hybrid（灵活）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tagged/Untagged&lt;/strong&gt;：终端发 Untagged，交换机间发 Tagged&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PVID&lt;/strong&gt;：为 Untagged 帧打标签（入向），或剥离标签（出向）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv4 分类&lt;/strong&gt;：A/B/C 类根据第一字节判断&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子网划分&lt;/strong&gt;：IP &amp;amp; 掩码 = 网络地址，网络地址 | (NOT 掩码) = 广播地址&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>C++ 知识点</title><link>https://youki.bbroot.com/posts/cs/cpp-knowledge-points/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/cs/cpp-knowledge-points/</guid><description>C++ 常用知识点速查：sort、max、getline、按位运算、vector</description><pubDate>Thu, 31 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;sort(first, last)&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;first&lt;/code&gt;：指向排序范围的起始位置（包含）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;last&lt;/code&gt;：指向排序范围的&lt;strong&gt;末尾的下一个位置&lt;/strong&gt;（不包含）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因此，&lt;code&gt;sort(sum, sum + 5)&lt;/code&gt; 排序的是 &lt;code&gt;sum[0]&lt;/code&gt; 到 &lt;code&gt;sum[4]&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;如果要降序排序，可以使用：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方法 1：greater&amp;lt;int&amp;gt;()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sort(a, a + 4, greater&amp;lt;int&amp;gt;());  // 结果：{9, 5, 2, 1}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方法 2：Lambda 表达式（C++11）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sort(a, a + 4, [](int x, int y) { return x &amp;gt; y; });
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;max(res, nums[i])&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;在 C++ 中，&lt;code&gt;res = max(res, nums[i]);&lt;/code&gt; 是用于更新变量 &lt;code&gt;res&lt;/code&gt; 为当前最大值的一种常见写法。它的作用是：&lt;strong&gt;比较 &lt;code&gt;res&lt;/code&gt; 和 &lt;code&gt;nums[i]&lt;/code&gt;，并将较大的值赋给 &lt;code&gt;res&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;s.length()和s.size()区别&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;不同点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;length()&lt;/code&gt; 主要用于字符串，&lt;code&gt;size()&lt;/code&gt; 更通用&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;length()&lt;/code&gt; 是 &lt;code&gt;std::string&lt;/code&gt; 的成员函数，&lt;strong&gt;不适用于 STL 容器&lt;/strong&gt;（如 &lt;code&gt;vector&lt;/code&gt;、&lt;code&gt;list&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt; 是 STL 容器的通用接口，&lt;strong&gt;适用于 &lt;code&gt;string&lt;/code&gt; 和所有 STL 容器&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;判断质数的函数&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;bool fun(int x) {
    if (x &amp;lt;= 1)
        return false;
    if (x == 2)
        return true;
    for (int i = 2; i * i &amp;lt;= x; i++)
        if (x % i == 0)
            return false;
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;getline(cin, s)&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;在 C++ 中，&lt;code&gt;getline(cin, s)&lt;/code&gt; 用于从标准输入（键盘）读取&lt;strong&gt;一行字符串&lt;/strong&gt;，并存储到 &lt;code&gt;std::string&lt;/code&gt; 变量 &lt;code&gt;s&lt;/code&gt; 中。它比 &lt;code&gt;cin &amp;gt;&amp;gt; s&lt;/code&gt; 更强大，可以读取包含空格的字符串。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;混合 &lt;code&gt;cin &amp;gt;&amp;gt;&lt;/code&gt; 和 &lt;code&gt;getline&lt;/code&gt; 时要注意缓冲区&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;如果先 &lt;code&gt;cin &amp;gt;&amp;gt;&lt;/code&gt; 再 &lt;code&gt;getline&lt;/code&gt;，&lt;code&gt;cin&lt;/code&gt; 会留下 &lt;code&gt;\n&lt;/code&gt; 在缓冲区，导致 &lt;code&gt;getline&lt;/code&gt; 直接读取空行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方法：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;cin &amp;gt;&amp;gt;&lt;/code&gt; 后加 &lt;code&gt;cin.ignore()&lt;/code&gt; 清除缓冲区：&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;&lt;code&gt;getline&lt;/code&gt; 可以指定分隔符（默认 &lt;code&gt;\n&lt;/code&gt;）&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;string data;
getline(cin, data, &apos;,&apos;);  // 读取直到遇到逗号
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;1. 按位与（&lt;code&gt;&amp;amp;&lt;/code&gt;）按位或（&lt;code&gt;|&lt;/code&gt;）&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;对两个数的二进制表示逐位比较，&lt;strong&gt;只有两个位都为 1 时，结果的该位才为 1&lt;/strong&gt;，否则为 0。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int a = 5;    // 二进制: 0101
int b = 3;    // 二进制: 0011
int c = a &amp;amp; b; // 结果:  0001 (十进制 1)
cout &amp;lt;&amp;lt; c;    // 输出 1
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;对两个数的二进制表示逐位比较，&lt;strong&gt;只要有一个位为 1，结果的该位就为 1&lt;/strong&gt;，否则为 0。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int a = 5;    // 二进制: 0101
int b = 3;    // 二进制: 0011
int c = a | b; // 结果:  0111 (十进制 7)
cout &amp;lt;&amp;lt; c;    // 输出 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;vector&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./remote-01.png&quot; alt=&quot;image-20250731112459215&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-02.png&quot; alt=&quot;image-20250731112731778&quot; /&gt;&lt;/p&gt;
&lt;p&gt;push_back()用于在容器的&lt;strong&gt;末尾&lt;/strong&gt;添加一个新元素。&lt;/p&gt;
</content:encoded></item><item><title>GFW 识别 CDN 相关笔记</title><link>https://youki.bbroot.com/posts/network/gfw-cdn-notes/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/gfw-cdn-notes/</guid><description>GFW 识别 Cloudflare CDN 代理的多重手段：SNI 明文、DNS 情报、流量指纹</description><pubDate>Sun, 27 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;参考讨论：&lt;a href=&quot;https://github.com/XTLS/BBS/issues/23&quot;&gt;GFW 大战 Cloudflare，以及积至公司推荐的火爆翻墙项目&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;1. SNI（服务器名称指示）—— 明文的&quot;门牌号&quot;&lt;/h2&gt;
&lt;h3&gt;什么是 SNI？&lt;/h3&gt;
&lt;p&gt;当你访问一个 HTTPS 网站时，在加密连接建立之前，你会明文发送一个字段叫 &lt;strong&gt;SNI&lt;/strong&gt;，告诉服务器你要访问哪个域名。&lt;/p&gt;
&lt;p&gt;比如访问 &lt;code&gt;worker-demo.yourname.workers.dev&lt;/code&gt;，这个域名会以&lt;strong&gt;明文&lt;/strong&gt;形式在网络上传输。&lt;/p&gt;
&lt;h3&gt;GFW 如何利用 SNI？&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;正常用户访问的 SNI：
- www.baidu.com
- www.bilibili.com
- api.github.com

翻墙用户访问的 SNI：
- proxy123.username.workers.dev  ← Workers 默认域名
- tunnel.abcd.pages.dev          ← Pages 域名
- *.trycloudflare.com            ← Tunnel 临时域名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;识别逻辑：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;GFW 可以维护一个&quot;Workers 子域名黑名单&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只要检测到 SNI 包含 &lt;code&gt;workers.dev&lt;/code&gt;、&lt;code&gt;pages.dev&lt;/code&gt; 等特征，或匹配已知的翻墙 Worker 子域名，就直接阻断&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;即使你用自定义域名（如 &lt;code&gt;my-proxy.com&lt;/code&gt;），如果 GFW 发现这个域名指向 Cloudflare Workers IP 且有异常流量模式，也会被标记&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;长时间连接检测：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;正常 HTTPS 连接平均持续 30-60 秒&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代理连接可能持续 30 分钟以上，且期间一直有数据传输&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GFW 标记&quot;持续活跃的 HTTPS 长连接&quot;为可疑&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;流量指纹（Packet Size &amp;amp; Timing）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;你的代理工具（如 Clash、V2Ray）在加密数据时，会产生特定大小的数据包&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比如每 2 秒发送一个 1400 字节的心跳包&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GFW 通过机器学习识别这种&quot;机械式&quot;的流量模式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;综合举例：GFW 如何判定你在用 edgetunnel&lt;/h2&gt;
&lt;p&gt;假设你在用 edgetunnel（一个基于 Workers 的代理）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DNS 阶段&lt;/strong&gt;：你的电脑查询 &lt;code&gt;edgetunnel.yourname.workers.dev&lt;/code&gt; → &lt;strong&gt;可疑&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TLS 握手阶段&lt;/strong&gt;：SNI 明文显示 &lt;code&gt;yourname.workers.dev&lt;/code&gt; → &lt;strong&gt;确认是 Workers&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;指纹检测&lt;/strong&gt;：JA3 显示这不是浏览器，是代理客户端 → &lt;strong&gt;确认是代理&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;流量分析&lt;/strong&gt;：连接持续 2 小时，上下行流量对称，心跳包规律 → &lt;strong&gt;确认是代理隧道&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IP 分析&lt;/strong&gt;：你连接的 Cloudflare IP 是 &lt;code&gt;104.16.x.x&lt;/code&gt;，不是最优节点 → &lt;strong&gt;确认使用了优选 IP&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结论：阻断该连接，或限速，或记录你的 IP 进行重点监控。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GFW 识别 Workers 代理的多重手段：

1. SNI 明文检查（第一层）
   workers.dev / pages.dev → 直接标记

2. 自定义域名（第二层）
   yourdomain.com → 指向 104.16.x.x 
                 → 流量特征异常 
                 → 回溯发现是 Workers IP 
                 → 标记

3. DNS 情报收集（第三层）
   监控全国 DNS 查询日志
   → 发现大量 .workers.dev 查询来自特定用户群
   → 分析这些域名
   → 发现是代理工具
   → 加入黑名单

4. IP + 流量指纹（第四层）
   无论域名是什么
   → 只要连接到 Workers IP 段
   → 且 JA3/流量模式匹配代理特征
   → 精准识别
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;补充：为什么机场较少用 Hysteria&lt;/h2&gt;
&lt;p&gt;一个常见疑问：Hysteria 协议出来挺久了，为什么少见机场采用？大致原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;迁移成本&lt;/strong&gt;：Hysteria 较新，原有机场既有协议用得好，没必要冒风险换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;QUIC 被限流&lt;/strong&gt;：Hysteria 基于 QUIC（UDP），运营商对 UDP 的 QoS 限流比较狠，高峰期尤其明显，自己建一个对比测速就能感受到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;替代方案够强&lt;/strong&gt;：目前抗审查能力很强的组合是 &lt;strong&gt;VLESS + Reality&lt;/strong&gt;——VLESS 开销小，Reality 伪装到位，对机场来说够用且稳定。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以选型不是&quot;新就一定好&quot;，而是在抗审查、稳定性、带宽开销之间权衡。&lt;/p&gt;
</content:encoded></item><item><title>计算机网络基础：OSI 模型与网络设备</title><link>https://youki.bbroot.com/posts/network/osi-model/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/network/osi-model/</guid><description>OSI 七层模型、TCP/IP 四层模型、集线器/交换机/路由器原理、计算机网络性能指标</description><pubDate>Sun, 27 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. OSI七层模型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./remote-01.png&quot; alt=&quot;OSI&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-02.png&quot; alt=&quot;OSI七层&quot; /&gt;&lt;/p&gt;
&lt;p&gt;OSI定义了网络互连的七层框架，即ISO开放互连系统参考模型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;应用层&lt;/strong&gt;（Application Layer）：提供用户接口和应用程序之间的通信服务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;表示层&lt;/strong&gt;（Presentation Layer）：负责数据的格式化、加密和压缩&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会话层&lt;/strong&gt;（Session Layer）：管理应用程序之间的通信会话&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传输层&lt;/strong&gt;（Transport Layer）：为应用程序提供端到端的数据传输服务，主要使用 TCP 和 UDP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络层&lt;/strong&gt;（Network Layer）：负责数据包的路由和转发，以及网络中的寻址和拥塞控制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据链路层&lt;/strong&gt;（Data Link Layer）：提供点对点的数据传输服务，将比特流转换为数据帧&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理层&lt;/strong&gt;（Physical Layer）：在物理媒介上传输原始比特流&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. TCP/IP四层模型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./remote-03.png&quot; alt=&quot;TCP/IP模型&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;应用层&lt;/strong&gt;：处理用户与网络应用程序之间的通信（HTTP、FTP、SMTP等）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传输层&lt;/strong&gt;：提供端到端的数据传输服务（TCP、UDP）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络层&lt;/strong&gt;：负责数据包的路由和转发（IP协议）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络接口层&lt;/strong&gt;：管理网络硬件设备和物理媒介之间的通信&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;应用层常见协议&lt;/strong&gt;：HTTP、FTP、SMTP、POP3、IMAP、DNS、HTTPS、SSH、SNMP、Telnet&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传输层常见协议&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TCP：可靠的、面向连接的数据传输（文件传输、网页浏览）&lt;/li&gt;
&lt;li&gt;UDP：无连接的数据传输（音视频传输、在线游戏）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;网络层常见协议&lt;/strong&gt;：IP、ICMP、ARP、RARP、IPv6&lt;/p&gt;
&lt;h2&gt;3. 专用术语&lt;/h2&gt;
&lt;h3&gt;(1) 实体&lt;/h3&gt;
&lt;p&gt;实体是指任何可发送或接收信息的硬件或软件进程。对等实体是指通信双方相同层次中的实体。&lt;/p&gt;
&lt;h3&gt;(2) 协议&lt;/h3&gt;
&lt;p&gt;协议是控制两个对等实体在&quot;水平方向&quot;进行&quot;逻辑通信&quot;的规则的集合。&lt;/p&gt;
&lt;p&gt;协议有三大要素：&lt;strong&gt;语法、语义、同步&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;语法：定义通信双方所交换信息的格式&lt;/li&gt;
&lt;li&gt;语义：定义通信双方所要完成的操作&lt;/li&gt;
&lt;li&gt;同步：定义通信双方的时序关系&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(3) 服务&lt;/h3&gt;
&lt;p&gt;在协议的控制下，两个对等实体在水平方向的逻辑通信使得本层能够向上一层提供服务。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;协议数据单元&lt;/strong&gt;（PDU）：对等层次之间传送的数据包&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务数据单元&lt;/strong&gt;（SDU）：同一系统内层与层之间交换的数据包&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 计算机网络概念&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./part2-remote-01.png&quot; alt=&quot;网络设备&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;集线器（Hub）&lt;/strong&gt;：把多个结点连接起来，工作在物理层，普通民用领域已很少用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交换机（Switch）&lt;/strong&gt;：把多个结点连接起来，工作在数据链路层，家庭/公司/学校常用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路由器（Router）&lt;/strong&gt;：把多个计算机网络互相连接起来，工作在网络层&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Tips：计算机网络课程中的&quot;路由器&quot;和&quot;家用路由器&quot;有区别。家用路由器 = 路由器 + 交换机 + 其他功能&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;5. 计算机网络的组成和功能&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./part2-remote-04.png&quot; alt=&quot;计算机网络的组成&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./part2-remote-05.png&quot; alt=&quot;计算机网络的功能&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;6. 电路交换、报文交换、分组交换&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./part2-remote-11.png&quot; alt=&quot;电路交换&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./part2-remote-12.png&quot; alt=&quot;分组交换与报文交换&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.1 电路交换 vs 分组交换时延计算&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;条件&lt;/strong&gt;：报文共 x(bit)，源到终点经 k 段链路，每段传播时延 d(s)，带宽 b(bit/s)。电路交换建立时间 s(s)。分组交换将报文划分为长度 p(bit) 的分组（首部长度和排队时间忽略不计）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./switching-delay-01.png&quot; alt=&quot;电路交换时延&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./switching-delay-02.png&quot; alt=&quot;分组交换时延&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;电路交换时延&lt;/strong&gt; = s + x/b + kd&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分组交换时延&lt;/strong&gt; = x/b + (k−1)×(p/b) + kd&lt;/p&gt;
&lt;p&gt;令电路交换时延 &amp;gt; 分组交换时延，解得：&lt;strong&gt;s &amp;gt; (k−1)×p/b&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最优分组长度&lt;/strong&gt;：设分组数据部分长度为 p，首部为 h，总时延 D：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./switching-delay-03.png&quot; alt=&quot;最优分组长度推导&quot; /&gt;&lt;/p&gt;
&lt;p&gt;令 dD/dp = 0，解得 &lt;strong&gt;p = √(xh/(k−1))&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;7. 计算机网络分类&lt;/h2&gt;
&lt;p&gt;按覆盖范围分类：**局域网（LAN）**覆盖较小区域（如办公室、校园）；**城域网（MAN）**覆盖城市范围；**广域网（WAN）**覆盖国家或全球范围；**个人区域网（PAN）**覆盖个人周围约10米范围。按拓扑结构可分为总线型、星型、环型、网状型等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./part2-remote-21.png&quot; alt=&quot;计算机网络分类&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;8. 性能指标&lt;/h2&gt;
&lt;p&gt;信道（Channel）：表示向某一方向传送信息的通道。一条通信线路在逻辑上对应一条发送信道和一条接收信道。&lt;/p&gt;
&lt;p&gt;主要性能指标包括：&lt;strong&gt;速率&lt;/strong&gt;（数据传输速率，单位 bit/s）、&lt;strong&gt;带宽&lt;/strong&gt;（信道能通过的最高频率范围，单位 Hz 或 bit/s）、&lt;strong&gt;吞吐量&lt;/strong&gt;（单位时间内通过某个网络的实际数据量）、&lt;strong&gt;时延&lt;/strong&gt;（发送时延 + 传播时延 + 排队时延 + 处理时延）、&lt;strong&gt;往返时间 RTT&lt;/strong&gt;、&lt;strong&gt;丢包率&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./part2-remote-28.png&quot; alt=&quot;性能指标-带宽与速率&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./part2-remote-29.png&quot; alt=&quot;性能指标-时延&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;9. 分层架构&lt;/h2&gt;
&lt;p&gt;分层架构将复杂的网络通信问题分解为若干层次，每层实现特定功能并为上层提供服务。OSI 采用七层模型，TCP/IP 采用四层模型，两者可对应映射。各层的协议数据单元（PDU）不同：物理层为比特、数据链路层为帧、网络层为分组/包、传输层为报文段。层与层之间通过**服务访问点（SAP）**进行交互。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./part2-remote-39.png&quot; alt=&quot;分层架构&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;10. 各层功能速查&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层次&lt;/th&gt;
&lt;th&gt;传输单位&lt;/th&gt;
&lt;th&gt;主要功能&lt;/th&gt;
&lt;th&gt;典型协议&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;物理层&lt;/td&gt;
&lt;td&gt;比特&lt;/td&gt;
&lt;td&gt;比特流的透明传输（电气、物理特性）&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据链路层&lt;/td&gt;
&lt;td&gt;帧&lt;/td&gt;
&lt;td&gt;结点到结点的可靠传输，帧同步、差错控制、流量控制、访问控制&lt;/td&gt;
&lt;td&gt;以太网、PPP、HDLC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;网络层&lt;/td&gt;
&lt;td&gt;分组&lt;/td&gt;
&lt;td&gt;源主机到目的主机的分组传输（跨越多个网络）&lt;/td&gt;
&lt;td&gt;IP、ICMP、ARP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;运输层&lt;/td&gt;
&lt;td&gt;报文&lt;/td&gt;
&lt;td&gt;进程到进程的可靠传输&lt;/td&gt;
&lt;td&gt;TCP、UDP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;应用层&lt;/td&gt;
&lt;td&gt;报文&lt;/td&gt;
&lt;td&gt;提供用户接口和网络服务&lt;/td&gt;
&lt;td&gt;HTTP、FTP、DNS、SMTP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;易混点&lt;/strong&gt;：数据链路层提供的是&quot;分组在一个网络（或一段链路）上传输服务&quot;，网络层提供的是&quot;跨网络的主机到主机传输服务&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>链表基础与 LeetCode 练习</title><link>https://youki.bbroot.com/posts/cs/linked-list/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/cs/linked-list/</guid><description>链表基础概念、五大操作</description><pubDate>Tue, 15 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;一、链表概念&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;与数组的区别&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组：连续内存空间，随机访问高效（O(1)），但插入/删除需移动元素（O(n)）&lt;/li&gt;
&lt;li&gt;链表：&lt;strong&gt;节点离散存储&lt;/strong&gt;，通过指针连接（逻辑连续，物理非连续）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;访问效率&lt;/strong&gt;：链表只能顺序访问（O(n)），但插入/删除只需修改指针（O(1)）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;节点结构（C++实现）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct ListNode {
    int val;         // 存储数据
    ListNode* next;  // 指向下一节点的指针
    ListNode(int x) : val(x), next(nullptr) {}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;二、链表底层机制&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内存分配原理&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;节点内存非连续 → &lt;strong&gt;动态分配&lt;/strong&gt;（C++用&lt;code&gt;new&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;创建节点：&lt;code&gt;ListNode* node = new ListNode(10);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;释放节点：&lt;code&gt;delete node;&lt;/code&gt;（避免内存泄漏）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指针操作图解&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;初始： head → A → B → C → nullptr
删除B：
  1. 定位A: A-&amp;gt;next = B-&amp;gt;next
  2. 释放B: delete B
结果： head → A → C → nullptr
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;三、链表五大基础操作（C++实现）&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;1. 遍历链表&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;void traverse(ListNode* head) {
    ListNode* cur = head;
    while (cur != nullptr) {
        cout &amp;lt;&amp;lt; cur-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;
        cur = cur-&amp;gt;next;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;2. 插入节点&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;头部插入&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void insertAtHead(ListNode*&amp;amp; head, int val) {
    ListNode* newNode = new ListNode(val);
    newNode-&amp;gt;next = head;
    head = newNode;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;尾部插入&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void insertAtTail(ListNode*&amp;amp; head, int val) {
    ListNode* newNode = new ListNode(val);
    if (head == nullptr) {
        head = newNode;
        return;
    }
    ListNode* cur = head;
    while (cur-&amp;gt;next != nullptr) {
        cur = cur-&amp;gt;next;
    }
    cur-&amp;gt;next = newNode;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;中间插入（在pos节点后插入）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void insertAfter(ListNode* pos, int val) {
    if (pos == nullptr) return;
    ListNode* newNode = new ListNode(val);
    newNode-&amp;gt;next = pos-&amp;gt;next;
    pos-&amp;gt;next = newNode;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;3. 删除节点&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;void deleteNode(ListNode*&amp;amp; head, int val) {
    if (head == nullptr) return;

    if (head-&amp;gt;val == val) {
        ListNode* temp = head;
        head = head-&amp;gt;next;
        delete temp;
        return;
    }

    ListNode* cur = head;
    while (cur-&amp;gt;next != nullptr &amp;amp;&amp;amp; cur-&amp;gt;next-&amp;gt;val != val) {
        cur = cur-&amp;gt;next;
    }

    if (cur-&amp;gt;next != nullptr) {
        ListNode* temp = cur-&amp;gt;next;
        cur-&amp;gt;next = cur-&amp;gt;next-&amp;gt;next;
        delete temp;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;4. 修改节点值&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;void updateNode(ListNode* head, int oldVal, int newVal) {
    ListNode* cur = head;
    while (cur != nullptr) {
        if (cur-&amp;gt;val == oldVal) {
            cur-&amp;gt;val = newVal;
            return;
        }
        cur = cur-&amp;gt;next;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;5. 查找节点&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;ListNode* searchNode(ListNode* head, int val) {
    ListNode* cur = head;
    while (cur != nullptr) {
        if (cur-&amp;gt;val == val) {
            return cur;
        }
        cur = cur-&amp;gt;next;
    }
    return nullptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;四、关键点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;头指针的特殊处理&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;头节点可能被修改 → 使用&lt;code&gt;ListNode*&amp;amp;&lt;/code&gt;（指针引用）或二级指针&lt;/li&gt;
&lt;li&gt;空链表判断：&lt;code&gt;if (head == nullptr)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;边界条件检查&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入/删除头节点&lt;/li&gt;
&lt;li&gt;操作空链表&lt;/li&gt;
&lt;li&gt;处理尾节点（&lt;code&gt;next&lt;/code&gt;指向&lt;code&gt;nullptr&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内存管理要点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次&lt;code&gt;new&lt;/code&gt;后必须对应&lt;code&gt;delete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;删除节点时需先保存下一节点指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;哨兵节点技巧（简化操作）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ListNode* dummy = new ListNode(0);
dummy-&amp;gt;next = head;
// ...执行操作...
head = dummy-&amp;gt;next;
delete dummy;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;五、链表 vs 数组&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;数组&lt;/th&gt;
&lt;th&gt;链表&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;内存连续性&lt;/td&gt;
&lt;td&gt;连续&lt;/td&gt;
&lt;td&gt;非连续&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;访问方式&lt;/td&gt;
&lt;td&gt;O(1)随机访问&lt;/td&gt;
&lt;td&gt;O(n)顺序访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;插入/删除成本&lt;/td&gt;
&lt;td&gt;O(n)需要移动元素&lt;/td&gt;
&lt;td&gt;O(1)修改指针&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;扩容&lt;/td&gt;
&lt;td&gt;需重新分配内存&lt;/td&gt;
&lt;td&gt;动态增加节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;头部插入&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;局部性原理&lt;/td&gt;
&lt;td&gt;优&lt;/td&gt;
&lt;td&gt;差&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;六、LeetCode 练习&lt;/h3&gt;
&lt;h4&gt;1. 删除链表中的节点（LeetCode 237）&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;问题本质&lt;/strong&gt;：在只给定被删除节点（非尾节点）的情况下，实现节点删除&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void deleteNode(struct ListNode* node) {
    node-&amp;gt;val = node-&amp;gt;next-&amp;gt;val;
    node-&amp;gt;next = node-&amp;gt;next-&amp;gt;next;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无法访问前驱节点，因此采用&quot;值替换+跳过&quot;策略&lt;/li&gt;
&lt;li&gt;时间复杂度 O(1)，空间复杂度 O(1)&lt;/li&gt;
&lt;li&gt;特例处理：不能删除尾节点（题目保证）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 反转链表（LeetCode 206）&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;迭代法&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./remote-01.png&quot; alt=&quot;反转链表图解&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode *cur = head, *pre = nullptr;
        while(cur != nullptr) {
            ListNode* tmp = cur-&amp;gt;next;
            cur-&amp;gt;next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;迭代步骤详解&lt;/strong&gt;（每轮循环的指针状态变化）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;步骤&lt;/th&gt;
&lt;th&gt;状态描述&lt;/th&gt;
&lt;th&gt;图示&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;cur=1, tmp=2, pre=null，反转 1→null&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;./remote-02.png&quot; alt=&quot;step1&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;pre=cur 进行中，准备进入下一步&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;./remote-03.png&quot; alt=&quot;step2&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;pre=1, cur=2, tmp=3，1→null 已反转&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;./remote-04.png&quot; alt=&quot;step3&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;pre=cur 进行中，准备进入下一步&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;./remote-05.png&quot; alt=&quot;step4&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;pre=2, cur=3, tmp=4，2→1→null&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;./remote-06.png&quot; alt=&quot;step5&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;pre=cur 进行中，准备进入下一步&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;./remote-07.png&quot; alt=&quot;step6&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;pre=3, cur=4, tmp=5，3→2→1→null&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;./remote-08.png&quot; alt=&quot;step7&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;pre=cur 进行中，准备进入下一步&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;./remote-09.png&quot; alt=&quot;step8&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;pre=4, cur=5, tmp=null，4→3→2→1→null&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;./remote-10.png&quot; alt=&quot;step9&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;pre=cur 进行中，准备进入下一步&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;./remote-11.png&quot; alt=&quot;step10&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;循环结束：5→4→3→2→1→null，返回 pre&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;./remote-12.png&quot; alt=&quot;step11&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;递归法&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        return recur(head, nullptr);
    }
private:
    ListNode* recur(ListNode* cur, ListNode* pre) {
        if (cur == nullptr) return pre;
        ListNode* res = recur(cur-&amp;gt;next, cur);
        cur-&amp;gt;next = pre;
        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;时间复杂度&lt;/th&gt;
&lt;th&gt;空间复杂度&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;迭代法&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;内存敏感场景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;递归法&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;代码简洁要求场景&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;3. 设计链表（LeetCode 707）&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;双向链表实现框架&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct MyLinkedListNode {
    int val;
    struct MyLinkedListNode *prev;
    struct MyLinkedListNode *next;
} Node;

typedef struct {
    Node *head;
    Node *tail;
    int size;
} MyLinkedList;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;设计原则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚拟头尾节点：统一处理边界情况&lt;/li&gt;
&lt;li&gt;size维护：避免多余遍历&lt;/li&gt;
&lt;li&gt;指针安全：每次操作前检查NULL&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. K 个一组翻转链表（LeetCode 25）&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;问题描述&lt;/strong&gt;：给你链表的头节点 &lt;code&gt;head&lt;/code&gt; ，每 &lt;code&gt;k&lt;/code&gt; 个节点一组进行翻转，请你返回修改后的链表。如果节点总数不是 &lt;code&gt;k&lt;/code&gt; 的整数倍，那么请将最后剩余的节点保持原有顺序。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./part3-remote-02.png&quot; alt=&quot;K个一组翻转图解&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;原始链表：1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9
按 k=4 分组：
  组1: 1 → 2 → 3 → 4
  组2: 5 → 6 → 7 → 8 → 9 (剩余不足 4 个，保持原序)

翻转后：4 → 3 → 2 → 1 → 5 → 6 → 7 → 8 → 9
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    // 翻转 [a, b) 区间内的链表节点（不包含 b）
    ListNode* reverse(ListNode* a, ListNode* b) {
        ListNode *pre = nullptr, *cur = a, *next;
        while (cur != b) {
            next = cur-&amp;gt;next;
            cur-&amp;gt;next = pre;
            pre = cur;
            cur = next;
        }
        return pre;  // 返回翻转后的新头
    }

    ListNode* reverseKGroup(ListNode* head, int k) {
        if (head == nullptr) return nullptr;

        ListNode *a = head, *b = head;
        // 前进 k 步，找到本组尾部
        for (int i = 0; i &amp;lt; k; i++) {
            if (b == nullptr) return head;  // 不足 k 个，保持原序
            b = b-&amp;gt;next;
        }

        // 翻转前 k 个
        ListNode* newHead = reverse(a, b);
        // 递归处理后续链表
        a-&amp;gt;next = reverseKGroup(b, k);
        return newHead;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;[a, b)&lt;/code&gt; 半开区间翻转，避免对尾节点做特殊处理&lt;/li&gt;
&lt;li&gt;递归处理后续分组，每次处理 k 个节点&lt;/li&gt;
&lt;li&gt;不足 k 个的剩余节点保持原序（不翻转）&lt;/li&gt;
&lt;li&gt;时间复杂度 O(n)，空间复杂度 O(n/k) 的递归栈&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Vue.js 常见问题速查</title><link>https://youki.bbroot.com/posts/frontend/vue-q-and-a/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/frontend/vue-q-and-a/</guid><description>Vue.js 常见问题速查：插值语法、v-if、ref、onMounted、TypeScript 速记</description><pubDate>Fri, 27 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;1. &lt;code&gt;{{}}&lt;/code&gt; 是什么？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;文本插值语法&lt;/strong&gt;
Vue 模板中使用双大括号 &lt;code&gt;{{}}&lt;/code&gt; 进行&lt;strong&gt;文本插值&lt;/strong&gt;，将数据绑定到 HTML 中。
&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;{{ message }}&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup&amp;gt;
const message = &quot;Hello Vue!&quot;;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;message&lt;/code&gt; 是响应式数据，若值变化，视图会自动更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意：&lt;/strong&gt; 插值内容会被自动转义，防止 XSS 攻击。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;2. &lt;code&gt;v-if&lt;/code&gt; 是什么？怎么用？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;条件渲染指令&lt;/strong&gt;
&lt;code&gt;v-if&lt;/code&gt; 用于根据条件决定是否渲染某个元素（&lt;strong&gt;惰性渲染&lt;/strong&gt;）。
&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div v-if=&quot;isVisible&quot;&amp;gt;显示内容&amp;lt;/div&amp;gt;
  &amp;lt;div v-else&amp;gt;隐藏内容&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup&amp;gt;
const isVisible = true;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;特点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;条件为 &lt;code&gt;false&lt;/code&gt; 时，元素不会出现在 DOM 中。&lt;/li&gt;
&lt;li&gt;可搭配 &lt;code&gt;v-else&lt;/code&gt;、&lt;code&gt;v-else-if&lt;/code&gt; 使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;3. &lt;code&gt;const count = ref(0);&lt;/code&gt; 是什么？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;声明响应式变量&lt;/strong&gt;
使用 &lt;code&gt;ref()&lt;/code&gt; 创建一个响应式引用，常用于基础类型（如数字、字符串）。
&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;计数器：{{ count }}&amp;lt;/div&amp;gt;
  &amp;lt;button @click=&quot;count++&quot;&amp;gt;+1&amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup&amp;gt;
import { ref } from &apos;vue&apos;;
const count = ref(0);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ref(0)&lt;/code&gt; 返回一个响应式对象，通过 &lt;code&gt;.value&lt;/code&gt; 访问/修改值（在 JavaScript 中），但在模板中直接使用变量名。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;count&lt;/code&gt; 变化时，视图会自动更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;4. &lt;code&gt;onMounted(() =&amp;gt; { ... })&lt;/code&gt; 是什么？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;生命周期钩子函数&lt;/strong&gt;
&lt;code&gt;onMounted&lt;/code&gt; 是 Vue 的生命周期钩子，在组件挂载到 DOM 后执行。
&lt;strong&gt;示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div id=&quot;app&quot;&amp;gt;组件已加载&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup&amp;gt;
import { onMounted } from &apos;vue&apos;;

onMounted(() =&amp;gt; {
  console.log(&apos;组件已挂载&apos;);
  // 可在此进行初始化操作（如请求数据）
});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常见用途：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发起 API 请求获取数据。&lt;/li&gt;
&lt;li&gt;初始化第三方库（如地图、图表）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;5. &lt;code&gt;ts&lt;/code&gt; 是什么？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;TypeScript 简称&lt;/strong&gt;
TypeScript 是 JavaScript 的超集，添加了&lt;strong&gt;静态类型检查&lt;/strong&gt;功能。
&lt;strong&gt;核心优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提早发现错误（如变量类型不匹配）。&lt;/li&gt;
&lt;li&gt;支持面向对象编程（类、接口等）。&lt;/li&gt;
&lt;li&gt;更好的代码提示和重构能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Vue 项目中使用 TypeScript：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;{{ message }}&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
// 定义类型
interface User {
  name: string;
  age: number;
}

// 使用类型
const user: User = { name: &quot;Alice&quot;, age: 25 };
const message: string = `用户：${user.name}`;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 或 &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; 标签中添加 &lt;code&gt;lang=&quot;ts&quot;&lt;/code&gt; 即可启用 TypeScript。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Vue 工程化与 Element Plus 实战笔记</title><link>https://youki.bbroot.com/posts/frontend/vue-element-plus/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/frontend/vue-element-plus/</guid><description>整理 Vue 工程化、create-vue、npm、项目结构和 Element Plus 组件使用。</description><pubDate>Sun, 22 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记记录 Vue 工程化开发和 Element Plus 的入门实践，重点从脚手架、依赖管理、项目结构和组件库使用开始。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;前端工程化强调模块化、组件化、规范化和自动化。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;create-vue&lt;/code&gt; 可快速生成 Vue 工程化项目。&lt;/li&gt;
&lt;li&gt;npm 负责前端依赖安装和脚本运行。&lt;/li&gt;
&lt;li&gt;Element Plus 提供常用的企业级 UI 组件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Vue工程化&lt;/h2&gt;
&lt;p&gt;前面我们在介绍Vue的时候，我们讲到Vue是一款用于构建用户界面的渐进式JavaScript框架 。（官方：https://cn.vuejs.org/）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./01.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那在前面的课程中，我们已经学习了Vue的基本语法、表达式、指令，并基于Vue的核心包，完成了Vue的案例。 那今天呢，我们要来讲解的基于Vue进行整站开发。&lt;/p&gt;
&lt;p&gt;所以现在企业开发中更加讲究前端工程化方式的开发，主要包括如下4个特点:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./02.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模块化：将js和css等，做成一个个可复用模块&lt;/li&gt;
&lt;li&gt;组件化：我们将UI组件，css样式，js行为封装成一个个的组件，便于管理&lt;/li&gt;
&lt;li&gt;规范化：我们提供一套标准的规范的目录接口和编码规范，所有开发人员遵循这套规范&lt;/li&gt;
&lt;li&gt;自动化：项目的构建，测试，部署全部都是自动完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以对于&lt;strong&gt;前端工程化，说白了，就是在企业级的前端项目开发中，把前端开发所需要的工具、技术、流程、经验进行规范化和标准化&lt;/strong&gt;。从而统一开发规范、提升开发效率，降低开发难度、提高复用等等。接下来我们就需要学习vue的官方提供的脚手架帮我们完成前端的工程化。&lt;/p&gt;
&lt;h3&gt;环境准备&lt;/h3&gt;
&lt;h4&gt;介绍&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;介绍：create-vue是Vue官方提供的最新的脚手架工具，用于快速生成一个工程化的Vue项目。&lt;/li&gt;
&lt;li&gt;create-vue提供了如下功能：
&lt;ul&gt;
&lt;li&gt;统一的目录结构&lt;/li&gt;
&lt;li&gt;本地调试&lt;/li&gt;
&lt;li&gt;热部署&lt;/li&gt;
&lt;li&gt;单元测试&lt;/li&gt;
&lt;li&gt;集成打包上线&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;而要想使用create-vue来创建vue项目，则必须安装依赖环境：NodeJS&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;npm介绍&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;**npm：**Node Package Manager，是NodeJS的软件包管理器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./03.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在开发前端项目的过程中，我们需要相关的依赖，就可以直接通过 &lt;code&gt;npm install xxx&lt;/code&gt; 命令，直接从远程仓库中将依赖直接下载到本地了。&lt;/p&gt;
&lt;h3&gt;Vue项目创建&lt;/h3&gt;
&lt;h4&gt;项目创建&lt;/h4&gt;
&lt;p&gt;创建一个工程化的Vue项目，执行命令：&lt;code&gt;npm create vue``@3.3.4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./04.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;详细步骤说明：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Project name：&lt;/code&gt;------------------》项目名称，默认值：vue-project，可输入想要的项目名称。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Add TypeScript?&lt;/code&gt; ----------------》是否加入TypeScript组件？默认值：No。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Add JSX Support?&lt;/code&gt; --------------》是否加入JSX支持？默认值：No。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Add Vue Router...&lt;/code&gt;--------------》是否为单页应用程序开发添加Vue Router路由管理组件？默认值：No。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Add Pinia ...&lt;/code&gt;----------------------》是否添加Pinia组件来进行状态管理？默认值：No。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Add Vitest ...&lt;/code&gt;---------------------》是否添加Vitest来进行单元测试？默认值：No。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Add an End-to-End ...&lt;/code&gt;-----------》是否添加端到端测试？默认值No。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Add ESLint for code quality?&lt;/code&gt; ---》是否添加ESLint来进行代码质量检查？默认值：No。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**提示：**执行上述指令，将会安装并执行 create-vue，它是 Vue 官方的项目脚手架工具&lt;/p&gt;
&lt;p&gt;项目创建完成以后，进入&lt;code&gt;vue-project01&lt;/code&gt; 项目目录，执行命令安装当前项目的依赖：&lt;code&gt;npm install&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./05.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;创建项目以及安装依赖的过程，都是需要联网的。【如果网络不太好，可能会造成依赖下载不完整报错，继续再次执行 命令安装。】&lt;/p&gt;
&lt;h4&gt;项目结构&lt;/h4&gt;
&lt;p&gt;我们可以使用VsCode直接打开这个Vue项目。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./06.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是我们创建的第一个项目结构，接下来呢，我们来介绍一下这个项目的结构。如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./07.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在上述的目录中，我们以后操作的最多的目录，就是src目录，因为我们需要在这个目录下来编写前端代码。&lt;/p&gt;
&lt;h3&gt;Vue项目开发流程&lt;/h3&gt;
&lt;p&gt;如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./08.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中&lt;code&gt;*.vue&lt;/code&gt;是Vue项目中的组件文件，在Vue项目中也称为单文件组件（&lt;a href=&quot;https://cn.vuejs.org/guide/scaling-up/sfc.html&quot;&gt;SFC&lt;/a&gt;，Single-File Components）。Vue 的单文件组件会将一个组件的逻辑 (JS)，模板 (HTML) 和样式 (CSS) 封装在同一个文件里（&lt;code&gt;*.vue&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./09.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;API风格&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Vue的组件有两种不同的风格：&lt;strong&gt;组合式API&lt;/strong&gt; 和 &lt;strong&gt;选项式API&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;**组合式API：**是Vue3提供的一种基于函数的组件编写方式，通过使用函数来组织和复用组件的逻辑。它提供了一种更灵活、更可组合的方式来编写组件。代码形式如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
import { ref, onMounted } from &apos;vue&apos;;
const count = ref(0); //声明响应式变量

function increment(){ //声明函数
   count.value++;
}

onMounted(() =&amp;gt; { //声明钩子函数
  console.log(&apos;Vue Mounted....&apos;); 
})
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
   &amp;lt;input type=&quot;button&quot; @click=&quot;increment&quot;&amp;gt; Api Demo1 Count : {{ count }}
&amp;lt;/template&amp;gt;

&amp;lt;style scoped&amp;gt;
   
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;setup&lt;/code&gt;：是一个标识，告诉Vue需要进行一些处理，让我们可以更简洁的使用组合式API。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ref()&lt;/code&gt;：接收一个内部值，返回一个响应式的ref对象，此对象只有一个指向内部值的属性 value。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;onMounted()&lt;/code&gt;：在组合式API中的钩子方法，注册一个回调函数，在组件挂载完成后执行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;选项式API&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**选项式API：**可以用包含多个选项的对象来描述组件的逻辑，如：&lt;code&gt;data&lt;/code&gt;，&lt;code&gt;methods&lt;/code&gt;，&lt;code&gt;mounted&lt;/code&gt;等。选项定义的属性都会暴露在函数内部的&lt;code&gt;this&lt;/code&gt;上，它会指向当前的组件实例。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
export default{
   data() {
      return {
         count: 0
      }
   },
   methods: {
      increment: function(){
         this.count++
      }
   },
   mounted() {
      console.log(&apos;vue mounted.....&apos;);
   }
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;input type=&quot;button&quot; @click=&quot;increment&quot;&amp;gt;Api Demo1 Count :  {{ count }}
&amp;lt;/template&amp;gt;

&amp;lt;style scoped&amp;gt;

&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Vue中的组合式API使用时，是没有this对象的，this对象是undefined。&lt;/p&gt;
&lt;h2&gt;ElementPlus&lt;/h2&gt;
&lt;h3&gt;介绍&lt;/h3&gt;
&lt;p&gt;Element：是饿了么公司前端开发团队提供的一套基于 Vue3 的网站组件库，用于快速构建网页。&lt;/p&gt;
&lt;p&gt;Element 提供了很多组件（组成网页的部件）供我们使用。例如 超链接、按钮、图片、表格等等。&lt;/p&gt;
&lt;p&gt;官方网站：https://element-plus.org/zh-CN/#/zh-CN&lt;/p&gt;
&lt;p&gt;如下图所示就是我们开发的页面和ElementPlus提供的效果对比：可以发现ElementPlus提供的各式各样好看的按钮。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./10.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ElementPlus的学习方式和我们之前的学习方式不太一样，对于ElementPlus，我们作为一个后台开发者，只需要&lt;strong&gt;学会如何从 ElementPlus 的官网拷贝组件到我们自己的页面中，并且做一些修改即可&lt;/strong&gt;。 我们主要学习的是ElementPlus中提供的常用组件，至于其他组件同学们可以通过我们这几个组件的学习掌握到ElementPlus的学习技巧，然后课后自行学习。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Vue 工程化把前端项目从单个页面脚本推进到可维护的项目结构。理解脚手架、npm、组件库和目录规范后，再使用 Element Plus 组件会更顺。&lt;/p&gt;
</content:encoded></item><item><title>前后端分离开发笔记</title><link>https://youki.bbroot.com/posts/frontend/frontend-backend-separation/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/frontend/frontend-backend-separation/</guid><description>前后端分离开发笔记：工程拆分、Vue Router 路由、Axios 接口调用与跨域处理</description><pubDate>Sat, 21 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前后台分离开发方式，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./01.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们将原先的工程分为前端工程和后端工程这2个工程，然后前端工程交给专业的前端人员开发，后端工程交给专业的后端人员开发。&lt;/p&gt;
&lt;p&gt;前端页面需要数据，可以通过发送异步请求，从后台工程获取。但是，我们前后台是分开来开发的，那么前端人员怎么知道后台返回数据的格式呢？后端人员开发，怎么知道前端人员需要的数据格式呢？&lt;/p&gt;
&lt;p&gt;所以针对这个问题，我们前后台统一制定一套规范！我们前后台开发人员都需要遵循这套规范开发，这就是我们的 &lt;strong&gt;接口文档&lt;/strong&gt;。接口文档有离线版和在线版本，接口文档示可以查询今天提供 &lt;strong&gt;资料/接口文档&lt;/strong&gt; 里面的资料。&lt;/p&gt;
&lt;p&gt;那么接口文档的内容怎么来的呢？是我们后台开发者根据产品经理提供的产品原型和需求文档所撰写出来的，产品原型示例可以参考今天提供&lt;strong&gt;资料/页面原型&lt;/strong&gt; 里面的资料。&lt;/p&gt;
&lt;p&gt;那么基于前后台分离开发的模式下，我们后台开发者开发一个功能的具体流程如何呢？如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./02.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;需求分析：首先我们需要阅读需求文档，分析需求，理解需求。&lt;/li&gt;
&lt;li&gt;接口定义：查询接口文档中关于需求的接口的定义，包括地址，参数，响应数据类型等等&lt;/li&gt;
&lt;li&gt;前后台并行开发：各自按照接口文档进行开发，实现需求&lt;/li&gt;
&lt;li&gt;测试：前后台开发完了，各自按照接口文档进行测试&lt;/li&gt;
&lt;li&gt;前后端联调测试：前端工程请求后端工程，测试功能&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;1. VueRouter&lt;/h2&gt;
&lt;h3&gt;1. 介绍&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./03.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vue Router：Vue的官方路由。 为Vue提供富有表现力、可配置的、方便的路由。&lt;/li&gt;
&lt;li&gt;Vue中的路由，主要定义的是路径与组件之间的对应关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如，我们打开一个网站，点击左侧菜单，地址栏的地址发生变化。 地址栏地址一旦发生变化，在主区域显示对应的页面组件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./04.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./05.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;VueRouter主要由以下三个部分组成，如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./06.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VueRouter：路由器类，根据路由请求在路由视图中动态渲染选中的组件&lt;/li&gt;
&lt;li&gt;&amp;lt;router-link&amp;gt;：请求链接组件，浏览器会解析成&amp;lt;a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;router-view&amp;gt;：动态视图组件，用来渲染展示与路由路径对应的组件&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 基础路由配置&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1). 在&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;views/layout/index.vue&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;中，调整代码，具体调整位置如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在左侧菜单栏的 &lt;code&gt;&amp;lt;el-menu&amp;gt;&lt;/code&gt; 标签上添加 &lt;code&gt;router&lt;/code&gt; 属性，这会让 Element Plus 的 &lt;code&gt;&amp;lt;el-menu&amp;gt;&lt;/code&gt; 组件自动根据路由来激活对应的菜单项。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;&amp;lt;router-view&amp;gt;&lt;/code&gt; 组件来渲染根据路由动态变化的内容。&lt;/li&gt;
&lt;li&gt;确保每个 &lt;code&gt;&amp;lt;el-menu-item&amp;gt;&lt;/code&gt; 的 &lt;code&gt;index&lt;/code&gt; 属性值与你想要导航到的路径相匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
// 无需额外导入，因为我们只是使用了 Element Plus 和 Vue Router 的基本功能
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div class=&quot;common-layout&quot;&amp;gt;
    &amp;lt;el-container&amp;gt;
      &amp;lt;!-- Header 区域 --&amp;gt;
      &amp;lt;el-header class=&quot;header&quot;&amp;gt;
        &amp;lt;span class=&quot;title&quot;&amp;gt;Tlias智能学习辅助系统&amp;lt;/span&amp;gt;
        &amp;lt;span class=&quot;right_tool&quot;&amp;gt;
          &amp;lt;a href=&quot;&quot;&amp;gt;
            &amp;lt;el-icon&amp;gt;&amp;lt;EditPen /&amp;gt;&amp;lt;/el-icon&amp;gt; 修改密码 &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp; |  &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;
          &amp;lt;/a&amp;gt;
          &amp;lt;a href=&quot;&quot;&amp;gt;
            &amp;lt;el-icon&amp;gt;&amp;lt;SwitchButton /&amp;gt;&amp;lt;/el-icon&amp;gt; 退出登录
          &amp;lt;/a&amp;gt;
        &amp;lt;/span&amp;gt;
      &amp;lt;/el-header&amp;gt;
      
      &amp;lt;el-container&amp;gt;
        &amp;lt;!-- 左侧菜单 --&amp;gt;
        &amp;lt;el-aside width=&quot;200px&quot; class=&quot;aside&quot;&amp;gt;

          &amp;lt;el-menu router&amp;gt;
            &amp;lt;!-- 首页菜单 --&amp;gt;
            &amp;lt;el-menu-item index=&quot;/index&quot;&amp;gt;
              &amp;lt;el-icon&amp;gt;&amp;lt;Promotion /&amp;gt;&amp;lt;/el-icon&amp;gt; 首页
            &amp;lt;/el-menu-item&amp;gt;
            
            &amp;lt;!-- 班级管理菜单 --&amp;gt;
            &amp;lt;el-sub-menu index=&quot;/manage&quot;&amp;gt;
              &amp;lt;template #title&amp;gt;
                &amp;lt;el-icon&amp;gt;&amp;lt;Menu /&amp;gt;&amp;lt;/el-icon&amp;gt; 班级学员管理
              &amp;lt;/template&amp;gt;
              &amp;lt;el-menu-item index=&quot;/clazz&quot;&amp;gt;
                &amp;lt;el-icon&amp;gt;&amp;lt;HomeFilled /&amp;gt;&amp;lt;/el-icon&amp;gt;班级管理
              &amp;lt;/el-menu-item&amp;gt;
              &amp;lt;el-menu-item index=&quot;/stu&quot;&amp;gt;
                &amp;lt;el-icon&amp;gt;&amp;lt;UserFilled /&amp;gt;&amp;lt;/el-icon&amp;gt;学员管理
              &amp;lt;/el-menu-item&amp;gt;
            &amp;lt;/el-sub-menu&amp;gt;
            
            &amp;lt;!-- 系统信息管理 --&amp;gt;
            &amp;lt;el-sub-menu index=&quot;/system&quot;&amp;gt;
              &amp;lt;template #title&amp;gt;
                &amp;lt;el-icon&amp;gt;&amp;lt;Tools /&amp;gt;&amp;lt;/el-icon&amp;gt;系统信息管理
              &amp;lt;/template&amp;gt;
              &amp;lt;el-menu-item index=&quot;/dept&quot;&amp;gt;
                &amp;lt;el-icon&amp;gt;&amp;lt;HelpFilled /&amp;gt;&amp;lt;/el-icon&amp;gt;部门管理
              &amp;lt;/el-menu-item&amp;gt;
              &amp;lt;el-menu-item index=&quot;/emp&quot;&amp;gt;
                &amp;lt;el-icon&amp;gt;&amp;lt;Avatar /&amp;gt;&amp;lt;/el-icon&amp;gt;员工管理
              &amp;lt;/el-menu-item&amp;gt;
            &amp;lt;/el-sub-menu&amp;gt;

            &amp;lt;!-- 数据统计管理 --&amp;gt;
            &amp;lt;el-sub-menu index=&quot;/report&quot;&amp;gt;
              &amp;lt;template #title&amp;gt;
                &amp;lt;el-icon&amp;gt;&amp;lt;Histogram /&amp;gt;&amp;lt;/el-icon&amp;gt;数据统计管理
              &amp;lt;/template&amp;gt;
              &amp;lt;el-menu-item index=&quot;/report/emp&quot;&amp;gt;
                &amp;lt;el-icon&amp;gt;&amp;lt;InfoFilled /&amp;gt;&amp;lt;/el-icon&amp;gt;员工信息统计
              &amp;lt;/el-menu-item&amp;gt;
              &amp;lt;el-menu-item index=&quot;/report/stu&quot;&amp;gt;
                &amp;lt;el-icon&amp;gt;&amp;lt;Share /&amp;gt;&amp;lt;/el-icon&amp;gt;学员信息统计
              &amp;lt;/el-menu-item&amp;gt;
              &amp;lt;el-menu-item index=&quot;/log&quot;&amp;gt;
                &amp;lt;el-icon&amp;gt;&amp;lt;Document /&amp;gt;&amp;lt;/el-icon&amp;gt;日志信息统计
              &amp;lt;/el-menu-item&amp;gt;
            &amp;lt;/el-sub-menu&amp;gt;
          &amp;lt;/el-menu&amp;gt;
        &amp;lt;/el-aside&amp;gt;
        
        &amp;lt;!-- 主展示区域 --&amp;gt;
        &amp;lt;el-main&amp;gt;
          &amp;lt;router-view&amp;gt;&amp;lt;/router-view&amp;gt;
        &amp;lt;/el-main&amp;gt;
      &amp;lt;/el-container&amp;gt;
    &amp;lt;/el-container&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;style scoped&amp;gt;
.header {
  background-image: linear-gradient(to right, #00547d, #007fa4, #00aaa0, #00d072, #a8eb12);
}

.title {
  color: white;
  font-size: 40px;
  font-family: 楷体;
  line-height: 60px;
  font-weight: bolder;
}

.right_tool{
  float: right;
  line-height: 60px;
}

a {
  color: white;
  text-decoration: none;
}

.aside {
  width: 220px;
  border-right: 1px solid #ccc;
  height: 730px;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2). 在 router/index.js 中配置请求路径与组件之间的关系。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createRouter, createWebHistory} from &apos;vue-router&apos;;

import IndexView from &apos;@/views/index/index.vue&apos;;
import ClazzView from &apos;@/views/clazz/index.vue&apos;;
import StuView from &apos;@/views/stu/index.vue&apos;;
import DeptView from &apos;@/views/dept/index.vue&apos;;
import EmpView from &apos;@/views/emp/index.vue&apos;;
import EmpReportView from &apos;@/views/report/emp/index.vue&apos;;
import StuReportView from &apos;@/views/report/stu/index.vue&apos;;
import LogView from &apos;@/views/log/index.vue&apos;;
import LoginView from &apos;@/views/login/index.vue&apos;;

const routes = [
  { path: &apos;/index&apos;, component: IndexView },
  { path: &apos;/clazz&apos;, component: ClazzView },
  { path: &apos;/stu&apos;, component: StuView },
  { path: &apos;/dept&apos;, component: DeptView },
  { path: &apos;/emp&apos;, component: EmpView },
  { path: &apos;/report/emp&apos;, component: EmpReportView },
  { path: &apos;/report/stu&apos;, component: StuReportView },
  { path: &apos;/log&apos;, component: LogView },
  { path: &apos;/login&apos;, component: LoginView },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过这么两步操作之后，我们就可以看到，在页面上，点击左侧菜单，右侧主展示区域，就会显示出对应的页面了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./07.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./08.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那要完成这个功能效果，我们就需要用到Vue生态中的路由 &lt;strong&gt;&lt;code&gt;Vue-Router&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1. 完善路由配置&lt;/h3&gt;
&lt;p&gt;上述我们只是完成了最基本的路由配置。 并经过测试我们发现，如果我们访问 /login 路径，会发现，登录页面是在layout页面中嵌套展示的，这肯定是不合适的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./09.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那接下来，我们就来优化一下路由的配置。最终配置形式如下，在 &lt;code&gt;router/index.js&lt;/code&gt; 中做如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createRouter, createWebHistory } from &apos;vue-router&apos;

import IndexView from &apos;@/views/index/index.vue&apos;
import ClazzView from &apos;@/views/clazz/index.vue&apos;
import DeptView from &apos;@/views/dept/index.vue&apos;
import EmpView from &apos;@/views/emp/index.vue&apos;
import LogView from &apos;@/views/log/index.vue&apos;
import StuView from &apos;@/views/stu/index.vue&apos;
import EmpReportView from &apos;@/views/report/emp/index.vue&apos;
import StuReportView from &apos;@/views/report/stu/index.vue&apos;
import LayoutView from &apos;@/views/layout/index.vue&apos;
import LoginView from &apos;@/views/login/index.vue&apos;

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
     path: &apos;/&apos;, 
     name: &apos;&apos;,
     component: LayoutView,
     redirect: &apos;/index&apos;, //重定向
     children: [
      {path: &apos;index&apos;, name: &apos;index&apos;, component: IndexView},
      {path: &apos;clazz&apos;, name: &apos;clazz&apos;, component: ClazzView},
      {path: &apos;stu&apos;, name: &apos;stu&apos;, component: StuView},
      {path: &apos;dept&apos;, name: &apos;dept&apos;, component: DeptView},
      {path: &apos;emp&apos;, name: &apos;emp&apos;, component: EmpView},
      {path: &apos;log&apos;, name: &apos;log&apos;, component: LogView},
      {path: &apos;empReport&apos;, name: &apos;empReport&apos;, component: EmpReportView},
      {path: &apos;stuReport&apos;, name: &apos;stuReport&apos;, component: StuReportView},
     ]
    },
    {path: &apos;/login&apos;, name: &apos;login&apos;, component: LoginView}
  ]
})

export default router
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体的执行访问流程如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./10.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当点击左侧菜单栏的员工管理菜单时，最终地址栏会访问路径 &lt;code&gt;/emp &lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;此时VueRouter，会自动的到所配置的路由表（&lt;code&gt;router/index.js&lt;/code&gt;）中，查找与该路径对应的组件，并展示在路由展示组件&lt;code&gt;&amp;lt;router-view&amp;gt;&lt;/code&gt; 对应的位置中。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;思考：直接在Vue组件中，基于axios发送异步请求，存在什么问题？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./11.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们刚才在完成部门列表查询时，是直接基于axios发送异步请求，直接将接口的请求地址放在组件文件 &lt;code&gt;.vue&lt;/code&gt; 中。 而如果开发一个大型的项目，组件文件可能会很多很多很多，如果前端开发完毕，进行前后端联调测试了，需要修改请求地址，那么此时，就需要找到每一个 &lt;code&gt;.vue&lt;/code&gt; 文件，然后挨个修改。 所以上述的代码，虽然实现了动态加载数据的功能。 但是存在以下问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求路径难以维护&lt;/li&gt;
&lt;li&gt;数据解析繁琐&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. 程序优化&lt;/h4&gt;
&lt;p&gt;1). 为了解决上述问题，我们在前端项目开发时，通常会定义一个请求处理的工具类  - &lt;code&gt;src/utils/request.js&lt;/code&gt; 。 在这个工具类中，对axios进行了封装。 具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import axios from &apos;axios&apos;

//创建axios实例对象
const request = axios.create({
  baseURL: &apos;/api&apos;,
  timeout: 600000
})

//axios的响应 response 拦截器
request.interceptors.response.use(
  (response) =&amp;gt; { //成功回调
    return response.data
  },
  (error) =&amp;gt; { //失败回调
    return Promise.reject(error)
  }
)

export default request
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2). 而与服务端进行异步交互的逻辑，通常会按模块，封装在一个单独的API中，如：&lt;code&gt;src/api/dept.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import request from &quot;@/utils/request&quot;

//列表查询
export const queryAllApi = () =&amp;gt; request.get(&apos;/depts&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3). 修改 &lt;code&gt;src/views/dept/index.vue&lt;/code&gt; 中的代码&lt;/p&gt;
&lt;p&gt;现在就不需要每次直接调用axios发送异步请求了，只需要将我们定义的对应模块的API导入进来，就可以直接使用了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
import {ref, onMounted} from &apos;vue&apos;
import {queryAllApi} from &apos;@/api/dept&apos;

//声明列表展示数据
let deptList= ref([])

//动态加载数据-查询部门
const queryAll = async () =&amp;gt; {
  const result = await queryAllApi()
  deptList.value = result.data
}

//钩子函数
onMounted(() =&amp;gt; {
  queryAll()
})

// 编辑部门 - 根据ID查询回显数据
const handleEdit = (id) =&amp;gt; {
  console.log(`Edit item with ID ${id}`);
  // 在这里实现编辑功能
};

// 删除部门 - 根据ID删除部门
const handleDelete = (id) =&amp;gt; {
  console.log(`Delete item with ID ${id}`);
  // 在这里实现删除功能
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;做完上面这三步之后，我们打开浏览器发现，并不能访问到接口数据。原因是因为，目前请求路径不对。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./12.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;4). 在 &lt;code&gt;vite.config.js&lt;/code&gt; 中配置前端请求服务器的信息&lt;/p&gt;
&lt;p&gt;在服务器中配置代理proxy的信息，并在配置代理时，执行目标服务器。 以及url路径重写的规则。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./13.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { fileURLToPath, URL } from &apos;node:url&apos;

import { defineConfig } from &apos;vite&apos;
import vue from &apos;@vitejs/plugin-vue&apos;
import vueJsx from &apos;@vitejs/plugin-vue-jsx&apos;

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
  ],
  resolve: {
    alias: {
      &apos;@&apos;: fileURLToPath(new URL(&apos;./src&apos;, import.meta.url))
    }
  },
  server: {
    proxy: {
      &apos;/api&apos;: {
        target: &apos;http://localhost:8080&apos;,
        secure: false,
        changeOrigin: true,
        rewrite: (path) =&amp;gt; path.replace(/^\/api/, &apos;&apos;),
      }
    }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，我们就可以启动服务器端的程序（将之前开发的服务端程序启动起来测试一下 ），进行测试了（【&lt;strong&gt;注意：测试时, 记得将令牌校验的过滤器及拦截器, 以及记录日志的AOP程序 全部注释&lt;/strong&gt;】）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./14.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>异常处理</title><link>https://youki.bbroot.com/posts/java/file-upload/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/java/file-upload/</guid><description>整理 Spring Boot 文件上传、OSS 使用思路、统一响应和全局异常处理。</description><pubDate>Sat, 21 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记记录文件上传和后端异常处理的课程内容。正文里有手机号示例和云服务上下文，公开前需要再做一次人工脱敏复核。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;文件上传常见流程是后端接收文件，再上传到对象存储。&lt;/li&gt;
&lt;li&gt;未处理异常时，框架默认响应可能不符合统一接口规范。&lt;/li&gt;
&lt;li&gt;全局异常处理器可以集中处理 Controller 层抛出的异常。&lt;/li&gt;
&lt;li&gt;统一响应结构有助于前端稳定解析错误信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文件上传使用阿里云oss，后端写好了，apifox通过&lt;/p&gt;
&lt;h2&gt;1. 异常处理&lt;/h2&gt;
&lt;h3&gt;1. 问题分析&lt;/h3&gt;
&lt;p&gt;当我们在修改部门数据的时候，如果输入一个在数据库表中已经存在的手机号，点击保存按钮之后，前端提示了错误信息，但是返回的结果并不是统一的响应结果，而是框架默认返回的错误结果 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./01.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;状态码为500，表示服务器端异常，我们打开idea，来看一下，服务器端出了什么问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./02.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上述错误信息的含义是，&lt;code&gt;emp&lt;/code&gt;员工表的&lt;code&gt;phone&lt;/code&gt;手机号字段的值重复了，因为在数据库表&lt;code&gt;emp&lt;/code&gt;中已经有了&lt;code&gt;138xxxx0027&lt;/code&gt;这个手机号了，我们之前设计这张表时，为&lt;code&gt;phone&lt;/code&gt;字段建议了唯一约束，所以该字段的值是不能重复的。&lt;/p&gt;
&lt;p&gt;而当我们再将该员工的手机号也设置为 &lt;code&gt;138xxxx0027&lt;/code&gt;，就违反了唯一约束，此时就会报错。&lt;/p&gt;
&lt;p&gt;我们来看一下出现异常之后，最终服务端给前端响应回来的数据长什么样。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./03.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;响应回来的数据是一个JSON格式的数据。但这种JSON格式的数据还是我们开发规范当中所提到的统一响应结果Result吗？显然并不是。由于返回的数据不符合开发规范，所以前端并不能解析出响应的JSON数据 。&lt;/p&gt;
&lt;p&gt;接下来我们需要思考的是出现异常之后，当前案例项目的异常是怎么处理的？ 答案：没有做任何的异常处理&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./04.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当我们没有做任何的异常处理时，我们三层架构处理异常的方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mapper接口在操作数据库的时候出错了，此时异常会往上抛(谁调用Mapper就抛给谁)，会抛给service。&lt;/li&gt;
&lt;li&gt;service 中也存在异常了，会抛给controller。&lt;/li&gt;
&lt;li&gt;而在controller当中，我们也没有做任何的异常处理，所以最终异常会再往上抛。最终抛给框架之后，框架就会返回一个JSON格式的数据，里面封装的就是错误的信息，但是框架返回的JSON格式的数据并不符合我们的开发规范。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 解决方案&lt;/h3&gt;
&lt;p&gt;那么在三层构架项目中，出现了异常，该如何处理?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案一：在所有Controller的所有方法中进行try…catch处理&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：代码臃肿（不推荐）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./05.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**方案二：**&lt;strong&gt;全局异常处理器&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;好处：简单、优雅（推荐）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./06.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. 全局异常处理器&lt;/h3&gt;
&lt;p&gt;我们该怎么样定义全局异常处理器？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义全局异常处理器非常简单，就是定义一个类，在类上加上一个注解@RestControllerAdvice，加上这个注解就代表我们定义了一个全局异常处理器。&lt;/li&gt;
&lt;li&gt;在全局异常处理器当中，需要定义一个方法来捕获异常，在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@RestControllerAdvice
public class GlobalExceptionHandler {
    
    //处理异常
    @ExceptionHandler
    public Result ex(Exception e){//方法形参中指定能够处理的异常类型
        e.printStackTrace();//打印堆栈中的异常信息
        //捕获到异常之后，响应一个标准的Result
        return Result.error(&quot;对不起,操作失败,请联系管理员&quot;);
    }
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@RestControllerAdvice = @ControllerAdvice + @ResponseBody&lt;/p&gt;
&lt;p&gt;处理异常的方法返回值会转换为json后再响应给前端&lt;/p&gt;
&lt;p&gt;重新启动SpringBoot服务，打开浏览器，再来测试一下 修改员工 这个操作，我们依然设置已存在的 &lt;code&gt;138xxxx0027&lt;/code&gt;这个手机号：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./07.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时，我们可以看到，出现异常之后，异常已经被全局异常处理器捕获了。然后返回的错误信息，被前端程序正常解析，然后提示出了对应的错误提示信息。&lt;/p&gt;
&lt;p&gt;以上就是全局异常处理器的使用，主要涉及到两个注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@RestControllerAdvice  //表示当前类为全局异常处理器&lt;/li&gt;
&lt;li&gt;@ExceptionHandler  //指定可以捕获哪种类型的异常进行处理&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;文件上传和异常处理都属于后端接口稳定性的一部分。前者关注资源如何接收与保存，后者关注错误如何以统一格式返回给前端。&lt;/p&gt;
</content:encoded></item><item><title>登录认证与 JWT 令牌笔记</title><link>https://youki.bbroot.com/posts/java/jwt-auth/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/java/jwt-auth/</guid><description>整理登录接口、认证流程、JWT 令牌生成解析、过滤器和拦截器。</description><pubDate>Sat, 21 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记整理后端登录认证的基础流程，包括登录接口、JWT 令牌和请求过滤。正文包含 token、密码等教学示例，公开前需要确认没有真实凭据。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;登录成功后通常返回用户基础信息和认证令牌。&lt;/li&gt;
&lt;li&gt;JWT 可用于在请求之间携带认证状态。&lt;/li&gt;
&lt;li&gt;服务端需要在过滤器或拦截器中校验请求令牌。&lt;/li&gt;
&lt;li&gt;认证逻辑需要特别注意密钥、过期时间和异常响应。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;1).&lt;/strong&gt; &lt;strong&gt;准备实体类&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;LoginInfo&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;， 封装登录成功后， 返回给前端的数据&lt;/strong&gt; &lt;strong&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 登录成功结果封装类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
    private Integer id; //员工ID
    private String username; //用户名
    private String name; //姓名
    private String token; //令牌
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2).  定义&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;LoginController&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@RestController
public class LoginController {

    @Autowired
    private EmpService empService;

    @PostMapping(&quot;/login&quot;)
    public Result login(@RequestBody Emp emp){
        log.info(&quot;员工来登录啦 , {}&quot;, emp);
        LoginInfo loginInfo = empService.login(emp);
        if(loginInfo != null){
            return Result.success(loginInfo);
        }
        return Result.error(&quot;用户名或密码错误~&quot;);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3).&lt;/strong&gt; **&lt;code&gt;EmpService&lt;/code&gt;**&lt;strong&gt;接口中增加 login 登录方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 登录
 */
LoginInfo login(Emp emp);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4).&lt;/strong&gt;  &lt;strong&gt;&lt;code&gt;EmpServiceImpl&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;实现login方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public LoginInfo login(Emp emp) {
    Emp empLogin = empMapper.getUsernameAndPassword(emp);
    if(empLogin != null){
        LoginInfo loginInfo = new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), null);
        return loginInfo;
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5).&lt;/strong&gt; **&lt;code&gt;EmpMapper&lt;/code&gt;**&lt;strong&gt;增加接口方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据用户名和密码查询员工信息
 */
@Select(&quot;select * from emp where username = #{username} and password = #{password}&quot;)
Emp getUsernameAndPassword(Emp emp);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 登录校验&lt;/h2&gt;
&lt;p&gt;什么是登录校验？&lt;/p&gt;
&lt;p&gt;所谓登录校验，指的是我们在服务器端接收到浏览器发送过来的请求之后，首先我们要对请求进行校验。先要校验一下用户登录了没有，如果用户已经登录了，就直接执行对应的业务操作就可以了；如果用户没有登录，此时就不允许他执行相关的业务操作，直接给前端响应一个错误的结果，最终跳转到登录页面，要求他登录成功之后，再来访问对应的数据。&lt;/p&gt;
&lt;h3&gt;1. 思路&lt;/h3&gt;
&lt;p&gt;了解完什么是登录校验之后，接下来我们分析一下登录校验大概的实现思路。&lt;/p&gt;
&lt;p&gt;首先我们在宏观上先有一个认知：&lt;/p&gt;
&lt;p&gt;前面在讲解HTTP协议的时候，我们提到HTTP协议是无状态协议。什么又是无状态的协议？&lt;/p&gt;
&lt;p&gt;所谓无状态，指的是每一次请求都是独立的，下一次请求并不会携带上一次请求的数据。而浏览器与服务器之间进行交互，基于HTTP协议也就意味着现在我们通过浏览器来访问了登陆这个接口，实现了登陆的操作，接下来我们在执行其他业务操作时，服务器也并不知道这个员工到底登陆了没有。因为HTTP协议是无状态的，两次请求之间是独立的，所以是无法判断这个员工到底登陆了没有。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./01.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那应该怎么来实现登录校验的操作呢？具体的实现思路可以分为两部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在员工登录成功后，需要将用户登录成功的信息存起来，记录用户已经登录成功的标记。&lt;/li&gt;
&lt;li&gt;在浏览器发起请求时，需要在服务端进行统一拦截，拦截后进行登录校验。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;想要判断员工是否已经登录，我们需要在员工登录成功之后，存储一个登录成功的标记，接下来在每一个接口方法执行之前，先做一个条件判断，判断一下这个员工到底登录了没有。如果是登录了，就可以执行正常的业务操作，如果没有登录，会直接给前端返回一个错误的信息，前端拿到这个错误信息之后会自动的跳转到登录页面。&lt;/p&gt;
&lt;p&gt;我们程序中所开发的查询功能、删除功能、添加功能、修改功能，都需要使用以上套路进行登录校验。此时就会出现：相同代码逻辑，每个功能都需要编写，就会造成代码非常繁琐。&lt;/p&gt;
&lt;p&gt;为了简化这块操作，我们可以使用一种技术：统一拦截技术。&lt;/p&gt;
&lt;p&gt;通过统一拦截的技术，我们可以来拦截浏览器发送过来的所有的请求，拦截到这个请求之后，就可以通过请求来获取之前所存入的登录标记，在获取到登录标记且标记为登录成功，就说明员工已经登录了。如果已经登录，我们就直接放行(意思就是可以访问正常的业务接口了)。&lt;/p&gt;
&lt;p&gt;我们要完成以上操作，会涉及到web开发中的两个技术：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;会话技术：用户登录成功之后，在后续的每一次请求中，都可以获取到该标记。&lt;/li&gt;
&lt;li&gt;统一拦截技术：过滤器Filter、拦截器Interceptor&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面我们先学习会话技术，然后再学习统一拦截技术。&lt;/p&gt;
&lt;h3&gt;1. 会话技术&lt;/h3&gt;
&lt;p&gt;介绍了登录校验的大概思路之后，我们先来学习下会话技术。&lt;/p&gt;
&lt;h4&gt;1. 介绍&lt;/h4&gt;
&lt;p&gt;什么是会话？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在我们日常生活当中，会话指的就是谈话、交谈。&lt;/li&gt;
&lt;li&gt;在web开发当中，会话指的就是浏览器与服务器之间的一次连接，我们就称为一次会话。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在用户打开浏览器第一次访问服务器的时候，这个会话就建立了，直到有任何一方断开连接，此时会话就结束了。在一次会话当中，是可以包含多次请求和响应的。&lt;/p&gt;
&lt;p&gt;比如：打开了浏览器来访问web服务器上的资源（浏览器不能关闭、服务器不能断开）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第1次：访问的是登录的接口，完成登录操作&lt;/li&gt;
&lt;li&gt;第2次：访问的是部门管理接口，查询所有部门数据&lt;/li&gt;
&lt;li&gt;第3次：访问的是员工管理接口，查询员工数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只要浏览器和服务器都没有关闭，以上3次请求都属于一次会话当中完成的。&lt;/p&gt;
&lt;p&gt;需要注意的是：会话是和浏览器关联的，当有三个浏览器客户端和服务器建立了连接时，就会有三个会话。同一个浏览器在未关闭之前请求了多次服务器，这多次请求是属于同一个会话。比如：1、2、3这三个请求都是属于同一个会话。当我们关闭浏览器之后，这次会话就结束了。而如果我们是直接把web服务器关了，那么所有的会话就都结束了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./02.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;知道了会话的概念了，接下来我们再来了解下会话跟踪。&lt;/p&gt;
&lt;p&gt;**会话跟踪：**一种维护浏览器状态的方法，服务器需要识别多次请求是否来自于同一浏览器，以便在同一次会话的多次请求间共享数据。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;服务器会接收很多的请求，但是服务器是需要识别出这些请求是不是同一个浏览器发出来的。比如：1和2这两个请求是不是同一个浏览器发出来的，3和5这两个请求不是同一个浏览器发出来的。如果是同一个浏览器发出来的，就说明是同一个会话。如果是不同的浏览器发出来的，就说明是不同的会话。而识别多次请求是否来自于同一浏览器的过程，我们就称为会话跟踪。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们使用会话跟踪技术就是要完成在同一个会话中，多个请求之间进行共享数据。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么要共享数据呢？&lt;/p&gt;
&lt;p&gt;由于HTTP是无状态协议，在后面请求中怎么拿到前一次请求生成的数据呢？此时就需要在一次会话的多次请求之间进行数据共享&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;会话跟踪技术有两种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cookie（客户端会话跟踪技术）：数据存储在客户端浏览器当中&lt;/li&gt;
&lt;li&gt;Session（服务端会话跟踪技术）：数据存储在储在服务端&lt;/li&gt;
&lt;li&gt;令牌技术&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;1. 方案三 - 令牌技术&lt;/h5&gt;
&lt;p&gt;这里我们所提到的令牌，其实它就是一个用户身份的标识，看似很高大上，很神秘，其实本质就是一个字符串。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./03.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果通过令牌技术来跟踪会话，我们就可以在浏览器发起请求。在请求登录接口的时候，如果登录成功，我就可以生成一个令牌，令牌就是用户的合法身份凭证。接下来我在响应数据的时候，我就可以直接将令牌响应给前端。&lt;/p&gt;
&lt;p&gt;接下来我们在前端程序当中接收到令牌之后，就需要将这个令牌存储起来。这个存储可以存储在 cookie 当中，也可以存储在其他的存储空间(比如：localStorage)当中。&lt;/p&gt;
&lt;p&gt;接下来，在后续的每一次请求当中，都需要将令牌携带到服务端。携带到服务端之后，接下来我们就需要来校验令牌的有效性。如果令牌是有效的，就说明用户已经执行了登录操作，如果令牌是无效的，就说明用户之前并未执行登录操作。&lt;/p&gt;
&lt;p&gt;此时，如果是在同一次会话的多次请求之间，我们想共享数据，我们就可以将共享的数据存储在令牌当中就可以了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优缺点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：
&lt;ul&gt;
&lt;li&gt;支持PC端、移动端&lt;/li&gt;
&lt;li&gt;解决集群环境下的认证问题&lt;/li&gt;
&lt;li&gt;减轻服务器的存储压力（无需在服务器端存储）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点：需要自己实现（包括令牌的生成、令牌的传递、令牌的校验）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;针对于这三种方案，现在企业开发当中使用的最多的就是第三种令牌技术进行会话跟踪。而前面的这两种传统的方案，现在企业项目开发当中已经很少使用了。所以在我们的课程当中，我们也将会采用令牌技术来解决案例项目当中的会话跟踪问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./04.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JWT令牌最典型的应用场景就是登录认证：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在浏览器发起请求来执行登录操作，此时会访问登录的接口，如果登录成功之后，我们需要生成一个jwt令牌，将生成的 jwt令牌返回给前端。&lt;/li&gt;
&lt;li&gt;前端拿到jwt令牌之后，会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端。&lt;/li&gt;
&lt;li&gt;服务端统一拦截请求之后，先来判断一下这次请求有没有把令牌带过来，如果没有带过来，直接拒绝访问，如果带过来了，还要校验一下令牌是否是有效。如果有效，就直接放行进行请求的处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在JWT登录认证的场景中我们发现，整个流程当中涉及到两步操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在登录成功之后，要生成令牌。&lt;/li&gt;
&lt;li&gt;每一次请求当中，要接收令牌并对令牌进行校验。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;稍后我们再来学习如何来生成jwt令牌，以及如何来校验jwt令牌。&lt;/p&gt;
&lt;h3&gt;1. JWT令牌&lt;/h3&gt;
&lt;p&gt;前面我们介绍了基于令牌技术来实现会话追踪。这里所提到的令牌就是用户身份的标识，其本质就是一个字符串。令牌的形式有很多，我们使用的是功能强大的 JWT令牌。&lt;/p&gt;
&lt;h4&gt;1. 介绍&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;JWT全称 JSON Web Token  （官网：https://jwt.io/），定义了一种简洁的、自包含的格式，用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在，这些信息是可靠的。
&lt;ul&gt;
&lt;li&gt;简洁：是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。&lt;/li&gt;
&lt;li&gt;自包含：指的是jwt令牌，看似是一个随机的字符串，但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如：可以直接在jwt令牌中存储用户的相关信息。&lt;/li&gt;
&lt;li&gt;简单来讲，jwt就是将原始的json数据格式进行了安全的封装，这样就可以直接基于jwt在通信双方安全的进行信息传输了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JWT的组成： （JWT令牌由三个部分组成，三个部分之间使用英文的点来分割）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一部分：Header(头）， 记录令牌类型、签名算法等。 例如：{&quot;alg&quot;:&quot;HS256&quot;,&quot;type&quot;:&quot;JWT&quot;}&lt;/li&gt;
&lt;li&gt;第二部分：Payload(有效载荷），携带一些自定义信息、默认信息等。 例如：{&quot;id&quot;:&quot;1&quot;,&quot;username&quot;:&quot;Tom&quot;}&lt;/li&gt;
&lt;li&gt;第三部分：Signature(签名），防止Token被篡改、确保安全性。将header、payload，并加入指定秘钥，通过指定签名算法计算而来。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;签名的目的就是为了防jwt令牌被篡改，而正是因为jwt令牌最后一个部分数字签名的存在，所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了，整个令牌在校验的时候都会失败，所以它是非常安全可靠的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./05.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JWT是如何将原始的JSON格式数据，转变为字符串的呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其实在生成JWT令牌时，会对JSON格式的数据进行一次编码：进行base64编码&lt;/li&gt;
&lt;li&gt;Base64：是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码，那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9，一个加号，一个斜杠，加起来就是64个字符。任何数据经过base64编码之后，最终就会通过这64个字符来表示。当然还有一个符号，那就是等号。等号它是一个补位的符号&lt;/li&gt;
&lt;li&gt;需要注意的是Base64是编码方式，而不是加密方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. 生成和校验&lt;/h4&gt;
&lt;p&gt;简单介绍了JWT令牌以及JWT令牌的组成之后，接下来我们就来学习基于Java代码如何生成和校验JWT令牌。&lt;/p&gt;
&lt;p&gt;1). 首先我们先来实现JWT令牌的生成。要想使用JWT令牌，需要先引入JWT的依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- JWT依赖--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jjwt&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;0.9.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在引入完JWT来赖后，就可以调用工具包中提供的API来完成JWT令牌的生成和校验。工具类：Jwts&lt;/p&gt;
&lt;p&gt;2). 生成JWT代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void testGenJwt() {
    Map&amp;lt;String, Object&amp;gt; claims = new HashMap&amp;lt;&amp;gt;();
    claims.put(&quot;id&quot;, 10);
    claims.put(&quot;username&quot;, &quot;itheima&quot;);

    String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, &quot;aXRjYXN0&quot;)
        .addClaims(claims)
        .setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))
        .compact();

    System.out.println(jwt);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行测试方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出的结果就是生成的JWT令牌,，通过英文的点分割对三个部分进行分割，我们可以将生成的令牌复制一下，然后打开JWT的官网，将生成的令牌直接放在Encoded位置，此时就会自动的将令牌解析出来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./06.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一部分解析出来，看到JSON格式的原始数据，所使用的签名算法为HS256。&lt;/p&gt;
&lt;p&gt;第二个部分是我们自定义的数据，之前我们自定义的数据就是id，还有一个exp代表的是我们所设置的过期时间。&lt;/p&gt;
&lt;p&gt;由于前两个部分是base64编码，所以是可以直接解码出来。但最后一个部分并不是base64编码，是经过签名算法计算出来的，所以最后一个部分是不会解析的。&lt;/p&gt;
&lt;p&gt;3). 实现了JWT令牌的生成，下面我们接着使用Java代码来校验JWT令牌(解析生成的令牌)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void testParseJwt() {
    Claims claims = Jwts.parser().setSigningKey(&quot;aXRjYXN0&quot;)
        .parseClaimsJws(&quot;eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiaXRoZWltYSIsImV4cCI6MTcwMTkwOTAxNX0.N-MD6DmoeIIY5lB5z73UFLN9u7veppx1K5_N_jS9Yko&quot;)
        .getBody();
    System.out.println(claims);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行测试方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{id=10, username=itheima, exp=1701909015}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;令牌解析后，我们可以看到id和过期时间，如果在解析的过程当中没有报错，就说明解析成功了。&lt;/p&gt;
&lt;p&gt;下面我们做一个测试：把令牌header中的数字9变为8，运行测试方法后发现报错：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./07.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;结论：&lt;strong&gt;篡改令牌中的任何一个字符，在对令牌进行解析时都会报错，所以JWT令牌是非常安全可靠的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们继续测试：修改生成令牌的时指定的过期时间，修改为1分钟。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void genJwt(){
    Map&amp;lt;String, Object&amp;gt; claims = new HashMap&amp;lt;&amp;gt;();
    claims.put(&quot;id&quot;, 10);
    claims.put(&quot;username&quot;, &quot;itheima&quot;);

    String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, &quot;aXRjYXN0&quot;)
        .addClaims(claims)
        .setExpiration(new Date(System.currentTimeMillis() + 60 * 1000)) //有效期60s
        .compact();
    System.out.println(jwt);
    //输出结果：eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjczMDA5NzU0fQ.RcVIR65AkGiax-ID6FjW60eLFH3tPTKdoK7UtE4A1ro
}

@Test
public void parseJwt(){
    Claims claims = Jwts.parser()
        .setSigningKey(&quot;aXRjYXN0&quot;)//指定签名密钥
        .parseClaimsJws(&quot;eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjczMDA5NzU0fQ.RcVIR65AkGiax-ID6FjW60eLFH3tPTKdoK7UtE4A1ro&quot;)
        .getBody();

    System.out.println(claims);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待1分钟之后运行测试方法发现也报错了，说明：&lt;strong&gt;JWT令牌过期后，令牌就失效了，解析的为非法令牌。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过以上测试，我们在使用JWT令牌时需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JWT校验时使用的签名秘钥，必须和生成JWT令牌时使用的秘钥是配套的。&lt;/li&gt;
&lt;li&gt;如果JWT令牌解析校验时报错，则说明 JWT令牌被篡改 或 失效了，令牌非法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. 登录时下发令牌&lt;/h4&gt;
&lt;p&gt;JWT令牌的生成和校验的基本操作我们已经学习完了，接下来我们就需要在案例当中通过JWT令牌技术来跟踪会话。具体的思路我们前面已经分析过了，主要就是两步操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;生成令牌
&lt;ol&gt;
&lt;li&gt;在登录成功之后来生成一个JWT令牌，并且把这个令牌直接返回给前端&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;校验令牌
&lt;ol&gt;
&lt;li&gt;拦截前端请求，从请求中获取到令牌，对令牌进行解析校验&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那我们首先来完成：登录成功之后生成JWT令牌，并且把令牌返回给前端。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;引入JWT工具类：在项目工程下创建 &lt;code&gt;com.itheima.util&lt;/code&gt; 包，并把提供JWT工具类复制到该包下&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;package com.itheima.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.Map;

public class JwtUtils {

    private static String signKey = &quot;SVRIRUlNQQ==&quot;;
    private static Long expire = 43200000L;

    /**
     * 生成JWT令牌
     * @return
     */
    public static String generateJwt(Map&amp;lt;String,Object&amp;gt; claims){
        String jwt = Jwts.builder()
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, signKey)
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
        return jwt;
    }

    /**
     * 解析JWT令牌
     * @param jwt JWT令牌
     * @return JWT第二部分负载 payload 中存储的内容
     */
    public static Claims parseJWT(String jwt){
        Claims claims = Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;完善 &lt;code&gt;EmpServiceImpl&lt;/code&gt;中的 &lt;code&gt;login&lt;/code&gt; 方法逻辑， 登录成功，生成JWT令牌并返回&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Override
public LoginInfo login(Emp emp) {
    Emp empLogin = empMapper.getUsernameAndPassword(emp);
    if(empLogin != null){
        //1. 生成JWT令牌
        Map&amp;lt;String,Object&amp;gt; dataMap = new HashMap&amp;lt;&amp;gt;();
        dataMap.put(&quot;id&quot;, empLogin.getId());
        dataMap.put(&quot;username&quot;, empLogin.getUsername());
        
        String jwt = JwtUtils.generateJwt(dataMap);
        LoginInfo loginInfo = new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), jwt);
        return loginInfo;
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启服务，打开 Apifox 测试登录接口：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./08.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开浏览器完成前后端联调操作：利用开发者工具，抓取一下网络请求&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./09.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;登录请求完成后，可以看到JWT令牌已经响应给了前端，此时前端就会将JWT令牌存储在浏览器本地。&lt;/p&gt;
&lt;p&gt;服务器响应的JWT令牌存储在本地浏览器哪里了呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在当前案例中，JWT令牌存储在浏览器的本地存储空间 &lt;code&gt;localstorage&lt;/code&gt;中了。 &lt;code&gt;localstorage&lt;/code&gt; 是浏览器的本地存储，在移动端也是支持的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./10.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们在发起一个查询部门数据的请求，此时我们可以看到在请求头中包含一个token(JWT令牌)，后续的每一次请求当中，都会将这个令牌携带到服务端。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./11.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1. 过滤器Filter&lt;/h3&gt;
&lt;p&gt;刚才通过浏览器的开发者工具，我们可以看到在后续的请求当中，都会在请求头中携带JWT令牌到服务端，而服务端需要统一拦截所有的请求，从而判断是否携带的有合法的JWT令牌。&lt;/p&gt;
&lt;p&gt;那怎么样来统一拦截到所有的请求校验令牌的有效性呢？这里我们会学习两种解决方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Filter过滤器&lt;/li&gt;
&lt;li&gt;Interceptor拦截器&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们首先来学习过滤器Filter。&lt;/p&gt;
&lt;h4&gt;1. Filter快速入门&lt;/h4&gt;
&lt;p&gt;什么是Filter？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Filter表示过滤器，是 JavaWeb三大组件(Servlet、Filter、Listener)之一。&lt;/li&gt;
&lt;li&gt;过滤器可以把对资源的请求拦截下来，从而实现一些特殊的功能
&lt;ul&gt;
&lt;li&gt;使用了过滤器之后，要想访问web服务器上的资源，必须先经过滤器，过滤器处理完毕之后，才可以访问对应的资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;过滤器一般完成一些通用的操作，比如：登录校验、统一编码处理、敏感字符处理等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./12.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下面我们通过Filter快速入门程序掌握过滤器的基本使用操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第1步，定义过滤器 ：1.定义一个类，实现 Filter 接口，并重写其所有方法。&lt;/li&gt;
&lt;li&gt;第2步，配置过滤器：Filter类上加 @WebFilter 注解，配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;1). 定义过滤器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class DemoFilter implements Filter {
    //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println(&quot;init ...&quot;);
    }

    //拦截到请求时,调用该方法,可以调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        System.out.println(&quot;拦截到了请求...&quot;);
    }

    //销毁方法, web服务器关闭时调用, 只调用一次
    public void destroy() {
        System.out.println(&quot;destroy ... &quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;init方法：过滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象，在创建过滤器对象的时候会自动调用init初始化方法，这个方法只会被调用一次。&lt;/li&gt;
&lt;li&gt;doFilter方法：这个方法是在每一次拦截到请求之后都会被调用，所以这个方法是会被调用多次的，每拦截到一次请求就会调用一次doFilter()方法。&lt;/li&gt;
&lt;li&gt;destroy方法： 是销毁的方法。当我们关闭服务器的时候，它会自动的调用销毁方法destroy，而这个销毁方法也只会被调用一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2). 配置过滤器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在定义完Filter之后，Filter其实并不会生效，还需要完成Filter的配置，Filter的配置非常简单，只需要在Filter类上添加一个注解：&lt;code&gt;@WebFilter&lt;/code&gt;，并指定属性&lt;code&gt;urlPatterns&lt;/code&gt;，通过这个属性指定过滤器要拦截哪些请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebFilter(urlPatterns = &quot;/*&quot;) //配置过滤器要拦截的请求路径（ /* 表示拦截浏览器的所有请求 ）
public class DemoFilter implements Filter {
    //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println(&quot;init ...&quot;);
    }

    //拦截到请求时,调用该方法,可以调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        System.out.println(&quot;拦截到了请求...&quot;);
    }

    //销毁方法, web服务器关闭时调用, 只调用一次
    public void destroy() {
        System.out.println(&quot;destroy ... &quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们在Filter类上面加了@WebFilter注解之后，接下来我们还需要在启动类上面加上一个注解&lt;code&gt;@ServletComponentScan&lt;/code&gt;，通过这个&lt;code&gt;@ServletComponentScan&lt;/code&gt;注解来开启SpringBoot项目对于Servlet组件的支持。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ServletComponentScan //开启对Servlet组件的支持
@SpringBootApplication
public class TliasManagementApplication {
    public static void main(String[] args) {
        SpringApplication.run(TliasManagementApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新启动服务，打开浏览器，执行部门管理的请求，可以看到控制台输出了过滤器中的内容：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./13.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;**注意事项：**在过滤器Filter中，如果不执行放行操作，将无法访问后面的资源。 放行操作：&lt;code&gt;chain.doFilter(request, response);&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;1. 登录校验过滤器&lt;/h4&gt;
&lt;h5&gt;1. 分析&lt;/h5&gt;
&lt;p&gt;过滤器Filter的快速入门以及使用细节我们已经介绍完了，接下来最后一步，我们需要使用过滤器Filter来完成案例当中的登录校验功能。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./14.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们先来回顾下前面分析过的登录校验的基本流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要进入到后台管理系统，我们必须先完成登录操作，此时就需要访问登录接口login。&lt;/li&gt;
&lt;li&gt;登录成功之后，我们会在服务端生成一个JWT令牌，并且把JWT令牌返回给前端，前端会将JWT令牌存储下来。&lt;/li&gt;
&lt;li&gt;在后续的每一次请求当中，都会将JWT令牌携带到服务端，请求到达服务端之后，要想去访问对应的业务功能，此时我们必须先要校验令牌的有效性。&lt;/li&gt;
&lt;li&gt;对于校验令牌的这一块操作，我们使用登录校验的过滤器，在过滤器当中来校验令牌的有效性。如果令牌是无效的，就响应一个错误的信息，也不会再去放行访问对应的资源了。如果令牌存在，并且它是有效的，此时就会放行去访问对应的web资源，执行相应的业务操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大概清楚了在Filter过滤器的实现步骤了，那在正式开发登录校验过滤器之前，我们思考两个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;所有的请求，拦截到了之后，都需要校验令牌吗 ？
&lt;ol&gt;
&lt;li&gt;答案：&lt;strong&gt;登录请求例外&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;拦截到请求后，什么情况下才可以放行，执行业务操作 ？
&lt;ol&gt;
&lt;li&gt;答案：&lt;strong&gt;有令牌，且令牌校验通过(合法)；否则都返回未登录错误结果&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;1. 具体流程&lt;/h5&gt;
&lt;p&gt;我们要完成登录校验，主要是利用Filter过滤器实现，而Filter过滤器的流程步骤：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./15.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;基于上面的业务流程，我们分析出具体的操作步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取请求url&lt;/li&gt;
&lt;li&gt;判断请求url中是否包含login，如果包含，说明是登录操作，放行&lt;/li&gt;
&lt;li&gt;获取请求头中的令牌（token）&lt;/li&gt;
&lt;li&gt;判断令牌是否存在，如果不存在，响应 401&lt;/li&gt;
&lt;li&gt;解析token，如果解析失败，响应 401&lt;/li&gt;
&lt;li&gt;放行&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;1. 代码实现&lt;/h5&gt;
&lt;p&gt;在 &lt;code&gt;com.itheima.filter&lt;/code&gt; 包下创建&lt;code&gt;TokenFilter&lt;/code&gt;，具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.itheima.filter;

import com.itheima.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.springframework.util.StringUtils;
import java.io.IOException;

/**
 * 令牌校验过滤器
 */
@Slf4j
@WebFilter(urlPatterns = &quot;/*&quot;)
public class TokenFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        //1. 获取请求url。
        String url = request.getRequestURL().toString();

        //2. 判断请求url中是否包含login，如果包含，说明是登录操作，放行。
        if(url.contains(&quot;login&quot;)){ //登录请求
            log.info(&quot;登录请求 , 直接放行&quot;);
            chain.doFilter(request, response);
            return;
        }

        //3. 获取请求头中的令牌（token）。
        String jwt = request.getHeader(&quot;token&quot;);

        //4. 判断令牌是否存在，如果不存在，返回错误结果（未登录）。
        if(!StringUtils.hasLength(jwt)){ //jwt为空
            log.info(&quot;获取到jwt令牌为空, 返回错误结果&quot;);
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return;
        }

        //5. 解析token，如果解析失败，返回错误结果（未登录）。
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {
            e.printStackTrace();
            log.info(&quot;解析令牌失败, 返回错误结果&quot;);
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return;
        }

        //6. 放行。
        log.info(&quot;令牌合法, 放行&quot;);
        chain.doFilter(request , response);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登录校验的过滤器我们编写完成了，接下来我们就可以重新启动服务来做一个测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试1：未登录是否可以访问部门管理页面&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先关闭浏览器，重新打开浏览器，在地址栏中输入：http://localhost:90&lt;/p&gt;
&lt;p&gt;由于用户没有登录，登录校验过滤器返回错误信息，前端页面根据返回的错误信息结果，自动跳转到登录页面了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./16.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试2：先进行登录操作，再访问部门管理页面&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;登录校验成功之后，可以正常访问相关业务操作页面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./17.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;1. Filter详解&lt;/h4&gt;
&lt;p&gt;Filter过滤器的快速入门程序我们已经完成了，接下来我们就要详细的介绍一下过滤器Filter在使用中的一些细节。主要介绍以下3个方面的细节：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;过滤器的执行流程&lt;/li&gt;
&lt;li&gt;过滤器的拦截路径配置&lt;/li&gt;
&lt;li&gt;过滤器链&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;1. 执行流程&lt;/h5&gt;
&lt;p&gt;首先我们先来看下过滤器的执行流程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./18.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;过滤器当中我们拦截到了请求之后，如果希望继续访问后面的web资源，就要执行放行操作，放行就是调用 FilterChain对象当中的doFilter()方法，在调用doFilter()这个方法之前所编写的代码属于放行之前的逻辑。&lt;/p&gt;
&lt;p&gt;在放行后访问完 web 资源之后还会回到过滤器当中，回到过滤器之后如有需求还可以执行放行之后的逻辑，放行之后的逻辑我们写在doFilter()这行代码之后。&lt;/p&gt;
&lt;p&gt;测试代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebFilter(urlPatterns = &quot;/*&quot;) 
public class DemoFilter implements Filter {
    
    @Override //初始化方法, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println(&quot;init 初始化方法执行了&quot;);
    }
    
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        
        System.out.println(&quot;DemoFilter   放行前逻辑.....&quot;);

        //放行请求
        filterChain.doFilter(servletRequest,servletResponse);

        System.out.println(&quot;DemoFilter   放行后逻辑.....&quot;);
        
    }

    @Override //销毁方法, 只调用一次
    public void destroy() {
        System.out.println(&quot;destroy 销毁方法执行了&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动之后运行测试：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./19.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;1. 拦截路径&lt;/h5&gt;
&lt;p&gt;执行流程我们搞清楚之后，接下来再来介绍一下过滤器的拦截路径，Filter可以根据需求，配置不同的拦截资源路径：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;拦截路径&lt;/th&gt;
&lt;th&gt;urlPatterns值&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;拦截具体路径&lt;/td&gt;
&lt;td&gt;/login&lt;/td&gt;
&lt;td&gt;只有访问 /login 路径时，才会被拦截&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;目录拦截&lt;/td&gt;
&lt;td&gt;/emps/*&lt;/td&gt;
&lt;td&gt;访问/emps下的所有资源，都会被拦截&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;拦截所有&lt;/td&gt;
&lt;td&gt;/*&lt;/td&gt;
&lt;td&gt;访问所有资源，都会被拦截&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;下面我们来测试&quot;拦截具体路径&quot;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@WebFilter(urlPatterns = &quot;/login&quot;)  //拦截/login具体路径
public class DemoFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println(&quot;DemoFilter   放行前逻辑.....&quot;);

        //放行请求
        filterChain.doFilter(servletRequest,servletResponse);

        System.out.println(&quot;DemoFilter   放行后逻辑.....&quot;);
    }


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1. 过滤器链&lt;/h5&gt;
&lt;p&gt;最后我们在来介绍下过滤器链，什么是过滤器链呢？所谓过滤器链指的是在一个web应用程序当中，可以配置多个过滤器，多个过滤器就形成了一个过滤器链。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./20.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;比如：在我们web服务器当中，定义了两个过滤器，这两个过滤器就形成了一个过滤器链。&lt;/p&gt;
&lt;p&gt;而这个链上的过滤器在执行的时候会一个一个的执行，会先执行第一个Filter，放行之后再来执行第二个Filter，如果执行到了最后一个过滤器放行之后，才会访问对应的web资源。&lt;/p&gt;
&lt;p&gt;访问完web资源之后，按照我们刚才所介绍的过滤器的执行流程，还会回到过滤器当中来执行过滤器放行后的逻辑，而在执行放行后的逻辑的时候，顺序是反着的。&lt;/p&gt;
&lt;p&gt;先要执行过滤器2放行之后的逻辑，再来执行过滤器1放行之后的逻辑，最后在给浏览器响应数据。&lt;/p&gt;
&lt;p&gt;过滤器链上过滤器的执行顺序：注解配置的Filter，优先级是按照过滤器类名（字符串）的自然排序。 比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AbcFilter&lt;/li&gt;
&lt;li&gt;DemoFilter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个过滤器来说，AbcFilter 会先执行，DemoFilter会后执行。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;登录认证的主线是“登录生成凭证，请求携带凭证，服务端校验凭证”。JWT 和过滤器只是实现这条主线的常见工具，公开前尤其要检查示例密钥和令牌是否只是教学占位。&lt;/p&gt;
</content:encoded></item><item><title>MySQL 基础与 SQL 语句笔记</title><link>https://youki.bbroot.com/posts/java/mysql/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/java/mysql/</guid><description>整理数据库、关系型数据库、SQL 分类、表操作和 MySQL 常用语法。</description><pubDate>Sat, 21 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记记录 MySQL 入门阶段的核心概念和 SQL 基础语法。当前内容偏课堂摘录，后续精修时可以把 DDL、DML、DQL、DCL 分开整理。&lt;/p&gt;
&lt;h3&gt;1. 相关概念&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据库：英文为 DataBase，简称DB，它是存储和管理数据的仓库。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据库管理系统（DataBase Management System，简称DBMS），是操作和管理数据库的大型软件。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;将来我们只需要操作这个软件，就可以通过这个软件来操纵和管理数据库了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQL（Structured Query Language，简称SQL）：结构化查询语言，它是操作关系型数据库的编程语言，定义了一套操作关系型数据库的统一标准。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.1 关系型数据库（RDBMS）&lt;/h4&gt;
&lt;p&gt;概念：建立在关系模型基础上，由多张相互连接的&lt;strong&gt;二维表&lt;/strong&gt;组成的数据库。而所谓二维表，指的是由行和列组成的表，如下图：&lt;/p&gt;
&lt;p&gt;二维表的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用表存储数据，格式统一，便于维护&lt;/li&gt;
&lt;li&gt;使用SQL语言操作，标准统一，使用方便，可用于复杂查询&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.2 数据模型&lt;/h4&gt;
&lt;p&gt;介绍完了关系型数据库之后，接下来我们再来看一看在Mysql数据库当中到底是如何来存储数据的，也就是Mysql 的数据模型。&lt;/p&gt;
&lt;p&gt;MySQL是关系型数据库，是基于二维表进行数据存储的，具体的结构图下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过MySQL客户端连接数据库管理系统DBMS，然后通过DBMS操作数据库。&lt;/li&gt;
&lt;li&gt;使用MySQL客户端，向数据库管理系统发送一条SQL语句，由数据库管理系统根据SQL语句指令去操作数据库中的表结构及数据。&lt;/li&gt;
&lt;li&gt;一个数据库服务器中可以创建多个数据库，一个数据库中也可以包含多张表，而一张表中又可以包含多行记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1. SQL语句&lt;/h2&gt;
&lt;p&gt;SQL：结构化查询语言。一门操作关系型数据库的编程语言，定义操作所有关系型数据库的统一标准。SQL语句根据其功能被分为四大类：DDL、DML、DQL、DCL 。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分类&lt;/th&gt;
&lt;th&gt;全称&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DDL&lt;/td&gt;
&lt;td&gt;Data Definition Language&lt;/td&gt;
&lt;td&gt;数据定义语言，用来定义数据库对象(数据库，表，字段)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DML&lt;/td&gt;
&lt;td&gt;Data Manipulation Language&lt;/td&gt;
&lt;td&gt;数据操作语言，用来对数据库表中的数据进行增删改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DQL&lt;/td&gt;
&lt;td&gt;Data Query Language&lt;/td&gt;
&lt;td&gt;数据查询语言，用来查询数据库中表的记录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DCL&lt;/td&gt;
&lt;td&gt;Data Control Language&lt;/td&gt;
&lt;td&gt;数据控制语言，用来创建数据库用户、控制数据库的访问权限&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;1.1 DDL语句&lt;/h3&gt;
&lt;h4&gt;1.1 表操作&lt;/h4&gt;
&lt;p&gt;学习完了DDL语句当中关于数据库的操作之后，接下来我们继续学习DDL语句当中关于表结构的操作。&lt;/p&gt;
&lt;p&gt;关于表结构的操作也是包含四个部分：创建表、查询表、修改表、删除表。&lt;/p&gt;
&lt;h5&gt;1.1 创建&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;语法：
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;  create table  表名(
          字段1  字段1类型 [约束]  [comment  字段1注释 ],
          字段2  字段2类型 [约束]  [comment  字段2注释 ],
          ......
          字段n  字段n类型 [约束]  [comment  字段n注释 ] 
  ) [ comment  表注释 ] ;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意： [ ] 中的内容为可选参数； 最后一个字段后面没有逗号&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;例：建表语句：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;		create table tb_user (
		    id int comment &apos;ID,唯一标识&apos;,   # id是一行数据的唯一标识（不能重复）
		    username varchar(20) comment &apos;用户名&apos;,
		    name varchar(10) comment &apos;姓名&apos;,
		    age int comment &apos;年龄&apos;,
		    gender char(1) comment &apos;性别&apos;
		) comment &apos;用户表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1.2 约束&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;概念：所谓约束就是作用在表中字段上的规则，用于限制存储在表中的数据。&lt;/li&gt;
&lt;li&gt;作用：就是来保证数据库当中数据的正确性、有效性和完整性。（后面的学习会验证这些）&lt;/li&gt;
&lt;li&gt;在MySQL数据库当中，提供了以下5种约束：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;约束&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;关键字&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;非空约束&lt;/td&gt;
&lt;td&gt;限制该字段值不能为null&lt;/td&gt;
&lt;td&gt;not null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;唯一约束&lt;/td&gt;
&lt;td&gt;保证字段的所有数据都是唯一、不重复的&lt;/td&gt;
&lt;td&gt;unique&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主键约束&lt;/td&gt;
&lt;td&gt;主键是一行数据的唯一标识，要求非空且唯一&lt;/td&gt;
&lt;td&gt;primary key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;默认约束&lt;/td&gt;
&lt;td&gt;保存数据时，如果未指定该字段值，则采用默认值&lt;/td&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;外键约束&lt;/td&gt;
&lt;td&gt;让两张表的数据建立连接，保证数据的一致性和完整性&lt;/td&gt;
&lt;td&gt;foreign key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意：约束是作用于表中字段上的，可以在创建表/修改表的时候添加约束。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主键自增：auto_increment&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次插入新的行记录时，数据库自动生成id字段(主键)下的值&lt;/li&gt;
&lt;li&gt;具有auto_increment的数据列是一个正数序列开始增长(从1开始自增)&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;1.3 数据类型&lt;/h5&gt;
&lt;p&gt;在上面建表语句中，我们在指定字段的数据类型时，用到了int 、varchar、char，那么在MySQL中除了以上的数据类型，还有哪些常见的数据类型呢？ 接下来,我们就来详细介绍一下MySQL的数据类型。&lt;/p&gt;
&lt;p&gt;MySQL中的数据类型有很多，主要分为三类：数值类型、字符串类型、日期时间类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1). 数值类型&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;大小&lt;/th&gt;
&lt;th&gt;有符号(SIGNED)范围&lt;/th&gt;
&lt;th&gt;无符号(UNSIGNED)范围&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TINYINT&lt;/td&gt;
&lt;td&gt;1byte&lt;/td&gt;
&lt;td&gt;(-128，127)&lt;/td&gt;
&lt;td&gt;(0，255)&lt;/td&gt;
&lt;td&gt;小整数值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMALLINT&lt;/td&gt;
&lt;td&gt;2bytes&lt;/td&gt;
&lt;td&gt;(-32768，32767)&lt;/td&gt;
&lt;td&gt;(0，65535)&lt;/td&gt;
&lt;td&gt;大整数值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MEDIUMINT&lt;/td&gt;
&lt;td&gt;3bytes&lt;/td&gt;
&lt;td&gt;(-8388608，8388607)&lt;/td&gt;
&lt;td&gt;(0，16777215)&lt;/td&gt;
&lt;td&gt;大整数值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INT/INTEGER&lt;/td&gt;
&lt;td&gt;4bytes&lt;/td&gt;
&lt;td&gt;(-2147483648，2147483647)&lt;/td&gt;
&lt;td&gt;(0，4294967295)&lt;/td&gt;
&lt;td&gt;大整数值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BIGINT&lt;/td&gt;
&lt;td&gt;8bytes&lt;/td&gt;
&lt;td&gt;(-2^63，2^63-1)&lt;/td&gt;
&lt;td&gt;(0，2^64-1)&lt;/td&gt;
&lt;td&gt;极大整数值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FLOAT&lt;/td&gt;
&lt;td&gt;4bytes&lt;/td&gt;
&lt;td&gt;(-3.402823466 E+38，3.402823466351 E+38)&lt;/td&gt;
&lt;td&gt;0 和 (1.175494351 E-38，3.402823466 E+38)&lt;/td&gt;
&lt;td&gt;单精度浮点数值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOUBLE&lt;/td&gt;
&lt;td&gt;8bytes&lt;/td&gt;
&lt;td&gt;(-1.7976931348623157 E+308，1.7976931348623157 E+308)&lt;/td&gt;
&lt;td&gt;0 和 (2.2250738585072014 E-308，1.7976931348623157 E+308)&lt;/td&gt;
&lt;td&gt;双精度浮点数值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DECIMAL&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;依赖于M(精度)和D(标度)的值&lt;/td&gt;
&lt;td&gt;依赖于M(精度)和D(标度)的值&lt;/td&gt;
&lt;td&gt;小数值(精确定点数)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;2). 字符串类型&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;大小&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CHAR&lt;/td&gt;
&lt;td&gt;0-255 bytes&lt;/td&gt;
&lt;td&gt;定长字符串(需要指定长度)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VARCHAR&lt;/td&gt;
&lt;td&gt;0-65535 bytes&lt;/td&gt;
&lt;td&gt;变长字符串(需要指定长度)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TINYBLOB&lt;/td&gt;
&lt;td&gt;0-255 bytes&lt;/td&gt;
&lt;td&gt;不超过255个字符的二进制数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TINYTEXT&lt;/td&gt;
&lt;td&gt;0-255 bytes&lt;/td&gt;
&lt;td&gt;短文本字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BLOB&lt;/td&gt;
&lt;td&gt;0-65 535 bytes&lt;/td&gt;
&lt;td&gt;二进制形式的长文本数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TEXT&lt;/td&gt;
&lt;td&gt;0-65 535 bytes&lt;/td&gt;
&lt;td&gt;长文本数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MEDIUMBLOB&lt;/td&gt;
&lt;td&gt;0-16 777 215 bytes&lt;/td&gt;
&lt;td&gt;二进制形式的中等长度文本数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MEDIUMTEXT&lt;/td&gt;
&lt;td&gt;0-16 777 215 bytes&lt;/td&gt;
&lt;td&gt;中等长度文本数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LONGBLOB&lt;/td&gt;
&lt;td&gt;0-4 294 967 295 bytes&lt;/td&gt;
&lt;td&gt;二进制形式的极大文本数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LONGTEXT&lt;/td&gt;
&lt;td&gt;0-4 294 967 295 bytes&lt;/td&gt;
&lt;td&gt;极大文本数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;char 与 varchar 都可以描述字符串，char是定长字符串，指定长度多长，就占用多少个字符，和字段值的长度无关 。而varchar是变长字符串，指定的长度为最大占用长度 。相对来说，char的性能会更高些。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3). 日期时间类型&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;大小&lt;/th&gt;
&lt;th&gt;范围&lt;/th&gt;
&lt;th&gt;格式&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DATE&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1000-01-01 至 9999-12-31&lt;/td&gt;
&lt;td&gt;YYYY-MM-DD&lt;/td&gt;
&lt;td&gt;日期值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIME&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;-838:59:59 至 838:59:59&lt;/td&gt;
&lt;td&gt;HH:MM:SS&lt;/td&gt;
&lt;td&gt;时间值或持续时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YEAR&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1901 至 2155&lt;/td&gt;
&lt;td&gt;YYYY&lt;/td&gt;
&lt;td&gt;年份值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATETIME&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1000-01-01 00:00:00 至 9999-12-31 23:59:59&lt;/td&gt;
&lt;td&gt;YYYY-MM-DD HH:MM:SS&lt;/td&gt;
&lt;td&gt;混合日期和时间值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIMESTAMP&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1970-01-01 00:00:01 至 2038-01-19 03:14:07&lt;/td&gt;
&lt;td&gt;YYYY-MM-DD HH:MM:SS&lt;/td&gt;
&lt;td&gt;混合日期和时间值，时间戳&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关于表   &lt;em&gt;结构&lt;/em&gt;   的查看、修改、删除操作，工作中一般都是直接基于图形化界面操作。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改数据库表结构的具体语法：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;添加字段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 添加字段
alter table 表名 add  字段名  类型(长度)  [comment 注释]  [约束];

-- 比如： 为tb_emp表添加字段qq，字段类型为 varchar(11)
alter table tb_emp add  qq  varchar(11) comment &apos;QQ号码&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 DML语句&lt;/h3&gt;
&lt;p&gt;DML英文全称是Data Manipulation Language(数据操作语言)，用来对数据库中表的数据记录进行增、删、改操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;添加数据（INSERT）&lt;/li&gt;
&lt;li&gt;修改数据（UPDATE）&lt;/li&gt;
&lt;li&gt;删除数据（DELETE）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.2.1 增加(insert)&lt;/h4&gt;
&lt;h5&gt;1.2.1.1 &lt;strong&gt;语法&lt;/strong&gt;&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;向指定字段添加数据&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;insert into 表名 (字段名1, 字段名2) values (值1, 值2);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;全部字段添加数据&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;insert into 表名 values (值1, 值2, ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;批量添加数据（指定字段）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;insert into 表名 (字段名1, 字段名2) values (值1, 值2), (值1, 值2);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;批量添加数据（全部字段）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;insert into 表名 values (值1, 值2, ...), (值1, 值2, ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1.2.1.2 案例演示&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;案例1：向emp表的username, name, gender, phone, create_time, update_time字段插入数据&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- 因为设计表时create_time, update_time两个字段不能为NULL，所以也做为要插入的字段
insert into emp(username, name, gender, phone, create_time, update_time)
values (&apos;wuji&apos;, &apos;张无忌&apos;, 1, &apos;13309091231&apos;, now(), now());
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.2.2 修改(update)&lt;/h4&gt;
&lt;h5&gt;1.2.2.1 语法&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;update 表名 set 字段名1 = 值1 , 字段名2 = 值2 , .... [where 条件] ;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1.2.2.2 案例演示&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;update emp set name=&apos;张三&apos;, update_time=now() where id=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.2.3 删除(delete)&lt;/h4&gt;
&lt;h5&gt;1.2.3.1 语法&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;delete from 表名  [where  条件] ;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1.2.3.2 案例演示&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;delete from emp where id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3 DQL语句&lt;/h3&gt;
&lt;h4&gt;1.3.1 介绍&lt;/h4&gt;
&lt;p&gt;DQL英文全称是Data Query Language(数据查询语言)，用来查询数据库表中的记录。&lt;/p&gt;
&lt;p&gt;查询关键字：&lt;strong&gt;SELECT&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;查询操作是所有SQL语句当中最为常见，也是最为重要的操作。在一个正常的业务系统中，查询操作的使用频次是要远高于增删改操作的。当我们打开某个网站或APP所看到的展示信息，都是通过从数据库中查询得到的，而在这个查询过程中，还会涉及到条件、排序、分页等操作。&lt;/p&gt;
&lt;h4&gt;1.3.2 语法，语法结构如下：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;SELECT
        字段列表
FROM
        表名列表
WHERE
        条件列表
GROUP  BY
        分组字段列表
HAVING
        分组后条件列表
ORDER BY
        排序字段列表
LIMIT
        分页参数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们今天会将上面的完整语法拆分为以下几个部分学习：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基本查询（不带任何条件）&lt;/li&gt;
&lt;li&gt;条件查询（where）&lt;/li&gt;
&lt;li&gt;分组查询（group by）&lt;/li&gt;
&lt;li&gt;排序查询（order by）&lt;/li&gt;
&lt;li&gt;分页查询（limit）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.3.3 基本查询，不带任何的查询条件。&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;语法如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询多个字段&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select 字段1, 字段2, 字段3 from  表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;查询所有字段（通配符）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select *  from  表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;设置别名&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select 字段1 [ as 别名1 ] , 字段2 [ as 别名2 ]  from  表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;去除重复记录&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select distinct 字段列表 from  表名;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;案例演示:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;案例1：查询指定字段 name，entry_date并返回&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select name,entry_date from emp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;* 号代表查询所有字段，在实际开发中尽量少用（不直观、影响效率）&lt;/p&gt;
&lt;h4&gt;1.3.4 条件查询&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;select  字段列表  from   表名   where   条件列表 ; -- 条件列表：意味着可以有多个条件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;学习条件查询就是学习条件的构建方式，而在SQL语句当中构造条件的运算符分为两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比较运算符&lt;/li&gt;
&lt;li&gt;逻辑运算符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用的比较运算符如下:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;比较运算符&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;&lt;/td&gt;
&lt;td&gt;大于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;=&lt;/td&gt;
&lt;td&gt;大于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;&lt;/td&gt;
&lt;td&gt;小于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;=&lt;/td&gt;
&lt;td&gt;小于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;=&lt;/td&gt;
&lt;td&gt;等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;&amp;gt; 或 !=&lt;/td&gt;
&lt;td&gt;不等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;between ... and ...&lt;/td&gt;
&lt;td&gt;在某个范围之内(含最小、最大值)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;in(...)&lt;/td&gt;
&lt;td&gt;在in之后的列表中的值，多选一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;like 占位符&lt;/td&gt;
&lt;td&gt;模糊匹配(_匹配单个字符, %匹配任意个字符)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is null&lt;/td&gt;
&lt;td&gt;是null&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;常用的逻辑运算符如下:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;逻辑运算符&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;and 或 &amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;并且 (多个条件同时成立)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;or 或 ||&lt;/td&gt;
&lt;td&gt;或者 (多个条件任意一个成立)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;not 或 !&lt;/td&gt;
&lt;td&gt;非 , 不是&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;案例1：查询 姓名 为 &apos;杨逍&apos; 的员工&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select id, username, password, name, gender, phone, salary, job, image, entry_date, create_time, update_time
from emp
where name = &apos;杨逍&apos;; -- 字符串使用&apos;&apos;或&quot;&quot;包含
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;案例2：查询 薪资小于等于 5000 的员工信息&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select id, username, password, name, gender, phone, salary, job, image, entry_date, create_time, update_time
from emp
where salary &amp;lt;=5000;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：查询为NULL的数据时，不能使用 &lt;code&gt;= null&lt;/code&gt; 或 &lt;code&gt;！=null&lt;/code&gt; 。得使用 &lt;code&gt;is null&lt;/code&gt; 或 &lt;code&gt;is not null&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;之前我们做的查询都是横向查询，就是根据条件一行一行的进行判断，而使用聚合函数查询就是纵向查询，它是对一列的值进行计算，然后返回一个结果值。（将一列数据作为一个整体，进行纵向计算）&lt;/p&gt;
&lt;p&gt;常用聚合函数：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;count&lt;/td&gt;
&lt;td&gt;统计数量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max&lt;/td&gt;
&lt;td&gt;最大值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;min&lt;/td&gt;
&lt;td&gt;最小值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;avg&lt;/td&gt;
&lt;td&gt;平均值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sum&lt;/td&gt;
&lt;td&gt;求和&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意 : 聚合函数会忽略空值，对NULL值不作为统计。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;count ：按照列去统计有多少行数据。
&lt;ul&gt;
&lt;li&gt;在根据指定的列统计的时候，如果这一列中有null的行，该行不会被统计在其中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;sum ：计算指定列的数值和，如果不是数值类型，那么计算结果为0&lt;/li&gt;
&lt;li&gt;max ：计算指定列的最大值&lt;/li&gt;
&lt;li&gt;min ：计算指定列的最小值&lt;/li&gt;
&lt;li&gt;avg ：计算指定列的平均值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;案例演示:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;案例1：统计该企业员工数量&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- count(字段)
select count(id) from emp;-- 结果：30
select count(job) from emp;-- 结果：29 （聚合函数对NULL值不做计算）

-- count(常量)
select count(0) from emp;
select count(&apos;A&apos;) from emp;

-- count(*)  推荐此写法（MySQL底层进行了优化）
select count(*) from emp;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.3.5 分组查询 按照某一列或者某几列，把相同的数据进行合并输出。&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;- 分组其实就是按列进行分类(指定列下相同的数据归为一类)，然后可以对分类完的数据进行合并计算。
- 分组查询通常会使用聚合函数进行计算。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;语法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select  字段列表  from  表名  [where 条件]  group by 分组字段名  [having 分组后过滤条件];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;案例演示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;案例1：根据性别分组 , 统计男性和女性员工的数量&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select gender, count(*)
from emp
group by gender; -- 按照gender字段进行分组（gender字段下相同的数据归为一组）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意事项:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分组之后，查询的字段一般为聚合函数和分组字段，查询其他字段无任何意义&lt;/li&gt;
&lt;li&gt;执行顺序：where &amp;gt; 聚合函数 &amp;gt; having&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;where与having区别（面试题）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行时机不同：where是分组之前进行过滤，不满足where条件，不参与分组；而having是分组之后对结果进行过滤。&lt;/li&gt;
&lt;li&gt;判断条件不同：where不能对聚合函数进行判断，而having可以。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.3.6 排序查询开发中是非常常见的一个操作，有升序排序，也有降序排序。&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;语法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select  字段列表  
from   表名   
[where  条件列表] 
[group by  分组字段 ] 
order  by  字段1  排序方式1 , 字段2  排序方式2 … ;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;排序方式：
&lt;ul&gt;
&lt;li&gt;ASC ：升序（默认值）&lt;/li&gt;
&lt;li&gt;DESC：降序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;案例演示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;案例1：根据入职时间, 对员工进行升序排序&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select id, username, password, name, gender, phone, salary, job, image, entry_date, create_time, update_time
from emp
order by entry_date ASC; -- 按照entrydate字段下的数据进行升序排序

select id, username, password, name, gender, phone, salary, job, image, entry_date, create_time, update_time
from emp
order by  entry_date; -- 默认就是ASC（升序）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意事项：如果是升序, 可以不指定排序方式ASC&lt;/p&gt;
&lt;h4&gt;1.3.7 分页查询业务系统开发时，也是非常常见的一个功能，日常我们在网站中看到的各种各样的分页条，后台也都需要借助于数据库的分页操作。&lt;/h4&gt;
&lt;p&gt;分页查询语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select  字段列表  from  表名  limit  起始索引, 查询记录数 ;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;案例1：从起始索引0开始查询员工数据, 每页展示5条记录&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select id, username, password, name, gender, phone, salary, job, image, entry_date, create_time, update_time
from emp
limit 0 , 5; -- 从索引0开始，向后取5条记录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意事项:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;起始索引从0开始。           计算公式 ：起始索引 = （查询页码 - 1）* 每页显示记录数&lt;/li&gt;
&lt;li&gt;分页查询是数据库的方言，不同的数据库有不同的实现，MySQL中是LIMIT&lt;/li&gt;
&lt;li&gt;如果查询的是第一页数据，起始索引可以省略，直接简写为 limit  条数&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;MySQL 入门可以先按“概念、表结构、SQL 分类、查询与分页”来理解。等基础语句熟悉后，再继续整理索引、事务和性能相关内容。&lt;/p&gt;
</content:encoded></item><item><title>Spring 事务管理学习笔记</title><link>https://youki.bbroot.com/posts/java/spring-transaction/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/java/spring-transaction/</guid><description>整理事务一致性问题、@Transactional 注解、回滚规则和 Spring 事务控制。</description><pubDate>Sat, 21 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记整理 Spring 中事务管理的使用场景和基础写法。核心问题是：当一个业务操作包含多次数据库写入时，如何保证它们要么全部成功，要么全部失败。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;事务用于保证一组业务操作的数据一致性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Transactional&lt;/code&gt; 可以交给 Spring 管理事务开启、提交和回滚。&lt;/li&gt;
&lt;li&gt;异常类型、传播行为和回滚规则会影响事务是否生效。&lt;/li&gt;
&lt;li&gt;事务一般放在业务层，而不是控制器或数据访问层。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. Spring事务管理&lt;/h3&gt;
&lt;h4&gt;1. 分析&lt;/h4&gt;
&lt;p&gt;在上述实现的新增员工的功能中，一旦在保存员工基本信息后出现异常。 我们就会发现，员工信息保存成功，但是工作经历信息保存失败，造成了数据的不完整不一致。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./01.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;产生原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先执行新增员工的操作，这步执行完毕，就已经往员工表 &lt;code&gt;emp&lt;/code&gt; 插入了数据。&lt;/li&gt;
&lt;li&gt;执行 1/0 操作，抛出异常&lt;/li&gt;
&lt;li&gt;抛出异常之前，下面所有的代码都不会执行了，批量保存工作经历信息，这个操作也不会执行 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时就出现问题了，员工基本信息保存了，员工的工作经历信息未保存，业务操作前后数据不一致。&lt;/p&gt;
&lt;p&gt;而要想保证操作前后，数据的一致性，就需要让新增员工中涉及到的两个业务操作，要么全部成功，要么全部失败 。 那我们如何，让这两个操作要么全部成功，要么全部失败呢 ？&lt;/p&gt;
&lt;p&gt;那就可以通过事务来实现，因为一个事务中的多个业务操作，要么全部成功，要么全部失败。&lt;/p&gt;
&lt;p&gt;此时，我们就需要在新增员工功能中添加事务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./02.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在方法运行之前，开启事务，如果方法成功执行，就提交事务，如果方法执行的过程当中出现异常了，就回滚事务。&lt;/p&gt;
&lt;p&gt;**思考：**开发中所有的业务操作，一旦我们要进行控制事务，是不是都是这样的套路？&lt;/p&gt;
&lt;p&gt;**答案：**是的。&lt;/p&gt;
&lt;p&gt;所以在spring框架当中就已经把事务控制的代码都已经封装好了，并不需要我们手动实现。我们使用了spring框架，我们只需要通过一个简单的注解@Transactional就搞定了。&lt;/p&gt;
&lt;h4&gt;1. Transactional注解&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;注解：&lt;/strong&gt;@Transactional&lt;/p&gt;
&lt;p&gt;**作用：**就是在当前这个方法执行开始之前来开启事务，方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常，就会进行事务的回滚操作。&lt;/p&gt;
&lt;p&gt;**位置：**业务层的方法上、类上、接口上&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法上：当前方法交给spring进行事务管理&lt;/li&gt;
&lt;li&gt;类上：当前类中所有的方法都交由spring进行事务管理&lt;/li&gt;
&lt;li&gt;接口上：接口下所有的实现类当中所有的方法都交给spring 进行事务管理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来，我们就可以在业务方法save上加上 &lt;code&gt;@Transactional&lt;/code&gt; 来控制事务 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Transactional
@Override
public void save(Emp emp) {
    //1.补全基础属性
    emp.setCreateTime(LocalDateTime.now());
    emp.setUpdateTime(LocalDateTime.now());
    //2.保存员工基本信息
    empMapper.insert(emp);

    int i = 1/0;

    //3. 保存员工的工作经历信息 - 批量
    Integer empId = emp.getId();
    List&amp;lt;EmpExpr&amp;gt; exprList = emp.getExprList();
    if(!CollectionUtils.isEmpty(exprList)){
        exprList.forEach(empExpr -&amp;gt; empExpr.setEmpId(empId));
        empExprMapper.insertBatch(exprList);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@Transactional注解：我们一般会在业务层当中来控制事务，因为在业务层当中，一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务，我们就可以将多个数据访问操作控制在一个事务范围内。&lt;/p&gt;
&lt;p&gt;说明：可以在&lt;code&gt;application.yml&lt;/code&gt;配置文件中开启事务管理日志，这样就可以在控制看到和事务相关的日志信息了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#spring事务管理日志
logging: 
  level: 
    org.springframework.jdbc.support.JdbcTransactionManager: debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们再次添加员工，看看控制台输出的日志信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./03.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加Spring事务管理后，由于服务端程序引发了异常，所以事务进行回滚。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./04.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开数据库，我们会看到 &lt;code&gt;emp&lt;/code&gt; 表 与 &lt;code&gt;emp_expr&lt;/code&gt; 表中都没有对应的数据信息，保证了数据的一致性、完整性。&lt;/p&gt;
&lt;h4&gt;1. 事务进阶&lt;/h4&gt;
&lt;p&gt;前面我们通过spring事务管理注解@Transactional已经控制了业务层方法的事务。接下来我们要来详细的介绍一下@Transactional事务管理注解的使用细节。我们这里主要介绍@Transactional注解当中的两个常见的属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;异常回滚的属性：&lt;code&gt;rollbackFor &lt;/code&gt;&lt;/li&gt;
&lt;li&gt;事务传播行为：&lt;code&gt;propagation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们先来学习下rollbackFor属性。&lt;/p&gt;
&lt;h5&gt;1. rollbackFor&lt;/h5&gt;
&lt;p&gt;我们在之前编写的业务方法上添加了@Transactional注解，来实现事务管理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Transactional
@Override
public void save(Emp emp) {
    //1.补全基础属性
    emp.setCreateTime(LocalDateTime.now());
    emp.setUpdateTime(LocalDateTime.now());
    //2.保存员工基本信息
    empMapper.insert(emp);
        
    int i = 1/0;
        
    //3. 保存员工的工作经历信息 - 批量
    Integer empId = emp.getId();
    List&amp;lt;EmpExpr&amp;gt; exprList = emp.getExprList();
    if(!CollectionUtils.isEmpty(exprList)){
        exprList.forEach(empExpr -&amp;gt; empExpr.setEmpId(empId));
        empExprMapper.insertBatch(exprList);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上业务功能save方法在运行时，会引发除0的算术运算异常(运行时异常)，出现异常之后，由于我们在方法上加了&lt;code&gt;@Transactional&lt;/code&gt;注解进行事务管理，所以发生异常会执行rollback回滚操作，从而保证事务操作前后数据是一致的。&lt;/p&gt;
&lt;p&gt;下面我们在做一个测试，我们修改业务功能代码，在模拟异常的位置上直接抛出Exception异常（编译时异常）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Transactional
@Override
public void save(Emp emp) {
    //1.补全基础属性
    emp.setCreateTime(LocalDateTime.now());
    emp.setUpdateTime(LocalDateTime.now());
    //2.保存员工基本信息
    empMapper.insert(emp);
        
    //模拟：异常发生
    if(true){
        throw new Exception(&quot;出现异常了~~~&quot;);
    }
        
    //3. 保存员工的工作经历信息 - 批量
    Integer empId = emp.getId();
    List&amp;lt;EmpExpr&amp;gt; exprList = emp.getExprList();
    if(!CollectionUtils.isEmpty(exprList)){
        exprList.forEach(empExpr -&amp;gt; empExpr.setEmpId(empId));
        empExprMapper.insertBatch(exprList);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;说明：在service中向上抛出一个Exception编译时异常之后，由于是controller调用service，所以在controller中要有异常处理代码，此时我们选择在controller中继续把异常向上抛。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;重新启动服务后，打开Apifox进行测试，请求添加员工的接口：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./05.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过Apifox返回的结果，我们看到抛出异常了。然后我们在回到IDEA的控制台来看一下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./06.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看到数据库的事务居然提交了，并没有进行回滚。&lt;/p&gt;
&lt;p&gt;通过以上测试可以得出一个结论：&lt;strong&gt;默认情况下，只有出现RuntimeException(运行时异常)才会回滚事务。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假如我们想让所有的异常都回滚，需要来配置@Transactional注解当中的rollbackFor属性，通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Transactional(rollbackFor = Exception.class)
@Override
public void save(Emp emp) throws Exception {
    //1.补全基础属性
    emp.setCreateTime(LocalDateTime.now());
    emp.setUpdateTime(LocalDateTime.now());
    //2.保存员工基本信息
    empMapper.insert(emp);
        
    //int i = 1/0;
    if(true){
        throw new Exception(&quot;出异常啦....&quot;);
    }
        
    //3. 保存员工的工作经历信息 - 批量
    Integer empId = emp.getId();
    List&amp;lt;EmpExpr&amp;gt; exprList = emp.getExprList();
    if(!CollectionUtils.isEmpty(exprList)){
        exprList.forEach(empExpr -&amp;gt; empExpr.setEmpId(empId));
        empExprMapper.insertBatch(exprList);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们重新启动服务，测试新增员工的操作：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./07.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;控制台日志，可以看到因为出现了异常又进行了事务回滚。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./08.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结论：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在Spring的事务管理中，默认只有运行时异常 RuntimeException才会回滚。&lt;/li&gt;
&lt;li&gt;如果还需要回滚指定类型的异常，可以通过rollbackFor属性来指定。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;1. propagation&lt;/h5&gt;
&lt;h6&gt;1. 介绍&lt;/h6&gt;
&lt;p&gt;我们接着继续学习@Transactional注解当中的第二个属性propagation，这个属性是用来配置事务的传播行为的。&lt;/p&gt;
&lt;p&gt;什么是事务的传播行为呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;就是当一个事务方法被另一个事务方法调用时，这个事务方法应该如何进行事务控制。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：两个事务方法，一个A方法，一个B方法。在这两个方法上都添加了@Transactional注解，就代表这两个方法都具有事务，而在A方法当中又去调用了B方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./09.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所谓事务的传播行为，指的就是在A方法运行的时候，首先会开启一个事务，在A方法当中又调用了B方法， B方法自身也具有事务，那么B方法在运行的时候，到底是加入到A方法的事务当中来，还是B方法在运行的时候新建一个事务？这个就涉及到了事务的传播行为。&lt;/p&gt;
&lt;p&gt;我们要想控制事务的传播行为，在@Transactional注解的后面指定一个属性propagation，通过 propagation 属性来指定传播行为。接下来我们就来介绍一下常见的事务传播行为。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性值&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;REQUIRED&lt;/td&gt;
&lt;td&gt;【默认值】需要事务，有则加入，无则创建新事务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REQUIRES_NEW&lt;/td&gt;
&lt;td&gt;需要新事务，无论有无，总是创建新事务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SUPPORTS&lt;/td&gt;
&lt;td&gt;支持事务，有则加入，无则在无事务状态中运行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NOT_SUPPORTED&lt;/td&gt;
&lt;td&gt;不支持事务，在无事务状态下运行,如果当前存在已有事务,则挂起当前事务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MANDATORY&lt;/td&gt;
&lt;td&gt;必须有事务，否则抛异常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NEVER&lt;/td&gt;
&lt;td&gt;必须没事务，否则抛异常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;对于这些事务传播行为，我们只需要关注以下两个就可以了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;REQUIRED（默认值）&lt;/li&gt;
&lt;li&gt;REQUIRES_NEW&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h6&gt;1. 案例&lt;/h6&gt;
&lt;p&gt;接下来我们就通过一个案例来演示下事务传播行为propagation属性的使用。&lt;/p&gt;
&lt;p&gt;**需求：**在新增员工信息时，无论是成功还是失败，都要记录操作日志。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;准备日志表 emp_log、实体类EmpLog、Mapper接口EmpLogMapper&lt;/li&gt;
&lt;li&gt;在新增员工时记录日志&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;准备工作：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1). 创建数据库表 &lt;code&gt;emp_log&lt;/code&gt; 日志表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建员工日志表
create table emp_log(
    id int unsigned primary key auto_increment comment &apos;ID, 主键&apos;,
    operate_time datetime comment &apos;操作时间&apos;,
    info varchar(2000) comment &apos;日志信息&apos;
) comment &apos;员工日志表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2). 引入资料中提供的实体类：EmpLog&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmpLog {
    private Integer id; //ID
    private LocalDateTime operateTime; //操作时间
    private String info; //详细信息
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3). 引入资料中提供的Mapper接口：EmpLogMapper&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Mapper
public interface EmpLogMapper {
        //插入日志
    @Insert(&quot;insert into emp_log (operate_time, info) values (#{operateTime}, #{info})&quot;)
    public void insert(EmpLog empLog);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4). 引入资料中提供的业务接口：EmpLogService&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface EmpLogService {
        //记录新增员工日志
    public void insertLog(EmpLog empLog);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5). 引入资料中提供的业务实现类：EmpLogServiceImpl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class EmpLogServiceImpl implements EmpLogService {

    @Autowired
    private EmpLogMapper empLogMapper;

    @Transactional
    @Override
    public void insertLog(EmpLog empLog) {
        empLogMapper.insert(empLog);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;代码实现:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;业务实现类：EmpServiceImpl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Autowired
private EmpMapper empMapper;
@Autowired
private EmpExprMapper empExprMapper;
@Autowired
private EmpLogService empLogService;

@Transactional(rollbackFor = {Exception.class})
@Override
public void save(Emp emp) {
    try {
        //1.补全基础属性
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());

        //2.保存员工基本信息
        empMapper.insert(emp);

        int i = 1/0;

        //3. 保存员工的工作经历信息 - 批量
        Integer empId = emp.getId();
        List&amp;lt;EmpExpr&amp;gt; exprList = emp.getExprList();
        if(!CollectionUtils.isEmpty(exprList)){
            exprList.forEach(empExpr -&amp;gt; empExpr.setEmpId(empId));
            empExprMapper.insertBatch(exprList);
        }
    } finally {
        //记录操作日志
        EmpLog empLog = new EmpLog(null, LocalDateTime.now(), emp.toString());
        empLogService.insertLog(empLog);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;重新启动SpringBoot服务，测试新增员工操作 。我们可以看到控制台中输出的日志：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./10.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从日志中我们可以看到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行了插入员工数据的操作&lt;/li&gt;
&lt;li&gt;执行了插入日志操作&lt;/li&gt;
&lt;li&gt;程序发生Exception异常&lt;/li&gt;
&lt;li&gt;执行事务回滚（保存员工数据、插入操作日志 因为在一个事务范围内，两个操作都会被回滚）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后在 &lt;code&gt;emp_log&lt;/code&gt; 表中没有记录日志数据 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因分析:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;接下来我们就需要来分析一下具体是什么原因导致的日志没有成功的记录。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在执行 &lt;code&gt;save&lt;/code&gt; 方法时开启了一个事务&lt;/li&gt;
&lt;li&gt;当执行 &lt;code&gt;empLogService.insertLog&lt;/code&gt; 操作时，&lt;code&gt;insertLog&lt;/code&gt;设置的事务传播行是默认值REQUIRED，表示有事务就加入，没有则新建事务&lt;/li&gt;
&lt;li&gt;此时：&lt;code&gt;save&lt;/code&gt; 和 &lt;code&gt;insertLog&lt;/code&gt; 操作使用了同一个事务，同一个事务中的多个操作，要么同时成功，要么同时失败，所以当异常发生时进行事务回滚，就会回滚 &lt;code&gt;save&lt;/code&gt; 和  &lt;code&gt;insertLog&lt;/code&gt; 操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决方案：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;EmpLogServiceImpl&lt;/code&gt;类中insertLog方法上，添加 &lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Propagation.REQUIRES_NEW  ：不论是否有事务，都创建新事务  ，运行在一个独立的事务中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class EmpLogServiceImpl implements EmpLogService {

    @Autowired
    private EmpLogMapper empLogMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public void insertLog(EmpLog empLog) {
        empLogMapper.insert(empLog);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启SpringBoot服务，再次测试 新增员工的操作 ，会看到具体的日志如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./11.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那此时，&lt;code&gt;EmpServiceImpl&lt;/code&gt; 中的 &lt;code&gt;save&lt;/code&gt; 方法运行时，会开启一个事务。 当调用  &lt;code&gt;empLogService.insertLog(empLog)&lt;/code&gt;  时，也会创建一个新的事务，那此时，当 &lt;code&gt;insertLog&lt;/code&gt; 方法运行完毕之后，事务就已经提交了。 即使外部的事务出现异常，内部已经提交的事务，也不会回滚了，因为是两个独立的事务。&lt;/p&gt;
&lt;p&gt;到此事务传播行为已演示完成，事务的传播行为我们只需要掌握两个：REQUIRED、REQUIRES_NEW。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**REQUIRED：**大部分情况下都是用该传播行为即可。&lt;/li&gt;
&lt;li&gt;**REQUIRES_NEW：**当我们不希望事务之间相互影响时，可以使用该传播行为。比如：下订单前需要记录日志，不论订单保存成功与否，都需要保证日志记录能够记录成功。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 事务四大特性&lt;/h3&gt;
&lt;p&gt;面试题：事务有哪些特性？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原子性（Atomicity）：事务是不可分割的最小单元，要么全部成功，要么全部失败。&lt;/li&gt;
&lt;li&gt;一致性（Consistency）：事务完成时，必须使所有的数据都保持一致状态。&lt;/li&gt;
&lt;li&gt;隔离性（Isolation）：数据库系统提供的隔离机制，保证事务在不受外部并发操作影响的独立环境下运行。&lt;/li&gt;
&lt;li&gt;持久性（Durability）：事务一旦提交或回滚，它对数据库中的数据的改变就是永久的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事务的四大特性简称为：ACID&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./12.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原子性（Atomicity）&lt;/strong&gt; ：原子性是指事务包装的一组sql是一个不可分割的工作单元，事务中的操作要么全部成功，要么全部失败。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性（Consistency）&lt;/strong&gt;：一个事务完成之后数据都必须处于一致性状态。
&lt;ul&gt;
&lt;li&gt;如果事务成功的完成，那么数据库的所有变化将生效。&lt;/li&gt;
&lt;li&gt;如果事务执行出现错误，那么数据库的所有变化将会被回滚(撤销)，返回到原始状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隔离性（Isolation）&lt;/strong&gt;：多个用户并发的访问数据库时，一个用户的事务不能被其他用户的事务干扰，多个并发的事务之间要相互隔离。
&lt;ul&gt;
&lt;li&gt;一个事务的成功或者失败对于其他的事务是没有影响。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久性（Durability）&lt;/strong&gt;：一个事务一旦被提交或回滚，它对数据库的改变将是永久性的，哪怕数据库发生异常，重启之后数据亦然存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;事务管理解决的是业务操作和数据库状态一致的问题。学习 Spring 事务时，除了会用 &lt;code&gt;@Transactional&lt;/code&gt;，还要理解异常、回滚和 ACID 特性之间的关系。&lt;/p&gt;
</content:encoded></item><item><title>编程基础：从输入输出到算法入门</title><link>https://youki.bbroot.com/posts/cs/competition-basics/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/cs/competition-basics/</guid><description>编程入门必备知识：输入输出、常用函数、变量命名惯例、链表基础、踩坑记录</description><pubDate>Thu, 12 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文整理编程学习过程中的基础知识和实用技巧，涵盖输入输出、常用函数、数据结构、惯例以及踩坑记录。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;1. 基础语法要点&lt;/h2&gt;
&lt;h3&gt;1.1 输入输出&lt;/h3&gt;
&lt;h4&gt;基本输入输出&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using namespace std;

int main() {
    int a, b;
    cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b;        // 读取输入
    cout &amp;lt;&amp;lt; a + b &amp;lt;&amp;lt; endl; // 输出结果
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;读取整行输入&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;getline&lt;/code&gt; 可以正确处理包含空格的字符串输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string line;
getline(cin, line);  // 读取整行，包括空格
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;输入流写进判断条件&lt;/h4&gt;
&lt;p&gt;持续读取直到输入结束：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a;
while (cin &amp;gt;&amp;gt; a) {
    // 处理每个输入的值
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 常用函数&lt;/h3&gt;
&lt;h4&gt;swap - 交换变量&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;int a = 1, b = 2;
swap(a, b);  // 交换后 a=2, b=1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;sort - 排序函数&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;sort&lt;/code&gt; 是 C++ 标准库 &lt;code&gt;&amp;lt;algorithm&amp;gt;&lt;/code&gt; 中的排序函数，平均时间复杂度 O(n log n)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;algorithm&amp;gt;

int arr[] = {5, 2, 9, 1};

// 默认升序排序
sort(arr, arr + 4);  // 结果：1, 2, 5, 9

// 降序排序
sort(arr, arr + 4, greater&amp;lt;int&amp;gt;());  // 结果：9, 5, 2, 1

// 对 vector 排序
vector&amp;lt;int&amp;gt; v = {5, 2, 9, 1};
sort(v.begin(), v.end());

// 自定义排序规则
bool cmp(int a, int b) { return a &amp;gt; b; }
sort(arr, arr + 4, cmp);  // 降序
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;参数说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个参数：排序起始位置的指针/迭代器&lt;/li&gt;
&lt;li&gt;第二个参数：排序结束位置的后一位指针/迭代器（左闭右开区间 &lt;code&gt;[begin, end)&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;max - 取最大值&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;int res = 0;
res = max(res, nums[i]);  // 更新最大值
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;pow - 幂运算&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;double result = pow(2, 3);  // 计算 2^3 = 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;strlen - 字符串长度&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;char t[] = &quot;hello&quot;;
int len = strlen(t + 1);  // 从 t[1] 开始计算长度
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3 变量与数据结构&lt;/h3&gt;
&lt;h4&gt;vector 初始化&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; nums(5);  // 初始化大小为5的向量，元素为 {0, 0, 0, 0, 0}
vector&amp;lt;int&amp;gt; v = {1, 2, 3};  // 列表初始化
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;结构体 vs map 的选择&lt;/h4&gt;
&lt;p&gt;根据题目特点选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;结构体&lt;/strong&gt;：适合需要存储多个相关字段的情况&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;map&lt;/strong&gt;：适合需要按键值快速查找的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;string 数组的限制&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 注意：大量 string 数组可能导致编译器溢出
string arr[100000];  // 可能有问题，考虑使用 vector&amp;lt;string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 常用技巧&lt;/h2&gt;
&lt;h3&gt;2.1 变量命名惯例&lt;/h3&gt;
&lt;p&gt;竞赛中常用的缩写命名：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;缩写&lt;/th&gt;
&lt;th&gt;全称&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;visited&lt;/td&gt;
&lt;td&gt;标记节点/状态是否已访问，用于图遍历（DFS/BFS）、动态规划、回溯算法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ind&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;index&lt;/td&gt;
&lt;td&gt;表示数组、列表中的索引位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tmp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;temporary&lt;/td&gt;
&lt;td&gt;临时变量，暂存中间值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ans&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;answer&lt;/td&gt;
&lt;td&gt;存储最终结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;res&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;result&lt;/td&gt;
&lt;td&gt;存储计算结果&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2.2 实用技巧&lt;/h3&gt;
&lt;h4&gt;单个 &amp;amp; 取余&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;int x = 7;
int result = x &amp;amp; 1;  // 取余操作，判断奇偶
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;#define int long long 与 signed main()&lt;/h4&gt;
&lt;p&gt;竞赛中为了避免整数溢出，常用宏定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define int long long  // 把所有 int 替换成 long long

// 但 main() 必须返回 int，所以用 signed 代替
signed main() {
    // signed 不会被宏替换，符合 main() 返回类型要求
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;动态内存管理&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// C++ 方式
int *p = new int;       // 分配 1 个 int
int *arr = new int[10]; // 分配数组

delete p;        // 释放单个
delete[] arr;    // 释放数组
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;强制类型转换&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;double d = 3.14;
int i = (int)d;  // C 风格强制转换
int j = static_cast&amp;lt;int&amp;gt;(d);  // C++ 风格，更安全
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 数据结构基础&lt;/h2&gt;
&lt;h3&gt;3.1 链表&lt;/h3&gt;
&lt;h4&gt;链表结构定义&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;struct Node {
    int val;
    Node *next;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;now-&amp;gt;next 的含义&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;now&lt;/code&gt; 是一个 &lt;code&gt;Node*&lt;/code&gt; 类型指针，指向链表中的某个节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next&lt;/code&gt; 是 &lt;code&gt;Node&lt;/code&gt; 结构体的成员，类型是 &lt;code&gt;Node*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;now-&amp;gt;next&lt;/code&gt; 指向：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;下一个节点&lt;/strong&gt;（如果存在）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;NULL&lt;/code&gt;&lt;/strong&gt;（如果当前节点是链表的最后一个节点）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;链表基本操作示例&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

struct Node {
    int val;
    Node *next;
};

int main() {
    int n;
    cin &amp;gt;&amp;gt; n;
    
    Node Head;
    Node* now = &amp;amp;Head;
    
    // 创建链表
    int tmp;
    for (int i = 0; i &amp;lt; n; i++) {
        cin &amp;gt;&amp;gt; tmp;
        now-&amp;gt;next = new Node;
        now = now-&amp;gt;next;
        now-&amp;gt;val = tmp;
    }
    
    // 删除指定位置的节点
    int m;
    cin &amp;gt;&amp;gt; m;
    while (m--) {
        now = &amp;amp;Head;
        cin &amp;gt;&amp;gt; tmp;
        for (int i = 0; i &amp;lt; tmp - 1; i++) {
            now = now-&amp;gt;next;
        }
        now-&amp;gt;next = now-&amp;gt;next-&amp;gt;next;
    }
    
    // 遍历输出
    now = &amp;amp;Head;
    while (now-&amp;gt;next != NULL) {
        now = now-&amp;gt;next;
        cout &amp;lt;&amp;lt; now-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;
    }
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 踩坑记录（经验总结）&lt;/h2&gt;
&lt;h3&gt;4.1 函数返回值必须写 return 0&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：函数在某些条件下没有返回值，这是&lt;strong&gt;未定义行为(UB)&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int fun(int x) {
    if (x % 7 == 0) return 1;
    // 错误：当 x 不含 7 时没有返回值！
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;后果&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地编译器可能默认返回 0（巧合正确）&lt;/li&gt;
&lt;li&gt;在线评测系统可能陷入死循环或返回随机值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：一定要写 &lt;code&gt;return 0;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;4.2 注意题目数据类型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;仔细阅读题目要求的数据类型范围&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;long long&lt;/code&gt; 避免整数溢出&lt;/li&gt;
&lt;li&gt;注意浮点数精度问题&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 数组搜索超时问题&lt;/h3&gt;
&lt;p&gt;数组搜索如果使用朴素算法，可能因为超时无法通过。考虑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用更高效的算法（如二分查找）&lt;/li&gt;
&lt;li&gt;使用合适的数据结构（如 set、map）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.4 格式化输出 %.10lg&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;%.10lg&lt;/code&gt; 是 &lt;code&gt;printf&lt;/code&gt; 中的格式化控制符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.10&lt;/code&gt;：最大保留 10 位有效数字（不是小数点后 10 位）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;l&lt;/code&gt;：参数是 &lt;code&gt;double&lt;/code&gt; 类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;g&lt;/code&gt;：智能选择输出格式
&lt;ul&gt;
&lt;li&gt;指数小于 -4 或大于等于精度时，使用科学计数法&lt;/li&gt;
&lt;li&gt;否则使用常规小数格式&lt;/li&gt;
&lt;li&gt;自动删除末尾无效的零&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.5 scanf 安全警告 (Error C4996)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：微软编译器中 &lt;code&gt;scanf&lt;/code&gt; 被标记为不安全。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;scanf_s&lt;/code&gt;（微软特有）&lt;/li&gt;
&lt;li&gt;或者在文件开头添加：&lt;code&gt;#pragma warning(disable:4996)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;或者使用 &lt;code&gt;cin&lt;/code&gt; 代替&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 完整代码示例&lt;/h2&gt;
&lt;h3&gt;5.1 sort 排序找第 m 大数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

const int N = 1e3 + 7;  // 定义数组最大长度
int n, m, a[N];         // 全局数组，默认初始化为 0

int main() {
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m;       // 输入元素个数 n 和要找的第 m 大数
    
    for (int i = 1; i &amp;lt;= n; i++)
        cin &amp;gt;&amp;gt; a[i];     // 输入数据存入 a[1]~a[n]
    
    sort(a + 1, a + 1 + n);  // 对 a[1]~a[n] 排序
    
    cout &amp;lt;&amp;lt; a[n - m + 1] &amp;lt;&amp;lt; endl;  // 输出第 m 大的数
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;排序后数组是升序，&lt;code&gt;a[n]&lt;/code&gt; 是最大值&lt;/li&gt;
&lt;li&gt;第 &lt;code&gt;m&lt;/code&gt; 大的数位于 &lt;code&gt;a[n - m + 1]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 自定义排序规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

// 自定义比较函数：降序
bool cmp(int a, int b) {
    return a &amp;gt; b;
}

int main() {
    int arr[] = {5, 2, 9, 1};
    sort(arr, arr + 4, cmp);  // 降序排序
    
    for (int i = 0; i &amp;lt; 4; i++)
        cout &amp;lt;&amp;lt; arr[i] &amp;lt;&amp;lt; &quot; &quot;;  // 输出：9 5 2 1
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 总结&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;学习路径建议&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先掌握基础语法和输入输出&lt;/li&gt;
&lt;li&gt;熟悉常用函数（sort、swap、max 等）&lt;/li&gt;
&lt;li&gt;了解竞赛中的命名惯例和实用技巧&lt;/li&gt;
&lt;li&gt;学习基础数据结构（数组、链表、vector）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;常见错误清单&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 函数必须有返回值&lt;/li&gt;
&lt;li&gt;[ ] 注意数据类型范围&lt;/li&gt;
&lt;li&gt;[ ] 排序区间是左闭右开 &lt;code&gt;[begin, end)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] 链表操作注意空指针&lt;/li&gt;
&lt;li&gt;[ ] 大数组考虑使用 vector 动态分配&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>常用算法笔记：BFS、贪心、前缀和与差分</title><link>https://youki.bbroot.com/posts/cs/bfs-ai-optimized/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/cs/bfs-ai-optimized/</guid><description>BFS 广度优先搜索、贪心算法、前缀和与差分、字典序最小序列构造</description><pubDate>Thu, 05 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. BFS 广度优先搜索&lt;/h2&gt;
&lt;h3&gt;1.1 核心思想&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;层级遍历&lt;/strong&gt;：从起点逐层向外扩展，使用队列（FIFO）保证访问顺序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：无权图最短路径、连通性检测、迷宫问题&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 算法步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;将起始节点放入队列，并标记为已访问&lt;/li&gt;
&lt;li&gt;从队列中取出一个节点，访问其所有未被访问的相邻节点，并将这些节点加入队列&lt;/li&gt;
&lt;li&gt;重复步骤 2，直到队列为空&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.3 C++ 模板代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;vector&amp;gt;
using namespace std;

void BFS(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; graph, int start) {
    vector&amp;lt;bool&amp;gt; visited(graph.size(), false);
    queue&amp;lt;int&amp;gt; q;
    q.push(start);
    visited[start] = true;

    while (!q.empty()) {
        int node = q.front();
        q.pop();
        cout &amp;lt;&amp;lt; node &amp;lt;&amp;lt; &quot; &quot;; // 处理节点
        for (int neighbor : graph[node]) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                q.push(neighbor);
            }
        }
    }
}

// 示例：邻接表图遍历
int main() {
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; graph = {{1,2}, {0,3}, {0,3,4}, {1,2,4}, {2,3}};
    cout &amp;lt;&amp;lt; &quot;BFS顺序：&quot;;
    BFS(graph, 0); // 输出：0 1 2 3 4
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.4 BFS 最短路径（带路径记录）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
using namespace std;

vector&amp;lt;string&amp;gt; shortest_path(unordered_map&amp;lt;string, vector&amp;lt;string&amp;gt;&amp;gt;&amp;amp; graph, 
                           string start, string end) {
    queue&amp;lt;string&amp;gt; q;
    unordered_map&amp;lt;string, bool&amp;gt; visited;
    unordered_map&amp;lt;string, string&amp;gt; parent;  // 记录路径父节点
    
    q.push(start);
    visited[start] = true;
    parent[start] = &quot;&quot;;

    while (!q.empty()) {
        string cur = q.front();
        q.pop();
        if (cur == end) {
            // 反向回溯路径
            vector&amp;lt;string&amp;gt; path;
            for (string at = end; at != &quot;&quot;; at = parent[at]) 
                path.push_back(at);
            reverse(path.begin(), path.end());
            return path;
        }
        for (string neighbor : graph[cur]) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                parent[neighbor] = cur;  // 记录父节点
                q.push(neighbor);
            }
        }
    }
    return {}; // 无路径
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.5 复杂度分析&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时间复杂度&lt;/strong&gt;：O(V + E)，其中 V 是节点数，E 是边数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空间复杂度&lt;/strong&gt;：O(V)，最坏情况下需要存储所有节点&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.6 与 DFS 的区别&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;BFS&lt;/th&gt;
&lt;th&gt;DFS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;遍历方式&lt;/td&gt;
&lt;td&gt;按层级&lt;/td&gt;
&lt;td&gt;按深度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据结构&lt;/td&gt;
&lt;td&gt;队列&lt;/td&gt;
&lt;td&gt;栈/递归&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最短路径&lt;/td&gt;
&lt;td&gt;✅ 无权图最优&lt;/td&gt;
&lt;td&gt;❌ 不保证&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空间占用&lt;/td&gt;
&lt;td&gt;较多（队列）&lt;/td&gt;
&lt;td&gt;较少（递归栈）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适用场景&lt;/td&gt;
&lt;td&gt;最短路径、层级遍历&lt;/td&gt;
&lt;td&gt;路径存在性、回溯&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 贪心算法&lt;/h2&gt;
&lt;h3&gt;2.1 核心思想&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;局部最优&lt;/strong&gt;：每一步选择当前最优解，无后效性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用条件&lt;/strong&gt;：问题需满足&lt;strong&gt;贪心选择性&lt;/strong&gt;和&lt;strong&gt;最优子结构&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 C++ 模板与示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;vector&amp;gt;
using namespace std;

// 活动选择问题（最早结束优先）
struct Activity { int start, end; };
bool compare(Activity a, Activity b) { return a.end &amp;lt; b.end; }

int maxActivities(vector&amp;lt;Activity&amp;gt;&amp;amp; acts) {
    sort(acts.begin(), acts.end(), compare);
    int count = 1, lastEnd = acts[0].end;
    for (int i = 1; i &amp;lt; acts.size(); i++) {
        if (acts[i].start &amp;gt;= lastEnd) {
            count++;
            lastEnd = acts[i].end;
        }
    }
    return count;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 典型应用场景&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;策略&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;活动安排&lt;/td&gt;
&lt;td&gt;按结束时间排序&lt;/td&gt;
&lt;td&gt;选择相容活动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Huffman 编码&lt;/td&gt;
&lt;td&gt;合并频率最低节点&lt;/td&gt;
&lt;td&gt;需优先队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最小硬币找零&lt;/td&gt;
&lt;td&gt;面值从大到小遍历&lt;/td&gt;
&lt;td&gt;仅适用特定面值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2.4 贪心的局限性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;反例&lt;/strong&gt;：硬币面值 {1, 3, 4}，找零 6&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;贪心解：{4, 1, 1}（3 枚）&lt;/li&gt;
&lt;li&gt;最优解：{3, 3}（2 枚）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：贪心不一定全局最优，需数学证明贪心策略的正确性。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 前缀和与差分&lt;/h2&gt;
&lt;h3&gt;3.1 一维前缀和&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;用途&lt;/strong&gt;：快速计算区间和，预处理 O(n)，查询 O(1)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; prefix(n + 1, 0);
for (int i = 1; i &amp;lt;= n; i++) 
    prefix[i] = prefix[i-1] + arr[i-1];

// 查询 [L, R] 和：sum = prefix[R] - prefix[L-1];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;应用场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;区间求和问题&lt;/li&gt;
&lt;li&gt;子数组和等于 k&lt;/li&gt;
&lt;li&gt;结合哈希表优化&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 二维前缀和&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;用途&lt;/strong&gt;：子矩阵求和（如 LeetCode 304）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 预处理
vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; sum(n+1, vector&amp;lt;int&amp;gt;(m+1, 0));
for (int i = 1; i &amp;lt;= n; i++)
    for (int j = 1; j &amp;lt;= m; j++)
        sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + matrix[i-1][j-1];

// 查询 (x1,y1) 到 (x2,y2) 和：
int area = sum[x2][y2] - sum[x1-1][y2] - sum[x2][y1-1] + sum[x1-1][y1-1];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 差分&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;用途&lt;/strong&gt;：高效处理区间增减（如批量加减 k）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; diff(n + 2, 0); // 多开空间防越界

void add(int l, int r, int k) {
    diff[l] += k;
    diff[r+1] -= k;
}

// 还原数组
for (int i = 1; i &amp;lt;= n; i++) 
    arr[i] = arr[i-1] + diff[i];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4 前缀和与差分的关系&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;前缀和&lt;/th&gt;
&lt;th&gt;差分&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;主要用途&lt;/td&gt;
&lt;td&gt;快速查询&lt;/td&gt;
&lt;td&gt;快速修改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;时间复杂度&lt;/td&gt;
&lt;td&gt;O(1) 查询&lt;/td&gt;
&lt;td&gt;O(1) 修改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;逆操作&lt;/td&gt;
&lt;td&gt;差分的前缀和 = 原数组&lt;/td&gt;
&lt;td&gt;前缀和的差分 = 原数组&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;结合使用&lt;/strong&gt;：先差分批量更新，再前缀和得到最终数组。&lt;/p&gt;
&lt;h3&gt;3.5 典型例题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前缀和&lt;/strong&gt;：区间和、子数组和等于 k、LeetCode 304&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;差分&lt;/strong&gt;：区间覆盖统计（MC0351）、航班预订统计（LeetCode 1109）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 字典序最小序列&lt;/h2&gt;
&lt;h3&gt;4.1 定义&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;字典序&lt;/strong&gt;：从左到右逐字符比较，首个不同位字符小的序列更小。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;apple&quot; &amp;lt; &quot;apricot&quot;（第三个字符 &apos;p&apos; &amp;lt; &apos;r&apos;）&lt;/li&gt;
&lt;li&gt;[1, 2, 3] &amp;lt; [1, 3, 2]（第二个元素 2 &amp;lt; 3）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 贪心构造&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：每一步选当前可用的最小字符，确保后续合法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 拼接字符串使字典序最小
sort(strs.begin(), strs.end(), [](string a, string b) {
    return a + b &amp;lt; b + a;
});
string ans;
for (string s : strs) ans += s;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. C++ Queue 操作&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;push()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;队尾插入元素&lt;/td&gt;
&lt;td&gt;&lt;code&gt;q.push(10);&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pop()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除队首元素&lt;/td&gt;
&lt;td&gt;&lt;code&gt;q.pop();&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;front()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;访问队首元素&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int x = q.front();&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;back()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;访问队尾元素&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int y = q.back();&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;empty()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检测队列是否为空&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if (q.empty()) { ... }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;size()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回队列元素数量&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int len = q.size();&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;注意事项&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;front()&lt;/code&gt; 和 &lt;code&gt;pop()&lt;/code&gt; 前需检查队列非空，否则行为未定义&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vector&lt;/code&gt; 不能作为底层容器（缺少 &lt;code&gt;pop_front()&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Logback 日志框架入门笔记</title><link>https://youki.bbroot.com/posts/java/logback/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/java/logback/</guid><description>整理为什么需要日志框架、Logback 基础配置、日志级别和配置文件写法。</description><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记整理 Java 项目中使用日志框架的基本原因和 Logback 的入门配置。当前主要保留课堂示例，后续可以补充生产环境日志切分、日志级别策略和问题排查案例。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;System.out.println&lt;/code&gt; 不适合长期承担项目日志职责。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;日志框架可以控制输出级别、输出位置和格式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Logback 常通过 &lt;code&gt;logback.xml&lt;/code&gt; 配置 appender、encoder 和 root level。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;合理的日志级别有助于调试、监控和后续维护。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;之前我们编写程序时，也可以通过 &lt;code&gt;System.out.println(...) &lt;/code&gt;来输出日志，为什么我们还要学习单独的日志技术呢？&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​    这是因为，如果通过  &lt;code&gt;System.out.println(...) &lt;/code&gt; 来记录日志，会存在以下几点问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;硬编码。所有的记录日志的代码，都是硬编码，没有办法做到灵活控制，要想不输出这个日志了，只能删除掉记录日志的代码。&lt;/li&gt;
&lt;li&gt;只能输出日志到控制台。&lt;/li&gt;
&lt;li&gt;不便于程序的扩展、维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，在现在的项目开发中，我们一般都会使用专业的日志框架，来解决这些问题。&lt;/p&gt;
&lt;h3&gt;1. 日志框架&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./01.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1. Logback入门&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1). 准备工作：引入logback的依赖（springboot中无需引入，在springboot中已经传递了此依赖）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2). 引入配置文件&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;logback.xml&lt;/code&gt;&lt;/strong&gt;  &lt;strong&gt;（资料中已经提供，拷贝进来，放在&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;src/main/resources&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;目录下； 或者直接AI生成）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;!-- 控制台输出 --&amp;gt;
    &amp;lt;appender name=&quot;STDOUT&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
        &amp;lt;encoder class=&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;&amp;gt;
            &amp;lt;!--格式化输出：%d表示日期，%thread表示线程名，%-5level：级别从左显示5个字符宽度  %msg：日志消息，%n是换行符 --&amp;gt;
            &amp;lt;pattern&amp;gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n&amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;!-- 日志输出级别 --&amp;gt;
    &amp;lt;root level=&quot;ALL&quot;&amp;gt;
        &amp;lt;appender-ref ref=&quot;STDOUT&quot; /&amp;gt;
    &amp;lt;/root&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3). 记录日志：定义日志记录对象Logger，记录日志&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LogTest {
    
    //定义日志记录对象
    private static final Logger log = LoggerFactory.getLogger(LogTest.class);

    @Test
    public void testLog(){
        log.debug(&quot;开始计算...&quot;);
        int sum = 0;
        int[] nums = {1, 5, 3, 2, 1, 4, 5, 4, 6, 7, 4, 34, 2, 23};
        for (int i = 0; i &amp;lt; nums.length; i++) {
            sum += nums[i];
        }
        log.info(&quot;计算结果为: &quot;+sum);
        log.debug(&quot;结束计算...&quot;);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行单元测试，可以在控制台中看到输出的日志，如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./02.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们可以看到在输出的日志信息中，不仅输出了日志的信息，还包括：日志的输出时间、线程名、具体在那个类中输出的。&lt;/p&gt;
&lt;h3&gt;1. Logback配置文件&lt;/h3&gt;
&lt;p&gt;Logback日志框架的配置文件叫 &lt;code&gt;logback.xml&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;该配置文件是对Logback日志框架输出的日志进行控制的，可以来配置输出的格式、位置及日志开关等。&lt;/p&gt;
&lt;p&gt;常用的两种输出日志的位置：控制台、系统文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1). 如果需要输出日志到控制台。添加如下配置：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 控制台输出 --&amp;gt;
&amp;lt;appender name=&quot;STDOUT&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
    &amp;lt;encoder class=&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;&amp;gt;
            &amp;lt;!--格式化输出：%d 表示日期，%thread 表示线程名，%-5level表示级别从左显示5个字符宽度，%msg表示日志消息，%n表示换行符 --&amp;gt;
            &amp;lt;pattern&amp;gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n&amp;lt;/pattern&amp;gt;
    &amp;lt;/encoder&amp;gt;
&amp;lt;/appender&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2). 如果需要输出日志到文件。添加如下配置：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 按照每天生成日志文件 --&amp;gt;
&amp;lt;appender name=&quot;FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&amp;gt;
    &amp;lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&quot;&amp;gt;
        &amp;lt;!-- 日志文件输出的文件名, %i表示序号 --&amp;gt;
        &amp;lt;FileNamePattern&amp;gt;D:/tlias-%d{yyyy-MM-dd}-%i.log&amp;lt;/FileNamePattern&amp;gt;
        &amp;lt;!-- 最多保留的历史日志文件数量 --&amp;gt;
        &amp;lt;MaxHistory&amp;gt;30&amp;lt;/MaxHistory&amp;gt;
        &amp;lt;!-- 最大文件大小，超过这个大小会触发滚动到新文件，默认为 10MB --&amp;gt;
        &amp;lt;maxFileSize&amp;gt;10MB&amp;lt;/maxFileSize&amp;gt;
    &amp;lt;/rollingPolicy&amp;gt;

    &amp;lt;encoder class=&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;&amp;gt;
        &amp;lt;!--格式化输出：%d 表示日期，%thread 表示线程名，%-5level表示级别从左显示5个字符宽度，%msg表示日志消息，%n表示换行符 --&amp;gt;
        &amp;lt;pattern&amp;gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n&amp;lt;/pattern&amp;gt;
    &amp;lt;/encoder&amp;gt;
&amp;lt;/appender&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3). 日志开关配置 （开启日志（ALL），取消日志（OFF））&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 日志输出级别 --&amp;gt;
&amp;lt;root level=&quot;ALL&quot;&amp;gt;
    &amp;lt;!--输出到控制台--&amp;gt;
    &amp;lt;appender-ref ref=&quot;STDOUT&quot; /&amp;gt;
    &amp;lt;!--输出到文件--&amp;gt;
    &amp;lt;appender-ref ref=&quot;FILE&quot; /&amp;gt;
&amp;lt;/root&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. Logback日志级别&lt;/h3&gt;
&lt;p&gt;日志级别指的是日志信息的类型，日志都会分级别，常见的日志级别如下（优先级由低到高）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;日志级别&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;记录方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;trace&lt;/td&gt;
&lt;td&gt;追踪，记录程序运行轨迹 【使用很少】&lt;/td&gt;
&lt;td&gt;log.trace(&quot;...&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;debug&lt;/td&gt;
&lt;td&gt;调试，记录程序调试过程中的信息，实际应用中一般将其视为最低级别 【使用较多】&lt;/td&gt;
&lt;td&gt;log.debug(&quot;...&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;info&lt;/td&gt;
&lt;td&gt;记录一般信息，描述程序运行的关键事件，如：网络连接、io操作 【使用较多】&lt;/td&gt;
&lt;td&gt;log.info(&quot;...&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warn&lt;/td&gt;
&lt;td&gt;警告信息，记录潜在有害的情况 【使用较多】&lt;/td&gt;
&lt;td&gt;log.warn(&quot;...&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;error&lt;/td&gt;
&lt;td&gt;错误信息 【使用较多】&lt;/td&gt;
&lt;td&gt;log.error(&quot;...&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可以在配置文件&lt;code&gt;logback.xml&lt;/code&gt;中，灵活的控制输出那些类型的日志。（大于等于配置的日志级别的日志才会输出）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 日志输出级别 --&amp;gt;
&amp;lt;root level=&quot;info&quot;&amp;gt;
    &amp;lt;!--输出到控制台--&amp;gt;
    &amp;lt;appender-ref ref=&quot;STDOUT&quot; /&amp;gt;
    &amp;lt;!--输出到文件--&amp;gt;
    &amp;lt;appender-ref ref=&quot;FILE&quot; /&amp;gt;
&amp;lt;/root&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;日志框架解决的是可控制、可扩展、可排查的问题。掌握 Logback 的配置结构和日志级别后，再结合真实项目决定哪些信息应该输出到控制台、文件或监控系统。&lt;/p&gt;
</content:encoded></item><item><title>数据库多表关系与 MyBatis 查询笔记</title><link>https://youki.bbroot.com/posts/java/mysql-relations/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/java/mysql-relations/</guid><description>整理一对多、多对多、外键约束、逻辑外键和 MyBatis 多表查询。</description><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记整理数据库表关系和后端查询实现，重点是理解外键约束、逻辑外键以及多表查询在业务代码中的落地方式。公开前建议再复核示例字段是否需要脱敏。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;外键约束用于建立表之间的数据关系，保证一致性和完整性。&lt;/li&gt;
&lt;li&gt;实际项目中常见物理外键和逻辑外键两种取舍。&lt;/li&gt;
&lt;li&gt;一对多、多对多关系会影响表结构和查询方式。&lt;/li&gt;
&lt;li&gt;MyBatis 多表查询需要同时考虑 SQL、实体对象和结果映射。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一对多&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;外键约束：让两张表的数据建立连接，保证数据的一致性和完整性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对应的关键字：foreign key&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;外键约束的语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 创建表时指定
create table 表名(
        字段名    数据类型,
        ...
        [constraint]   [外键名称]  foreign  key (外键字段名)   references   主表 (主表列名)        
);


-- 建完表后，添加外键
alter table  表名  add constraint  外键名称  foreign key(外键字段名) references 主表(主表列名);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方式1：通过SQL语句操作&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 修改表： 添加外键约束
alter table emp  add  constraint  fk_dept_id  foreign key (dept_id)  references  dept(id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方式2：图形化界面操作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在左侧菜单栏，在emp表上右键，选择 &lt;code&gt;modify Table... (old UI)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./01.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当我们添加了外键之后，再删除ID为3的部门，就会发现，此时数据库报错了，不允许删除。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./02.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;外键约束（foreign key）：保证了数据的完整性和一致性。&lt;/p&gt;
&lt;h4&gt;1. 物理外键与逻辑外键&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;物理外键&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;概念：使用foreign key定义外键关联另外一张表。&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ul&gt;
&lt;li&gt;影响增、删、改的效率（需要检查外键关系）。&lt;/li&gt;
&lt;li&gt;仅用于单节点数据库，不适用于分布式、集群场景。&lt;/li&gt;
&lt;li&gt;容易引发数据库的死锁问题，消耗性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逻辑外键&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;概念：在业务层逻辑中，解决外键关联。&lt;/li&gt;
&lt;li&gt;通过逻辑外键，就可以很方便的解决上述问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在现在的企业开发中，很少会使用物理外键，都是使用逻辑外键。 甚至在一些数据库开发规范中，会明确指出禁止使用物理外键 foreign key&lt;/p&gt;
&lt;h3&gt;1. 一对一&lt;/h3&gt;
&lt;p&gt;其实一对一我们可以看成一种特殊的一对多。一对多我们是怎么设计表关系的？是不是在多的一方添加外键。同样我们也可以通过外键来体现一对一之间的关系，我们只需要在任意一方来添加一个外键就可以了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一对一 ：在任意一方加入外键，关联另外一方的主键，并且设置外键为唯一的(UNIQUE)&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2. 多对多&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;实现关系：建立第三张中间表，中间表至少包含两个外键，分别关联两方主键&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SQL脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 学生表
create table tb_student(
    id int auto_increment primary key comment &apos;主键ID&apos;,
    name varchar(10) comment &apos;姓名&apos;,
    no varchar(10) comment &apos;学号&apos;
) comment &apos;学生表&apos;;
-- 学生表测试数据
insert into tb_student(name, no) values (&apos;黛绮丝&apos;, &apos;2000100101&apos;),
                                        (&apos;谢逊&apos;, &apos;2000100102&apos;),
                                        (&apos;殷天正&apos;, &apos;2000100103&apos;),
                                        (&apos;韦一笑&apos;, &apos;2000100104&apos;);

-- 课程表
create table tb_course(
   id int auto_increment primary key comment &apos;主键ID&apos;,
   name varchar(10) comment &apos;课程名称&apos;
) comment &apos;课程表&apos;;
-- 课程表测试数据
insert into tb_course (name) values (&apos;Java&apos;), (&apos;PHP&apos;), (&apos;MySQL&apos;) , (&apos;Hadoop&apos;);

-- 学生课程表（中间表）
create table tb_student_course(
   id int auto_increment comment &apos;主键&apos; primary key,
   student_id int not null comment &apos;学生ID&apos;,
   course_id  int not null comment &apos;课程ID&apos;,
   constraint fk_courseid foreign key (course_id) references tb_course (id),
   constraint fk_studentid foreign key (student_id) references tb_student (id)
)comment &apos;学生课程中间表&apos;;

-- 学生课程表测试数据
insert into tb_student_course(student_id, course_id) values (1,1),(1,2),(1,3),(2,2),(2,3),(3,4);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;多对多 ：需要建立一张中间表，中间表中有两个外键字段，分别关联两方的主键。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;1. 多表查询&lt;/h2&gt;
&lt;h4&gt;1. 介绍&lt;/h4&gt;
&lt;p&gt;多表查询：查询时从多张表中获取所需数据&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;单表查询的SQL语句：select  字段列表  from  表名;&lt;/p&gt;
&lt;p&gt;那么要执行多表查询，只需要使用逗号分隔多张表即可，如： select   字段列表  from  表1, 表2;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;**笛卡尔积：**笛卡尔乘积是指在数学中，两个集合(A集合和B集合)的所有组合情况。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./03.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在多表查询时，需要消除无效的笛卡尔积，只保留表关联部分的数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./04.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在SQL语句中，如何去除无效的笛卡尔积呢？只需要给多表查询加上连接查询的条件即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from emp , dept where emp.dept_id = dept.id ;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1. 分类&lt;/h4&gt;
&lt;p&gt;多表查询可以分为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./05.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;连接查询
&lt;ol&gt;
&lt;li&gt;内连接：相当于查询A、B交集部分数据&lt;/li&gt;
&lt;li&gt;外连接
&lt;ul&gt;
&lt;li&gt;左外连接：查询左表所有数据(包括两张表交集部分数据)&lt;/li&gt;
&lt;li&gt;右外连接：查询右表所有数据(包括两张表交集部分数据)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;子查询&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1. 内连接&lt;/h3&gt;
&lt;p&gt;内连接查询：查询两表或多表中交集部分数据。&lt;/p&gt;
&lt;p&gt;内连接从语法上可以分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;隐式内连接&lt;/li&gt;
&lt;li&gt;显式内连接&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;隐式内连接语法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select  字段列表   from   表1 , 表2   where  条件 ... ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;显式内连接语法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select  字段列表   from   表1  [ inner ]  join 表2  on  连接条件 ... ;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;案例1：查询所有员工的ID，姓名，及所属的部门名称&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;隐式内连接实现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;  select emp.id, emp.name, dept.name from emp , dept where emp.dept_id = dept.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显式内连接实现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;  select emp.id, emp.name, dept.name from emp inner join dept on emp.dept_id = dept.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;案例2：查询 性别为男, 且工资 高于8000 的员工的ID, 姓名, 及所属的部门名称&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;隐式内连接实现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;  select emp.id, emp.name, dept.name from emp , dept where emp.dept_id = dept.id and emp.gender = 1 and emp.salary &amp;gt; 8000;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显式内连接实现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;  select emp.id, emp.name, dept.name from emp inner join dept on emp.dept_id = dept.id where emp.gender = 1 and emp.salary &amp;gt; 8000;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在多表联查时，我们指定字段时，需要在字段名前面加上表名，来指定具体是哪一张的字段。 如：emp.dept_id&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;给表起别名简化书写：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select  字段列表 from 表1 as 别名1 , 表2 as  别名2  where  条件 ... ;

select  字段列表 from 表1 别名1 , 表2  别名2  where  条件 ... ;  -- as 可以省略
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用了别名的多表查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select e.id, e.name, d.name from emp as e , dept as d where e.dept_id = d.id and e.gender = 1 and e.salary &amp;gt; 8000;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意事项:&lt;/strong&gt; 一旦为表起了别名，就不能再使用表名来指定对应的字段了，此时只能够使用别名来指定字段。&lt;/p&gt;
&lt;h3&gt;1. 外连接&lt;/h3&gt;
&lt;p&gt;外连接分为两种：左外连接 和 右外连接。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;左外连接语法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select  字段列表   from   表1  left  [ outer ]  join 表2  on  连接条件 ... ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;左外连接相当于查询表1(左表)的所有数据，当然也包含表1和表2交集部分的数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;右外连接语法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select  字段列表   from   表1  right  [ outer ]  join 表2  on  连接条件 ... ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;右外连接相当于查询表2(右表)的所有数据，当然也包含表1和表2交集部分的数据。&lt;/p&gt;
&lt;p&gt;案例1：查询员工表 所有 员工的姓名, 和对应的部门名称 (左外连接)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 左外连接：以left join关键字左边的表为主表，查询主表中所有数据，以及和主表匹配的右边表中的数据
select e.name , d.name  from emp as e left join dept as d on e.dept_id = d.id ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;案例2：查询部门表 所有 部门的名称, 和对应的员工名称 (右外连接)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 右外连接：以right join关键字右边的表为主表，查询主表中所有数据，以及和主表匹配的左边表中的数据
select e.name , d.name from emp as e right join dept as d on e.dept_id = d.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;案例3：查询工资 高于8000 的 所有员工的姓名, 和对应的部门名称 (左外连接)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select e.name , d.name  from emp as e left join dept as d on e.dept_id = d.id where e.salary &amp;gt; 8000;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意事项：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;左外连接和右外连接是可以相互替换的，只需要调整连接查询时SQL语句中表的先后顺序就可以了。而我们在日常开发使用时，更偏向于左外连接。&lt;/p&gt;
&lt;h3&gt;1. 子查询&lt;/h3&gt;
&lt;h4&gt;1. 介绍&lt;/h4&gt;
&lt;p&gt;SQL语句中嵌套select语句，称为嵌套查询，又称子查询。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT  *  FROM   t1   WHERE  column1 =  ( SELECT  column1  FROM  t2 ... );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;子查询外部的语句可以是insert / update / delete / select 的任何一个，最常见的是 select。&lt;/p&gt;
&lt;p&gt;根据子查询结果的不同分为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;标量子查询（子查询结果为单个值 [一行一列]）&lt;/li&gt;
&lt;li&gt;列子查询（子查询结果为一列，但可以是多行）&lt;/li&gt;
&lt;li&gt;行子查询（子查询结果为一行，但可以是多列）&lt;/li&gt;
&lt;li&gt;表子查询（子查询结果为多行多列[相当于子查询结果是一张表]）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;子查询可以书写的位置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;where之后&lt;/li&gt;
&lt;li&gt;from之后&lt;/li&gt;
&lt;li&gt;select之后&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;子查询的要点是，先对需求做拆分，明确具体的步骤，然后再逐条编写SQL语句。 最终将多条SQL语句合并为一条。&lt;/p&gt;
&lt;h4&gt;1. 标量子查询&lt;/h4&gt;
&lt;p&gt;子查询返回的结果是单个值(数字、字符串、日期等)，最简单的形式，这种子查询称为&lt;strong&gt;标量子查询&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;常用的操作符： =   &amp;lt;&amp;gt;   &amp;gt;    &amp;gt;=    &amp;lt;   &amp;lt;=&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;案例1：查询 最早入职 的员工信息&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- 1. 查询最早的入职时间
select min(entry_date) from emp;  -- 结果: 2000-01-01

-- 2. 查询入职时间 = 最早入职时间的员工信息
select * from emp where entry_date = &apos;2000-01-01&apos;;

-- 3. 合并为一条SQL
select * from emp where entry_date = (select min(entry_date) from emp);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;案例2：查询在 阮小五 入职之后入职的员工信息&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- 1. 查询 &quot;阮小五&quot; 的入职日期
select entry_date from emp where name = &apos;阮小五&apos;; -- 结果: 2015-01-01

-- 2. 根据上述查询到的这个入职日期, 查询在该日期之后入职的员工信息
select * from emp where entry_date &amp;gt; &apos;2015-01-01&apos;;

-- 3. 合并SQL为一条SQL
select * from emp where entry_date &amp;gt; (select entry_date from emp where name = &apos;阮小五&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1. 列子查询&lt;/h4&gt;
&lt;p&gt;子查询返回的结果是一列(可以是多行)，这种子查询称为列子查询。&lt;/p&gt;
&lt;p&gt;常用的操作符：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作符&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;in&lt;/td&gt;
&lt;td&gt;在指定的集合范围之内，多选一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;not in&lt;/td&gt;
&lt;td&gt;不在指定的集合范围之内&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;案例1：查询 &quot;教研部&quot; 和 &quot;咨询部&quot; 的所有员工信息&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- 1. 查询 &quot;教研部&quot; 和 &quot;咨询部&quot; 的部门ID
select id from dept where name = &apos;教研部&apos; or name = &apos;咨询部&apos;; -- 结果: 3,2

-- 2. 根据上面查询出来的部门ID, 查询员工信息
select * from emp where dept_id in(3,2);

-- 3. 合并SQL为一条SQL语句
select * from emp where dept_id in (select id from dept where name = &apos;教研部&apos; or name = &apos;咨询部&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1. 行子查询&lt;/h4&gt;
&lt;p&gt;子查询返回的结果是一行(可以是多列)，这种子查询称为行子查询。&lt;/p&gt;
&lt;p&gt;常用的操作符：= 、&amp;lt;&amp;gt; 、IN 、NOT IN&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;案例1：查询与 &quot;李忠&quot; 的薪资 及 职位都相同的员工信息&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- 1. 查询 &quot;李忠&quot; 的薪资和职位
select salary , job from emp where name = &apos;李忠&apos;; -- 结果: 5000, 5

-- 2. 根据上述查询到的薪资和职位 , 查询对应员工的信息
select * from emp where (salary, job) = (5000,5);

-- 3. 将两条SQL合并为一条SQL
select * from emp where (salary, job) = (select salary , job from emp where name = &apos;李忠&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1. 表子查询&lt;/h4&gt;
&lt;p&gt;子查询返回的结果是多行多列，常作为临时表，这种子查询称为&lt;strong&gt;表子查询&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;案例：&lt;em&gt;获取每个部门中薪资最高的员工信息&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- a. 获取每个部门的最高薪资
select dept_id, max(salary) from emp group by dept_id;

-- b. 查询每个部门中薪资最高的员工信息
select * from emp e , (select dept_id, max(salary) max_sal from emp group by dept_id) a
    where e.dept_id = a.dept_id and e.salary = a.max_sal;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1. 案例&lt;/h4&gt;
&lt;p&gt;根据需求，完成多表查询的SQL语句的编写。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\1. 查询 &quot;教研部&quot; 性别为 男，且在 &quot;2011-05-01&quot; 之后入职的员工信息 。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select e.* from emp as e , dept as d where e.dept_id = d.id and d.name = &apos;教研部&apos; and e.gender = 1 and e.entry_date &amp;gt; &apos;2011-05-01&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;\2. 查询工资 低于公司平均工资的 且 性别为男 的员工信息 。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select e.* from emp as e , dept as d where e.dept_id = d.id and e.salary &amp;lt; (select avg(salary) from emp) and e.gender = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;\3. 查询部门人数超过 10 人的部门名称 。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select d.name , count(*) from emp as e , dept as d where e.dept_id = d.id group by d.name having count(*) &amp;gt; 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;\4. 查询在 &quot;2010-05-01&quot; 后入职，且薪资高于 10000 的 &quot;教研部&quot; 员工信息，并根据薪资倒序排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;select * from emp e , dept d where e.dept_id = d.id and e.entry_date &amp;gt; &apos;2010-05-01&apos; and e.salary &amp;gt; 10000 and d.name = &apos;教研部&apos; order by e.salary desc;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;\5. 查询工资 低于本部门平均工资的员工信息 。【难】&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- 5.1 查询每个部门的平均工资
select dept_id, avg(salary) avg_sal from emp group by dept_id;

-- 5.2 查询工资 低于本部门平均工资 的员工信息 。
select e.* from emp e , (select dept_id, avg(salary) avg_sal from emp group by dept_id) as a
          where e.dept_id = a.dept_id and e.salary &amp;lt; a.avg_sal;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. 分页查询&lt;/h3&gt;
&lt;p&gt;要想从数据库中进行分页查询，我们要使用&lt;code&gt;LIMIT&lt;/code&gt;关键字，格式为：limit  开始索引  每页显示的条数。&lt;/p&gt;
&lt;p&gt;1). 查询第1页数据的SQL语句是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from emp  limit 0,10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2). 查询第2页数据的SQL语句是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from emp  limit 10,10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3). 查询第3页的数据的SQL语句是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from emp  limit 20,10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察以上SQL语句，发现： 开始索引一直在改变 ， 每页显示条数是固定的&lt;/p&gt;
&lt;p&gt;开始索引的计算公式：   &lt;code&gt;开始索引 = (当前页码 - 1)  *  每页显示条数&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们继续基于页面原型，继续分析，得出以下结论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;前端在请求服务端时，传递的参数
&lt;ol&gt;
&lt;li&gt;当前页码  page&lt;/li&gt;
&lt;li&gt;每页显示条数  pageSize&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;后端需要响应什么数据给前端
&lt;ol&gt;
&lt;li&gt;所查询到的数据列表（存储到List 集合中）&lt;/li&gt;
&lt;li&gt;总记录数&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./06.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;后台给前端返回的数据包含：List集合(数据列表)、total(总记录数)&lt;/p&gt;
&lt;p&gt;而这两部分我们通常封装到PageResult对象中，并将该对象转换为json格式的数据响应回给浏览器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult {
        private Long total; //总记录数
        private List rows; //当前页数据列表
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1. 接口描述&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1). 基本信息&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;请求路径：/emps&lt;/p&gt;
&lt;p&gt;请求方式：GET&lt;/p&gt;
&lt;p&gt;接口描述：该接口用于员工列表数据的条件分页查询&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;2). 请求参数&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数名称&lt;/th&gt;
&lt;th&gt;是否必须&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;张&lt;/td&gt;
&lt;td&gt;姓名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gender&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;性别 , 1 男 , 2 女&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;begin&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;2010/1/1&lt;/td&gt;
&lt;td&gt;范围匹配的开始时间(入职日期)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;end&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;2020/1/1&lt;/td&gt;
&lt;td&gt;范围匹配的结束时间(入职日期)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;page&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;分页查询的页码，如果未指定，默认为1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pageSize&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;分页查询的每页记录数，如果未指定，默认为10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;请求数据样例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/emps?name=张&amp;amp;gender=1&amp;amp;begin=2007-09-01&amp;amp;end=2022-09-01&amp;amp;page=1&amp;amp;pageSize=10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3). 响应数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;参数格式：application/json&lt;/p&gt;
&lt;p&gt;参数说明：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;是否必须&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;code&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;必须&lt;/td&gt;
&lt;td&gt;响应码, 1 成功 , 0 失败&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;msg&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;提示信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;data&lt;/td&gt;
&lt;td&gt;object&lt;/td&gt;
&lt;td&gt;必须&lt;/td&gt;
&lt;td&gt;返回的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- total&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;必须&lt;/td&gt;
&lt;td&gt;总记录数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- rows&lt;/td&gt;
&lt;td&gt;object []&lt;/td&gt;
&lt;td&gt;必须&lt;/td&gt;
&lt;td&gt;数据列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- id&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- username&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;用户名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- name&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;姓名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- password&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;密码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- gender&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;性别 , 1 男 ; 2 女&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- image&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;图像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- job&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- salary&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;薪资&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- entryDate&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;入职日期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- deptId&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;部门id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- deptName&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;部门名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- createTime&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;创建时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|- updateTime&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;非必须&lt;/td&gt;
&lt;td&gt;更新时间&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;1. PageHelper分页插件&lt;/h4&gt;
&lt;h5&gt;1. 介绍&lt;/h5&gt;
&lt;p&gt;前面我们已经完了基础的分页查询，大家会发现：分页查询功能编写起来比较繁琐。 而分页查询的功能是非常常见的，我们查询员工信息需要分页查询，将来在做其他项目时，查询用户信息、订单信息、商品信息等等都是需要进行分页查询的。&lt;/p&gt;
&lt;p&gt;而分页查询的思路、步骤是比较固定的。 在Mapper接口中定义两个方法执行两条不同的SQL语句：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查询总记录数&lt;/li&gt;
&lt;li&gt;指定页码的数据列表&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在Service当中，调用Mapper接口的两个方法，分别获取：总记录数、查询结果列表，然后在将获取的数据结果封装到PageBean对象中。&lt;/p&gt;
&lt;p&gt;大家思考下：在未来开发其他项目，只要涉及到分页查询功能(例：订单、用户、支付、商品)，都必须按照以上操作完成功能开发&lt;/p&gt;
&lt;p&gt;结论：原始方式的分页查询，存在着&quot;步骤固定&quot;、&quot;代码频繁&quot;的问题&lt;/p&gt;
&lt;p&gt;解决方案：可以使用一些现成的分页插件完成。对于Mybatis来讲现在最主流的就是PageHelper。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PageHelper是第三方提供的Mybatis框架中的一款功能强大、方便易用的分页插件，支持任何形式的单标、多表的分页查询。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;官网：https://pagehelper.github.io/&lt;/p&gt;
&lt;p&gt;那接下来，我们可以对比一下，使用PageHelper分页插件进行分页 与 原始方式进行分页代码实现的上的差别。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./07.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mapper接口层：
&lt;ul&gt;
&lt;li&gt;原始的分页查询功能中，我们需要在Mapper接口中定义两条SQL语句。&lt;/li&gt;
&lt;li&gt;PageHelper实现分页查询之后，只需要编写一条SQL语句，而且不需要考虑分页操作，就是一条正常的查询语句。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Service层：
&lt;ul&gt;
&lt;li&gt;需要根据页码、每页展示记录数，手动的计算起始索引。&lt;/li&gt;
&lt;li&gt;无需手动计算起始索引，直接告诉PageHelper需要查询那一页的数据，每页展示多少条记录即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;1. 代码实现&lt;/h5&gt;
&lt;p&gt;当使用了PageHelper分页插件进行分页，就无需再Mapper中进行手动分页了。 在Mapper中我们只需要进行正常的列表查询即可。在Service层中，调用Mapper的方法之前设置分页参数，在调用Mapper方法执行查询之后，解析分页结果，并将结果封装到PageResult对象中返回。&lt;/p&gt;
&lt;p&gt;1). 在pom.xml引入依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--分页插件PageHelper--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.github.pagehelper&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;pagehelper-spring-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.4.7&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2). EmpMapper&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 查询所有的员工及其对应的部门名称
 */
@Select(&quot;select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id&quot;)
public List&amp;lt;Emp&amp;gt; list();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3). EmpServiceImpl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public PageResult page(Integer page, Integer pageSize) {
    //1. 设置分页参数
    PageHelper.startPage(page,pageSize);

    //2. 执行查询
    List&amp;lt;Emp&amp;gt; empList = empMapper.list();
    Page&amp;lt;Emp&amp;gt; p = (Page&amp;lt;Emp&amp;gt;) empList;

    //3. 封装结果
    return new PageResult(p.getTotal(), p.getResult());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PageHelper实现分页查询时，SQL语句的结尾一定一定一定不要加分号(;).。&lt;/li&gt;
&lt;li&gt;PageHelper只会对紧跟在其后的第一条SQL语句进行分页处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 条件分页查询&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;条件分页查询在实际项目中非常常用，建议掌握。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;多表关系的核心是先把业务关系映射成表关系，再选择合适的查询和映射方式。外键是否落到数据库层，需要结合项目规模、性能和维护成本判断。&lt;/p&gt;
</content:encoded></item><item><title>RESTful、Apifox 与 CRUD 接口实践</title><link>https://youki.bbroot.com/posts/java/restful-crud/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/java/restful-crud/</guid><description>整理 REST 风格 URL、HTTP 动词、Apifox 接口调试和基础 CRUD 接口实现。</description><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记整理 RESTful 接口风格、Apifox 调试工具以及 Spring Boot 中 CRUD 接口的基础写法。后续精修时，可以把接口设计原则和代码实践拆成两个部分。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;REST 风格通过 URL 定位资源，通过 HTTP 动词表达操作。&lt;/li&gt;
&lt;li&gt;常见 CRUD 操作可对应 GET、POST、PUT、DELETE。&lt;/li&gt;
&lt;li&gt;Apifox 可用于接口文档管理、接口请求测试和 Mock。&lt;/li&gt;
&lt;li&gt;Spring Boot 中推荐使用 &lt;code&gt;@GetMapping&lt;/code&gt; 等派生注解限制请求方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;什么是REST风格呢?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REST（Representational State Transfer），表述性状态转换，它是一种软件架构风格。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;传统URL风格如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;http://localhost:8080/user/getById?id=1      GET：查询id为1的用户&lt;/li&gt;
&lt;li&gt;http://localhost:8080/user/saveUser            POST：新增用户&lt;/li&gt;
&lt;li&gt;http://localhost:8080/user/updateUser         POST：修改用户&lt;/li&gt;
&lt;li&gt;http://localhost:8080/user/deleteUser?id=1  GET：删除id为1的用户&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;基于REST风格URL如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;http://localhost:8080/users/1       GET：查询id为1的用户&lt;/li&gt;
&lt;li&gt;http://localhost:8080/users          POST：新增用户&lt;/li&gt;
&lt;li&gt;http://localhost:8080/users          PUT：修改用户&lt;/li&gt;
&lt;li&gt;http://localhost:8080/users/1       DELETE：删除id为1的用户&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中总结起来，就一句话：通过URL定位要操作的资源，通过HTTP动词(请求方式)来描述具体的操作。&lt;/p&gt;
&lt;p&gt;在REST风格的URL中，通过四种请求方式，来操作数据的增删改查。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GET ：  查询&lt;/li&gt;
&lt;li&gt;POST ：新增&lt;/li&gt;
&lt;li&gt;PUT ：  修改&lt;/li&gt;
&lt;li&gt;DELETE ：删除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们看到如果是基于REST风格，定义URL，URL将会&lt;strong&gt;更加简洁、更加规范、更加优雅&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;介绍：Apifox是一款集成了Api文档、Api调试、Api Mock、Api测试的一体化协作平台。&lt;/p&gt;
&lt;p&gt;作用：接口文档管理、接口请求测试、Mock服务。&lt;/p&gt;
&lt;h4&gt;1. 接口测试&lt;/h4&gt;
&lt;p&gt;经过测试，我们发现，现在我们其实是可以通过任何方式的请求来访问查询部门的这个接口的。 而在接口文档中，明确要求该接口的请求方式为GET，那么如何限制请求方式呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;方式一：在controller方法的@RequestMapping注解中通过method属性来限定。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;方式二：在controller方法上使用，@RequestMapping的衍生注解 @GetMapping。 该注解就是标识当前方法，必须以GET方式请求。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;	@RestController
	public class DeptController {
	
	    @Autowired
	    private DeptService deptService;
	
	    /**
	     * 查询部门列表
	     */
	    @GetMapping(&quot;/depts&quot;)
	    public Result list(){
	        List&amp;lt;Dept&amp;gt; deptList = deptService.findAll();
	        return Result.success(deptList);
	    }
	}
	```

上述两种方式，在项目开发中，推荐使用第二种方式，简洁、优雅。 

- GET方式：@GetMapping
- POST方式：@PostMapping
- PUT方式：@PutMapping
- DELETE方式：@DeleteMapping

#### 1. 数据封装

- 如果实体类属性名和数据库表查询返回的字段名不一致，不能自动封装。

 解决方案：

- 手动结果映射
- 起别名
- 开启驼峰命名

**3). 开启驼峰命名****(推荐)**

如果字段名与属性名符合驼峰命名规则，mybatis会自动通过驼峰命名规则映射。驼峰命名规则：   abc_xyz    =&amp;gt;   abcXyz

- 表中字段名：abc_xyz
- 类中属性名：abcXyz

在application.yml中做如下配置，开启开关。

```yaml
mybatis:
  configuration:
    map-underscore-to-camel-case: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。&lt;/p&gt;
&lt;h3&gt;1. 前后端联调&lt;/h3&gt;
&lt;h4&gt;2. 联调测试&lt;/h4&gt;
&lt;h4&gt;3. 请求访问流程&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./01.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1). 浏览器发起请求，请求的是localhost:90 ，那其实请求的是nginx服务器。&lt;/p&gt;
&lt;p&gt;2). 在nginx服务器中呢，并没有对请求直接进行处理，而是将请求转发给了后端的tomcat服务器，最终由tomcat服务器来处理该请求。&lt;/p&gt;
&lt;p&gt;这个过程就是通过nginx的反向代理实现的。 那为什么浏览器不直接请求后端的tomcat服务器，而是直接请求nginx服务器呢，主要有以下几点原因：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./02.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1). 安全：由于后端的tomcat服务器一般都会搭建集群，会有很多的服务器，把所有的tomcat暴露给前端，让前端直接请求tomcat，对于后端服务器是比较危险的。&lt;/p&gt;
&lt;p&gt;2). 灵活：基于nginx的反向代理实现，更加灵活，后端想增加、减少服务器，对于前端来说是无感知的，只需要在nginx中配置即可。&lt;/p&gt;
&lt;p&gt;3). 负载均衡：基于nginx的反向代理，可以很方便的实现后端tomcat的负载均衡操作。&lt;/p&gt;
&lt;p&gt;具体的请求访问流程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./03.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;location：用于定义匹配特定uri请求的规则。&lt;/li&gt;
&lt;li&gt;^~ /api/：表示精确匹配，即只匹配以/api/开头的路径。&lt;/li&gt;
&lt;li&gt;rewrite：该指令用于重写匹配到的uri路径。&lt;/li&gt;
&lt;li&gt;proxy_pass：该指令用于代理转发，它将匹配到的请求转发给位于后端的指令服务器。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 删除部门&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案二：通过Spring提供的&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;@RequestParam&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;注解，将请求参数绑定给方法形参&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@DeleteMapping(&quot;/depts&quot;)
public Result delete(@RequestParam(&quot;id&quot;) Integer deptId){
    System.out.println(&quot;根据ID删除部门: &quot; + deptId);
    return Result.success();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@RequestParam&lt;/code&gt; 注解的value属性，需要与前端传递的参数名保持一致 。&lt;/p&gt;
&lt;p&gt;@RequestParam注解required属性默认为true，代表该参数必须传递，如果不传递将报错。 如果参数可选，可以将属性设置为false。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方案三：如果请求参数名与形参变量名相同，直接定义方法形参即可接收。（省略@RequestParam）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@DeleteMapping(&quot;/depts&quot;)
public Result delete(Integer id){
    System.out.println(&quot;根据ID删除部门: &quot; + id);
    return Result.success();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于以上的这三种方案呢，我们&lt;strong&gt;推荐第三种方案&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1. 代码实现&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1). Controller层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;DeptMapper&lt;/code&gt;  中，增加 &lt;code&gt;delete&lt;/code&gt; 方法，代码实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据id删除部门 - delete http://localhost:8080/depts?id=1
 */
@DeleteMapping(&quot;/depts&quot;)
public Result delete(Integer id){
    System.out.println(&quot;根据id删除部门, id=&quot; + id);
    deptService.deleteById(id);
    return Result.success();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2). Service层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;DeptService&lt;/code&gt; 中，增加 &lt;code&gt;deleteById&lt;/code&gt; 方法，代码实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据id删除部门
 */
void deleteById(Integer id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;DeptServiceImpl&lt;/code&gt; 中，增加 &lt;code&gt;deleteById&lt;/code&gt; 方法，代码实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void deleteById(Integer id) {
    deptMapper.deleteById(id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3). Mapper层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;DeptMapper&lt;/code&gt; 中，增加 &lt;code&gt;deleteById&lt;/code&gt; 方法，代码实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据id删除部门
 */
@Delete(&quot;delete from dept where id = #{id}&quot;)
void deleteById(Integer id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果mapper接口方法形参只有一个普通类型的参数，&lt;code&gt;#{…}&lt;/code&gt; 里面的属性名可以随便写，如：&lt;code&gt;#{id}&lt;/code&gt;、&lt;code&gt;#{value}&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于 DML 语句来说，执行完毕，也是有返回值的，返回值代表的是增删改操作，影响的记录数，所以可以将执行 DML 语句的方法返回值设置为 Integer。 但是一般开发时，是不需要这个返回值的，所以也可以设置为void。&lt;/p&gt;
&lt;h2&gt;1. 新增部门&lt;/h2&gt;
&lt;h3&gt;1. json参数接收&lt;/h3&gt;
&lt;p&gt;我们看到，在controller中，需要接收前端传递的请求参数。 那接下来，我们就先来看看在服务器端的Controller程序中，如何获取json格式的参数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JSON格式的参数，通常会使用一个实体对象进行接收 。&lt;/li&gt;
&lt;li&gt;规则：JSON数据的键名与方法形参对象的属性名相同，并需要使用&lt;code&gt;@RequestBody&lt;/code&gt;注解标识。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前端传递的请求参数格式为json，内容如下：&lt;code&gt;{&quot;name&quot;:&quot;研发部&quot;}&lt;/code&gt;。这里，我们可以通过一个对象来接收，只需要保证对象中有name属性即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./04.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1. 代码实现&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1). Controller层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;DeptController&lt;/code&gt;中增加方法save，具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 新增部门 - POST http://localhost:8080/depts   请求参数：{&quot;name&quot;:&quot;研发部&quot;}
 */
@PostMapping(&quot;/depts&quot;)
public Result save(@RequestBody Dept dept){
    System.out.println(&quot;新增部门, dept=&quot; + dept);
    deptService.save(dept);
    return Result.success();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2). Service层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;DeptService&lt;/code&gt;中增加接口方法save，具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 新增部门
 */
void save(Dept dept);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;DeptServiceImpl&lt;/code&gt;中增加save方法，完成添加部门的操作，具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void save(Dept dept) {
    //补全基础属性
    dept.setCreateTime(LocalDateTime.now());
    dept.setUpdateTime(LocalDateTime.now());
    //保存部门
    deptMapper.insert(dept);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3). Mapper层&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 保存部门
 */
@Insert(&quot;insert into dept(name,create_time,update_time) values(#{name},#{createTime},#{updateTime})&quot;)
void insert(Dept dept);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在mapper接口中，需要传递多个参数，可以把多个参数封装到一个对象中。 在SQL语句中获取参数的时候，&lt;code&gt;#{...}&lt;/code&gt; 里面写的是对象的属性名【注意是属性名，不是表的字段名】。&lt;/p&gt;
&lt;h2&gt;1. 修改部门&lt;/h2&gt;
&lt;p&gt;对于任何业务的修改功能来说，一般都会分为两步进行：查询回显、修改数据。&lt;/p&gt;
&lt;h3&gt;1. 查询回显&lt;/h3&gt;
&lt;h4&gt;1. 需求&lt;/h4&gt;
&lt;p&gt;当我们点击 &quot;编辑&quot; 的时候，需要根据ID查询部门数据，然后用于页面回显展示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./05.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;1. 接口描述&lt;/h4&gt;
&lt;p&gt;参照参照课程资料中提供的接口文档。 &lt;code&gt;部门管理&lt;/code&gt; -&amp;gt; &lt;code&gt;根据ID查询&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;1. 路径参数接收&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;/depts/1&lt;/code&gt;，&lt;code&gt;/depts/2&lt;/code&gt; 这种在url中传递的参数，我们称之为&lt;strong&gt;路径参数&lt;/strong&gt;。 那么如何接收这样的路径参数呢 ？&lt;/p&gt;
&lt;p&gt;路径参数：通过请求URL直接传递参数，使用{…}来标识该路径参数，需要使用 **&lt;code&gt;@PathVariable&lt;/code&gt;**获取路径参数。如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./06.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果路径参数名与controller方法形参名称一致，&lt;code&gt;@PathVariable&lt;/code&gt;注解的value属性是可以省略的。&lt;/p&gt;
&lt;h4&gt;1. 代码实现&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1). Controller层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;DeptController&lt;/code&gt; 中增加 &lt;code&gt;getById&lt;/code&gt;方法，具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据ID查询 - GET http://localhost:8080/depts/1
 */
@GetMapping(&quot;/depts/{id}&quot;)
public Result getById(@PathVariable Integer id){
    System.out.println(&quot;根据ID查询, id=&quot; + id);
    Dept dept = deptService.getById(id);
    return Result.success(dept);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2). Service层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;DeptService&lt;/code&gt; 中增加 &lt;code&gt;getById&lt;/code&gt;方法，具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据id查询部门
 */
Dept getById(Integer id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;DeptServiceImpl&lt;/code&gt; 中增加 &lt;code&gt;getById&lt;/code&gt;方法，具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Dept getById(Integer id) {
    return deptMapper.getById(id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3). Mapper层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;DeptMapper&lt;/code&gt; 中增加 &lt;code&gt;getById&lt;/code&gt; 方法，具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* 根据ID查询部门数据
*/
@Select(&quot;select id, name, create_time, update_time from dept where id = #{id}&quot;)
Dept getById(Integer id);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. 修改数据&lt;/h3&gt;
&lt;h4&gt;1. 需求&lt;/h4&gt;
&lt;p&gt;查询回显回来之后，就可以对部门的信息进行修改了，修改完毕之后，点击确定，此时，就需要根据ID修改部门的数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./07.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过接口文档，我们可以看到前端传递的请求参数是json格式的请求参数，在Controller的方法中，我们可以通过 &lt;code&gt;@RequestBody&lt;/code&gt; 注解来接收，并将其封装到一个对象中。&lt;/p&gt;
&lt;h4&gt;1. 代码实现&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1). Controller层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;DeptController&lt;/code&gt; 中增加 &lt;code&gt;update&lt;/code&gt; 方法，具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 修改部门 - PUT http://localhost:8080/depts  请求参数：{&quot;id&quot;:1,&quot;name&quot;:&quot;研发部&quot;}
 */
@PutMapping(&quot;/depts&quot;)
public Result update(@RequestBody Dept dept){
    System.out.println(&quot;修改部门, dept=&quot; + dept);
    deptService.update(dept);
    return Result.success();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2). Service层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;DeptService&lt;/code&gt; 中增加 &lt;code&gt;update&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 修改部门
 */
void update(Dept dept);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;DeptServiceImpl&lt;/code&gt; 中增加 &lt;code&gt;update&lt;/code&gt; 方法。 由于是修改操作，每一次修改数据，都需要更新updateTime。所以，具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void update(Dept dept) {
    //补全基础属性
    dept.setUpdateTime(LocalDateTime.now());
    //保存部门
    deptMapper.update(dept);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3). Mapper层&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;DeptMapper&lt;/code&gt; 中增加 &lt;code&gt;update&lt;/code&gt; 方法，具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 更新部门
 */
@Update(&quot;update dept set name = #{name},update_time = #{updateTime} where id = #{id}&quot;)
void update(Dept dept);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1. @RequestMapping&lt;/h4&gt;
&lt;p&gt;到此呢，关于基本的部门的增删改查功能，我们已经实现了。  我们会发现，我们在 &lt;code&gt;DeptController&lt;/code&gt; 中所定义的方法，所有的请求路径，都是 &lt;code&gt;/depts&lt;/code&gt; 开头的，只要操作的是部门数据，请求路径都是 &lt;code&gt;/depts&lt;/code&gt; 开头。&lt;/p&gt;
&lt;p&gt;那么这个时候，我们其实是可以把这个公共的路径 &lt;code&gt;/depts&lt;/code&gt; 抽取到类上的，那在各个方法上，就可以省略了这个 &lt;code&gt;/depts&lt;/code&gt; 路径。 代码如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./08.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个完整的请求路径，应该是类上的 @RequestMapping 的value属性 + 方法上的 @RequestMapping的value属性。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;RESTful 的关键是资源路径和 HTTP 动词的配合，Apifox 则适合用来验证接口行为。CRUD 代码部分后续可以进一步整理成“接口设计”和“Controller 实现”两个小节。&lt;/p&gt;
</content:encoded></item><item><title>Vue 基础语法与数据驱动视图笔记</title><link>https://youki.bbroot.com/posts/frontend/vue-basics/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/frontend/vue-basics/</guid><description>整理 Vue 渐进式框架概念、数据驱动视图、插值表达式、常用指令和生命周期。</description><pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记整理 Vue 入门阶段的核心概念：渐进式框架、数据驱动视图、插值表达式和常用指令。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vue 是用于构建用户界面的渐进式 JavaScript 框架。&lt;/li&gt;
&lt;li&gt;Vue 应用通过数据驱动视图，模板中可以使用插值表达式。&lt;/li&gt;
&lt;li&gt;常用指令包括 &lt;code&gt;v-for&lt;/code&gt;、&lt;code&gt;v-bind&lt;/code&gt;、&lt;code&gt;v-if&lt;/code&gt;、&lt;code&gt;v-show&lt;/code&gt;、&lt;code&gt;v-model&lt;/code&gt; 和 &lt;code&gt;v-on&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;生命周期有助于理解组件从创建到挂载、更新、销毁的过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;vue&lt;/h3&gt;
&lt;h4&gt;介绍&lt;/h4&gt;
&lt;p&gt;vue是一款用于&lt;strong&gt;构建用户界面&lt;/strong&gt;的&lt;strong&gt;渐进式&lt;/strong&gt;的JavaScript&lt;strong&gt;框架&lt;/strong&gt;。&lt;a href=&quot;https://cn.vuejs.org/&quot;&gt;官方网站&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;构建用户界面&lt;/strong&gt;
基于数据渲染出用户看到的界面，也就是所谓的 &lt;em&gt;构建用户界面&lt;/em&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渐进式&lt;/strong&gt;
所谓&lt;em&gt;渐进&lt;/em&gt;，指的是我们使用Vue框架呢，我们不需要把所有的组件、语法全部学习完毕才可以使用Vue。 而是，我们学习一点就可以使用一点了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;框架&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;框架：就是一套完整的项目解决方案，用于快速构建项目 。这是我们接触的第一个框架，那在我们后面的学习中，我们还会学习很多的java语言中的框架，那通过这些框架呢，就可以来快速开发java项目，提高开发效率。&lt;/li&gt;
&lt;li&gt;优点：大大提升前端项目的开发效率 。&lt;/li&gt;
&lt;li&gt;缺点：需要理解记忆框架的使用规则 。（参照官网）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;程序&lt;/h4&gt;
&lt;h5&gt;1. 准备工作：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;准备元素（div），交给 Vue 控制&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2. 数据驱动视图：&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;准备数据。 在创建Vue应用实例的时候，传入了一个js对象，在这个js对象中，我们要定义一个data方法，这个data方法的返回值就是Vue中的数据。&lt;/li&gt;
&lt;li&gt;通过插值表达式渲染页面。 插值表达式的写法：{{...}}&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;实现&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
  &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
  &amp;lt;title&amp;gt;Vue-快速入门&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;div id=&quot;app&quot;&amp;gt;
    {{message}}
  &amp;lt;/div&amp;gt;

  &amp;lt;script type=&quot;module&quot;&amp;gt;
    import { createApp } from &apos;https://unpkg.com/vue@3/dist/vue.esm-browser.js&apos;
    createApp({
      data(){
        return {
          message: &apos;Hello Vue&apos;
        }
      }
    }).mount(&apos;#app&apos;)
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述入门程序编写时，需要注意这么几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vue中定义数据，必须通过data方法来定义，data方法返回值是一个对象，在这个对象中定义数据。&lt;/li&gt;
&lt;li&gt;插值表达式中编写的变量，一定是Vue中定义的数据，如果插值表达式中编写了一个变量，但是在Vue中未定义，将会报错 。&lt;/li&gt;
&lt;li&gt;Vue应用实例接管的区域是 &lt;code&gt;#app&lt;/code&gt;，超出这个范围，就不受 Vue 控制了，所以 Vue 的插值表达式应写在 &lt;code&gt;&amp;lt;div id=&quot;app&quot;&amp;gt;...&amp;lt;/div&amp;gt;&lt;/code&gt; 里面。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;vue指令&lt;/h4&gt;
&lt;h5&gt;1. &lt;strong&gt;v-for&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;作用：列表渲染，遍历容器的元素或者对象的属性
语法：&lt;code&gt;&amp;lt;tr v-for=&quot;(item,index) in items&quot; :key=&quot;item.id&quot;&amp;gt;{{ item }}&amp;lt;/tr&amp;gt;&lt;/code&gt;
参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;items 为遍历的数组&lt;/li&gt;
&lt;li&gt;item 为遍历出来的元素&lt;/li&gt;
&lt;li&gt;index 为索引/下标，从0开始 ；可以省略，省略index语法： v-for = &quot;item in items&quot;
key：&lt;/li&gt;
&lt;li&gt;作用：给元素添加的唯一标识，便于vue进行列表项的正确排序复用，提升渲染性能&lt;/li&gt;
&lt;li&gt;推荐使用id作为key（唯一），不推荐使用index作为key（会变化，不对应）
&lt;strong&gt;注意：遍历的数组，必须在data中定义； 要想让哪个标签循环展示多次，就在哪个标签上使用 v-for 指令。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2. &lt;strong&gt;v-bind&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;作用：动态为HTML标签绑定属性值，如设置href，src，style样式等。
语法：&lt;code&gt;v-bind:属性名=&quot;属性值&quot;&lt;/code&gt;，例如 &lt;code&gt;&amp;lt;img v-bind:src=&quot;item.image&quot; width=&quot;30px&quot;&amp;gt;&lt;/code&gt;
简化：&lt;code&gt;:属性名=&quot;属性值&quot;&lt;/code&gt;，例如 &lt;code&gt;&amp;lt;img :src=&quot;item.image&quot; width=&quot;30px&quot;&amp;gt;&lt;/code&gt;
&lt;strong&gt;注意：v-bind 所绑定的数据，必须在data中定义/或基于data中定义的数据而来。&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;3. &lt;strong&gt;v-if &amp;amp; v-show&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;作用：这两类指令，都是用来控制元素的显示与隐藏的
v-if：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;语法：v-if=&quot;表达式&quot;，表达式值为 true，显示；false，隐藏&lt;/li&gt;
&lt;li&gt;原理：基于条件判断，来控制创建或移除元素节点（条件渲染）&lt;/li&gt;
&lt;li&gt;场景：要么显示，要么不显示，不频繁切换的场景&lt;/li&gt;
&lt;li&gt;其它：可以配合 v-else-if / v-else 进行链式调用条件判断
示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;   &amp;lt;!-- 基于v-if/v-else-if/v-else指令来展示职位这一列 --&amp;gt;
  &amp;lt;td&amp;gt;
    &amp;lt;span v-if=&quot;emp.job === &apos;1&apos;&quot;&amp;gt;班主任&amp;lt;/span&amp;gt;
    &amp;lt;span v-else-if=&quot;emp.job === &apos;2&apos;&quot;&amp;gt;讲师&amp;lt;/span&amp;gt;
    &amp;lt;span v-else-if=&quot;emp.job === &apos;3&apos;&quot;&amp;gt;学工主管&amp;lt;/span&amp;gt;
    &amp;lt;span v-else-if=&quot;emp.job === &apos;4&apos;&quot;&amp;gt;教研主管&amp;lt;/span&amp;gt;
    &amp;lt;span v-else-if=&quot;emp.job === &apos;5&apos;&quot;&amp;gt;咨询师&amp;lt;/span&amp;gt;
    &amp;lt;span v-else&amp;gt;其他&amp;lt;/span&amp;gt;
  &amp;lt;/td&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;4.&lt;strong&gt;v-show&lt;/strong&gt;&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;语法：v-show=&quot;表达式&quot;，表达式值为 true，显示；false，隐藏&lt;/li&gt;
&lt;li&gt;原理：基于CSS样式display来控制显示与隐藏&lt;/li&gt;
&lt;li&gt;场景：频繁切换显示隐藏的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;5. v-model&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;作用：在表单元素上使用，双向数据绑定。可以方便的 获取 或 设置 表单项数据&lt;/li&gt;
&lt;li&gt;语法：v-model=&quot;变量名&quot;&lt;/li&gt;
&lt;li&gt;这里的双向数据绑定，是指 Vue中的数据变化，会影响视图中的数据展示 。 视图中的输入的数据变化，也会影响Vue的数据模型 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;6. v-on&lt;/h5&gt;
&lt;p&gt;作用：为html标签绑定事件（添加事件监听）
语法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;v-on:事件名=&quot;方法名&quot;&lt;/li&gt;
&lt;li&gt;简写为 @事件名=&quot;…&quot;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;input type=&quot;button&quot; value=&quot;点我一下试试&quot; v-on:click=&quot;handle&quot;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;input type=&quot;button&quot; value=&quot;点我一下试试&quot; @click=&quot;handle&quot;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里的handle函数，就需要在Vue应用实例创建的时候创建出来，在methods定义。&lt;/p&gt;
&lt;h3&gt;Ajax&lt;/h3&gt;
&lt;h4&gt;介绍&lt;/h4&gt;
&lt;p&gt;Ajax: 全称Asynchronous JavaScript And XML，异步的JavaScript和XML。其作用有如下2点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与服务器进行数据交换：通过Ajax可以给服务器发送请求，并获取服务器响应的数据。&lt;/li&gt;
&lt;li&gt;异步交互：可以在不重新加载整个页面的情况下，与服务器交换数据并更新部分网页的技术，如：搜索联想、用户名是否可用的校验等等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Axios&lt;/h4&gt;
&lt;p&gt;Axios是对原生的AJAX进行封装，简化书写。&lt;/p&gt;
&lt;h5&gt;async、await&lt;/h5&gt;
&lt;p&gt;如果使用axios中提供的.then(function(){....}).catch(function(){....})，这种回调函数的写法，会使得代码的可读性和维护性变差。 而为了解决这个问题，我们可以使用两个关键字，分别是：async、await。
可以通过async、await可以让异步变为同步操作。async就是来声明一个异步方法，await是用来等待异步任务执行。&lt;/p&gt;
&lt;p&gt;代码修改前：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;search() {
    //基于axios发送异步请求，请求https://web-server.itheima.net/emps/list，根据条件查询员工列表
    axios.get(`https://web-server.itheima.net/emps/list?name=${this.searchForm.name}&amp;amp;gender=${this.searchForm.gender}&amp;amp;job=${this.searchForm.job}`).then(res =&amp;gt; {
      this.empList = res.data.data
    })
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码修改后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  async search() {
    //基于axios发送异步请求，请求https://web-server.itheima.net/emps/list，根据条件查询员工列表
    const result = await axios.get(`https://web-server.itheima.net/emps/list?name=${this.searchForm.name}&amp;amp;gender=${this.searchForm.gender}&amp;amp;job=${this.searchForm.job}`);
    this.empList = result.data.data;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后，代码就变成同步操作了，一行一行的从前往后执行。 在前端项目开发中，经常使用这两个关键字配合，使得代码的可读性和可维护性变高。&lt;/p&gt;
&lt;h3&gt;6. Vue生命周期&lt;/h3&gt;
&lt;p&gt;vue的生命周期：指的是vue对象从创建到销毁的过程。
vue的生命周期包含8个阶段：每触发一个生命周期事件，会自动执行一个生命周期方法，这些生命周期方法也被称为钩子方法。
&lt;strong&gt;其中我们需要重点关注的是mounted，其他的我们了解即可。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;mounted：挂载完成，Vue初始化成功，HTML页面渲染成功。以后我们一般用于页面初始化自动的ajax请求后台数据&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Vue 入门可以先围绕“数据如何驱动页面变化”来理解。插值表达式、指令、事件绑定和生命周期是后续学习组件化开发的基础。&lt;/p&gt;
</content:encoded></item><item><title>JDBC 与 MyBatis 入门笔记</title><link>https://youki.bbroot.com/posts/java/jdbc-mybatis/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/java/jdbc-mybatis/</guid><description>MyBatis 持久层框架基础：单元测试、日志配置、注解与 XML 映射 CRUD</description><pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;1.介绍MyBatis&lt;/h3&gt;
&lt;p&gt;MyBatis是一款优秀的 &lt;strong&gt;持久层&lt;/strong&gt; &lt;strong&gt;框架&lt;/strong&gt;，用于简化JDBC的开发。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./01.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;持久层：指的是就是数据访问层(dao)，是用来操作数据库的。&lt;/li&gt;
&lt;li&gt;框架：是一个半成品软件，是一套可重用的、通用的、软件基础代码模型。在框架的基础上进行软件开发更加高效、规范、通用、可拓展。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在创建出来的springboot工程中，在引导类所在包下，在创建一个包 &lt;code&gt;mapper&lt;/code&gt; 。在 &lt;code&gt;mapper&lt;/code&gt; 包下创建一个接口 &lt;code&gt;UserMapper&lt;/code&gt; ，这是一个持久层接口（Mybatis的持久层接口规范一般都叫 XxxMapper）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;单元测试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在创建出来的SpringBoot工程中，在src下的test目录下，已经自动帮我们创建好了测试类 ，并且在测试类上已经添加了注解 &lt;code&gt;@SpringBootTest&lt;/code&gt;，代表该测试类已经与SpringBoot整合。&lt;/p&gt;
&lt;p&gt;该测试类在运行时，会自动通过引导类加载Spring的环境（IOC容器）。我们要测试那个bean对象，就可以直接通过&lt;code&gt;@Autowired&lt;/code&gt;注解直接将其注入进行，然后就可以测试了。&lt;/p&gt;
&lt;p&gt;测试类代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest
class SpringbootMybatisQuickstartApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testFindAll(){
        List&amp;lt;User&amp;gt; userList = userMapper.findAll();
        for (User user : userList) {
            System.out.println(user);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1. 配置Mybatis日志输出&lt;/h5&gt;
&lt;p&gt;默认情况下，在Mybatis中，SQL语句执行时，我们并看不到SQL语句的执行日志。 在&lt;code&gt;application.properties&lt;/code&gt;加入如下配置，即可查看日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#mybatis的配置
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;数据库连接池&lt;/h4&gt;
&lt;p&gt;数据库连接池的好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资源重用&lt;/li&gt;
&lt;li&gt;提升系统响应速度&lt;/li&gt;
&lt;li&gt;避免数据库连接遗漏&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. 增删改查操作&lt;/h4&gt;
&lt;h5&gt;1. 删除&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;需求：根据ID删除用户信息&lt;/li&gt;
&lt;li&gt;SQL：delete from user where id = 5;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据id删除
 */
@Delete(&quot;delete from user where id = #{id}&quot;)
public void deleteById(Integer id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Mybatis中，我们可以通过参数占位符号 &lt;code&gt;#{...}&lt;/code&gt; 来占位，在调用&lt;code&gt;deleteById&lt;/code&gt;方法时，传递的参数值，最终会替换占位符。&lt;/p&gt;
&lt;h5&gt;1. 新增&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;需求：添加一个用户&lt;/li&gt;
&lt;li&gt;SQL：insert into user(username,password,name,age) values(&apos;zhouyu&apos;,&apos;******&apos;,&apos;周瑜&apos;,20);&lt;/li&gt;
&lt;li&gt;Mapper接口：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 添加用户
 */
@Insert(&quot;insert into user(username,password,name,age) values(#{username},#{password},#{name},#{age})&quot;)
public void insert(User user);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在SQL语句中，我们需要传递多个参数，我们可以把多个参数封装到一个对象中。然后在SQL语句中，我们可以通过&lt;code&gt;#{对象属性名}&lt;/code&gt;的方式，获取到对象中封装的属性值。&lt;/p&gt;
&lt;h5&gt;1. 修改&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;需求：根据ID更新用户信息&lt;/li&gt;
&lt;li&gt;SQL：update user set username = &apos;zhouyu&apos;, password = &apos;******&apos;, name = &apos;周瑜&apos;, age = 20 where id = 1；&lt;/li&gt;
&lt;li&gt;Mapper接口方法：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据id更新用户信息
 */
@Update(&quot;update user set username = #{username},password = #{password},name = #{name},age = #{age} where id = #{id}&quot;)
public void update(User user);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1. 查询&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;需求：根据用户名和密码查询用户信息&lt;/li&gt;
&lt;li&gt;SQL：select * from user where username = &apos;zhouyu&apos; and password = &apos;******&apos;&lt;/li&gt;
&lt;li&gt;Mapper接口方法：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据用户名和密码查询用户信息
 */
@Select(&quot;select * from user where username = #{username} and password = #{password}&quot;)
public User findByUsernameAndPassword(@Param(&quot;username&quot;) String username, @Param(&quot;password&quot;) String password);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@param注解的作用是为接口的方法形参起名字的。（由于用户名唯一的，所以查询返回的结果最多只有一个，可以直接封装到一个对象中）&lt;/p&gt;
&lt;p&gt;**说明：**基于官方骨架创建的springboot项目中，接口编译时会保留方法形参名，@Param注解可以省略 (#{形参名})。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在Mybatis中使用XML映射文件方式开发，需要符合一定的规范：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;XML映射文件的名称与Mapper接口名称一致，并且将XML映射文件和Mapper接口放置在相同包下（同包同名）&lt;/li&gt;
&lt;li&gt;XML映射文件的namespace属性为Mapper接口全限定名一致&lt;/li&gt;
&lt;li&gt;XML映射文件中sql语句的id与Mapper接口中的方法名一致，并保持返回类型一致。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./02.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;1. XML配置文件实现&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;第1步： 创建XML映射文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./03.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./04.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第2步：编写XML映射文件&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;xml映射文件中的dtd约束，直接从mybatis官网复制即可; 或者直接AI生成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper
  PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
  &quot;https://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;
&amp;lt;mapper namespace=&quot;&quot;&amp;gt;
 
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第3步：配置&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;a. XML映射文件的namespace属性为Mapper接口全限定名&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper
        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;https://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;
&amp;lt;mapper namespace=&quot;com.itheima.mapper.UserMapper&quot;&amp;gt;

&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;b. XML映射文件中sql语句的id与Mapper接口中的方法名一致，并保持返回类型一致&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper
        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;https://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;
&amp;lt;mapper namespace=&quot;com.itheima.mapper.EmpMapper&quot;&amp;gt;

    &amp;lt;!--查询操作--&amp;gt;
    &amp;lt;select id=&quot;findAll&quot; resultType=&quot;com.itheima.pojo.User&quot;&amp;gt;
        select * from user
    &amp;lt;/select&amp;gt;
    
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;resultType 属性的值，与查询返回的单条记录封装的类型一致。&lt;/p&gt;
&lt;p&gt;运行测试类，执行结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./05.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;1. SpringBoot配置文件&lt;/h2&gt;
&lt;p&gt;我们可以来对比一下，采用 &lt;code&gt;application.properties&lt;/code&gt; 和 &lt;code&gt;application.yml&lt;/code&gt; 来配置同一段信息(数据库连接信息)，两者之间的配置对比：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./06.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./07.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1. 语法&lt;/h3&gt;
&lt;p&gt;简单的了解过springboot所支持的配置文件，以及不同类型配置文件之间的优缺点之后，接下来我们就来了解下yml配置文件的基本语法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大小写敏感&lt;/li&gt;
&lt;li&gt;数值前边必须有空格，作为分隔符&lt;/li&gt;
&lt;li&gt;使用缩进表示层级关系，缩进时，不允许使用Tab键，只能用空格（idea中会自动将Tab转换为空格）&lt;/li&gt;
&lt;li&gt;缩进的空格数目不重要，只要相同层级的元素左侧对齐即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#&lt;/code&gt;表示注释，从这个字符一直到行尾，都会被解析器忽略&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./08.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;了解完yml格式配置文件的基本语法之后，接下来我们再来看下yml文件中常见的数据格式。在这里我们主要介绍最为常见的两类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义对象或Map集合&lt;/li&gt;
&lt;li&gt;定义数组、list或set集合&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;对象/Map集合&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;user:
  name: zhangsan
  age: 18
  password: ******
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;数组/List/Set集合&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;hobby: 
  - java
  - game
  - sport
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在yml格式的配置文件中，如果配置项的值是以 0 开头的，值需要使用 &apos;&apos; 引起来，因为以0开头在yml中表示8进制的数据。&lt;/p&gt;
</content:encoded></item><item><title>Spring Boot Web 基础学习笔记</title><link>https://youki.bbroot.com/posts/java/spring-boot/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/java/spring-boot/</guid><description>整理 HTTP 请求响应、Spring Boot Web 入门、分层解耦、Bean 声明和组件扫描。</description><pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记从 HTTP 协议基础切入，逐步整理 Spring Boot Web 开发里的请求处理、响应格式和分层解耦。正文内容较长，后续精修时适合拆成多篇专题文章。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;HTTP 是基于请求-响应模型的无状态协议。&lt;/li&gt;
&lt;li&gt;Spring Boot Web 通过注解和 Controller 处理请求。&lt;/li&gt;
&lt;li&gt;后端项目通常按 Controller、Service、Mapper/DAO 分层组织。&lt;/li&gt;
&lt;li&gt;Bean 声明、组件扫描和依赖注入是 Spring 应用结构的基础。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Springboot&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;阿里云提供的脚手架，网址为：https://start.aliyun.com&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1. HTTP特点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基于TCP协议:&lt;/strong&gt; 面向连接，安全&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;TCP是一种面向连接的(建立连接之前是需要经过三次握手)、可靠的、基于字节流的传输层通信协议，在数据传输方面更安全&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基于请求-响应模型:&lt;/strong&gt;   一次请求对应一次响应（先请求后响应）&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;请求和响应是一一对应关系，没有请求，就没有响应&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HTTP协议是无状态协议:&lt;/strong&gt;  对于数据没有记忆能力。每次请求-响应都是独立的&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;无状态指的是客户端发送HTTP请求给服务端之后，服务端根据请求响应数据，响应完后，不会记录任何信息。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;缺点:  多次请求间不能共享数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优点:  速度快&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;请求之间无法共享数据会引发的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如：京东购物。加入购物车和去购物车结算是两次请求&lt;/li&gt;
&lt;li&gt;由于HTTP协议的无状态特性，加入购物车请求响应结束后，并未记录加入购物车是何商品&lt;/li&gt;
&lt;li&gt;发起去购物车结算的请求后，因为无法获取哪些商品加入了购物车，会导致此次请求无法正确展示数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;具体使用的时候，我们发现京东是可以正常展示数据的，原因是Java早已考虑到这个问题，并提出了使用会话技术(Cookie、Session)来解决这个问题。具体如何来做，我们后面课程中会讲到。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;HTTP协议又分为：请求协议和响应协议&lt;/p&gt;
&lt;h4&gt;1. HTTP请求协议&lt;/h4&gt;
&lt;h5&gt;1. 介绍&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求协议&lt;/strong&gt;：**浏览器将数据以请求格式发送到服务器。包括：**请求行、请求头 、请求体&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;- &lt;strong&gt;GET方式的请求协议：&lt;/strong&gt;&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;请求行&lt;/strong&gt; ：HTTP请求中的第一行数据。由：&lt;code&gt;请求方式&lt;/code&gt;、&lt;code&gt;资源路径&lt;/code&gt;、&lt;code&gt;协议/版本&lt;/code&gt;组成（之间使用空格分隔）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;请求头&lt;/strong&gt; ：第二行开始，上图黄色部分内容就是请求头。格式为key: value形式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;http是个无状态的协议，所以在请求头设置浏览器的一些自身信息和想要响应的形式。这样服务器在收到信息后，就可以知道是谁，想干什么了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常见的HTTP请求头有:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;请求头&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;表示请求的主机名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User-Agent&lt;/td&gt;
&lt;td&gt;浏览器版本。 例如：Chrome浏览器的标识类似Mozilla/5.0 ...Chrome/79 ，IE浏览器的标识类似Mozilla/5.0 (Windows NT ...)like Gecko&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept&lt;/td&gt;
&lt;td&gt;表示浏览器能接收的资源类型，如text/*，image/&lt;em&gt;或者&lt;/em&gt;/*表示所有；&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept-Language&lt;/td&gt;
&lt;td&gt;表示浏览器偏好的语言，服务器可以据此返回不同语言的网页；&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept-Encoding&lt;/td&gt;
&lt;td&gt;表示浏览器可以支持的压缩类型，例如gzip, deflate等。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Type&lt;/td&gt;
&lt;td&gt;请求主体的数据类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Length&lt;/td&gt;
&lt;td&gt;数据主体的大小（单位：字节）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;strong&gt;POST方式的请求协议：&lt;/strong&gt;&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求行&lt;/strong&gt;：包含请求方式、资源路径、协议/版本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求头&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求体&lt;/strong&gt;：存储请求参数
&lt;ul&gt;
&lt;li&gt;请求体和请求头之间是有一个空行隔开（作用：用于标记请求头结束）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GET请求和POST请求的区别：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;区别方式&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;GET请求&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;POST请求&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;请求参数&lt;/td&gt;
&lt;td&gt;请求参数在请求行中。&amp;lt;br/&amp;gt;例：/brand/findAll?name=OPPO&amp;amp;status=1&lt;/td&gt;
&lt;td&gt;请求参数在请求体中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;请求参数长度&lt;/td&gt;
&lt;td&gt;请求参数长度有限制(浏览器不同限制也不同)&lt;/td&gt;
&lt;td&gt;请求参数长度没有限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全性&lt;/td&gt;
&lt;td&gt;安全性低。原因：请求参数暴露在浏览器地址栏中。&lt;/td&gt;
&lt;td&gt;安全性相对高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;2.  HTTP响应协议&lt;/h4&gt;
&lt;h5&gt;1. 格式介绍&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;响应协议：服务器将数据以响应格式返回给浏览器。包括：&lt;strong&gt;响应行 、响应头 、响应体&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;响应行：响应数据的第一行。响应行由&lt;code&gt;协议及版本&lt;/code&gt;、&lt;code&gt;响应状态码&lt;/code&gt;、&lt;code&gt;状态码描述&lt;/code&gt;组成&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;协议/版本：HTTP/1.1&lt;/li&gt;
&lt;li&gt;响应状态码：200&lt;/li&gt;
&lt;li&gt;状态码描述：OK&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;响应头(以上图中黄色部分)：响应数据的第二行开始。格式为key：value形式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;http是个无状态的协议，所以可以在请求头和响应头中设置一些信息和想要执行的动作，这样，对方在收到信息后，就可以知道你是谁，你想干什么&lt;/li&gt;
&lt;li&gt;常见的HTTP响应头有:&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;		Content-Type：表示该响应内容的类型，例如text/html，image/jpeg ；
		
		Content-Length：表示该响应内容的长度（字节数）；
		
		Content-Encoding：表示该响应压缩算法，例如gzip ；
		
		Cache-Control：指示客户端应如何缓存，例如max-age=300表示可以最多缓存300秒 ;
		
		Set-Cookie: 告诉浏览器为当前页面所在的域设置cookie ;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;响应体(以上图中绿色部分)： 响应数据的最后一部分。存储响应的数据
&lt;ul&gt;
&lt;li&gt;响应体和响应头之间有一个空行隔开（作用：用于标记响应头结束）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;响应状态码&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态码分类&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1xx&lt;/td&gt;
&lt;td&gt;响应中 --- 临时状态码。表示请求已经接受，告诉客户端应该继续请求或者如果已经完成则忽略&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2xx&lt;/td&gt;
&lt;td&gt;成功 --- 表示请求已经被成功接收，处理已完成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3xx&lt;/td&gt;
&lt;td&gt;重定向 --- 重定向到其它地方，让客户端再发起一个请求以完成整个处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4xx&lt;/td&gt;
&lt;td&gt;客户端错误 --- 处理发生错误，责任在客户端，如：客户端的请求一个不存在的资源，客户端未被授权，禁止访问等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5xx&lt;/td&gt;
&lt;td&gt;服务器端错误 --- 处理发生错误，责任在服务端，如：服务端抛出异常，路由出错，HTTP版本不支持等&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;关于响应状态码，我们先主要认识三个状态码，其余的等后期用到了再去掌握：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;200 ok&lt;/code&gt;   客户端请求成功&lt;/li&gt;
&lt;li&gt;&lt;code&gt;404 Not Found&lt;/code&gt;  请求资源不存在&lt;/li&gt;
&lt;li&gt;&lt;code&gt;500 Internal Server Error&lt;/code&gt;  服务端发生不可预期的错误&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;设置响应数据&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;以下内容了解即可，实际开发中较少直接使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Web服务器对HTTP协议的响应数据进行了封装(HttpServletResponse)，并在调用Controller方法的时候传递给了该方法。这样，就使得程序员不必直接对协议进行操作，让Web开发更加便捷。&lt;/p&gt;
&lt;p&gt;代码演示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.itheima;

import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
public class ResponseController {

    @RequestMapping(&quot;/response&quot;)
    public void response(HttpServletResponse response) throws IOException {
        //1.设置响应状态码
        response.setStatus(401);
        //2.设置响应头
        response.setHeader(&quot;name&quot;,&quot;itcast&quot;);
        //3.设置响应体
        response.setContentType(&quot;text/html;charset=utf-8&quot;);
        response.setCharacterEncoding(&quot;utf-8&quot;);
        response.getWriter().write(&quot;&amp;lt;h1&amp;gt;hello response&amp;lt;/h1&amp;gt;&quot;);
    }

    @RequestMapping(&quot;/response2&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; response2(HttpServletResponse response) throws IOException {
        return ResponseEntity
                .status(401)
                .header(&quot;name&quot;,&quot;itcast&quot;)
                .body(&quot;&amp;lt;h1&amp;gt;hello response&amp;lt;/h1&amp;gt;&quot;);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. SpringBootWeb&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;SpringBoot 的自动配置原理较复杂，核心是 @SpringBootApplication 注解的组合使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;@ResponseBody注解：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类型：方法注解、类注解&lt;/li&gt;
&lt;li&gt;位置：书写在Controller方法上或类上&lt;/li&gt;
&lt;li&gt;作用：将方法返回值直接响应给浏览器，如果返回值类型是实体对象/集合，将会转换为JSON格式后在响应给浏览器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是在我们所书写的Controller中，只在类上添加了@RestController注解、方法添加了@RequestMapping注解，并没有使用@ResponseBody注解，怎么给浏览器响应呢？&lt;/p&gt;
&lt;p&gt;这是因为，我们在类上加了@RestController注解，而这个注解是由两个注解组合起来的，分别是：@Controller 、@ResponseBody。 那也就意味着，我们在类上已经添加了@ResponseBody注解了，而一旦在类上加了@ResponseBody注解，就相当于该类所有的方法中都已经添加了@ResponseBody注解。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：前后端分离的项目中，一般直接在请求处理类上加@RestController注解，就无需在方法上加@ResponseBody注解了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1. 分层解耦&lt;/h4&gt;
&lt;h5&gt;1. 三层架构&lt;/h5&gt;
&lt;p&gt;在我们进行程序设计以及程序开发时，尽可能让每一个接口、类、方法的职责更单一些（单一职责原则）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;单一职责原则：一个类或一个方法，就只做一件事情，只管一块功能。&lt;/p&gt;
&lt;p&gt;这样就可以让类、接口、方法的复杂度更低，可读性更强，扩展性更好，也更利于后期的维护。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在我们项目开发中呢，可以将代码分为三层，如图所示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller：控制层。接收前端发送的请求，对请求进行处理，并响应数据。&lt;/li&gt;
&lt;li&gt;Service：业务逻辑层。处理具体的业务逻辑。&lt;/li&gt;
&lt;li&gt;Dao：数据访问层(Data Access Object)，也称为持久层。负责数据访问操作，包括数据的增、删、改、查。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;基于三层架构的程序执行流程，如图所示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前端发起的请求，由Controller层接收（Controller响应数据给前端）&lt;/li&gt;
&lt;li&gt;Controller层调用Service层来进行逻辑处理（Service层处理完后，把处理结果返回给Controller层）&lt;/li&gt;
&lt;li&gt;Serivce层调用Dao层（逻辑处理过程中需要用到的一些数据要从Dao层获取）&lt;/li&gt;
&lt;li&gt;Dao层操作文件中的数据（Dao拿到的数据会返回给Service层）&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;更多分层解耦实践请参考官方文档。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;2. 分层解耦&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;软件设计原则：高内聚低耦合。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;**高内聚：**指的是一个模块中各个元素之间的联系的紧密程度，如果各个元素(语句、程序段)之间的联系程度越高，则内聚性越高，即 &quot;高内聚&quot;。&lt;/p&gt;
&lt;p&gt;**低耦合：**指的是软件中各个层、模块之间的依赖关联程序越低越好。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;解耦思路&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;之前我们在编写代码时，需要什么对象，就直接new一个就可以了。 这种做法呢，层与层之间代码就耦合了，当service层的实现变了之后， 我们还需要修改controller层的代码。
那应该怎么解耦呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1). 首先不能在EmpController中使用new对象。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2). 将要用到的对象交给一个容器管理。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3). 应用程序中用到这个对象，就直接从容器中获取&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那问题来了，我们如何将对象交给容器管理呢？ 程序运行时，容器如何为程序提供依赖的对象呢？&lt;/p&gt;
&lt;p&gt;我们想要实现上述解耦操作，就涉及到Spring中的两个核心概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;控制反转：&lt;/strong&gt; Inversion Of Control，简称&lt;strong&gt;IOC&lt;/strong&gt;。对象的创建控制权由程序自身转移到外部（容器），这种思想称为控制反转。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为：IOC容器或Spring容器。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;依赖注入：&lt;/strong&gt; Dependency Injection，简称&lt;strong&gt;DI&lt;/strong&gt;。容器为应用程序提供运行时，所依赖的资源，称之为依赖注入。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序运行时需要某个资源，此时容器就为其提供这个资源。&lt;/li&gt;
&lt;li&gt;例：EmpController程序运行时需要EmpService对象，Spring容器就为其提供并注入EmpService对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**bean对象：**IOC容器中创建、管理的对象，称之为：bean对象。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. IOC&amp;amp;DI入门&lt;/h3&gt;
&lt;h3&gt;1. IOC&amp;amp;DI入门&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1). 将Service及Dao层的实现类，交给IOC容器管理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在实现类加上 &lt;code&gt;@Component&lt;/code&gt; 注解，就代表把当前类产生的对象交给IOC容器管理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A. UserDaoImpl&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class UserDaoImpl implements UserDao {
    @Override
    public List&amp;lt;String&amp;gt; findAll() {
        InputStream in = this.getClass().getClassLoader().getResourceAsStream(&quot;user.txt&quot;);
        ArrayList&amp;lt;String&amp;gt; lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList&amp;lt;&amp;gt;());
        return lines;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;B. UserServiceImpl&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    @Override
    public List&amp;lt;User&amp;gt; findAll() {
        List&amp;lt;String&amp;gt; lines = userDao.findAll();
        List&amp;lt;User&amp;gt; userList = lines.stream().map(line -&amp;gt; {
            String[] parts = line.split(&quot;,&quot;);
            Integer id = Integer.parseInt(parts[0]);
            String username = parts[1];
            String password = parts[2];
            String name = parts[3];
            Integer age = Integer.parseInt(parts[4]);
            LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;));
            return new User(id, username, password, name, age, updateTime);
        }).collect(Collectors.toList());
        return userList;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2). 为Controller 及 Service注入运行时所依赖的对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A. UserServiceImpl&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;
    
    @Override
    public List&amp;lt;User&amp;gt; findAll() {
        List&amp;lt;String&amp;gt; lines = userDao.findAll();
        List&amp;lt;User&amp;gt; userList = lines.stream().map(line -&amp;gt; {
            String[] parts = line.split(&quot;,&quot;);
            Integer id = Integer.parseInt(parts[0]);
            String username = parts[1];
            String password = parts[2];
            String name = parts[3];
            Integer age = Integer.parseInt(parts[4]);
            LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;));
            return new User(id, username, password, name, age, updateTime);
        }).collect(Collectors.toList());
        return userList;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;B. UserController&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class UserController {
    
    @Autowired
    private UserService userService;

    @RequestMapping(&quot;/list&quot;)
    public List&amp;lt;User&amp;gt; list(){
        //1.调用Service
        List&amp;lt;User&amp;gt; userList = userService.findAll();
        //2.响应数据
        return userList;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动服务，运行测试。 打开浏览器，地址栏直接访问：http://localhost:8080/user.html 。 依然正常访问，就说明入门程序完成了。 已经完成了层与层之间的解耦。&lt;/p&gt;
&lt;h3&gt;1. IOC详解&lt;/h3&gt;
&lt;p&gt;通过IOC和DI的入门程序呢，我们已经基本了解了IOC和DI的基础操作。接下来呢，我们学习下IOC控制反转和DI依赖注入的细节。&lt;/p&gt;
&lt;h5&gt;4.3.4.1 Bean的声明&lt;/h5&gt;
&lt;p&gt;前面我们提到IOC控制反转，就是将对象的控制权交给Spring的IOC容器，由IOC容器创建及管理对象。IOC容器创建的对象称为bean对象。&lt;/p&gt;
&lt;p&gt;在之前的入门案例中，要把某个对象交给IOC容器管理，需要在类上添加一个注解：&lt;strong&gt;&lt;code&gt;@Component&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而Spring框架为了更好的标识web应用程序开发当中，bean对象到底归属于哪一层，又提供了@Component的衍生注解：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注解&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;位置&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;@Component&lt;/td&gt;
&lt;td&gt;声明bean的基础注解&lt;/td&gt;
&lt;td&gt;不属于以下三类时，用此注解&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Controller&lt;/td&gt;
&lt;td&gt;@Component的衍生注解&lt;/td&gt;
&lt;td&gt;标注在控制层类上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Service&lt;/td&gt;
&lt;td&gt;@Component的衍生注解&lt;/td&gt;
&lt;td&gt;标注在业务层类上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Repository&lt;/td&gt;
&lt;td&gt;@Component的衍生注解&lt;/td&gt;
&lt;td&gt;标注在数据访问层类上（由于与mybatis整合，用的少）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;那么此时，我们就可以使用 &lt;code&gt;@Service&lt;/code&gt; 注解声明Service层的bean。 使用 &lt;code&gt;@Repository&lt;/code&gt; 注解声明Dao层的bean。 代码实现如下：&lt;/p&gt;
&lt;p&gt;Service层:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    @Override
    public List&amp;lt;User&amp;gt; findAll() {
        List&amp;lt;String&amp;gt; lines = userDao.findAll();
        List&amp;lt;User&amp;gt; userList = lines.stream().map(line -&amp;gt; {
            String[] parts = line.split(&quot;,&quot;);
            Integer id = Integer.parseInt(parts[0]);
            String username = parts[1];
            String password = parts[2];
            String name = parts[3];
            Integer age = Integer.parseInt(parts[4]);
            LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;));
            return new User(id, username, password, name, age, updateTime);
        }).collect(Collectors.toList());
        return userList;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dao层:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Repository
public class UserDaoImpl implements UserDao {
    @Override
    public List&amp;lt;String&amp;gt; findAll() {
        InputStream in = this.getClass().getClassLoader().getResourceAsStream(&quot;user.txt&quot;);
        ArrayList&amp;lt;String&amp;gt; lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList&amp;lt;&amp;gt;());
        return lines;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意1&lt;/strong&gt;：声明bean的时候，可以通过注解的value属性指定bean的名字，如果没有指定，默认为类名首字母小写。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意2&lt;/strong&gt;：使用以上四个注解都可以声明bean，但是在springboot集成web开发中，声明控制器bean只能用@Controller。&lt;/p&gt;
&lt;h5&gt;4.3.4.2 组件扫描&lt;/h5&gt;
&lt;p&gt;问题：使用前面学习的四个注解声明的bean，一定会生效吗？&lt;/p&gt;
&lt;p&gt;答案：不一定。（原因：bean想要生效，还需要被组件扫描）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前面声明bean的四大注解，要想生效，还需要被组件扫描注解 &lt;code&gt;@ComponentScan&lt;/code&gt; 扫描。&lt;/li&gt;
&lt;li&gt;该注解虽然没有显式配置，但是实际上已经包含在了启动类声明注解 &lt;code&gt;@SpringBootApplication&lt;/code&gt; 中，默认扫描的范围是启动类所在包及其子包。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，我们在项目开发中，只需要按照如上项目结构，将项目中的所有的业务类，都放在启动类所在包的子包中，就无需考虑组件扫描问题。&lt;/p&gt;
&lt;h3&gt;1. DI详解&lt;/h3&gt;
&lt;p&gt;上一小节我们讲解了控制反转IOC的细节，接下来呢，我们学习依赖注解DI的细节。&lt;/p&gt;
&lt;p&gt;依赖注入，是指IOC容器要为应用程序去提供运行时所依赖的资源，而资源指的就是对象。&lt;/p&gt;
&lt;p&gt;在入门程序案例中，我们使用了@Autowired这个注解，完成了依赖注入的操作，而这个Autowired翻译过来叫：自动装配。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Autowired&lt;/code&gt;注解，默认是按照&lt;strong&gt;类型&lt;/strong&gt;进行自动装配的（去IOC容器中找某个类型的对象，然后完成注入操作）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;入门程序举例：在EmpController运行的时候，就要到IOC容器当中去查找EmpService这个类型的对象，而我们的IOC容器中刚好有一个EmpService这个类型的对象，所以就找到了这个类型的对象完成注入操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1. @Autowired用法&lt;/h4&gt;
&lt;p&gt;@Autowired 进行依赖注入，常见的方式，有如下三种：&lt;/p&gt;
&lt;p&gt;1). 属性注入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class UserController {

    //方式一: 属性注入
    @Autowired
    private UserService userService;
    
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：代码简洁、方便快速开发。&lt;/li&gt;
&lt;li&gt;缺点：隐藏了类之间的依赖关系、可能会破坏类的封装性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2). 构造函数注入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class UserController {

    //方式二: 构造器注入
    private final UserService userService;
    
    @Autowired //如果当前类中只存在一个构造函数, @Autowired可以省略
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
 }   
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：能清晰地看到类的依赖关系、提高了代码的安全性。&lt;/li&gt;
&lt;li&gt;缺点：代码繁琐、如果构造参数过多，可能会导致构造函数臃肿。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意：如果只有一个构造函数，@Autowired注解可以省略。（通常来说，也只有一个构造函数）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3). setter注入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 用户信息Controller
 */
@RestController
public class UserController {
    
    //方式三: setter注入
    private UserService userService;
    
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    
}    
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：保持了类的封装性，依赖关系更清晰。&lt;/li&gt;
&lt;li&gt;缺点：需要额外编写setter方法，增加了代码量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在项目开发中，基于@Autowired进行依赖注入时，基本都是第一种和第二种方式。（官方推荐第二种方式，因为会更加规范）但是在企业项目开发中，很多的项目中，也会选择第一种方式因为更加简洁、高效（在规范性方面进行了妥协）。&lt;/p&gt;
&lt;h4&gt;1. 注意事项&lt;/h4&gt;
&lt;p&gt;那如果在IOC容器中，存在多个相同类型的bean对象，会出现什么情况呢？&lt;/p&gt;
&lt;p&gt;在下面的例子中，我们准备了两个UserService的实现类，并且都交给了IOC容器管理。 代码如下：&lt;/p&gt;
&lt;p&gt;此时，我们启动项目会发现，控制台报错了：&lt;/p&gt;
&lt;p&gt;出现错误的原因呢，是因为在Spring的容器中，UserService这个类型的bean存在两个，框架不知道具体要注入哪个bean使用，所以就报错了。&lt;/p&gt;
&lt;p&gt;如何解决上述问题呢？Spring提供了以下几种解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@Primary&lt;/li&gt;
&lt;li&gt;@Qualifier&lt;/li&gt;
&lt;li&gt;@Resource&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;方案一：使用@Primary注解&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当存在多个相同类型的Bean注入时，加上@Primary注解，来确定默认的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Primary
@Service
public class UserServiceImpl implements UserService {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方案二：使用@Qualifier注解&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;指定当前要注入的bean对象。 在@Qualifier的value属性中，指定注入的bean的名称。 @Qualifier注解不能单独使用，必须配合@Autowired使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class UserController {

    @Qualifier(&quot;userServiceImpl&quot;)
    @Autowired
    private UserService userService;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方案三：使用@Resource注解&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class UserController {
        
    @Resource(name = &quot;userServiceImpl&quot;)
    private UserService userService;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;面试题：@Autowird 与 @Resource的区别&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@Autowired 是spring框架提供的注解，而@Resource是JDK提供的注解&lt;/li&gt;
&lt;li&gt;@Autowired 默认是按照类型注入，而@Resource是按照名称注入&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;附录：常见状态码&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态码&lt;/th&gt;
&lt;th&gt;英文描述&lt;/th&gt;
&lt;th&gt;解释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;客户端请求成功，即处理成功，这是我们最想看到的状态码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;302&lt;/td&gt;
&lt;td&gt;Found&lt;/td&gt;
&lt;td&gt;指示所请求的资源已移动到由Location响应头给定的 URL，浏览器会自动重新访问到这个页面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;304&lt;/td&gt;
&lt;td&gt;Not Modified&lt;/td&gt;
&lt;td&gt;告诉客户端，你请求的资源至上次取得后，服务端并未更改，你直接用你本地缓存吧。隐式重定向&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;Bad Request&lt;/td&gt;
&lt;td&gt;客户端请求有语法错误，不能被服务器所理解&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;Forbidden&lt;/td&gt;
&lt;td&gt;服务器收到请求，但是拒绝提供服务，比如：没有权限访问相关资源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;Not Found&lt;/td&gt;
&lt;td&gt;请求资源不存在，一般是URL输入有误，或者网站资源被删除了&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;405&lt;/td&gt;
&lt;td&gt;Method Not Allowed&lt;/td&gt;
&lt;td&gt;请求方式有误，比如应该用GET请求方式的资源，用了POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;428&lt;/td&gt;
&lt;td&gt;Precondition Required&lt;/td&gt;
&lt;td&gt;服务器要求有条件的请求，告诉客户端要想访问该资源，必须携带特定的请求头&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;Too Many Requests&lt;/td&gt;
&lt;td&gt;指示用户在给定时间内发送了太多请求（“限速”），配合 Retry-After(多长时间后可以请求)响应头一起使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;431&lt;/td&gt;
&lt;td&gt;Request Header Fields Too Large&lt;/td&gt;
&lt;td&gt;请求头太大，服务器不愿意处理请求，因为它的头部字段太大。请求可以在减少请求头域的大小后重新提交。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;Internal Server Error&lt;/td&gt;
&lt;td&gt;服务器发生不可预期的错误。服务器出异常了，赶紧看日志去吧&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;td&gt;Service Unavailable&lt;/td&gt;
&lt;td&gt;服务器尚未准备好处理请求，服务器刚刚启动，还未初始化好&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;状态码大全：https://cloud.tencent.com/developer/chapter/13553&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;这篇笔记覆盖了 Spring Boot Web 入门中最常见的一组基础知识：HTTP 请求响应、分层结构、依赖注入和状态码。后续可以拆成协议基础、Controller 编写和项目分层三篇来精修。&lt;/p&gt;
</content:encoded></item><item><title>JavaScript 基础语法笔记</title><link>https://youki.bbroot.com/posts/frontend/js-basics/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/frontend/js-basics/</guid><description>整理 JavaScript 变量、输出语句、函数、自定义对象、JSON 和 DOM 基础。</description><pubDate>Sat, 03 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记记录 JavaScript 入门阶段的基础语法，包括变量声明、函数写法、对象、JSON 和 DOM 操作。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;let&lt;/code&gt; 用于声明变量，&lt;code&gt;const&lt;/code&gt; 用于声明常量。&lt;/li&gt;
&lt;li&gt;函数可以用普通函数、函数表达式和箭头函数声明。&lt;/li&gt;
&lt;li&gt;对象和 JSON 是前端处理结构化数据的基础。&lt;/li&gt;
&lt;li&gt;DOM 操作可以让脚本读取和修改页面元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;js基础&lt;/h3&gt;
&lt;h4&gt;1. 变量/常量&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;特点：js是弱类型语言，变量可以存放不同类型的值&lt;/li&gt;
&lt;li&gt;声明：
&lt;ul&gt;
&lt;li&gt;let：声明变量&lt;/li&gt;
&lt;li&gt;const:声明常量，一旦声明，常量的值不能改变&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注意：
&lt;ul&gt;
&lt;li&gt;在早期的js中，声明变量还可以使用var,但是并不严谨（不推荐）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 输出语句&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;window.alert()：弹出警告框（使用频次较高）&lt;/li&gt;
&lt;li&gt;console.log()：写入浏览器控制台（使用频次较高）&lt;/li&gt;
&lt;li&gt;document.write()：向HTML的body内输出内容&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 函数&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;格式一&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;function 函数名(参数1,参数2..){
    要执行的代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;格式二：&lt;strong&gt;匿名函数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数表达式&lt;/li&gt;
&lt;li&gt;箭头函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 函数表达式
var add = function (a,b){
    return a + b;
}

// 箭头函数
var add = (a,b) =&amp;gt; {
    return a + b;
}

// 上述匿名函数声明好了之后，是将这个函数赋值给了add变量。 那我们就可以直接通过add函数直接调用，调用代码如下：
let result = add(10,20);
alert(result);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 自定义对象&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;let 对象名 = {
    属性名1: 属性值1,
    属性名2: 属性值2,
    属性名3: 属性值3,
    方法名称: function(形参列表){}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    //自定义对象
    let user = {
        name: &quot;Tom&quot;,
        age: 10,
        gender: &quot;男&quot;,
        sing: function(){
             console.log(&quot;悠悠的唱着最炫的民族风~&quot;);
         }
    }

    console.log(user.name);
    user.sing();
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：在定义对象中的方法时，尽量不要使用箭头函数（this）。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;5. JSON&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;JSON对象：JavaScript Object Notation，JavaScript对象标记法。JSON是通过JavaScript标记法书写的文本。
而由于语法简单，层级结构鲜明，现多用于作为数据载体，在网络中进行数据传输。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代码演示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//3. JSON - JS对象标记法
let person = {
  name: &apos;itcast&apos;,
  age: 18,
  gender: &apos;男&apos;
}
alert(JSON.stringify(person)); //js对象 --&amp;gt; json字符串

let personJson = &apos;{&quot;name&quot;: &quot;heima&quot;, &quot;age&quot;: 18}&apos;;
alert(JSON.parse(personJson).name);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6. JS DOM&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;DOM：Document Object Model 文档对象模型。也就是 JavaScript 将 HTML 文档的各个组成部分封装为对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;作用&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;改变 HTML 元素的内容&lt;/li&gt;
&lt;li&gt;改变 HTML 元素的样式（CSS）&lt;/li&gt;
&lt;li&gt;对 HTML DOM 事件作出反应&lt;/li&gt;
&lt;li&gt;添加和删除 HTML 元素&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;DOM操作&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DOM的核心思想：将网页的内容当做对象来处理，标签的所有属性在该对象上都可以找到，并且修改这个对象的属性，就会自动映射到标签身上。&lt;/li&gt;
&lt;li&gt;document对象
&lt;ul&gt;
&lt;li&gt;网页中所有内容都封装在document对象中&lt;/li&gt;
&lt;li&gt;它提供的属性和方法都是用来访问和操作网页内容的，如：document.write(…)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DOM操作步骤:
&lt;ul&gt;
&lt;li&gt;获取DOM元素对象&lt;/li&gt;
&lt;li&gt;操作DOM对象的属性或方法 (查阅文档)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码演示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
  &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
  &amp;lt;title&amp;gt;JS-DOM&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

  &amp;lt;h1 id=&quot;title1&quot;&amp;gt;11111&amp;lt;/h1&amp;gt;
  &amp;lt;h1&amp;gt;22222&amp;lt;/h1&amp;gt;
  &amp;lt;h1&amp;gt;33333&amp;lt;/h1&amp;gt;

  &amp;lt;script&amp;gt;
    //1. 修改第一个h1标签中的文本内容
    //1.1 获取DOM对象
    // let h1 = document.querySelector(&apos;#title1&apos;);
    //let h1 = document.querySelector(&apos;h1&apos;); // 获取第一个h1标签

    let hs = document.querySelectorAll(&apos;h1&apos;);

    //1.2 调用DOM对象中属性或方法
    hs[0].innerHTML = &apos;修改后的文本内容&apos;;
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;JavaScript 基础需要先掌握变量、函数、对象和 DOM 操作。后续精修时，可以把示例统一成更完整的小案例，避免只停留在语法片段。&lt;/p&gt;
</content:encoded></item><item><title>JavaScript 事件监听笔记</title><link>https://youki.bbroot.com/posts/frontend/js-events/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/frontend/js-events/</guid><description>整理 HTML 事件、addEventListener 语法、常见事件类型和表单交互。</description><pubDate>Sat, 03 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记整理 JavaScript 事件监听的基础概念和常见事件。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;HTML 事件是发生在页面元素上的交互行为。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addEventListener&lt;/code&gt; 可以把事件和处理函数绑定起来。&lt;/li&gt;
&lt;li&gt;常见事件包括点击、鼠标移入移出、键盘输入、聚焦和表单提交。&lt;/li&gt;
&lt;li&gt;事件监听常用于表单校验、按钮交互和动态页面行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;JS 事件监听&lt;/h3&gt;
&lt;h4&gt;事件介绍&lt;/h4&gt;
&lt;p&gt;什么是事件呢？HTML事件是发生在HTML元素上的 “事情”，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按钮被点击&lt;/li&gt;
&lt;li&gt;鼠标移到元素上&lt;/li&gt;
&lt;li&gt;输入框失去焦点&lt;/li&gt;
&lt;li&gt;按下键盘按键&lt;/li&gt;
&lt;li&gt;........&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而我们可以给这些事件绑定函数，当事件触发时，可以自动的完成对应的功能，这就是&lt;strong&gt;事件监听&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;例如：对于我们所说的百度注册页面，我们给用户名输入框的失去焦点事件绑定函数，当我们用户输入完内容，在标签外点击了鼠标，对于用户名输入框来说，失去焦点，然后执行绑定的函数，函数进行用户名内容的校验等操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;事件监听语法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;事件源.addEventListener(&apos;事件类型&apos;, 要执行的函数);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;演示&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
    &amp;lt;title&amp;gt;JS-事件-事件绑定&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
    &amp;lt;input type=&quot;button&quot; id=&quot;btn1&quot; value=&quot;点我一下1&quot;&amp;gt;
    &amp;lt;input type=&quot;button&quot; id=&quot;btn2&quot; value=&quot;点我一下2&quot;&amp;gt;

    &amp;lt;script&amp;gt;
        document.querySelector(&quot;#btn1&quot;).addEventListener(&apos;click&apos;, ()=&amp;gt;{
            alert(&quot;按钮1被点击了...&quot;);
        })
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;常见事件&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;

&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
    &amp;lt;title&amp;gt;JS-事件-常见事件&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
    &amp;lt;form action=&quot;&quot; style=&quot;text-align: center;&quot;&amp;gt;
        &amp;lt;input type=&quot;text&quot; name=&quot;username&quot; id=&quot;username&quot;&amp;gt;
        &amp;lt;input type=&quot;text&quot; name=&quot;age&quot; id=&quot;age&quot;&amp;gt;
        &amp;lt;input id=&quot;b1&quot; type=&quot;submit&quot; value=&quot;提交&quot;&amp;gt;
        &amp;lt;input id=&quot;b2&quot; type=&quot;button&quot; value=&quot;单击事件&quot;&amp;gt;
    &amp;lt;/form&amp;gt;

    &amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;

    &amp;lt;table width=&quot;800px&quot; border=&quot;1&quot; cellspacing=&quot;0&quot; align=&quot;center&quot;&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;th&amp;gt;学号&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;姓名&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;分数&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;评语&amp;lt;/th&amp;gt;
        &amp;lt;/tr&amp;gt;
        &amp;lt;tr align=&quot;center&quot;&amp;gt;
            &amp;lt;td&amp;gt;001&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;张三&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;90&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;很优秀&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
        &amp;lt;tr align=&quot;center&quot; id=&quot;last&quot;&amp;gt;
            &amp;lt;td&amp;gt;002&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;李四&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;92&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;优秀&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
    &amp;lt;/table&amp;gt;
    
    &amp;lt;script&amp;gt;
        //click: 鼠标点击事件
        document.querySelector(&apos;#b2&apos;).addEventListener(&apos;click&apos;, () =&amp;gt; {
            console.log(&quot;我被点击了...&quot;);
        })
        
        //mouseenter: 鼠标移入
        document.querySelector(&apos;#last&apos;).addEventListener(&apos;mouseenter&apos;, () =&amp;gt; {
            console.log(&quot;鼠标移入了...&quot;);
        })

        //mouseleave: 鼠标移出
        document.querySelector(&apos;#last&apos;).addEventListener(&apos;mouseleave&apos;, () =&amp;gt; {
            console.log(&quot;鼠标移出了...&quot;);
        })

        //keydown: 某个键盘的键被按下
        document.querySelector(&apos;#username&apos;).addEventListener(&apos;keydown&apos;, () =&amp;gt; {
            console.log(&quot;键盘被按下了...&quot;);
        })

        //keyup: 某个键盘的键被抬起
        document.querySelector(&apos;#username&apos;).addEventListener(&apos;keyup&apos;, () =&amp;gt; {
            console.log(&quot;键盘被抬起了...&quot;);
        })

        //blur: 失去焦点事件
        document.querySelector(&apos;#age&apos;).addEventListener(&apos;blur&apos;, () =&amp;gt; {
            console.log(&quot;失去焦点...&quot;);
        })

        //focus: 元素获得焦点
        document.querySelector(&apos;#age&apos;).addEventListener(&apos;focus&apos;, () =&amp;gt; {
            console.log(&quot;获得焦点...&quot;);
        })

        //input: 用户输入时触发
        document.querySelector(&apos;#age&apos;).addEventListener(&apos;input&apos;, () =&amp;gt; {
            console.log(&quot;用户输入时触发...&quot;);
        })

        //submit: 提交表单事件
        document.querySelector(&apos;form&apos;).addEventListener(&apos;submit&apos;, () =&amp;gt; {
            alert(&quot;表单被提交了...&quot;);
        })
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;

&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;事件监听是页面交互的入口。理解事件源、事件类型和回调函数之后，表单校验、按钮响应、键盘输入等常见交互都会更容易组织。&lt;/p&gt;
</content:encoded></item><item><title>JUnit 单元测试学习笔记</title><link>https://youki.bbroot.com/posts/java/junit/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/java/junit/</guid><description>整理测试阶段划分、JUnit 入门、参数化测试、断言和常见注解。</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记整理 Java 后端单元测试的基础概念和 JUnit 常用写法。当前仍保留课堂笔记结构，后续可以补充更完整的测试命名规范和典型业务场景。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;软件测试可分为单元测试、集成测试、系统测试和验收测试。&lt;/li&gt;
&lt;li&gt;JUnit 可以让测试代码与业务代码分离，并支持自动化运行。&lt;/li&gt;
&lt;li&gt;常用能力包括参数化测试、断言、生命周期注解和依赖范围配置。&lt;/li&gt;
&lt;li&gt;单元测试重点验证最小业务单元的正确性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 单元测试&lt;/h3&gt;
&lt;h4&gt;1.介绍&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;测试：是一种用来促进鉴定软件的正确性、完整性、安全性和质量的过程。&lt;/li&gt;
&lt;li&gt;阶段划分：单元测试、集成测试、系统测试、验收测试。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1). 单元测试&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;介绍：对软件的基本组成单位进行测试，最小测试单位。&lt;/li&gt;
&lt;li&gt;目的：检验软件基本组成单位的正确性。&lt;/li&gt;
&lt;li&gt;测试人员：开发人员&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2). 集成测试&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;介绍：将已分别通过测试的单元，按设计要求组合成系统或子系统，再进行的测试。&lt;/li&gt;
&lt;li&gt;目的：检查单元之间的协作是否正确。&lt;/li&gt;
&lt;li&gt;测试人员：开发人员&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3). 系统测试&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;介绍：对已经集成好的软件系统进行彻底的测试。&lt;/li&gt;
&lt;li&gt;目的：验证软件系统的正确性、性能是否满足指定的要求。&lt;/li&gt;
&lt;li&gt;测试人员：测试人员&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;4). 验收测试&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;介绍：交付测试，是针对用户需求、业务流程进行的正式的测试。&lt;/li&gt;
&lt;li&gt;目的：验证软件系统是否满足验收标准。&lt;/li&gt;
&lt;li&gt;测试人员：客户/需求方&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**测试方法：**白盒测试、黑盒测试 及 灰盒测试。&lt;/p&gt;
&lt;h4&gt;2. Junit入门&lt;/h4&gt;
&lt;p&gt;我们使用了JUnit单元测试框架进行测试，将会有以下优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;测试代码与源代码分开，便于维护。&lt;/li&gt;
&lt;li&gt;可根据需要进行自动化测试。&lt;/li&gt;
&lt;li&gt;可自动分析测试结果，产出测试报告。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;入门程序&lt;/h5&gt;
&lt;p&gt;需求：使用JUnit，对UserService中的业务方法进行单元测试，测试其正确性。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在pom.xml中，引入JUnit的依赖。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--Junit单元测试依赖--&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.junit.jupiter&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;junit-jupiter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;5.9.1&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;在test/java目录下，创建测试类，并编写对应的测试方法，并在方法上声明@Test注解。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void testGetAge(){
    Integer age = new UserService().getAge(&quot;110002200505091218&quot;);
    System.out.println(age);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;运行单元测试 (测试通过：绿色；测试失败：红色)。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;测试通过显示绿色&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;测试类的命名规范为：XxxxTest&lt;/li&gt;
&lt;li&gt;测试方法的命名规定为：public void xxx(){...}&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;参数化测试&lt;/h5&gt;
&lt;p&gt;​		参数化测试是一种测试方法，允许你使用不同的输入参数多次运行同一个测试方法，以验证代码在不同输入下的行为。JUnit 5 提供了 @ParameterizedTest 注解来实现参数化测试。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;核心概念
@ParameterizedTest：标记一个方法是参数化测试方法。
参数来源：通过 @ValueSource、@CsvSource、@MethodSource 等注解提供测试参数。
多次执行：每个参数都会触发一次测试方法的执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;3.  断言&lt;/h5&gt;
&lt;p&gt;JUnit提供了一些辅助方法，用来帮我们确定被测试的方法是否按照预期的效果正常工作，这种方式称为断言。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;断言方法&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;assertEquals(Object exp, Object act, String msg)&lt;/td&gt;
&lt;td&gt;检查两个值是否相等，不相等就报错。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertNotEquals(Object unexp, Object act, String msg)&lt;/td&gt;
&lt;td&gt;检查两个值是否不相等，相等就报错。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertNull(Object act, String msg)&lt;/td&gt;
&lt;td&gt;检查对象是否为null，不为null，就报错。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertNotNull(Object act, String msg)&lt;/td&gt;
&lt;td&gt;检查对象是否不为null，为null，就报错。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertTrue(boolean condition, String msg)&lt;/td&gt;
&lt;td&gt;检查条件是否为true，不为true，就报错。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertFalse(boolean condition, String msg)&lt;/td&gt;
&lt;td&gt;检查条件是否为false，不为false，就报错。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assertSame(Object exp, Object act, String msg)&lt;/td&gt;
&lt;td&gt;检查两个对象引用是否相等，不相等，就报错。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;示例演示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.itheima;

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class UserServiceTest {
    
    @Test
    public void testGetAge2(){
        Integer age = new UserService().getAge(&quot;110002200505091218&quot;);
        Assertions.assertNotEquals(18, age, &quot;两个值相等&quot;);
//        String s1 = new String(&quot;Hello&quot;);
//        String s2 = &quot;Hello&quot;;
//        Assertions.assertSame(s1, s2, &quot;不是同一个对象引用&quot;);
    }

    @Test
    public void testGetGender2(){
        String gender = new UserService().getGender(&quot;612429198904201611&quot;);
        Assertions.assertEquals(&quot;男&quot;, gender);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;常见注解&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注解&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;@Test&lt;/td&gt;
&lt;td&gt;测试类中的方法用它修饰才能成为测试方法，才能启动执行&lt;/td&gt;
&lt;td&gt;单元测试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@BeforeEach&lt;/td&gt;
&lt;td&gt;用来修饰一个实例方法，该方法会在每一个测试方法执行之前执行一次。&lt;/td&gt;
&lt;td&gt;初始化资源(准备工作)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@AfterEach&lt;/td&gt;
&lt;td&gt;用来修饰一个实例方法，该方法会在每一个测试方法执行之后执行一次。&lt;/td&gt;
&lt;td&gt;释放资源(清理工作)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@BeforeAll&lt;/td&gt;
&lt;td&gt;用来修饰一个静态方法，该方法会在所有测试方法之前只执行一次。&lt;/td&gt;
&lt;td&gt;初始化资源(准备工作)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@AfterAll&lt;/td&gt;
&lt;td&gt;用来修饰一个静态方法，该方法会在所有测试方法之后只执行一次。&lt;/td&gt;
&lt;td&gt;释放资源(清理工作)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ParameterizedTest&lt;/td&gt;
&lt;td&gt;参数化测试的注解 (可以让单个测试运行多次，每次运行时仅参数不同)&lt;/td&gt;
&lt;td&gt;用了该注解，就不需要@Test注解了&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ValueSource&lt;/td&gt;
&lt;td&gt;参数化测试的参数来源，赋予测试方法参数&lt;/td&gt;
&lt;td&gt;与参数化测试注解配合使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@DisplayName&lt;/td&gt;
&lt;td&gt;指定测试类、测试方法显示的名称 （默认为类名、方法名）&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;演示 &lt;code&gt;@BeforeEach&lt;/code&gt;，&lt;code&gt;@AfterEach&lt;/code&gt;，&lt;code&gt;@BeforeAll&lt;/code&gt;，&lt;code&gt;@AfterAll&lt;/code&gt; 注解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserServiceTest {

    @BeforeEach
    public void testBefore(){
        System.out.println(&quot;before...&quot;);
    }

    @AfterEach
    public void testAfter(){
        System.out.println(&quot;after...&quot;);
    }

    @BeforeAll //该方法必须被static修饰
    public static void testBeforeAll(){ 
        System.out.println(&quot;before all ...&quot;);
    }

    @AfterAll //该方法必须被static修饰
    public static void testAfterAll(){
        System.out.println(&quot;after all...&quot;);
    }

    @Test
    public void testGetAge(){
        Integer age = new UserService().getAge(&quot;110002200505091218&quot;);
        System.out.println(age);
    }
    
    @Test
    public void testGetGender(){
        String gender = new UserService().getGender(&quot;612429198904201611&quot;);
        System.out.println(gender);
    }
 }   
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;4.  依赖范围&lt;/h5&gt;
&lt;p&gt;依赖的jar包，默认情况下，可以在任何地方使用，在main目录下，可以使用；在test目录下，也可以使用。&lt;/p&gt;
&lt;p&gt;在maven中，如果希望限制依赖的使用范围，可以通过 &lt;code&gt;&amp;lt;scope&amp;gt;…&amp;lt;/scope&amp;gt;&lt;/code&gt; 设置其作用范围。&lt;/p&gt;
&lt;p&gt;作用范围：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主程序范围有效。（main文件夹范围内）&lt;/li&gt;
&lt;li&gt;测试程序范围有效。（test文件夹范围内）&lt;/li&gt;
&lt;li&gt;是否参与打包运行。（package指令范围内）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以在pom.xml中配置 &amp;lt;scope&amp;gt;&amp;lt;/scope&amp;gt; 属性来控制依赖范围。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;单元测试的重点不是“跑起来”，而是把最小业务单元的预期行为写清楚。JUnit 的注解、断言和参数化测试都是围绕这个目标服务的。&lt;/p&gt;
</content:encoded></item><item><title>Maven 项目构建与依赖管理笔记</title><link>https://youki.bbroot.com/posts/java/maven/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/java/maven/</guid><description>整理 Maven 的项目结构、坐标、依赖配置、仓库模型和构建生命周期。</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记记录 Maven 的基础使用方式，重点是理解 Maven 如何统一 Java 项目的结构、依赖和构建流程。后续精修时，可以把命令示例和 POM 配置拆成更清晰的实践小节。&lt;/p&gt;
&lt;h2&gt;本文要点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Maven 用来管理 Java 项目的依赖、构建流程和标准目录结构。&lt;/li&gt;
&lt;li&gt;POM 是 Maven 项目的核心描述文件，坐标由 &lt;code&gt;groupId&lt;/code&gt;、&lt;code&gt;artifactId&lt;/code&gt;、&lt;code&gt;version&lt;/code&gt; 组成。&lt;/li&gt;
&lt;li&gt;Maven 仓库分为本地仓库、中央仓库和远程私服。&lt;/li&gt;
&lt;li&gt;生命周期命令可以串联完成编译、测试、打包和发布。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. maven介绍&lt;/h3&gt;
&lt;h4&gt;1. 介绍&lt;/h4&gt;
&lt;p&gt;Maven 是一款用于管理和构建Java项目的工具，是Apache旗下的一个开源项目。&lt;/p&gt;
&lt;h4&gt;2. 作用&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;管理项目依赖：方便快捷的管理项目依赖的资源(jar包)，避免版本冲突问题。&lt;/li&gt;
&lt;li&gt;管理项目构建：通过Maven中的命令，就可以很方便的完成项目的编译(compile)、测试(test)、打包(package)、发布(deploy) 等操作。
而且这些操作都是跨平台的，也就是说无论你是Windows系统，还是Linux系统，还是Mac系统，这些命令都是支持的。&lt;/li&gt;
&lt;li&gt;Maven 还提供了标准、统一的项目结构。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. Maven概述&lt;/h3&gt;
&lt;h4&gt;1. Maven的作用：&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;方便的依赖管理&lt;/li&gt;
&lt;li&gt;统一的项目结构&lt;/li&gt;
&lt;li&gt;标准的项目构建流程&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2. Maven模型&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;项目对象模型 (Project Object Model)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;依赖管理模型(Dependency)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;构建生命周期/阶段(Build lifecycle &amp;amp; phases)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. Maven仓库&lt;/h4&gt;
&lt;p&gt;仓库：用于存储资源，管理各种jar包
仓库的本质就是一个目录(文件夹)，这个目录被用来存储开发中所有依赖(就是jar包)和插件&lt;/p&gt;
&lt;p&gt;Maven仓库分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地仓库：自己计算机上的一个目录(用来存储jar包)&lt;/li&gt;
&lt;li&gt;中央仓库：由Maven团队维护的全球唯一的。仓库地址：https://repo1.maven.org/maven2/&lt;/li&gt;
&lt;li&gt;远程仓库(私服)：一般由公司团队搭建的私有仓库&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. IDEA集成Maven&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Maven项目的目录结构:&lt;/strong&gt;
Maven项目的目录结构:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;maven-project01

        |---  src  (源代码目录和测试代码目录)
               |---  main (源代码目录)
                        |--- java (源代码java文件目录)
                        |--- resources (源代码配置文件目录)
              |---  test (测试代码目录)
                        |--- java (测试代码java目录)
                        |--- resources (测试代码配置文件目录)
        |--- target (编译、打包生成文件存放目录)
	
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. pom文件详解&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;POM (Project Object Model)：项目对象模型，用来描述当前的maven项目。&lt;/strong&gt;
pom文件详解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- &amp;lt;project&amp;gt; ：pom文件的根标签，表示当前maven项目
- &amp;lt;modelVersion&amp;gt;：声明项目描述遵循哪一个POM模型版本
  - 虽然模型本身的版本很少改变，但它仍然是必不可少的。目前POM模型版本是4.0.0
- 坐标 ：
  - &amp;lt;groupId&amp;gt; &amp;lt;artifactId&amp;gt; &amp;lt;version&amp;gt;
  - 定位项目在本地仓库中的位置，由以上三个标签组成一个坐标
- &amp;lt;maven.compiler.source&amp;gt; ：编译JDK的版本
- &amp;lt;maven.compiler.target&amp;gt; ：运行JDK的版本
- &amp;lt;project.build.sourceEncoding&amp;gt; : 设置项目的字符集
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4.  Maven坐标&lt;/h4&gt;
&lt;p&gt;什么是坐标？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Maven中的坐标是&lt;strong&gt;资源的唯一标识&lt;/strong&gt; , 通过该坐标可以唯一定位资源位置&lt;/li&gt;
&lt;li&gt;使用坐标来定义项目或引入项目中需要的依赖&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Maven坐标主要组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;groupId：定义当前Maven项目隶属组织名称（通常是域名反写，例如：com.itheima）&lt;/li&gt;
&lt;li&gt;artifactId：定义当前Maven项目名称（通常是模块名称，例如 order-service、goods-service）&lt;/li&gt;
&lt;li&gt;version：定义当前项目版本号
&lt;ul&gt;
&lt;li&gt;SNAPSHOT: 功能不稳定、尚处于开发中的版本，即快照版本&lt;/li&gt;
&lt;li&gt;RELEASE: 功能趋于稳定、当前更新停止，可以用于发行的版本&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 依赖配置&lt;/h3&gt;
&lt;h4&gt;1. 基本配置&lt;/h4&gt;
&lt;p&gt;依赖：指当前项目运行所需要的jar包。一个项目中可以引入多个依赖：&lt;/p&gt;
&lt;p&gt;例如：在当前工程中，我们需要用到logback来记录日志，此时就可以在maven工程的pom.xml文件中，引入logback的依赖。具体步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在pom.xml中编写&lt;code&gt;&amp;lt;dependencies&amp;gt;&lt;/code&gt;标签&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;&amp;lt;dependencies&amp;gt;&lt;/code&gt;标签中使用&lt;code&gt;&amp;lt;dependency&amp;gt;&lt;/code&gt;引入坐标&lt;/li&gt;
&lt;li&gt;定义坐标的 &lt;code&gt;groupId&lt;/code&gt;、&lt;code&gt;artifactId&lt;/code&gt;、&lt;code&gt;version&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5. 生命周期&lt;/h3&gt;
&lt;h4&gt;1. 介绍&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;clean：清理工作。&lt;/li&gt;
&lt;li&gt;default：核心工作。如：编译、测试、打包、安装、部署等。&lt;/li&gt;
&lt;li&gt;site：生成报告、发布站点等。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;每套生命周期包含一些阶段（phase），阶段是有顺序的，后面的阶段依赖于前面的阶段。
我们看到这三套生命周期，里面有很多很多的阶段，这么多生命周期阶段，其实我们常用的并不多，主要关注以下几个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;clean：移除上一次构建生成的文件&lt;/li&gt;
&lt;li&gt;compile：编译项目源代码&lt;/li&gt;
&lt;li&gt;test：使用合适的单元测试框架运行测试(junit)&lt;/li&gt;
&lt;li&gt;package：将编译后的文件打包，如：jar、war等&lt;/li&gt;
&lt;li&gt;install：安装项目到本地仓库&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Maven的生命周期是抽象的，这意味着生命周期本身不做任何实际工作。在Maven的设计中，实际任务（如源代码编译）都交由插件来完成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;生命周期的顺序是：clean --&amp;gt; validate --&amp;gt; compile --&amp;gt; test --&amp;gt; package --&amp;gt; verify --&amp;gt; install --&amp;gt; site --&amp;gt; deploy&lt;/li&gt;
&lt;li&gt;我们需要关注的就是：clean --&amp;gt;  compile --&amp;gt; test --&amp;gt; package  --&amp;gt; install&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 执行&lt;/h4&gt;
&lt;p&gt;在日常开发中，当我们要执行指定的生命周期时，有两种执行方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在idea工具右侧的maven工具栏中，选择对应的生命周期，双击执行&lt;/li&gt;
&lt;li&gt;在DOS命令行中，通过maven命令执行&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Maven 的核心价值是把 Java 项目的依赖、目录结构和构建步骤标准化。先理解 POM、坐标、仓库和生命周期，再记常用命令，会比单独背配置更容易串起来。&lt;/p&gt;
</content:encoded></item><item><title>HTML、CSS 基础与页面布局笔记</title><link>https://youki.bbroot.com/posts/frontend/html-basics/</link><guid isPermaLink="true">https://youki.bbroot.com/posts/frontend/html-basics/</guid><description>整理 CSS 引入方式、颜色写法、选择器、路径、盒子模型、Flex 布局和表单标签。</description><pubDate>Fri, 11 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇笔记整理 HTML 与 CSS 入门阶段的常用知识点，重点是页面样式引入、选择器、路径写法和基础布局。&lt;/p&gt;
&lt;h5&gt;1. css引入方式&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;行内样式：&lt;code&gt;&amp;lt;h1 style=&quot;...&quot;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;内部样式：&lt;code&gt;&amp;lt;style&amp;gt;...&amp;lt;/style&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;外部样式：在 &lt;code&gt;xxx.css&lt;/code&gt; 中编写样式，并通过 &lt;code&gt;&amp;lt;link href=&quot;...&quot;&amp;gt;&lt;/code&gt; 引入&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2.  颜色表示&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;关键字：red&lt;/li&gt;
&lt;li&gt;rgb表示法&lt;/li&gt;
&lt;li&gt;rgba表示法: rgba(255,0,0,0.5)&lt;/li&gt;
&lt;li&gt;十六进制&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;3.常见选择器写法&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;元素选择器：&lt;code&gt;标签名 { ... }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;类选择器：&lt;code&gt;.class属性值 { ... }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;id选择器：&lt;code&gt;#id属性值 { ... }&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;4. 路径的书写形式&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;绝对路径:
&lt;ul&gt;
&lt;li&gt;绝对磁盘路径：&lt;code&gt;&amp;lt;img src=&quot;C:\Users\Administrator\Desktop\HTML\img\logo.png&quot;&amp;gt;&lt;/code&gt;（不要使用）&lt;/li&gt;
&lt;li&gt;绝对网络路径：&lt;code&gt;&amp;lt;img src=&quot;https://i2.sinaimg.cn/dy/deco/2012/0613/yocc20120613img01/news_logo.png&quot;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;相对路径:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​        ./ : 当前目录 , ./ 可以省略的&lt;/p&gt;
&lt;p&gt;​        ../: 上一级目录&lt;/p&gt;
&lt;h3&gt;盒子模型&lt;/h3&gt;
&lt;h5&gt;1. 布局标签&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;标签：&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;标签：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一行只显示一个（独占一行）&lt;/li&gt;
&lt;li&gt;宽度默认是父元素的宽度，高度默认由内容撑开&lt;/li&gt;
&lt;li&gt;可以设置宽高（width、height）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;标签：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一行可以显示多个&lt;/li&gt;
&lt;li&gt;宽度和高度默认由内容撑开&lt;/li&gt;
&lt;li&gt;不可以设置宽高（width、height）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;hr&amp;gt;&lt;/code&gt; 标签：水平分割线&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; 标签：换行&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2. flex布局&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;父元素：&lt;code&gt;display: flex;&lt;/code&gt; 弹性布局&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-direction: row;&lt;/code&gt;：默认为 row 水平布局，设置主轴&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-start&lt;/code&gt;：从头开始排列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flex-end&lt;/code&gt;：从尾部开始排列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;center&lt;/code&gt;：在主轴上居中对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;space-around&lt;/code&gt;：平分剩余空间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;space-between&lt;/code&gt;：先两边贴边，再平分剩余空间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;justify-content: space-around;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;3. 表单标签：&lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;表单属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;action:表单数据提交的url地址&lt;/li&gt;
&lt;li&gt;method:表单提交方式&lt;/li&gt;
&lt;li&gt;get：表单数据拼接在url后面，?username=java&lt;/li&gt;
&lt;li&gt;post:表单数据在请求体中携带/大小没有限制
。注意：表单项必须有name属性才可以提交。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;4. 表单项标签&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; 的 &lt;code&gt;type&lt;/code&gt; 属性：&lt;code&gt;text&lt;/code&gt;、&lt;code&gt;password&lt;/code&gt;、&lt;code&gt;radio&lt;/code&gt;、&lt;code&gt;checkbox&lt;/code&gt;、&lt;code&gt;file&lt;/code&gt;、&lt;code&gt;date&lt;/code&gt;、&lt;code&gt;datetime-local&lt;/code&gt;、&lt;code&gt;time&lt;/code&gt;、&lt;code&gt;hidden&lt;/code&gt;、&lt;code&gt;button&lt;/code&gt;、&lt;code&gt;submit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; 定义下拉列表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; 定义文本域&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;HTML 与 CSS 入门的重点是先建立页面结构和样式规则的基本认识。选择器、盒子模型、Flex 布局和表单标签是后续做页面开发时最常反复使用的部分。&lt;/p&gt;
</content:encoded></item></channel></rss>