OMNeT仿真 初始安装配置 🧩 第 0 步：下载和安装 OMNeT++ 选择版本： 👉 OMNeT++ 6.0.1 或 6.0.2（官方 Windows 兼容） 下载地址（官网）： https://omnetpp.org/download/ 安装步骤概述： 下载 ZIP → 解压 打开 mingwenv.cmd（这个是必须的环境） 在弹出的终端执行： ./configure make 然后运行 omnetpp 就会启动图形化 IDE（Eclipse）。 这个过程大概 10–20 分钟。 第 1 步：下载 INET 框架 推荐与你的 OMNeT++ 版本兼容： OMNeT++ 6.x → 用 INET 4.5.x 下载 INET： https://github.com/inet-framework/inet/releases 解压后你会看到 INET 根目录。 在同样的 mingwenv 环境中运行： cd inet source setenv make makefiles make -j4 等待几分钟。 导入 INET 到 OMNeT++ IDE 在 OMNeT++ IDE 中： File → Import → 选择General文件夹 → Existing Projects into Workspace 选择 INET 根目录 → Finish 你的工程列表里会看到 INET。 网络仿真 第 0 步：打开IDE 进入你的 OMNeT++ 安装目录，双击 omnetpp.exe 第 1 步：创建你的项目 在 IDE 里： File → New → OMNeT++ Project → Empty Project 项目名：TrafficPrediction Finish 设置 TrafficPrediction 依赖 INET（非常关键） 现在手动设置 INET 依赖： 右键 TrafficPrediction 点击 Properties 左侧选 Project References 在右边勾选 inet 第 2 步：创建文件夹、文件 项目结构如下： UdpDemo/ │ ├── simulations/ │ ├── SimpleUdpNetwork.ned ← 网络拓扑（重点） │ ├── omnetpp.ini ← 仿真配置（重点） │ └── package.ned ← 自动生成（可以忽略） │ ├── src/ ← 本 demo 不需要写 C++，这个空着 │ └── Makefile ← OMNeT++ 自动管理 SimpleUdpNetwork.ned // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this program. If not, see http://www.gnu.org/licenses/. // package udpdemo.simulations; import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; import inet.node.inet.StandardHost; import inet.node.inet.Router; network SimpleUdpNetwork { submodules: configurator: Ipv4NetworkConfigurator { } hostA: StandardHost { @display("p=100,200"); } router: Router { @display("p=300,200"); } hostB: StandardHost { @display("p=500,200"); } connections allowunconnected: hostA.pppg++ <--> { delay = 1ms; datarate = 100Mbps; } <--> router.pppg++; router.pppg++ <--> { delay = 1ms; datarate = 100Mbps; } <--> hostB.pppg++; } omnetpp.ini [General] network = udpdemo.simulations.SimpleUdpNetwork sim-time-limit = 30s # 自动 ARP，减少日志噪声 **.arp.typename = "GlobalArp" # HostA 配置一个 UDP 发送应用 *.hostA.numApps = 1 *.hostA.app[0].typename = "UdpBasicApp" *.hostA.app[0].destAddresses = "hostB" *.hostA.app[0].destPort = 5000 *.hostA.app[0].messageLength = 1000B *.hostA.app[0].sendInterval = 1s *.hostA.app[0].startTime = 1s # HostB 配置一个 UDP 接收器 *.hostB.numApps = 1 *.hostB.app[0].typename = "UdpSink" *.hostB.app[0].localPort = 5000 第 3 步：编译项目 菜单： Project → Build All 第 4 步：运行仿真 右键 omnetpp.ini： Run As → OMNeT++ Simulation 运行后你会看到： hostA 每秒发出一个 UDP 包 hostB 收到包 router 转发 可以点击连接查看流量、抓包 可以打开 Result 分析包数吞吐量 命令行模式 第一步：修改 omnetpp.ini Cmdenv 如果不加限制，会在屏幕上疯狂打印每一条日志，这比图形界面还慢。请确保你的 [General] 部分包含以下设置： [General] # ... (你的其他配置) ... # ============================================================ # Cmdenv 性能优化配置 # ============================================================ cmdenv-express-mode = true # 开启极速模式 cmdenv-performance-display = false # 关闭性能数据显示 (CPU/内存占用等)，进一步减少I/O cmdenv-status-frequency = 10s # 每10秒才在屏幕上打印一次进度，防止刷屏 **.cmdenv-log-level = off # 彻底关闭模块的文本日志输出 (最关键的一行) # 确保不记录 eventlog (体积巨大且拖慢速度) record-eventlog = false 第二步：选择运行方式 1）在顶部菜单栏，点击 Run -> Run Configurations...。 2）在左侧栏找到 OMNeT++ Simulation，展开它，点击你的运行配置（通常叫 TrafficPrediction 或者 omnetpp.ini）。 3）在右侧面板中，点击 User Interface 选项卡（或者叫 Qt/CmdEnv tab）。 4）找到 User Interface 下拉菜单，将 Qtenv (图形界面) 改为 Cmdenv (命令行)。 5）点击右下角的 apply->Run。
Cursor 插件、环境安装 1）Chinese (Simplified) Language Pack，中文界面插件。 2）Extension Pack for Java 只要安装了 “Extension Pack for Java”，其它插件（Maven、Debugger、Language Support、Test Runner、Project Manager 等）都是它自动帮你装好的，不需要你一个个手动点安装。 这样打开项目文件夹（包含 pom.xml 的根目录）时： 它会自动检测项目类型 → 加载 Maven → 解析依赖。底部出现 Java: Ready、Building [Done]，说明依赖已经下载并索引完毕。默认使用的是系统环境变量MAVEN_HOME的地址。 或者在这里指定： 自动选择jdk版本： 查看pom中配置，Cursor 会用这个版本作为编译目标。 <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> 运行时版本由“系统环境或 Cursor 配置”决定，系统变量里的 JAVA_HOME，就是 你系统默认使用的 JDK。随便打开一个终端 java -version ，可以看到使用的是jdk17。 3） Spring Boot Extension Pack 左侧多出一个 Spring Boot Dashboard； 自动识别所有带 @SpringBootApplication 的项目； 可以直接在面板里点击 ▶️ 启动； application.yml会出现智能提示，比如： spring.datasource.url spring.datasource.username spring.datasource.password 4）IntelliJ IDEA Keybindings 装上它之后，你在 Cursor 里按的快捷键和 IDEA 几乎一样； 5）“Want to launch Cursor from the command line? You can add the code or cursor commands now, or later with Ctrl+Shift+P and typing ‘install code command’.” 这是一个 可选的小功能提示，翻译：“想要从命令行启动 Cursor 吗？你可以现在添加 code 或 cursor 命令，也可以之后按 Ctrl + Shift + P，输入 install code command 来添加。” 作用：直接通过命令行（例如 PowerShell、CMD、终端），打开项目或文件。 cursor . 表示： 用 Cursor 打开当前文件夹（.） 或者：cursor C:\project\group-buying-sys 表示： 用 Cursor 打开指定路径下的项目。 如何查看配置？ Ctrl+shift+P 快捷键，输入 open user settings 可以打开用户配置。 输入 project setting 可以打开项目配置文件 或者左下角JAVA PROJECTS，点击Configure JAVA Runtime： 运行JAVA项目 Maven编译 运行 1）打开启动类，点击run/debug 2） 编辑器界面 ┌────────────────────────────────────────────┐ │ 顶部菜单栏（File / 编辑 / 选择 / 查看...）│ ├────────────────────────────────────────────┤ │ 左侧侧边栏（文件树 / 项目 / Git 等） │ ├────────────────────────────────────────────┤ │ 中间主编辑区（代码编辑） │ ├────────────────────────────────────────────┤ │ 右侧 AI 面板（Cursor 智能助手 / Chat） │ ├────────────────────────────────────────────┤ │ 底部状态栏（分支 / 文件类型 / 行列信息等） │ └────────────────────────────────────────────┘ 如何使用 Cursor 的 AI 功能 功能 作用 快捷键（默认） 🧠 Inline Edit 选中一段代码 → Ctrl + K（或右键 “Ask Cursor”）→ 输入自然语言指令（如“优化正则表达式”） Ctrl + K 🤖 Agent 面板 打开一个“聊天窗口”，直接和 Cursor 对话（如“帮我写个登录验证函数”） Ctrl + I ✨ Tab 补全 在代码中输入时，Cursor 自动预测后续内容 Tab 🔁 重新生成建议 当灰色提示出现但不理想时，按 `Alt + ``（或右键 “Regenerate”） `Alt + `` 选择一段代码+CTRL+K 修改指定的代码，直接在代码编辑区弹出修改窗口调用大模型。CTRL+ENTER 或者鼠标点击 应用修改 CTRL+N 或者 **鼠标点击 **拒绝修改 CTRL+I 1）图中设置的Auto表示自动选择模型； 2）@ 可以添加文件、文件夹到当前对话上下文，如果你在代码编辑区选择一段代码再 CTRL+I 或者 CTRL+L，也会自动将它添加进来。还可以直接拖拽资源管理器中的文件到AI对话框中。 3）+Browser 模式可以让模型看到整个项目文件 任务 你可以直接问 Cursor 找函数定义 “verifyPassword 函数定义在哪个文件？” 查找引用 “verifyCode 被调用了多少次？” 理解架构 “帮我总结整个项目的模块结构。” 改代码 “把所有 JS 文件里的 var 改成 let。” 生成文档 “根据这个项目代码生成一份 README。” 4）对话模式 Agent模式，默认的对话模式，功能最全，AI 不仅能回答，还会“主动思考、规划和执行”。可以理解为：“帮我干活”的模式。 Plan 模式，它可以浏览整个项目文件，但不会直接改代码，而是会：把你的需求拆分成多个步骤；告诉你“下一步该干什么”；有时生成一份修改计划或脚本方案。 Ask 模式：问答模式，和网页版使用大模型一样。 TAB 一般注释起手，如果出现灰色预测文字时，直接 Tab 就能接受 AI 自动补全 ，按ESC可以拒绝这个建议；否则就是普通的缩进。 触发逻辑 传统 IDE（比如 VS Code、IntelliJ） 的补全是词法或语法级触发： 你输入 “pri” → 提示 “print()”； 它只是基于关键字匹配。 而 Cursor 的补全 是“语义级”的： AI 根据上下文 推断你下一步可能在做什么。 它会综合考虑： 当前光标前后的代码； 注释 / 函数签名； 文件名； 项目中的其他定义； 甚至最近你在这个 Tab 里做的事。 阶段 触发动作 AI 响应 检测阶段 监听输入变化（按键、换行、注释等） 分析上下文语义树（AST） 预测阶段 生成“意图向量” 模型推断用户下一步目标 展示阶段 在光标后显示灰色 ghost text 等待你按 Tab 接受 触发时机 类型 触发条件 示例 行为 函数触发 输入 function / def function check... 预测参数和函数体 控制语句触发 输入 if / for 等 if (phone) 预测条件或逻辑块 注释触发 输入 //、#、/** // 校验邮箱格式 按注释生成实现 模板触发 class / import / try class User 预测类体、导入内容 上下文模仿 类似已有代码块 第二个函数定义 模仿风格 文件结构触发 同名模块、utils utils. 补出函数名 空行预测 新行、缩进后 空一行 猜测下一个合逻辑语句 Index&Docs Codebase Indexing（代码索引）： Cursor 会扫描整个项目文件夹； 提取文件路径、函数定义、类名、依赖关系； 生成语义向量（embeddings）并存储（只保存索引，不上传源代码）。 界面说明：索引完成度100%；已被索引的文件数量为 78； Index New Folders： 当你创建或添加新文件夹时，Cursor 是否自动索引它。 Ignore Files in .cursorignore： 告诉 Cursor 哪些文件不要被索引。类似.gitignore Docs（文档索引）： 可以手动添加官方文档、API 文档、开发指南，让它当作参考知识。 添加一个 README.md 或内部 API 规范； 或添加外部文档（如一个 SDK 的文档网站）； 之后在 Chat 里用 @doc 引用 如何撤销修改 在AI编辑窗口提问，应用修改，点击Accept 应用完之后，若想回退版本，可以点击提问右边的回撤箭头。 整体思路（重要） 1）先打开项目，然后用browser模式，让AI对这个项目生成一个 .cursorignore 文件，自动忽略哪些文件无需添加进上下文。 2）打开设置，Index&docs，重新索引一下。 3）把项目相关的需求文档、接口文档、技术文档，全部录入到docs中，作为知识库以作备用。这与项目代码文件夹解耦的。 4）在项目根目录下创建 .cursorrules 文件，不需要 @ 引用，自动生效。 .cursorrules 就是 Cursor 的 AI 指令与代码风格规则文件，用来告诉 AI：在这个项目里写代码、命名、注释、格式化时都按照这些规则来做。是一个 系统级提示词 。 eg: ai_behavior: - 所有回答必须使用中文 - 代码中添加必要的注释 style: indentation: 4 spaces naming_convention: variables: camelCase AI 背后接收到的系统指令：你是一个 Java 开发助手。 遵循以下项目规则： 所有回答必须使用中文。 代码中添加必要的注释。 使用 4 空格缩进。 变量名使用 camelCase。 用户请求： 「写一个计算阶乘的 Java 函数」。
DeepWalk DeepWalk 的最终目标是—— 把图中的每个节点，用一个低维向量表示（embedding）出来，使得结构相似的节点在向量空间中也接近。 也就是说，如果两个节点在图上经常“一起出现”（相连、同社群等），那它们的 embedding 也应该相似。 主要思想 将图结构转化为“序列语料”： 在图上做 随机游走（random walks），生成顶点序列，类似于自然语言中的“句子”。 将随机游走得到的序列输入 Skip-Gram 模型，学习节点的低维嵌入表示。 学到的嵌入能捕捉到 邻居相似性、社区结构，可用于分类、聚类、链路预测等任务。 输入 & 输出 输入：图结构 $G=(V,E)$，随机游走参数（步长 $t$，次数 $\gamma$，窗口大小 $w$），嵌入维度 $d$。 输出：节点嵌入矩阵 $\Phi \in \mathbb{R}^{|V|\times d}$。 关键公式与目标 随机游走生成“句子”： 从某个顶点 $v_i$ 出发，采样一个长度为 $t$ 的随机游走序列： $$ W_{v_i} = (v_i, v_{i+1}, \dots, v_{i+t}) $$ 生成的序列 $W_{v_i}$ 就是一句“话”，用来训练 Skip-Gram 模型。在这些序列中，如果两个节点经常在相邻位置出现，就表示它们关系密切。 ["A", "B", "C", "D"] Skip-Gram 目标函数： 现在我们用 Word2Vec 的思想：对于一句话 ["A", "B", "C", "D"]，假设窗口大小 $w = 1$，也就是每个节点只看它的邻居。那么训练样本是这样的： 中心节点 它要预测的邻居 A B B A, C C B, D D C 模型要学的是： “给我节点 B，我要能预测出 A 和 C。” “给我节点 C，我要能预测出 B 和 D。” 于是就有这个公式，给定中心节点 $v_i$，最大化其上下文窗口内邻居节点的条件概率： $$ \max_{\Phi} \sum_{i=1}^{|V|} \sum_{u \in \mathcal{N}_w(v_i)} \log Pr(u \mid \Phi(v_i)) $$ 其中 $\mathcal{N}_w(v_i)$ 表示在随机游走序列中，窗口大小 $w$ 内的上下文节点。 我希望模型能学到这样一种 embedding：中心节点 $v_i$ 能够高概率预测出它周围的邻居 $u$。 优化形式（负对数似然）： 就是把上面的最大化目标，改写成一个最小化损失函数。 最大化“好事”（邻居出现的概率）= 最小化“坏事”（邻居预测错误的概率） $$ \min_{\Phi} - \log Pr\big( {v_{i-w},\dots,v_{i+w}} \mid \Phi(v_i) \big) $$ 概率建模（Hierarchical Softmax 或 Negative Sampling）： $$ Pr(u \mid \Phi(v_i)) = \frac{\exp\big( \Phi(u) \cdot \Phi(v_i) \big)}{\sum_{v \in V} \exp\big( \Phi(v) \cdot \Phi(v_i) \big)} $$ 这相当于用 Softmax 把所有节点的相似度转成概率分布。 其中： 分子：中心节点和邻居节点的相似度（点积越大越相似）； 分母：所有节点的相似度和（用来归一化成概率）。 ​ 如果 $u$ 是 $v_i$ 的邻居，那么 $\Phi(u)$ 与 $\Phi(v_i)$ 的点积就应更大。 例子 想象你是个“社交分析师”： 你观察到小明每天都和小红、小刚一起出现； 而小明几乎从不和小李在一起； 那你自然会说：小明和小红、小刚的关系更近。 DeepWalk 就是通过“随机游走 + 预测邻居”这个机制， 自动学出“谁跟谁关系近”的数值表示。 训练逻辑 1）加载 10+1 张图 A_list = [A100, A101, ..., A110] 我们取前 10 张作为输入，最后一张作为真实标签。 2）对每一个时间步的图做 DeepWalk for t in range(100, 110): emb_t = DeepWalk(A_t) 每张图 $A_t$ 都独立训练一个 节点 embedding（比如 400×64）。 400是节点数，64是嵌入维数 embedding 就是每个节点在这一时刻的“语义坐标”。 训练目标是：让在图里常相邻的节点在 embedding 空间也靠得近。 可以理解成： DeepWalk 把图结构变成“节点的向量表示”。 3）拼接成时间序列特征（10步） 对每对节点 (i, j)，你把过去10个时间步的 embedding 拿出来，计算每步的 Hadamard积（元素乘）： feats = [emb100[i]*emb100[j], emb101[i]*emb101[j], ..., emb109[i]*emb109[j]] X_ij = concatenate(feats) 每个节点对 (i, j) → 一个 10×64 = 640 维的特征； 这 640 维包含了它俩“过去10步的相似度变化轨迹”。 白话理解： 这个特征就像是“节点对关系的时间切片”。 如果两人（节点）经常靠得近 → 向量相似； 如果逐渐靠近 → 特征会呈现变化趋势。 4）第11步图作为真实答案（标签） A_target = A110 y_ij = 1 if 节点i和j在第110步相连 else 0 每个节点对 (i, j) 的标签就是第11步的真相； 如果在第11步有边 → 正样本； 没边 → 负样本。 5）平衡样本（1:1） 因为稀疏图中没连边的太多： 原来是 9,237 个正样本 vs 70,563 个负样本； 现在我们随机保留同样数量的负样本； 所以训练时正负样本平衡：9,237 : 9,237。 这样模型不会再“全猜没边”。 6）训练 MLP 来做预测 MLP(X) → 输出每个节点对在第110步连边的概率 输入：每个节点对的 640 维特征； 输出：一个 [0,1] 概率； Loss：二分类交叉熵（BCELoss）； 优化器：Adam。 白话解释： MLP 在学“什么样的历史关系轨迹会导致下一步产生连接”。 7）用真实 A110 对照预测结果 模型预测完后： 它给每对节点一个连边概率； 我们用阈值 0.5 分成「有边/无边」； 然后与真实的 $A_{110}$ 比较，得到： 指标 含义 AUC=0.8279 模型能区分哪些节点会连边、哪些不会（非常好） ACC=0.7223 有约 72% 的节点对被正确分类（相对平衡后的准确率） （1）静态图版本 你只有一张图，比如社交网络的当前关系： DeepWalk 学出 embedding； 然后用 MLP 或逻辑回归，预测那些“目前没边但 embedding 很接近”的节点对； 这些就是「潜在朋友」→ 即未来可能建立连接的边。 📘 所以静态链路预测其实是： 用当前结构的 embedding 相似度，预测将来是否可能连边。 （2）动态图版本 你有多帧图，比如第100~109步（连续的10个时间片）： 每一步都用 DeepWalk 得到 embedding； 然后把每个节点对的“10帧特征”拼起来； 喂给 MLP 预测第110步是否连边。 📘 动态版本其实是： 把「节点关系的历史轨迹」作为输入， 预测下一个时间步的关系变化。 DeepWalk 的本质： 是一个 图嵌入算法（Graph Embedding Method）， 输入图结构，输出节点的向量表示（embedding）。 也就是说： 每次运行 DeepWalk，相当于重新学习节点的 embedding； embedding 本身就是最终结果，不需要“恢复模型状态”去推理； 它不具备像神经网络那样“泛化到新数据”的功能。 所以： ✅ DeepWalk 的输出 = checkpoint ✅ DeepWalk 的 embedding = 可直接保存 / 复用的结果 ❌ 不需要保存训练中间的模型权重
Kubernetes (K8S) Kubernetes 是一个开源的容器编排平台，用于自动化部署、扩缩容（Pod 数量多了/少了自动调整）和自愈（坏掉的 Pod 会被自动重建） 基本架构 一个 Kubernetes 集群 (Cluster) = 控制平面 + 工作节点 的组合。要使用K8S，要先搭建集群，deployment.yaml 和 service.yaml 要提交到 K8S 集群，由它调度和运行。 控制平面 (Control Plane) 负责管理整个集群，不直接跑应用。 组件 作用 比喻 API Server 接收命令（kubectl）、提供统一接口 前台服务员 etcd 存储集群所有状态（配置、Pod 列表等） 账本/数据库 Scheduler 决定 Pod 跑在哪个 Node 上 领班 Controller Manager 保证实际状态符合预期（补 Pod、重建失败的 Pod） 经理 工作节点 (Worker Nodes) 实际跑应用容器。 组件 作用 Kubelet 负责跟 API Server 沟通，并启动/管理容器 容器运行时 运行容器（Docker、containerd） Kube-Proxy 管理网络规则，实现 Service 的负载均衡 graph TD A[控制平面] --> B[Node 1] A --> C[Node 2] A --> D[Node 3] B --> E[Pod] B --> F[Pod] C --> G[Pod] D --> H[Pod] K8S 核心概念 Pod 最小运行单元，一个 Pod 里可以有 1 个或多个容器。 共享网络和存储空间 Deployment 管 Pod 的「副本数」和「版本」。 支持滚动更新、回滚 Service 提供稳定的网络入口（因为 Pod IP 会变）。 负载均衡到一组 Pod。 ConfigMap & Secret ConfigMap：存储配置信息（非敏感）。 Secret：存储敏感信息（密码、Token），会加密。 基本使用方式 Deployment（定义 Pod 模板 + 副本数） apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: replicas: 3 # 要 3 个 Pod selector: matchLabels: app: my-app template: # Pod 模板 metadata: labels: app: my-app #给 Pod 打标签，Service 用 selector 找到这些 Pod 做负载均衡 spec: containers: - name: my-app #Pod名 image: my-registry/my-app:v1 # ← 指向 CI 推送后的镜像（仓库/名称:标签） ports: - containerPort: 8080 #容器对外提供服务的端口 Pod名可能是：my-app-6f7c9d7c5d-abcde，它里面的容器名：my-app 映射关系： kind: Deployment → 创建一个「Pod 管理器」 replicas: 3 → 保证始终有 3 个 Pod template → Pod 的样子（运行哪个镜像、开哪个端口） Service（提供统一访问入口） apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app: my-app # 找到所有带 app=my-app 标签的 Pod ports: - port: 80 # Service 入口端口 targetPort: 8080 # Pod 里容器的端口 type: LoadBalancer # 暴露给外部访问 映射关系： selector → 绑定到 Deployment 创建的 Pod，根据 Pod 的标签（my-app） ports → 用户访问 80 端口，会转发到 Pod 的 8080 type: LoadBalancer → 提供一个外部 IP 供用户访问 典型工作流程 sequenceDiagram 开发者->>Git: 提交代码 Git->>CI/CD: 触发构建 CI/CD->>镜像仓库: 构建Docker镜像 CI/CD->>K8S: kubectl apply K8S->>K8S: 创建/更新Pod K8S->>K8S: 健康检查 K8S->>用户: 服务可用 sequenceDiagram 开发者->>API Server: kubectl apply -f xxx.yaml API Server->>etcd: 写入期望状态 API Server->>Scheduler: 请求调度 Scheduler->>Node: 分配 Pod 到合适的节点 Node->>Kubelet: 启动容器 Controller Manager->>Cluster: 监控状态并修复 Service->>用户: 提供统一访问入口 👉 过程总结： 提交 YAML → API Server 接收并存到 etcd。 Scheduler 决定 Pod 去哪个 Node。 Node 上的 Kubelet 把容器启动。 Controller Manager 盯着数量，缺了就补。 Service 统一出口，用户不用关心 Pod IP。 常用操作 前提是1：集群必须已经搭好（minikube/kind/k3s/云厂商的 K8S）。 2：kubectl 已经配置好 kubeconfig 3：镜像能拉取到 如果是公有镜像（比如 Docker Hub），直接写就行。 如果是私有仓库，需要配 imagePullSecrets。 # 部署应用 kubectl apply -f deployment.yaml kubectl apply -f service.yaml # 查看集群资源 kubectl get pods -o wide kubectl get deployments kubectl get services # 扩缩容 kubectl scale deployment/my-app --replicas=5 # 滚动更新 kubectl set image deployment/my-app my-app=my-registry/my-app:v2 # 回滚 kubectl rollout undo deployment/my-app
CI/CD 自动部署项目 使用Gitea Actions 1）启用Gitea Actions： 编辑你的Gitea服务器配置文件 app.ini（通常位于 /etc/gitea/或 Gitea 安装目录），添加： [actions] ENABLED = true 然后重启Gitea服务。 2）在部署服务器上安装Runner： # 1. 下载并安装 act_runner (需要sudo) sudo wget https://gitea.com/gitea/act_runner/releases/download/v0.2.10/act_runner-0.2.10-linux-amd64 -O /usr/local/bin/act_runner sudo chmod +x /usr/local/bin/act_runner # 2. 为Runner创建专用工作目录（推荐使用绝对路径） sudo mkdir -p /opt/gitea-runner sudo chown $USER:$USER /opt/gitea-runner # 将目录所有权给当前用户 cd /opt/gitea-runner # 3. 注册Runner（重要：必须添加labels标签） act_runner register --no-interactive \ --instance https://你的gitea域名.com \ --token <你的令牌> \ --name my-runner \ --labels self-hosted,linux # 这个标签必须与工作流文件中的runs-on匹配 # 4. 启动Runner（后台运行） nohup act_runner daemon > runner.log 2>&1 & # 5. 查看运行状态和日志 tail -f runner.log ps aux | grep act_runner 3）令牌获取： 如果你想给整个实例用 → 站点管理员设置 → Actions → Runners。 如果只想给某个仓库用 → 进入该仓库 → Settings → Actions → Runners。 点击 New Runner，系统会生成一个 TOKEN。 例如：abcdef1234567890 还是设置整个实例好！！ 4）将act_runner注册为服务自启动 sudo vim /etc/systemd/system/act_runner.service 写入以下内容： [Unit] Description=Gitea Actions Runner After=network.target [Service] ExecStart=/usr/local/bin/act_runner daemon WorkingDirectory=/opt/gitea-runner User=zy123 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ExecStart 指向你安装的 act_runner 路径（你已经放在 /usr/local/bin/ 了）。 WorkingDirectory 可以写你打算存放 Runner 缓存的目录（比如 /home/zy123/act_runner，你已经在这里 register 过了）。 User=zy123 保证 Runner 以你当前用户运行，而不是 root。 重新加载 systemd 配置： sudo systemctl daemon-reexec 启动并开机自启: sudo systemctl enable --now act_runner 查看运行状态: systemctl status act_runner 5）在后端仓库根目录下，新建目录和文件： .gitea/workflows/deploy.yml 写入以下内容： name: Deploy Backend on: push: branches: [ "master" ] # master 分支推送时触发 jobs: build-and-deploy: runs-on: self-hosted steps: - name: Checkout code uses: actions/checkout@v3 - name: Create application-local.yml run: | mkdir -p src/main/resources/ cat << 'EOF' > src/main/resources/application-local.yml ${{ secrets.APPLICATION_LOCAL_YML }} EOF - name: Build and restart smile-picture run: | cd docs/tags/picture-v1.0 docker compose -f docker-compose-app-v1.0.yml build smile-picture-backend docker compose -f docker-compose-app-v1.0.yml up -d smile-picture-backend - name: Cleanup dangling images run: docker image prune -f 特别注意,application-local没有用git管理，把它存在了Actions的密钥中： 然后deploy.yml中可以把里面的内容复制过去。 6）工作原理 1.Runner 接收任务 当你 push 到 main 分支时： Gitea 会触发 workflow（.gitea/workflows/deploy.yml）。 服务器上的 act_runner 会接收到这个任务，并开始执行 jobs.build-and-deploy。 2.actions/checkout@v3 这一行的作用是：它会把你刚刚 push 的最新代码 完整克隆 到 Runner 的工作目录。 /home/<你的用户>/actions-runner/_work/<repo名>/<repo名>/ 所以，这时候你的项目在 Runner 的临时目录里已经是最新代码了，不需要 git pull。 3.后续 cd docs/tag/group-buy-v3.0 都是在 这个临时目录 里执行的。 相当于你在服务器上的临时副本中： 用 docker compose build 构建镜像 用 docker compose up -d 启动/更新容器 4.Runner 每次执行 workflow 时，工作目录都会 重新 checkout 最新代码。 默认不会自动清理旧目录，但可以在 workflow 里加 actions/checkout 的参数来控制，比如 clean: true。. 7）git push之后
🧠 神经网络训练流程基础笔记 1. 数据集（Dataset） 数据集包含了 样本 (sample) 和 标签 (label) 在你的场景里： 样本 = 一个时间步的图（节点特征矩阵 + 邻接矩阵） 标签 = 每个节点是否掉线（0/1） 2. Batch（批量） Batch = 一次训练用到的一小部分样本 batch_size 表示 每次梯度更新时用多少个样本 例如： 数据集有 1000 个样本 batch_size=8 → 每次训练只拿 8 个样本来算梯度 这样能减少显存占用，也让模型更新更频繁 3. Epoch（轮次） Epoch = 把整个数据集完整训练一遍 如果： 数据集有 1000 个样本 batch_size=8 那么 1 个 epoch 需要 1000 / 8 = 125 次迭代，才能用完所有样本 训练 20 个 epoch，就相当于把数据重复“看”了 20 遍 4. 训练循环流程 一次完整训练过程一般是这样： 初始化模型参数（随机权重） 重复若干 epoch 打乱训练数据 按 batch_size 切成小批次 对每个 batch： 前向传播 (forward) 把输入样本丢进模型 得到预测结果 计算损失 (loss) 预测 vs 真实标签 的差距 反向传播 (backward) 自动求导，算出梯度 参数更新 (step) 用优化器（SGD/Adam）更新参数，让模型预测更接近标签 最终得到训练好的模型 5. 为什么要用 batch？ 全量训练 (batch_size=1000) 优点：精确 缺点：显存占用大，更新慢 小批量训练 (batch_size=8~32) 优点：节省显存，更新更频繁，收敛快 缺点：梯度有点“噪声”（但通常能帮助模型泛化） 👉 所以主流方法是 小批量训练 + 多个 epoch。 6. 直观理解 把训练比作 学生背单词： 样本 = 单词 batch_size = 一次背多少个单词（8 个单词一组，还是 50 个单词一组） epoch = 把词表完整过一遍（1000 个单词都背一遍） 多个 epoch = 重复背很多遍（记得更牢） 7. 在你现在的任务中 数据：1000 时间步 batch_size=8 → 一次训练 8 个时间步的图 epochs=20 → 每个时间步都要用到，整个过程重复 20 遍 总共更新参数的次数 = 1000/8 * 20 = 2500 次 📌 一句话总结： 训练神经网络 = 前向传播（算预测） + 损失函数（算误差） + 反向传播（算梯度） + 参数更新（优化器） 重复这个过程，按 batch 分批喂数据，按 epoch 控制重复多少轮。
这是完全匹配你**第一个代码片段（向量化、时间驱动版本）**的文字描述。 这个模型在学术上通常被称为 Time-Discrete Random Walk 或 Memoryless Random Walk（无记忆随机游走），因为它没有“目标点”的概念，每一步都是独立的。 RW（Random Walk，随机游走 - 向量化/时间驱动版） 空间与时间： 正方形区域 $[0, L] \times [0, L]$（其中 $L$ 为 map_size），时间被离散化为固定的 $T$ 个步长（Steps）。 节点运动规则： 初始位置： 在区域内部均匀随机生成所有节点的起始坐标 $(x, y) \sim U(0, L)$。 瞬时移动（核心差异）： 模型不设定任何固定的“目标点”或“行程”。 在每一个时间步 $t$，每个节点都会独立地重新随机选择一个运动方向 $\theta_t \sim U(0, 2\pi)$ 和一个瞬时速度 $v_t \sim U(v_{\min}, v_{\max})$。 位置更新： 根据当前步选定的速度矢量进行位移： $$x_t = x_{t-1} + v_t \cos(\theta_t)$$ $$y_t = y_{t-1} + v_t \sin(\theta_t)$$ 边界处理（物理反射）： 代码实现了完全弹性碰撞（Reflection）。当节点试图跨越边界时，就像台球撞击桌边一样，其坐标会被镜像反弹回区域内（例如：若 $x < 0$，则重置为 $-x$；若 $x > L$，则重置为 $2L - x$），从而保证节点始终被限制在区域内部。 邻接生成： 设定通信半径 comm_radius，计算节点间的欧几里得距离。若距离 $\le$ 阈值则判定为连通，逐步构造邻接矩阵序列。 要点： 无记忆性（Memoryless）： 这一时刻的运动方向与上一时刻完全无关（马尔科夫性 $\alpha=0$ 的极端情况）。 布朗运动特征： 节点的轨迹呈现高度的锯齿状（Zig-zag），缺乏长距离的直线运动。 扩散特性： 相比于有目标点的模型（如 RWP, RD），这种纯随机游走的节点扩散速度较慢，倾向于在局部区域内徘徊较长时间。
Spring AI + Ollama 1. Ollama 定位：本地/容器化的 大模型推理服务引擎 作用：负责加载模型、执行推理、返回结果 支持的模型：Qwen、DeepSeek、nomic-embed-text（embedding 模型）等 交互方式：REST API（默认 http://localhost:11434） ollama run qwen2.5:7b-instruct 2. Spring AI 定位：Spring 官方推出的 AI 接入框架 作用：统一封装对各种 AI 服务的调用逻辑，让开发者更容易接入（OpenAI、Ollama 等） 关键点 OllamaChatModel：封装了调用 Ollama 的对话接口 OllamaOptions：指定模型、参数（温度、上下文大小等） PgVectorStore：对接向量数据库（Spring AI 提供的默认向量数据库适配器。） TokenTextSplitter：默认的文本切分器 （Spring AI 内置逻辑） 3.pgVectorStore pgVectorStore 是 Spring AI 提供的向量存储接口实现，底层基于 PostgreSQL + pgvector 插件。它在项目中的作用： 1）存储向量和元数据： pgVectorStore.accept(splits); 这行代码会： 1.将 splits 中每个 Document 的 text → 调用 embedding 模型 → 生成向量 2.将生成的向量、原始文本、以及文档元数据统一存入数据库表（默认表名：vector_store）； embedding：存储生成的向量； content：存储原始文本内容； metadata：以 JSONB 形式存储元数据。 2）检索时自动生成 SQL： List<Document> documents = pgVectorStore.similaritySearch(request); 在执行检索时，pgVectorStore 会自动生成 SQL 查询， 解析查询请求中的文本； 生成查询向量； 自动构造 SQL（结合 pgvector 的 <-> 向量相似度运算符）； 按相似度排序返回最相关的文档列表。 进一步地，结合 filterExpression（例如 metadata->>'knowledge' = 'xxx'）可在相似度计算的基础上进一步进行条件过滤。 配置 ai: ollama: base-url: http://ollama:11434 #告诉 Spring AI 去 http://ollama:11434 调用模型接口。 embedding: options: num-batch: 512 #批处理参数 model: nomic-embed-text #指定使用的 embedding 模型 rag: embed: nomic-embed-text 注意，需要提前下载模型： ollama pull deepseek-r1:1.5b ollama pull qwen2.5:7b-instruct ollama pull nomic-embed-text ollama list Docker如何开启GPU加速大模型推理 巨坑！ 默认是CPU跑大模型，deepseek1.5b勉强能跑动，但速度很慢，一换qwen2.5:7b模型瞬间就跑不动了，这才发现一直在用CPU跑！！ 如何排查？ 1.查看ollama日志： load_backend: loaded CPU backend from /usr/lib/ollama/libggml-cpu-alderlake.so 2.本地cmd命令行输入，查看显存占用率。 nvidia-smi | 0 NVIDIA GeForce RTX 4060 ... WDDM | 00000000:01:00.0 On | N/A | | N/A 58C P0 24W / 110W | 2261MiB / 8188MiB | 6% Default | 设备状态： RTX 4060显卡驱动（576.02）和CUDA 12.9环境正常 当前GPU利用率仅6%（GPU-Util列） 显存占用2261MB/8188MB（约27.6%） 可见模型推理的时候压根没有用GPU！！！ 解决 参考博客：1-3 Windows Docker Desktop安装与设置docker实现容器GPU加速_windows docker gpu-CSDN博客 1）配置WSL2，打开 PowerShell（以管理员身份运行），执行以下命令： wsl --install -d Ubuntu # 安装 Linux 发行版 wsl --set-default-version 2 # 设为默认版本 wsl --update # 更新内核 重启计算机以使更改生效。 2）检查wsl是否安装成功 C:\Users\zhangsan>wsl --list --verbose NAME STATE VERSION * Ubuntu Running 2 docker-desktop Running 2 这个命令会列出所有已安装的Linux发行版及其状态。如果看到列出的Linux发行版，说明WSL已成功安装。 3）安装docker desktop默认配置： 4）保险起见也启用一下开启 Windows 的 Hyper-V 虚拟化技术： 搜索“启用或关闭Windows功能”，勾选：Hyper-V 虚拟化技术。 5） 命令提示符输入 nvidia-smi C:\Users\zhangsan>nvidia-smi Tue Aug 19 21:18:11 2025 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 576.02 Driver Version: 576.02 CUDA Version: 12.9 | |-----------------------------------------+------------------------+----------------------+ | GPU Name Driver-Model | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 NVIDIA GeForce RTX 4060 ... WDDM | 00000000:01:00.0 On | N/A | | N/A 55C P4 12W / 129W | 2132MiB / 8188MiB | 33% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ 若显示GPU信息（如型号、显存等），则表示支持。 6）Docker Desktop配置**（重要）** 打开 Docker 设置 → Resources → WSL Integration → 启用 Ubuntu 实例。 进入 Docker 设置 → Docker Engine，添加以下配置： { "experimental": true, "features": { "buildkit": true }, "registry-mirrors": ["https://registry.docker-cn.com"] // 可选镜像加速 } 保存并重启 Docker。 7）验证 GPU 加速 docker run --rm -it --gpus=all nvcr.io/nvidia/k8s/cuda-sample:nbody nbody -gpu -benchmark NOTE: The CUDA Samples are not meant for performance measurements. Results may vary when GPU Boost is enabled. > Windowed mode > Simulation data stored in video memory > Single precision floating point simulation > 1 Devices used for simulation MapSMtoCores for SM 8.9 is undefined. Default to use 128 Cores/SM MapSMtoArchName for SM 8.9 is undefined. Default to use Ampere GPU Device 0: "Ampere" with compute capability 8.9 > Compute 8.9 CUDA device: [NVIDIA GeForce RTX 4060 Laptop GPU] 24576 bodies, total time for 10 iterations: 17.351 ms = 348.102 billion interactions per second = 6962.039 single-precision GFLOP/s at 20 flops per interaction 成功输出应包含 GPU 型号及性能指标 [NVIDIA GeForce RTX 4060 Laptop GPU] 8）Ollama验证： 在容器中也能显示GPU了，说明配置成功了！！！ version: '3.9' services: ollama: image: registry.cn-hangzhou.aliyuncs.com/xfg-studio/ollama:0.5.10 container_name: ollama restart: unless-stopped ports: - "11434:11434" volumes: - ./ollama:/root/.ollama runtime: nvidia environment: - NVIDIA_VISIBLE_DEVICES=all - NVIDIA_DRIVER_CAPABILITIES=all 什么原理？ GPU 驱动仍运行在 Windows 主机上；WSL2 作为 Linux 内核子系统，共享访问该 GPU 驱动的底层接口；Docker Desktop 在 WSL2 内运行（作为 Linux 虚拟环境）， 通过 NVIDIA Container Runtime (nvidia-container-toolkit) 把 GPU 设备挂载进容器； 最终容器内程序（如 PyTorch、Ollama）能直接使用 GPU 计算。 RAG（检索增强生成） postgre向量数据库 PostgreSQL也是结构化数据库（关系型数据库）！！！ 所有数据都遵守固定的结构（Schema）； 每一列（Column）定义了数据类型； 每一行（Row）表示一条记录； 表结构： @Bean public PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) { return PgVectorStore.builder(jdbcTemplate, embeddingModel).build(); } PgVectorStore 会自动检测并在首次使用时创建所需的向量表，默认表名是 vector_store；默认表结构是固定格式（id, content, metadata, embedding）。 PgAdmin软件下：ai-rag-knowledge -> 架构 -> public -> 表 -> vector_store id → 每条数据的唯一标识（主键/uuid）。 content → 存放 chunk 的原始文本。 metadata → 存放 JSON 格式的额外信息（文件名、路径、知识库标签等）。 embedding → 存放向量，类型是 vector(N)（N 由 Embedding 模型决定，比如 768）。 所有文件切分出来的 chunk 都存在这一张表里，每条记录就是一个 chunk。 表的主要用途 1）相似度检索（Retrieval）：用embedding <-> 查询向量 进行向量距离计算，找出与输入文本语义最相近的片段。其中<-> 是 pgvector 的距离运算符 2）结果展示（Content Retrieval）：检索后，取出距离最近的若干条记录的 content，作为模型回答时的上下文。 3）溯源/过滤：通过 metadata 字段中的信息（如文件名、标签、知识类别等）进行结果筛选或来源追踪，例如：其中 ->> 从 JSON 对象中取出某个字段的值，并以文本（text）形式返回。 INSERT INTO vector_store (content, metadata) VALUES ('公司年度财报分析', '{"knowledge": "finance", "source": "report.pdf"}'), ('销售策略优化', '{"knowledge": "marketing", "source": "plan.docx"}'), ('财务合规指引', '{"knowledge": "finance", "source": "policy.pdf"}'), ('技术架构设计', '{"knowledge": "engineering", "source": "design.md"}'); 现在你想只查出属于 “财务领域（finance）” 的内容，可以这样写： SELECT id, content, metadata FROM vector_store WHERE metadata->>'knowledge' = 'finance'; 查询 同一般的SQL语句。 vector_dims(embedding)是内置函数，返回维度大小。 还有 metadata 和embedding 列显示不下。 A. 索引/入库（Ingestion） 这里用了SpringAI提供的：TikaDocumentReader 和 pgVectorStore 文档读取器 用 TikaDocumentReader 把上传的 PDF、Word、TXT、PPT 等文件解析成 List<Document>。 每个 Document 包含 text 和 metadata（比如 page、source）。 相当于是“把二进制文件 → 转成纯文本段落”。 TikaDocumentReader documentReader = new TikaDocumentReader(resource); List<Document> documents = documentReader.get(); 清洗过滤 剔除空文本，避免无效数据进入后续流程。 List<Document> docs = new ArrayList<>(documents); docs.removeIf(d -> d.getText() == null || d.getText().trim().isEmpty()); if (docs.isEmpty()) { log.warn("文件内容为空，跳过处理: {}", normalizedPath); return; } 文档切分器 用 TokenTextSplitter 把每个 Document 再切成 chunk（默认 800 tokens 一段，可配置）。 目的是避免超长文本超过 Embedding 模型的输入限制。 @Bean public TokenTextSplitter tokenTextSplitter() { return TokenTextSplitter.builder() .withChunkSize(600) // 每段最多 600 token .withMinChunkSizeChars(300) // 每段至少 300 字符 .withMinChunkLengthToEmbed(10) .withMaxNumChunks(10000) .withKeepSeparator(true) .build(); } //使用，省略依赖注入 List<Document> splits = tokenTextSplitter.apply(docs); withMinChunkSizeChars(300): 如果某个切分块的文本少于 300 个字符，TokenTextSplitter 会避免直接拆分它，而是尝试 合并它与下一个切分块，直到符合字符数要求。 打元数据 给原始文档和 chunk 都加上 knowledge（ragTag）、path、original_filename 等信息。 方便后续检索时追溯来源。 docs.forEach(doc -> { doc.getMetadata().put("knowledge", ragTag); doc.getMetadata().put("path", normalizedPath); doc.getMetadata().put("original_filename", originalFilename); }); splits.forEach(doc -> { doc.getMetadata().put("knowledge", ragTag); doc.getMetadata().put("path", normalizedPath); doc.getMetadata().put("original_filename", originalFilename); }); Embedding 模型 对 每个 chunk 调用 Ollama 的 nomic-embed-text，生成一个固定维度的向量（如 768 维）。 注意：这是对 chunk 整体嵌入，不是对单个 token。 向量存储 //配置类 @Bean public PgVectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) { return PgVectorStore.builder(jdbcTemplate, embeddingModel).build(); } //使用，省略依赖注入 pgVectorStore.accept(splits); 用 pgvector 存储 [embedding 向量 + metadata + 原始文本]。 向量维度由 Embedding 模型决定，pgvector 表的维度必须保持一致（如 768/1024/1536）。 B. 检索/回答（Query） 接收用户问题 输入用户的问题文本 message。 相似度检索 使用同一个 Embedding 模型（如 nomic-embed-text）将问题转为查询向量。 在向量库中用 余弦相似度 / 内积 搜索相似 chunk。 可附加条件过滤：如 knowledge == 'xxx'，只在某个知识库范围内查。 常用参数： topK：取最相似的前 K 个结果（例如 5~8）。 minSimilarityScore（可选）：过滤低相关度结果。 // 1) 相似度检索（带 ragTag 过滤） List<Document> documents = pgVectorStore.similaritySearch( SearchRequest.builder() .query(message) .topK(8) .filterExpression("knowledge == '" + ragTag + "'") .build() ); List<Document> documents = pgVectorStore.similaritySearch(request); 拼装文档上下文 把检索到的文档片段拼接成系统提示中的 DOCUMENTS 部分。 可以在拼接时附带 metadata（如文件名、页码），方便溯源。 String documentContent = documents.stream() .map(doc -> "[来源:" + doc.getMetadata().get("original_filename") + "]\n" + doc.getText()) .collect(joining("\n\n---\n\n")); 构造提示词（Prompt） 使用 SystemPromptTemplate 注入 DOCUMENTS 内容。 System Prompt 应该放在 用户消息之前，确保模型优先遵循规则。 Message ragMessage = new SystemPromptTemplate(SYSTEM_PROMPT) .createMessage(Map.of("documents", documentContent)); List<Message> messages = new ArrayList<>(); messages.add(ragMessage); // 先放系统提示 messages.add(new UserMessage(message)); 调用对话模型（流式返回） return ollamaChatModel.stream(new Prompt( messages, OllamaOptions.builder().model(model).build() )); 优化 1.优化分词逻辑 这块比较复杂，要切的恰到好处...切的不大不小。 2.更新向量嵌入模型 MCP服务 简介 - 模型上下文协议 MCP 是“模型与外部世界沟通的桥梁”。它让模型不仅能聊天，还能调用本地文件系统、数据库、网络 API 等服务，实现“有工具可用的 AI”。 基础介绍 角色 说明 举例 🖥️ MCP Server（服务端） 提供“工具能力”，比如操作文件系统、调用系统命令、执行数据库查询等。 @modelcontextprotocol/server-filesystem、mcp-server-computer.jar 🤖 MCP Client（客户端） 嵌入在应用（你的 Spring Boot）中，让模型能访问这些工具。 由 spring-ai-mcp-client-webflux-spring-boot-starter 实现 快速使用 1）引入依赖（POM） <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId> </dependency> 2）配置 MCP 服务（Server） 创建配置文件：resources/config/mcp-servers-config.json { "mcpServers": { "filesystem": { "command": "npx.cmd", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "D:/folder/MCP-test", "D:/folder/MCP-test" ] }, "mcp-server-computer": { "command": "java", "args": [ "-Dspring.ai.mcp.server.stdio=true", "-jar", "D:/folder/study/apache-maven-3.8.4/mvn_repo/edu/whut/mcp/mcp-server-computer/1.0.0/mcp-server-computer-1.0.0.jar" ] } } } filesystem：定义了一个 MCP 文件系统服务； 通过 npx 启动； 调用包 @modelcontextprotocol/server-filesystem； 并将本地路径 D:/folder/MCP-test 暴露给模型访问。 mcp-server-computer：定义了一个自定义的 Java MCP 服务； 使用 java -jar 启动； 通过标准输入输出（stdio）与主程序通信。 Spring配置文件（application.yml）配置 MCP 服务加载路径 spring: ai: mcp: servers-config: classpath:config/mcp-servers-config.json 3）安装并启用 Filesystem MCP 服务 需要提前下载该文件服务，官网: https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem （创建、移动目录，读取、写入文件...） 先装 Node.js，配置环境变量 node -v npm -v 安装 MCP 文件系统服务 npm install -g @modelcontextprotocol/server-filesystem 4）配置客户端 Spring AI 通过 ChatClient 调用大模型，如果你使用多个模型（如 Ollama、OpenAI），可以通过不同的 ChatModel Bean 实例来区分。 @Bean public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi) { return OllamaChatModel.builder() .ollamaApi(ollamaApi) .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build()) .build(); } @Bean public ChatClient.Builder chatClientBuilder(OllamaChatModel ollamaChatModel) { return new DefaultChatClientBuilder( ollamaChatModel, ObservationRegistry.NOOP, (ChatClientObservationConvention) null ); } 注意，ollamaChatModel()是基础模型，创建与Ollama服务交互的底层Chat模型；chatClientBuilder()是客户端构建器，提供高级功能封装（如工具调用） 5）模型调用示例 ollamaChatModel 是连接本地 Ollama 服务的客户端； 具体使用哪个模型（deepseek-r1:1.5b、qwen2.5:7b-instruct 等）是在调用时通过 OllamaOptions 决定的: ChatResponse response = chatClientBuilder .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build()) .build() .prompt("你好，介绍一下你自己") .call(); 6）测试 MCP 调用 查看当前可用工具 @GetMapping("/tools") public Object listTools() { return Arrays.stream(tools.getToolCallbacks()) .map(cb -> Map.of( "name", cb.getName(), "description", cb.getDescription() )) .toList(); } 自动调用工具（含 MCP） @GetMapping("/test-workflow") public String testWorkflow(@RequestParam String question) { var chatClient = chatClientBuilder .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build()) .build(); return chatClient.prompt(question).tools(tools).call().chatResponse().toString(); } 当模型发现某个问题需要使用外部工具（如文件读写、计算服务）时，会自动调用注册在 MCP 中的工具进行操作。 有时候大模型不会去调用Tools，可能是模型能力不够。 注意，docker部署后，这个路径还没改！！！自然查不到可用的工具，因为不是在本地windows中了。 自定义MCP服务 目录结构 mcp-server-demo/ ├─ src/ │ └─ main/ │ ├─ java/…/DemoApplication.java │ ├─ java/…/NoteService.java // 你的工具：@Tool 方法 │ └─ resources/application.yml // 仅需几行 └─ pom.xml POM 依赖 <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-M6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> 关键配置 spring: application: name: mcp-server-demo ai: mcp: server: name: ${spring.application.name} version: 1.0.0 # 让它安静点，避免把 MCP JSON 混进正常日志 logging: level: org.springframework.ai.mcp: INFO pattern: console: server: main: banner-mode: off # 非 Web 进程（可选） # web-application-type: none 入口 & 工具 DemoApplication.java @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } 启动配置。 NoteService.java @Service public class NoteService { @Tool(description = "把输入转成大写并附带字符数") public Map<String, Object> toUpper( @ToolParam(name = "text", description = "要转换的文本") String text) { if (text == null) text = ""; return Map.of( "upper", text.toUpperCase(), "length", text.length() ); } } 注意，你的服务方法必须配置 @Tool(description = "获取电脑配置") 多一个注解，就向外多提供一个工具！！！ 生成Jar包 在客户端项目中引用 { "mcpServers": { "mcp-server-demo": { "command": "java", "args": [ "-Dspring.ai.mcp.server.stdio=true", "-jar", "C:/path/to/target/mcp-server-demo-1.0.0.jar" ] } } } 注意上一步生成的jar包要放到这个"C:/path/to/target/mcp-server-demo-1.0.0.jar"中。 底层调用逻辑 一、启动客户端 假设你运行的客户端配置里包含： "mcpServers": { "filesystem": {...}, "mcp-server-computer": {...} } 当客户端启动时，会做以下几步： 1）客户端启动子进程 客户端发现你声明了两个 MCP 服务器，于是准备分别启动它们。 npx.cmd -y @modelcontextprotocol/server-filesystem D:/folder/MCP-test D:/folder/MCP-test java -Dspring.ai.mcp.server.stdio=true -jar D:/folder/.../mcp-server-computer-1.0.0.jar 这两个命令都会启动新的独立进程，“挂在”客户端进程下面，通过标准输入/输出通信。 2）客户端 → 服务端 “握手”（initialize） 客户端通过每个子进程的 stdin 写入 MCP 初始化请求： { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "clientInfo": {"name": "MyClient", "version": "1.0"}, "capabilities": {} } } 3）服务端 → 客户端 回应握手结果 你的 mcp-server-computer.jar在内部收到这个 JSON-RPC 请求后，返回： { "jsonrpc": "2.0", "id": 1, "result": { "serverInfo": {"name": "mcp-server-computer", "version": "1.0.0"}, "capabilities": {"tools": true} } } 客户端读取这条 JSON（从 stdout），于是握手成功 4）客户端请求工具清单 紧接着客户端发： { "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} } 服务端返回： { "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "queryConfig", "description": "获取电脑配置", ... } ] } } 客户端现在就知道：“这个 MCP Server 提供一个叫 queryConfig 的工具”。 二、用户发起请求 1）用户发起问题：“请读取这台电脑的配置”。 2）客户端构造 Chat 请求 将用户消息 + 系统消息 + 工具的 JSON Schema（名称、入参、描述）一起发给大模型。 请求里不仅包含用户输入，还要告诉模型——“你可以调用这些工具，这些工具的参数长这样。” 在 Spring AI 中，这一步通常由 ChatClient 或底层 ChatModel 完成 @GetMapping("/test-workflow") public String testWorkflow(@RequestParam String question) { log.info("Workflow测试请求接收 - 问题: {}", question); var chatClient = chatClientBuilder .defaultOptions(OllamaOptions.builder().model("qwen2.5:7b-instruct").build()) .build(); ChatResponse response = chatClient .prompt(question) .system("你必须使用提供的工具完成任务，禁止凭空回答；若没有调用任何工具，不要返回答案。") .tools(tools) .call() .chatResponse(); log.info("工具调用情况: {}", tools.getToolCallbacks()); log.info("模型响应: {}", response); return response.toString(); } 实际发送给模型的底层 payload 类似这样： { "model": "qwen2.5:7b-instruct", "messages": [ { "role": "system", "content": "你必须使用提供的工具完成任务..." }, { "role": "user", "content": "在 D:/folder/MCP-test 下创建配置.txt..." } ], "tools": [ { "type": "function", "function": { "name": "queryConfig", "description": "获取电脑配置（操作系统、用户信息、Java环境等）", "parameters": { "type": "object", "properties": { "computer": { "type": "string", "description": "电脑名称，可为空" } } } } }, { "type": "function", "function": { "name": "write_file", "description": "创建一个文件并写入内容", "parameters": { "type": "object", "properties": { "path": {"type": "string"}, "content": {"type": "string"} } } } } ], "tool_choice": "auto" } 3）模型分析任务 → 决定调用哪个工具 大模型在接收到 prompt 后，会“思考”： “要回答这个问题，我需要外部信息吗？” “有一个叫 queryConfig 的工具能获取电脑配置，那我就调用它。” 现代指令模型（包括 Qwen、GPT-4o、Claude、Llama）都经过一种专门的微调（finetune）： 当输入里包含 "tools"、"parameters"、"function" 这样的字段时， 模型学会： 识别哪些任务需要调用工具； 生成标准 JSON 格式的 tool_calls； 不胡编乱造工具名； 遵守 schema 中参数名的拼写和结构。 于是它返回结构化响应（OpenAI function-calling 格式）： { "id": "chatcmpl-01", "object": "chat.completion", "choices": [ { "index": 0, "message": { "role": "assistant", "tool_calls": [ { "id": "call_1", "type": "function", "function": { "name": "queryConfig", "arguments": "{\"computer\": \"DESKTOP-01\"}" } } ] } } ] } 4）客户端执行工具调用（MCP 调用链） Spring AI 的 DefaultToolCallingManager 会检测到（框架自动实现）： if (response.hasToolCalls()) { executeToolCalls(response.getToolCalls()); } 客户端解析出模型请求调用的工具名 queryConfig，然后通过 MCP 协议向对应的 MCP Server（你的 mcp-server-computer）发送调用请求（通过 stdin）： { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "queryConfig", "arguments": { "computer": "DESKTOP-01" } } } 注意，默认的工具名就是函数名！！！ 你的 MCP Server（Spring Boot 进程）接收后，由框架自动路由到 @Tool 方法： @Service public class ComputerService { @Tool(description = "获取电脑配置") public ComputerFunctionResponse queryConfig( @ToolParam(name = "computer", description = "电脑名称") String computer ) { log.info("获取电脑配置信息: {}", computer); // 读取系统属性 + systeminfo 等命令 ... return response; // 返回 JSON 对象 } } 6）客户端将结果反馈回模型（第二轮） 客户端收到结果后，会把它作为新的上下文输入发回模型： { "role": "tool", "tool_call_id": "call_1", "name": "queryConfig", "content": "{\"osName\": \"Windows 11\", ... }" } 模型在这时再进行一轮“总结性思考”： “我得到了电脑配置结果，可以组织成自然语言回答。” 最终返回： { "role": "assistant", "content": "你的电脑运行的是 Windows 11，架构为 x64，用户名为 Jerry。" } sequenceDiagram participant User participant Client participant Model participant MCPServer as MCP Server User->>Client: "请读取这台电脑的配置" Client->>Model: 封装 Prompt + Tool Schema 发送请求 Model-->>Client: 返回 tool_calls: queryConfig(...) Client->>MCPServer: 通过 MCP stdin 发送 tools/call 请求 MCPServer-->>Client: 执行 Java @Tool 方法 返回 JSON 结果 Client->>Model: 将工具结果再发给模型 Model-->>Client: 生成自然语言回答 Client-->>User: 输出最终文本 如果有长的调用链 当模型需要调用多个工具（例如：create_directory → queryConfig → write_file）， Spring AI 会自动进入一个「循环式调用流程」，直到模型自己不再请求调用工具为止。 框架就会自动: 1.发现模型要调用工具； 2.执行工具； 3.把结果回灌给模型； 4.模型如果再要求调用下一个工具，再执行； 5.一直到模型返回纯文本。 while (true) { ChatResponse response = model.call(currentPrompt); if (response.hasToolCalls()) { // 1. 模型请求调用工具 for (ToolCall call : response.getToolCalls()) { Object result = executeTool(call.name, call.arguments); // 2. 把结果封装成“tool_result”消息 currentPrompt.addToolResult(call.id, call.name, result); } } else { // 没有工具调用 = 模型给出最终自然语言 return response; } }
设计模式 单例模式 懒汉 注意，单例模式的构造函数是私有的！ public class LazySingleton { private static volatile LazySingleton INSTANCE; private LazySingleton() {} public static LazySingleton getInstance() { if (INSTANCE == null) { // 第一次检查 synchronized (LazySingleton.class) { if (INSTANCE == null) { // 第二次检查 INSTANCE = new LazySingleton(); } } } return INSTANCE; } } 第一次检查：防止重复实例化、以及进行synchronized同步块（防止每次加锁的开销）。 第二次检查：防止有多个线程同时通过第一次检查，然后依次进入同步块后，创建N个实例。 volatile：防止指令重排序。 instance = new LazySingleton(); 正确顺序是： memory = allocate(); // 分配内存 ctorInstance(memory); // 调用构造函数，初始化对象 INSTANCE = memory; // 将对象引用赋值给 INSTANCE 由于 JVM 和 CPU 都可能对指令进行优化重排，步骤 ② 和 ③ 的顺序可能被交换！ memory = allocate(); // 分配内存 INSTANCE = memory; // 把引用赋给 INSTANCE（但未初始化完） ctorInstance(memory); // 调用构造函数 线程 A刚执行完赋值（3），但没调用构造函数；此时线程B来了，发现 INSTANCE != null，直接返回这个未初始化完成的对象！！！ 饿汉 public class EagerSingleton { // 类加载时就初始化实例 private static final EagerSingleton INSTANCE = new EagerSingleton(); // 私有构造函数 private EagerSingleton() {} // 全局访问点 public static EagerSingleton getInstance() { return INSTANCE; } } 工厂模式 简单工厂 // 产品接口 interface Product { void use(); } // 具体产品A class ConcreteProductA implements Product { @Override public void use() { System.out.println("使用产品A"); } } // 具体产品B class ConcreteProductB implements Product { @Override public void use() { System.out.println("使用产品B"); } } class SimpleFactory { // 根据参数创建不同的产品 public static Product createProduct(String type) { switch (type) { case "A": return new ConcreteProductA(); case "B": return new ConcreteProductB(); default: throw new IllegalArgumentException("未知产品类型"); } } } public class Client { public static void main(String[] args) { // 通过工厂创建产品 Product productA = SimpleFactory.createProduct("A"); productA.use(); // 输出: 使用产品A Product productB = SimpleFactory.createProduct("B"); productB.use(); // 输出: 使用产品B } } 缺点：添加新产品需要修改工厂类（违反开闭原则） 抽象工厂 抽象工厂模式是一种创建型设计模式，它提供一个接口用于创建相关或依赖对象的家族，而不需要明确指定具体类。 // 抽象产品接口 interface Button { void render(); } interface Checkbox { void render(); } // 具体产品实现 - Windows 风格 class WindowsButton implements Button { @Override public void render() { System.out.println("渲染一个 Windows 风格的按钮"); } } class WindowsCheckbox implements Checkbox { @Override public void render() { System.out.println("渲染一个 Windows 风格的复选框"); } } // 具体产品实现 - MacOS 风格 class MacOSButton implements Button { @Override public void render() { System.out.println("渲染一个 MacOS 风格的按钮"); } } class MacOSCheckbox implements Checkbox { @Override public void render() { System.out.println("渲染一个 MacOS 风格的复选框"); } } // 抽象工厂接口 interface GUIFactory { Button createButton(); Checkbox createCheckbox(); } // 具体工厂实现 - Windows class WindowsFactory implements GUIFactory { @Override public Button createButton() { return new WindowsButton(); } @Override public Checkbox createCheckbox() { return new WindowsCheckbox(); } } // 具体工厂实现 - MacOS class MacOSFactory implements GUIFactory { @Override public Button createButton() { return new MacOSButton(); } @Override public Checkbox createCheckbox() { return new MacOSCheckbox(); } } // 客户端代码 public class Application { private Button button; private Checkbox checkbox; public Application(GUIFactory factory) { button = factory.createButton(); checkbox = factory.createCheckbox(); } public void render() { button.render(); checkbox.render(); } public static void main(String[] args) { // 根据配置或环境选择工厂 GUIFactory factory; String osName = System.getProperty("os.name").toLowerCase(); if (osName.contains("win")) { factory = new WindowsFactory(); } else { factory = new MacOSFactory(); } Application app = new Application(factory); app.render(); } } 观察者模式 是一种行为型设计模式，定义对象之间一对多的依赖关系，当一个对象（被观察者 / Subject）状态发生变化时 ，会自动通知所有依赖它的对象（观察者 / Observer），实现事件通知机制。 典型应用场景： GUI 事件监听（按钮点击通知多个监听器） 消息订阅/发布系统（发布者-订阅者） Spring 的事件机制（ApplicationEvent） 数据模型变化 → 界面自动更新（MVC、MVVM） 模板方法 核心思想： 在抽象父类中定义算法骨架（固定执行顺序），把某些可变步骤留给子类重写；调用方只用模板方法，保证流程一致。 如果仅仅是把重复的方法抽取成公共函数，不叫模板方法！模板方法要设计算法骨架！！！ Client ───▶ AbstractClass ├─ templateMethod() ←—— 固定流程 │ step1() │ step2() ←—— 抽象，可变 │ step3() └─ hookMethod() ←—— 可选覆盖 ▲ │ extends ┌──────────┴──────────┐ │ ConcreteClassA/B… │ 示例： // 1. 抽象模板 public abstract class AbstractDialog { // 模板方法：固定调用顺序，设为 final 防止子类改流程 public final void show() { initLayout(); bindEvent(); beforeDisplay(); // 钩子，可选 display(); afterDisplay(); // 钩子，可选 } // 具体公共步骤 private void initLayout() { System.out.println("加载通用布局文件"); } // 需要子类实现的抽象步骤 protected abstract void bindEvent(); // 钩子方法，默认空实现 protected void beforeDisplay() {} protected void afterDisplay() {} private void display() { System.out.println("弹出对话框"); } } // 2. 子类：登录对话框 public class LoginDialog extends AbstractDialog { @Override protected void bindEvent() { System.out.println("绑定登录按钮事件"); } @Override protected void afterDisplay() { System.out.println("focus 到用户名输入框"); } } // 3. 调用 public class Demo { public static void main(String[] args) { AbstractDialog dialog = new LoginDialog(); dialog.show(); /* 输出： 加载通用布局文件 绑定登录按钮事件 弹出对话框 focus 到用户名输入框 */ } } 要点 复用公共流程：initLayout()、display() 写一次即可。 限制流程顺序：show() 定为 final，防止子类乱改步骤。 钩子方法：子类可选择性覆盖（如 beforeDisplay）。 策略模式 核心思想： 将可以互换的算法或行为抽象为独立的策略类，运行时由**上下文类（Context）**选择合适的策略对象去执行。调用方（Client）只依赖统一的接口，不关心具体实现。 ┌───────────────┐ │ Client │ └─────▲─────────┘ │ has-a ┌─────┴─────────┐ implements │ Context │────────────┐ ┌──────────────┐ │ (使用者) │ strategy └─▶│ Strategy A │ └───────────────┘ ├──────────────┤ │ Strategy B │ └──────────────┘ // 策略接口 public interface PaymentStrategy { void pay(int amount); } // 策略A：微信支付 @Service("wechat") public class WechatPay implements PaymentStrategy { public void pay(int amount) { System.out.println("使用微信支付 " + amount + " 元"); } } // 策略B：支付宝支付 @Service("alipay") public class Alipay implements PaymentStrategy { public void pay(int amount) { System.out.println("使用支付宝支付 " + amount + " 元"); } } // 上下文类 public class PaymentContext { private PaymentStrategy strategy; public PaymentContext(PaymentStrategy strategy) { this.strategy = strategy; } public void execute(int amount) { strategy.pay(amount); } } // 调用方 public class Main { public static void main(String[] args) { PaymentContext ctx = new PaymentContext(new WechatPay()); ctx.execute(100); ctx = new PaymentContext(new Alipay()); ctx.execute(200); } } 下面有更优雅的策略选择方式！ Spring集合自动注入 在策略、工厂、插件等模式中，经常需要维护**“策略名 → 策略对象”**的映射。Spring 可以通过 Map<String, 接口类型> 一次性注入所有实现类。 @Resource private final Map<String, IDiscountCalculateService> discountCalculateServiceMap; 字段类型：Map<String, IDiscountCalculateService> key—— Bean 的名字 默认是类名首字母小写 (mjCalculateService) 或者你在实现类上显式写的 @Service("MJ") value —— 那个实现类对应的实例 Spring 机制： 启动时扫描所有实现 IDiscountCalculateService 的 Bean。 把它们按 “BeanName → Bean 实例” 的映射注入到这张 Map 里。 你一次性就拿到了“策略字典”。 示例： // 上下文类：自动注入所有策略 Bean @Component @RequiredArgsConstructor public class PaymentContext { // key 为 Bean 名（如 "wechat"、"alipay"），value 为策略实例 private final Map<String, PaymentStrategy> paymentStrategyMap; public void pay(String strategyKey, int amount) { PaymentStrategy strategy = paymentStrategyMap.get(strategyKey); if (strategy == null) { throw new IllegalArgumentException("无匹配支付方式: " + strategyKey); } strategy.pay(amount); } } // 调用方示例 @Component @RequiredArgsConstructor public class PaymentService { private final PaymentContext paymentContext; public void process() { paymentContext.pay("wechat", 100); // 输出：使用微信支付 100 元 paymentContext.pay("alipay", 200); // 输出：使用支付宝支付 200 元 } } 模板方法+策略模式 本项目的价格试算同时用了策略模式 + 模板方法模式： 策略模式（Strategy）： IDiscountCalculateService 是策略接口；ZKCalculateService、ZJCalculateService ...是可替换的折扣策略（@Service("ZK") / @Service("ZJ") 作为选择键）。外部可以根据活动配置里的类型码选哪个实现来算价——这就是“运行时可切换算法”。 模板方法模式（Template Method）： AbstractDiscountCalculateService#calculate(...) 把共同流程固定下来（先进行人群校验 → 计算优惠后价格），并把“真正的计算”这一步延迟到子类通过 doCalculate(...) 实现。 责任链 应用场景：日志系统、审批流程、权限校验——任何需要将请求按阶段传递、并由某一环节决定是否继续或终止处理的地方，都非常适合责链模式。 场景：员工报销审批 组长审批 报销单先到组长这里。 组长要么通过，要么驳回；如果通过，就传递给下一个。 部门经理审批 组长通过后，报销单自动流转到部门经理。 部门经理再看金额和合理性，要么通过，要么驳回；如果通过，就继续往下。 财务审批 部门经理通过后，单子来到财务。 财务校验发票、预算，要么通过，要么驳回；如果通过，就继续。 总经理审批 如果金额超过某个阈值（比如 5 万），最后需要总经理签字。 总经理通过后，整个审批链结束。 典型的责任链模式要点： 解耦请求发送者和处理者：调用者只持有链头，不关心中间环节。 动态组装：通过 appendNext 可以灵活地增加、删除或重排链上的节点。 可扩展：新增处理逻辑只需继承 AbstractLogicLink 并实现 apply，不用改动已有代码。 单实例链 可以理解成“单向、单链表式的链条”：每个节点只知道自己的下一个节点（next），链头只有一个入口。 你可以在启动或运行时动态组装：head.appendNext(a).appendNext(b).appendNext(c); T / D / R 是啥？ T：请求的静态入参（本次请求的主要数据）。 D：动态上下文（链路里各节点共享、可读写的状态容器，比如日志收集、校验中间结果）。 R：最终返回结果类型。 1）接口定义：ILogicChainArmory<T, D, R> 提供添加节点方法和获取节点 // 定义了“链条组装”的最小能力：能拿到下一个节点、也能把下一个节点接上去 public interface ILogicChainArmory<T, D, R> { // 获取当前节点的“下一个”处理者 ILogicLink<T, D, R> next(); // 把新的处理者挂到当前节点后面，并返回它（方便链式 append） ILogicLink<T, D, R> appendNext(ILogicLink<T, D, R> next); } 2）ILogicLink<T, D, R> 继承自 ILogicChainArmory<T, D, R>，并额外声明了核心方法 apply // 真正的“处理节点”接口：在具备链条组装能力的基础上，还要能“处理请求” public interface ILogicLink<T, D, R> extends ILogicChainArmory<T, D, R> { R apply(T requestParameter, D dynamicContext) throws Exception; } 3）抽象基类：AbstractLogicLink，提供了责任链节点的通用骨架，（保存 next、实现 appendNext/next()、以及一个便捷的 protected next(...)，这样具体的节点类就不用重复这些代码，真正的业务处理逻辑仍然交由子类去实现 apply(...)。 // 抽象基类：大多数节点都可以继承它，避免重复写“组装链”的样板代码 public abstract class AbstractLogicLink<T, D, R> implements ILogicLink<T, D, R> { // 指向“下一个处理者”的引用 private ILogicLink<T, D, R> next; @Override public ILogicLink<T, D, R> next() { return next; } @Override public ILogicLink<T, D, R> appendNext(ILogicLink<T, D, R> next) { this.next = next; return next; // 返回 next 以便连续 append，类似 builder } /** * 便捷方法：当前节点决定“交给下一个处理者” */ protected R next(T requestParameter, D dynamicContext) throws Exception { // 直接把请求丢给下一个节点继续处理 // 注意：这里假设 next 一定存在；实际项目里建议判空以免 NPE（见下文改进建议） return next.apply(requestParameter, dynamicContext); } } 子类只需要继承 AbstractLogicLink 并实现 apply(...)： 能处理就处理（并可选择直接返回，终止链条）。 不处理或处理后仍需后续动作，就 return next(requestParameter, dynamicContext) 继续传递。 4）实现子类 @Component public class AuthLink extends AbstractLogicLink<Request, Context, Response> { @Override public Response apply(Request req, Context ctx) throws Exception { if (!ctx.isAuthenticated()) { // 未认证：立刻终止；也可以在这里构造一个标准错误响应返回 throw new UnauthorizedException(); } // 认证通过，继续下一个环节 return next(req, ctx); } } @Component public class LoggingLink extends AbstractLogicLink<Request, Context, Response> { @Override public Response apply(Request req, Context ctx) throws Exception { System.out.println("Request received: " + req); return next(req, ctx); } } @Component public class BusinessLogicLink extends AbstractLogicLink<Request, Context, Response> { @Override public Response apply(Request req, Context ctx) throws Exception { // 业务逻辑... return new Response(...); } } 5）组装链 @Configuration @RequiredArgsConstructor public class LogicChainFactory { private final AuthLink authLink; private final LoggingLink loggingLink; private final BusinessLogicLink businessLogicLink; @Bean public ILogicLink<Request, Context, Response> logicChain() { return authLink .appendNext(loggingLink) .appendNext(businessLogicLink); } } 示例图： AuthLink.apply └─▶ LoggingLink.apply └─▶ BusinessLogicLink.apply └─▶ 返回 Response 这种模式链上的每个节点都手动 next()到下一节点。 多实例链1 以上是单例链，即只能创建一条链；比如A->B->C，不能创建别的链，因为节点Bean是单例的，如果创别的链会导致指针引用错误！！！ 如果想变成多例链： 1）节点由默认的单例模式改为原型模式： @Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class A extends AbstractLogicLink<Req, Ctx, Resp> { ... } 2）组装链的时候注明不同链的bean名称： /** 全局唯一链：A -> B -> C */ @Bean("chainABC") public ILogicLink<Req, Ctx, Resp> chainABC() { A a = aProvider.getObject(); B b = bProvider.getObject(); C c = cProvider.getObject(); return a.appendNext(b).appendNext(c); // 返回链头 a } /** 全局唯一链：A -> C */ @Bean("chainAC") public ILogicLink<Req, Ctx, Resp> chainAC() { A a = aProvider.getObject(); C c = cProvider.getObject(); return a.appendNext(c); // 返回链头 a（另一套实例） } 多实例链2 前面是每个节点自己要维护下一个节点。 authLink.appendNext(loggingLink).appendNext(businessLogicLink); 而这里，节点只管apply，链路的遍历由容器（双向链表）来维护。 /** * 通用逻辑处理器接口 —— 责任链中的「节点」要实现的核心契约。 */ public interface ILogicHandler<T, D, R> { /** * 默认的 next占位实现，方便节点若不需要向后传递时直接返回 null。 */ default R next(T requestParameter, D dynamicContext) { return null; } /** * 节点的核心处理方法。 */ R apply(T requestParameter, D dynamicContext) throws Exception; } /** * 业务链路容器 —— 双向链表实现，同时实现 ILogicHandler，从而可以被当作单个节点使用。 */ public class BusinessLinkedList<T, D, R> extends LinkedList<ILogicHandler<T, D, R>> implements ILogicHandler<T, D, R>{ public BusinessLinkedList(String name) { super(name); } /** * BusinessLinkedList是头节点，它的apply方法就是循环调用后面的节点，直至返回。 * 遍历并执行链路。 */ @Override public R apply(T requestParameter, D dynamicContext) throws Exception { Node<ILogicHandler<T, D, R>> current = this.first; // 顺序执行，直到链尾或返回结果 while (current != null) { ILogicHandler<T, D, R> handler = current.item; R result = handler.apply(requestParameter, dynamicContext); if (result != null) { // 节点命中，立即返回 return result; } //result==null，则交给那一节点继续处理 current = current.next; } // 全链未命中 return null; } } /** * 链路装配工厂 —— 负责把一组 ILogicHandler 顺序注册到 BusinessLinkedList 中。 */ public class LinkArmory<T, D, R> { private final BusinessLinkedList<T, D, R> logicLink; /** * @param linkName 链路名称，便于日志排查 * @param logicHandlers 节点列表，按传入顺序链接 */ @SafeVarargs public LinkArmory(String linkName, ILogicHandler<T, D, R>... logicHandlers) { logicLink = new BusinessLinkedList<>(linkName); for (ILogicHandler<T, D, R> logicHandler: logicHandlers){ logicLink.add(logicHandler); } } /** 返回组装完成的链路 */ public BusinessLinkedList<T, D, R> getLogicLink() { return logicLink; } } //工厂类，可以定义多条责任链，每条有自己的Bean名称区分。 @Bean("tradeRuleFilter") public BusinessLinkedList<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> tradeRuleFilter(ActivityUsabilityRuleFilter activityUsabilityRuleFilter, UserTakeLimitRuleFilter userTakeLimitRuleFilter) { // 1. 组装链 LinkArmory<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> linkArmory = new LinkArmory<>("交易规则过滤链", activityUsabilityRuleFilter, userTakeLimitRuleFilter); // 2. 返回链容器（即可作为责任链使用） return linkArmory.getLogicLink(); } 示例图： BusinessLinkedList.apply ←─ 只有这一层在栈里 while 循环： ├─▶ 调用 ActivityUsability.apply → 返回 null → 继续 ├─▶ 调用 UserTakeLimit.apply → 返回 null → 继续 └─▶ 调用 ... → 返回 Result → break 链头拿着“游标”一个个跑，节点只告诉“命中 / 未命中”。 这里无需把节点改为原型模式，也可以实现多例链，因为由双向链表BusinessLinkedList 负责保存链路关系和推进执行，而ILogicHandler节点本身不再保存 next 指针，所以它们之间没有共享可变状态。 本项目中使用多实例链2，有以下场景： 一、拼团「锁单前」校验链 目标：在真正锁单前把“活动有效性 / 用户参与资格 / 可用库存”一口气校清楚，避免后续回滚。 1.活动有效性校验 ActivityUsability （当前时间是否早于活动截止时间） 2.用户可参与活动次数校验 UserTakeLimitRuleFilter（默认用户只可参与一次拼团） 3.剩余库存校验 TeamStockOccupyRuleFilter（可能同时有多人点击参与当前拼团，尝试抢占库存，仅部分人可通过校验。） 校验通过方可进行真正的锁单。 二、交易结算校验链 1.渠道黑名单校验 SCRuleFilter：某签约渠道下架/风控拦截，禁止结算。 2.外部单号校验 OutTradeNoRuleFilter：查营销订单；不存在或已退单（CLOSE）→ 不结算。 3.可结算时间校验 SettableRuleFilter：结算时间必须在拼团有效期内（outTradeTime < team.validEndTime），比如发起 拼团一个小时之内要结算完毕。 4.结束节点EndRuleFilter：整理上下文到返回对象，作为结算规则校验的产出。 检验通过方可进入真正的结算。 三、交易退单执行链 1.数据加载 DataNodeFilter：按 userId + outTradeNo 查询营销订单与拼团信息，写入上下文。 2.重复退单检查 UniqueRefundNodeFilter：订单已是 CLOSE → 视为幂等重复，直接返回。 3.退单策略执行 RefundOrderNodeFilter：依据“拼团态 + 订单态”选用具体退单策略 IRefundOrderStrategy，执行退款/解锁/改库并返回成功结果。 本身就是完整的退单流程。 规则树流程 结构：节点之间呈树状关系，一个节点可以路由到多个不同的“下一跳”。 执行规则：根据 上下文数据（request + context），选择不同的分支继续执行。 责任链一开始就组装成链，依次执行；规则树每个节点中的get() 方法会根据上下文选择不同的下一跳。 场景：风控规则树（用户支付时校验） RootNode：进入风控 RiskScoreNode （风险评分节点） 如果分数 < 30 → 通过节点（直接放行支付） 如果分数 30 ~ 60 → 人工审核节点（挂起订单，通知客服人工确认） 如果分数 > 60 → 拒绝节点（直接拦截交易） 整体分层思路 分层 作用 关键对象 通用模板层 抽象出与具体业务无关的「规则树」骨架，解决 如何找到并执行策略 的共性问题 StrategyMapper、StrategyHandler、AbstractStrategyRouter<T,D,R> 业务装配层 基于模板，自由拼装出 一棵 贴合业务流程的策略树 RootNode / SwitchNode / MarketNode / EndNode … 对外暴露层 通过 工厂 + 服务支持类 将整棵树封装成一个可直接调用的 StrategyHandler，并交给 Spring 整体托管 DefaultActivityStrategyFactory、AbstractGroupBuyMarketSupport 通用模板层：规则树的“骨架” 角色 职责 关系 StrategyMapper 映射器：依据 requestParameter + dynamicContext 选出 下一个 策略节点 被 AbstractStrategyRouter 调用 StrategyHandler 处理器：真正执行业务逻辑；apply 结束后可返回结果或继续路由 节点本身 / 路由器本身都是它的实现 AbstractStrategyRouter<T,D,R> 路由模板：① 调用 get(...) 找到合适的 StrategyHandler;② 调用该 handler 的 apply(...);③ 若未命中则走 defaultStrategyHandler 同时实现 StrategyMapper 与 StrategyHandler，但自身保持 抽象，把细节延迟到子类 业务装配层：一棵可编排的策略树 RootNode -> SwitchNode -> MarketNode -> EndNode ↘︎ OtherNode ... 每个节点 继承 AbstractGroupBuyMarketSupport（业务基类） 实现 get(...)：决定当前节点的下一跳是哪一个节点 实现 apply(...)：实现节点自身应做的业务动作（或继续下钻） 组合方式 路由是“数据驱动”的：并非工厂把链写死，而是节点在运行期根据 request + context 决定下一跳（可能是ERROR_NODE或END_NODE），灵活插拔。 对外暴露层：工厂 + 服务支持类 组件 主要职责 DefaultActivityStrategyFactory (@Service) 仅负责把 RootNode 暴露为 StrategyHandler 入口（交由 Spring 管理，方便注入）。 AbstractGroupBuyMarketSupport 业务服务基类：封装拼团场景下共用的查询、工具方法；供每个节点继承使用 本项目执行总览： 调用入口：factory.strategyHandler() → 返回 RootNode（实现了 StrategyHandler）。 执行流程： apply(...)：模板入口，先跑 multiThread(...) 预取/并发任务，再跑 doApply(...)。 doApply(...)：每个节点自己的业务；通常在末尾调用 router(...) 继续下一个节点（return router(request, ctx);）。也可以在某些节点“短路返回”，不再路由。 router(request, ctx)：内部调用当前节点的 get(...) 来挑选下一节点next，若存在就调用 next.apply(...) 递归推进；若不存在（或是到达 EndNode），则收束返回。 RootNode 校验必填参数：userId/goodsId/source/channel。 合法则路由到 SwitchNode；非法直接抛 ILLEGAL_PARAMETER。 SwitchNode（总开关、不区分活动，做总体的降级限流） 调用 repository.downgradeSwitch() 判断是否降级；是则抛 E0003。 调用 repository.cutRange(userId) 做切量；不在范围抛 E0004。 通过后路由到 MarketNode。 MarketNode multiThread(...) 中并发拉取： 拼团活动配置 GroupBuyActivityDiscountVO 商品信息 SkuVO 写入 DynamicContext doApply(...) 读取配置 + SKU（Stock Keeping Unit库存量单位），按 marketPlan 选 IDiscountCalculateService，计算 payPrice / deductionPrice 并写回上下文。 路由判定： 若配置/商品/deductionPrice 有缺失 → ErrorNode 否则 → TagNode TagNode（业务相关，部分人不在本次活动范围内！） 若活动没配置 tagId → 视为不限定人群：visible=true、enable=true。 否则通过 repository.isTagCrowdRange(tagId, userId) 判断是否在人群内，并据此更新 visible/enable。 路由到 EndNode。 EndNode 从 DynamicContext 读取：skuVO / payPrice / deductionPrice / groupBuyActivityDiscountVO / visible / enable； 构建并返回最终的 TrialBalanceEntity，链路终止。 ErrorNode 统一异常出口；若无配置/无商品，抛 E0002；否则可返回空结果作为兜底； 返回后走 defaultStrategyHandler（结束）。
测试 小明今天15岁了，它是天水小学上学。