Mock-Server开箱即用的Mock服务

复制代码
# Mock 系统管理台

https://gitee.com/welightyear/mock-server

> 一个**页面化管理的多协议模拟后端**:在同一个管理台里创建任意多个模拟后端,每个服务独占一个端口、按你的规则返回数据;同时还能把真实后端的流量**采集**下来,一键固化成模拟后端规则、或存进可重放的请求库。

前后端不分离、无前端构建步骤、内嵌数据库,`java -jar` 一条命令即可运行,适合本地联调、前后端并行开发、接口联调与回归验证。

> **界面术语**:页面标题为「**Mock 系统管理台**」。左侧菜单共 5 项——「**模拟后端**」即 Mock 服务(`kind=MOCK`,按手写规则返回数据);「**模拟前端**」是独立的重放请求库;「**采集数据**」负责采集真实后端流量;另有「仪表盘」与「系统管理」。代码与接口里仍沿用 `mock` / `MOCK` / `PROXY_BACK` 等标识符。

> English version: [README.en.md](README.en.md)

---

## 目录

- [整体架构](#整体架构)
- [功能特性](#功能特性)
- [界面导览](#界面导览)
- [技术栈](#技术栈)
- [快速开始](#快速开始)
- [使用指南](#使用指南)
  - [1. 创建一个模拟后端接口](#1-创建一个模拟后端接口)
  - [2. 让同一条规则按请求返回不同数据(响应模板)](#2-让同一条规则按请求返回不同数据响应模板)
  - [3. 采集真实后端响应并固化成模拟数据](#3-采集真实后端响应并固化成模拟数据)
  - [4. 提交到模拟前端并重放(模拟前端)](#4-提交到模拟前端并重放模拟前端)
  - [5. 多协议模拟后端(gRPC / WebSocket / TCP)](#5-多协议模拟后端grpc--websocket--tcp)
  - [6. 用户、权限与数据源](#6-用户权限与数据源)
  - [7. 从 OpenAPI / Swagger 导入规则](#7-从-openapi--swagger-导入规则)
  - [8. 让服务随管理端自动启动(开机自启)](#8-让服务随管理端自动启动开机自启)
  - [9. 切换界面语言(中英文)](#9-切换界面语言中英文)
  - [10. 备份与迁移配置(导出 / 导入)](#10-备份与迁移配置导出--导入)
- [规则匹配规则](#规则匹配规则)
- [REST API 一览](#rest-api-一览)
- [目录结构](#目录结构)
- [数据与持久化](#数据与持久化)
- [开发与验证](#开发与验证)
- [常见问题](#常见问题)
- [已知限制](#已知限制)


## 整体架构

```mermaid
flowchart LR
    FE["调用方<br/>浏览器 / Postman / curl"]

    subgraph ADMIN["管理台 :8080(Spring Boot + Tomcat)"]
        UI["Vue 2 + Element UI 页面"]
        API["REST API  /api/**"]
    end

    subgraph PORTS["动态端口(Jetty / 原生 TCP)"]
        MOCK["模拟后端 kind=MOCK<br/>按规则返回模拟数据"]
        CAP["采集服务 kind=PROXY_BACK<br/>采集时转发真实后端"]
    end

    REAL["真实后端"]

    FE -->|"想拿模拟数据"| MOCK
    FE -->|"想拿真实数据"| CAP
    CAP -->|"转发:真实后端地址 + 请求路径"| REAL
    REAL -->|"真实响应"| CAP
    CAP -->|"原样回给调用方,同时落库"| FE
    UI --> API
    API --> MOCK
    API --> CAP
```

两条典型链路:

1. **模拟后端链路**:调用方 → `模拟后端`端口 → 命中规则 → 返回预设数据。
2. **采集链路**:调用方 → `采集服务`端口 →(采集开启)转发真实后端 → 真实响应原样返回,前端请求与后端响应分开落库 →「提交到模拟前端」把请求(真实后端地址 + URL + 请求体)存入**模拟前端请求库**,随时可按原样重放;「提交到模拟后端」可固化规则 → 之后改由模拟后端链路返回这份数据。

---

## 功能特性

### 1. 页面化多端口模拟后端

- 在管理台里创建任意多个模拟后端,**每个服务绑定一个独立监听端口**(端口全局唯一,与「采集服务」共用端口空间)。
- 支持 4 种协议,启动时按协议自动选择运行时:**HTTP(HTTP/1.1 + 明文 HTTP/2 h2c)**、**gRPC**、**WebSocket**、**TCP**。
- 服务可随时「启动 / 停止」,运行状态实时可见;停止后端口立即释放。
- **启动前做真实端口探活**:不只看「应用内有没有服务占了这个端口」,还会真的探测系统里该端口是否被别的进程监听;被占用就**直接拒绝启动并说明原因**,而不是绑上去假装启动成功 —— Windows 的 `SO_REUSEADDR` 允许对已被监听的端口重复绑定,少了这一步就会出现「界面显示运行中、实际一个请求都收不到」的静默失败。
- **开机自启**:每个服务可单独设置是否随管理端启动自动拉起(默认开启)。启动时**逐个服务独立容错**——某个端口被别的进程占用只会跳过它并打一条警告,其余服务照常启动,管理端本身也不受影响。界面上还有「启动自启服务」按钮,改完开关不必重启管理端即可立即生效。
- 选中服务后,右侧面板展示**连接示例**(如 `curl -i http://127.0.0.1:9101/api/user/1`),并分「规则管理」与「请求日志」两个标签页。

### 2. 规则引擎

每条规则就是「**匹配什么** → **返回什么**」:

| 维度 | 说明 |
|---|---|
| 匹配方法 | `GET` / `POST` / `PUT` / `DELETE` / `PATCH` / `HEAD` / `OPTIONS` / `ANY` |
| 匹配路径 | 精确 `/api/user`;按段 `/api/user/{id}`(只吃一段,参数可被模板取用);前缀 `/api/user/*`;通配 `/api/*/detail` |
| 响应状态码 | 任意 HTTP 状态码 |
| 响应头 | 自定义 `Content-Type` 与任意额外响应头(值同样支持模板占位符) |
| 响应体 | 任意文本(JSON / XML / HTML…);打开「响应模板」后可按请求动态渲染,见 [响应模板](#4-响应模板同一条规则按请求返回不同数据) |
| 模拟延迟 | 每条规则可设置延迟毫秒数,模拟慢接口 |
| 启用开关 | 规则可临时禁用而不删除 |

匹配优先级:**精确 &gt; 按段 `{id}` &gt; 前缀 `/*`**。

### 3. 从 OpenAPI / Swagger 导入规则

后端给了 Swagger / OpenAPI 文档时,不用再逐条手抄接口——把文档导入即可自动生成规则:

- **支持两套规范、两种文档格式**:OpenAPI 3.x(`openapi` 字段)与 Swagger 2.0(`swagger` 字段);格式 **JSON 与 YAML 都支持,自动识别**(以 `{` / `[` 开头按 JSON 读,否则按 YAML 读),预览里会显示实际识别到的格式。YAML 的锚点 `&x` / 别名 `*x`、多行块标量 `|`、`---` 起始符、注释、flow style,以及**没加引号的标量**(`swagger: 2.0` 这种会被 YAML 读成数字)都能正常处理。
- **近乎零额外依赖**:JSON 用项目里已有的 Jackson 读,YAML 用 spring-boot-starter 本来就带着的 **snakeyaml**(`SafeConstructor`),没有引入 swagger-parser 这类重型依赖。顺带得到两个好处:YAML 的**锚点 `&x` / 别名 `*x` 能原生展开**(Jackson 的 YAML 解析器到 2.13 仍只会把别名读成空对象),且只构造 Map / List / 标量,带 `!!java.net.URL` 这类全局标签的内容不会被实例化。
- **YAML 的两个坑都已处理**:① 关掉了 YAML 1.1 的 TIMESTAMP 隐式解析 —— 否则未加引号的 `2026-01-01` 会被读成 epoch 毫秒数字,文档里日期形式的 `example` / `default` / `version` 全毁;② 在转换成 JSON 树之前先按引用计一遍节点数(预算按文档长度缩放),拦住「别名被层层套用」与自引用结构,不会把内存撑爆。
- **两步走,先预览再确认**:解析是 **dry-run**,不写库;预览给出「将新增 / 将更新 / 已最新 / 已改过 / 被占用 / 跳过」清单和每个接口将生成的响应体,确认后才写入。
- **接口清单按 tag 分组**、可勾选、可全选,每个接口显示方法、路径、名称、改动点与提示信息。
- **路径前缀自动识别**:取 OpenAPI 3 的 `servers[0].url` 的 path 部分(如 `/api/v3`),或 Swagger 2 的 `basePath`(如 `/v2`);也可以在界面上手动改或清空。
- **路径参数按段匹配,参数名原样保留**:`/users/{id}` 保留成 `/users/{id}`(中段同理,`/orders/{orderId}/items`)。`{id}` **只吃一段**,所以 `/users/{id}` 不会再去命中 `/users/search`、`/users/1/profile` 这类更深的路径——这一点比早期的「一律转成 `*` 按前缀匹配」精确得多。捕获到的参数还能直接在响应模板里用 `{{request.path.id}}` 取值。
- **响应体按业界共识的优先级生成**:`example` > `default` > `const` > `enum[0]` > 按 schema 生成;`format`(date-time / email / uuid / uri / byte…)、`minimum` / `maximum`、`minItems`、`allOf` 合并、`$ref` 引用(含嵌套)都会处理。
- **状态码选择**:取**最小的 2xx**;文档里没写 2xx 时回退到 `default`,再回退到最小的具体状态码,此时状态码统一按 **200** 生成并明确提示(真实文档里很常见——Swagger 2.0 版 petstore 的 `POST /pet` 就只声明了 405,不回退的话 20 个接口只剩 9 个能导)。
- **两种导入方式**:
  - **只新增(append,默认)**:已存在「方法 + 路径」的接口一律跳过,**绝不覆盖已有规则**;重复导入同一份文档不会产生重复规则。
  - **同步更新(merge)**:后端改了文档之后**增量更新**,只动真正变了的接口(见下条)。
- **同步更新(merge)靠「同步指纹 + 已改标记」判断该不该覆盖**:每条导入规则记着上次同步时内容的指纹 `spec_hash`(覆盖名称、方法、路径、状态码、Content-Type、响应体这些**文档管得着的字段**),以及是否被手工改过的 `dirty` 标记。merge 时按 `specKey` 把文档接口与导入来源的规则配成对:
  - 内容有变化 → **就地更新**(保留规则 id,以及延迟、启用开关、额外响应头这些本地设置);
  - 内容一致 → **已最新**,一条都不写;
  - 被手工改过 → **已改过(冲突)**,默认跳过,需要逐条勾选(或 `force: true` 批量强制)才覆盖;
  - 同「方法 + 路径」已被**手工规则**或文档里另一个接口占用 → **被占用**,无论如何都不覆盖。

  `dirty` 是**派生的**:每次读写都拿当前内容与 `spec_hash` 现算,所以「改一下再改回来」会自己恢复成未修改。规则列表里被改过的导入规则会多一个「已改」标记。
- **会提示「文档里已经删掉的接口」**:merge 预览会列出 `orphans`——本地还留着、但当前文档里已经没有的导入规则。**只列表,不自动删**(同一个服务可能同步过好几份文档,谁是孤儿无从判断,删错了就是数据丢失)。
- **「无法导入」与「已存在」分开统计**:文档里本身导不了的(如没有响应定义)与已存在的分别计数,提示文案不会混在一起。
- 循环 `$ref` 会自动断开(限制嵌套深度 8 层),不会栈溢出。
- 导入的规则带 `source=OPENAPI` 与 `specKey`(形如 `GET /users/{id}`,用文档原始路径),规则列表里有「导入」标签区分来源。

### 4. 响应模板(同一条规则按请求返回不同数据)

`responseBody` 与响应头默认是**写死的字符串**,每次请求原样返回。打开规则上的 **「响应模板」** 开关后,里面的 `{{...}}` 占位符会在**每次请求命中规则之后、写响应之前**用这次请求的内容替换掉——于是同一条规则被不同请求命中,就会渲染出不同的响应:详情页看得出 id 变化、翻页真的翻得动、POST 提交的数据能原样带回来。

取值类占位符:

| 占位符 | 说明 |
| --- | --- |
| `{{request.method}}` | 请求方法 |
| `{{request.path}}` | 实际请求路径 |
| `{{request.path.id}}` | 路径参数(规则路径写成 `/users/{id}` 才有) |
| `{{request.query.page}}` | 查询参数,取第一个值;`{{request.query.tag[1]}}` 取第二个 |
| `{{request.header.X-Token}}` | 请求头,名字**大小写不敏感** |
| `{{request.body}}` | 请求体原文 |
| `{{request.body.data.id}}` | 请求体按 JSON 路径取值,支持 `list[0].id` 这种数组下标 |

现算函数(每次请求都重算,不来自请求):

| 占位符 | 说明 |
| --- | --- |
| `{{now}}` / `{{now('yyyy-MM-dd HH:mm:ss')}}` | 当前时间,可给格式 |
| `{{uuid}}` | 随机 UUID |
| `{{random.int(1, 100)}}` | 区间随机整数(无参默认 0~100) |
| `{{random.string(8)}}` | 随机小写字母数字串(无参默认 8 位) |
| `{{random.bool}}` / `{{random.float(0, 1)}}` / `{{random.pick('a','b')}}` | 其余随机值 |

三处细节是按「别把人坑到」来设计的:

- **`??` 给默认值**:`{{request.query.page ?? 1}}`。查询参数没带时用 1,避免出现 `{"page": }` 这种把 JSON 弄坏的结果。
- **JSON 字符串内自动转义**:模板整体看起来是 JSON 时,落在字符串字面量**内部**的占位符会被自动转义,所以传 `q=he"llo` 也不会把响应体弄成非法 JSON;落在引号**外面**的占位符原样插入,方便写 `{"page": {{request.query.page}}}` 这种数字字段。纯文本模板(如 `text/plain`)不做转义。
- **「写错了」和「这次没带」分得清**:命名空间拼错(`{{request.quert.page}}`)或规则路径里没有这个参数时,占位符**原样保留**并给出告警,一眼就能看到;而「写法对、这次请求没带」替换成空串,属运行时正常情况,不告警。想输出字面量 `{{` 写 `\{{`。

规则编辑弹窗里有 **「插入变量」** 下拉(点一下插到光标处,并自动帮你把模板开关打开)和 **「试渲染」** 面板——填一组样例路径 / Query / 请求头 / 请求体,就能看到渲染结果和告警,不用真起服务试。试渲染走的是**和真实请求同一套**渲染实现,不会出现「预览对了线上不对」。

> 只对 HTTP 协议的规则生效;gRPC / WebSocket / TCP 的载荷是字节流,不做模板。
> 「是否开启模板」属于本地设置,**不参与** OpenAPI 增量同步的指纹——开关它不会让导入的规则变成「已改过」。

### 5. 采集真实流量(采集数据)

把真实流量的「请求」与「响应」都抓下来,作为模拟数据的来源,避免手写大量「假数据」:

- 「采集数据」页管理的是**独立的一类采集服务**:自己占一个端口,并配置一个**真实后端地址**;只支持 HTTP。
- **开启采集时**:请求被转发到 `真实后端地址 + 请求路径`,真实响应**原样回给调用方**,同时**分开保存两份数据**:
  - **前端请求**(FRONT):前端发了什么 —— 方法、路径、请求头、请求体;
  - **后端响应**(BACK):后端回了什么 —— 状态码、响应头、响应体、耗时。
- **未开启采集时**:该端口一律返回 `404`,并给出可操作提示 —— 采集服务只负责采集,不提供固定响应。
- 请求日志里**每一条**采集数据都有两个去向:
  - **「提交到模拟前端」**(前端 / 后端记录都有):把这条请求(**真实后端地址 + URL + 请求头 + 请求体**)存入「模拟前端」请求库(不实际发起调用),**不需要选择目标模拟后端**,之后可在「模拟前端」页按原样重放;
  - **「提交到模拟后端」**(仅「后端」记录,即带响应状态码的那条):按「方法 + 路径」把这条真实响应**固化成目标模拟后端的一条规则**(同一方法与路径重复提交只更新、不新增)。
- 请求日志支持**关键词过滤**(方法 / 路径 / query / 请求体)、**自动刷新开关**、**清空日志**(二次确认)。
- 采集服务上若还留着旧版本写入的历史规则,面板会提示并给出一键 **清空历史规则**(规则统一归「模拟后端」维护,清空不影响采集数据)。
- 地址做了严格校验:**只能填到端口**(如 `http://127.0.0.1:9000`),带路径 / query / fragment 会被直接拦下并提示正确写法——因为转发是 `真实后端地址 + 请求路径` 直接拼接,多填路径会导致路径重复而 404。

### 6. 模拟前端(请求库与重放)

「模拟前端」是一个**独立的请求库**,内容只有一个来源:「采集数据」页里点「提交到模拟前端」提交过来的请求。因此这里**没有服务选择框**——库里存的不是某个端口的流量,而是一批「随时可以重发」的请求。请求库**按用户隔离**,每个账号只看得到自己提交的请求。

每条记录保存:

- **真实后端地址**:采集它时所属采集服务配置的真实后端地址,也是重放时的默认目标;
- **URL**:方法、路径、query;
- **请求头与请求体**(列表里直接给出请求体预览)。

列表字段:收藏 / 时间 / 真实后端地址 / 方法 / URL / 请求体 / 操作。在此基础上支持:

- **收藏**重点请求(内存储满时优先淘汰非收藏;清空时默认保留收藏);
- **持久化**:记录写入 `mock_proxy_record` 表,重启后自动载回;内存中每方向保留 300(前端)/ 500(后端)条,库中每方向保留最近 1000 条;单次重放最多 200 条;
- **关键词过滤**(按方法 / 路径 / query / 真实后端地址)与 **只看收藏**;
- **自动刷新开关**(3 秒轮询;重放面板打开期间自动暂停,避免刷新把勾选清掉);
- **每行都有「重放」按钮**:点它即可重发这一条(默认发往它自己保存的真实后端地址),也可以在重放面板里填一个统一目标地址把整批发到别处(例如发给另一个环境的服务做回归验证),或把范围切换成「仅收藏 / 全部」做批量重放;
- **删除带二次确认**:删单条、清空(保留收藏 / 全部清空两档)都需要确认后才执行;
- **详情弹窗**:查看完整请求头 / 请求体,并可直接「重放这一条」;
- 重放流量带内部标记头 `X-Mock-Replay`,**不会被再次记录**,该标记头也不会被转发给真实后端。

### 7. 请求日志

- 内存中每个服务保留最近 **200 条**(供前端秒开),数据库中每服务保留最近 **1000 条**(每 5 分钟后台清理)。
- 写入走**有界队列 + 单写线程批量入库**,队列满时丢弃最旧一条,**绝不阻塞模拟后端请求本身**。
- 重启后从数据库载回最近日志,历史不丢。

### 8. 用户与权限

- 内置管理员账号 `admin`;用户分 `ADMIN` / `USER` 两种角色。
- 登录使用 `X-Token`(或 `Authorization: Bearer`)会话,有效期 **8 小时**,密码以 BCrypt 存储。
- **数据隔离**:模拟后端归属创建者,普通用户只能看到和操作自己的服务(越权返回 403),管理员可见全部。
- 「系统管理」(用户管理、持久化管理)仅管理员可见,前端隐藏菜单 + 后端双重校验。

### 9. 持久化管理(运行期切换数据源)

- 默认使用 **H2 文件数据库**(`./data/mockdb.mv.db`),零配置开箱即用。
- 可在界面上切换到 **MySQL**:填写连接信息 → **测试连接** → 检查目标库 → 选择切换方式:
  - `overwrite`:把当前内存/库中的数据**覆盖写入**目标库;
  - `load`:**从目标库载入**数据。
- 表结构由程序自动创建,切换过程不会丢数据。

### 10. 仪表盘

服务数、运行中端口数、规则总数、协议分布与当前数据源一览;管理员额外看到系统用户数与在线会话数。

### 11. 中英文界面(i18n)

界面文案与接口错误消息都做了双语,右上角可随时切换(见 [使用指南 9](#9-切换界面语言中英文))。要点:

| 部分 | 做法 |
|---|---|
| 前端引擎 | 自研 `js/i18n.js`(零依赖、无构建):语言包用 `I18N.register(lang, {...})` 注入,`Vue.observable({lang})` 让 `$t()` 产生响应式依赖,切换后**当前页面即时重渲染**,不需要刷新 |
| 前端文案 | `js/locale/zh.js` / `en.js`,581 个 key,按模块前缀分组(`common` / `app` / `server` / `rule` / `oa` / `proxy` / `front` / `user` / `persist` / `transfer` …) |
| 语言偏好 | 存 `localStorage` 的 `mock_lang`,**不跟随浏览器语言**;每次请求带 `X-Lang` 头把当前语言告诉后端 |
| 后端文案 | `src/main/resources/i18n/messages_zh.properties` / `messages_en.properties`,143 个 key(含全局异常处理的 `err.common.*`);`LocaleFilter` 读 `X-Lang` 写进 `I18n` 的 ThreadLocal,请求结束清除;`ResourceBundleMessageSource` 按 UTF-8 读取 |
| 日志 | **一律中文,不随界面语言变**——本地化日志只会让 grep 变难 |

`I18N.t(key, params)` 的兜底顺序是「当前语言 → 默认语言 → 返回 key 本身」,所以漏译时界面上会直接显示 `server.xxx` 这样的 key,比显示空白更容易发现。`tools/static-check.js` 会校验两份语言包的 key 集合一致、且源码里 `$t()` 用到的 key 都已定义。

另外,后端 `I18n.t` 会把**数字参数先转成字符串**再交给 `MessageFormat`:`MessageFormat` 默认按 `NumberFormat` 格式化数字,会给四位数以上加千分位 —— 端口 `9101` 会渲染成 `9,101`、节点上限 `20000` 会渲染成 `20,000`。项目里的数字参数全是**标识 / 计数**而不是需要本地化的数量(两份语言包也没有 `{0,number}` 这类格式类型),所以从根上关掉这一层。

### 12. 统一的错误响应与全局异常处理

所有失败响应——不论来自哪个控制器、哪一层——结构完全一致:

```json
{ "error": "服务名称不能为空", "code": 400 }
```

前端 `js/api.js` 只读 `error` 字段,任何出口漏了它,界面上就只剩「请求失败(状态码 xxx)」。为此分三层兜住:

| 层 | 负责的失败 | 说明 |
|---|---|---|
| 业务显式返回 | 校验失败(400) | 各控制器的 `bad()`,结构与上面一致 |
| `GlobalExceptionHandler`<br>`@RestControllerAdvice` | 进入控制器之后抛出的异常 | `ApiException`(401 / 403 / 404);`IllegalArgumentException` → 400;请求体不是合法 JSON → 400;参数类型不符 → 400;方法不支持 → 405;内容类型不支持 → 415 |
| `ApiErrorController`<br>`ErrorController` | **不经过控制器**的失败 | 路由 404、静态资源缺失、Filter 抛异常——容器会把请求转到 `/error`,这里换成同一结构并保留原始状态码 |

**500 绝不外泄内部信息**:未预期的异常只在响应里回一句「服务器内部错误,请查看后端日志」,异常类型、SQL、类名、堆栈只写进后端日志(日志按约定保持中文)。错误文案同样跟随 `X-Lang`;注意 `OncePerRequestFilter` 默认**跳过 ERROR 转发**,`LocaleFilter` 覆写了 `shouldNotFilterErrorDispatch()`,否则打错地址时的 404 提示会固定成中文。

### 13. 配置导出 / 导入(备份与跨环境搬运)

整个系统的资产(服务 + 规则)只存在数据库里,所以还需要一条把它搬出去、再搬回来的路:

- **导出**(`GET /api/servers/export`,可加 `?kind=MOCK|PROXY_BACK`):返回一份自包含的 JSON 备份;界面上一键下载成文件,缩进两格,可直接放进 Git 看 diff。
- **导入**(`POST /api/servers/import`):**按服务 id upsert** —— id 已存在就整体覆盖,不存在就新增,所以**同一份文件重复导入是幂等的**(内容没变的服务识别成 `unchanged`,不产生任何写库动作)。
- **先预演再落库**:界面先带 `?dryRun=true` 跑一遍,把「新增 / 覆盖 / 未变化 / 跳过」摆出来确认,确认后才真正写入 —— 哪些会被覆盖、哪些端口冲突,在动手之前就是可见的。

三条宁可「报出来」也不「猜着办」的原则:

| 情况 | 处理 |
|---|---|
| 端口与库里其它服务冲突 | **跳过该服务并给出原因**,绝不自动改端口(自动改端口会让它「看起来导入成功」却监听在别处) |
| 名称 / 端口 / 类型 / 协议 / 真实后端地址 / 规则校验不通过 | 跳过该条并逐条列出原因,其余照常导入 |
| 同一份文件里两个服务用了同一个端口 | 只留第一条,第二条报「在备份文件里被多个服务重复使用」 |

导入是**忠实还原**:备份里带着什么就写什么,不会因为服务类型之类的原因顺手删掉备份里的内容。另外三点:

- **归属**:备份里那位主人还在系统里时归属保持不变(还原备份不该改变归属);只有原主人已不存在时才归到导入者名下,并在结果里单独提示改派了多少条。
- **不自动重启**:改动照常落库,但运行实例用的是启动那一刻的配置,所以会在结果里列出「需重启生效」的服务名。
- **未写的字段**:手写的「局部备份」可以只写 id / 名称 / 端口 / 规则,其余字段取默认值;而 `createdAt` 与归属这两项会沿用库里已有记录 —— 它们的默认值分别是「当前时刻」和「空」,若每次都取默认值,同一份文件重复导入就会被判成「有变化」反复覆写。

单次导入上限 500 个服务 / 5000 条规则,界面上的单个文件不超过 5 MB。界面入口在「系统管理 → 配置备份」下(仅管理员可见),但接口本身按**归属**鉴权:普通用户可以导出/导入自己可见的服务,不能借 id 覆盖别人的(否则伪造一个 id 就能改别人的配置)。

---

## 界面导览

管理台左侧菜单共 5 项:

| 菜单 | 作用 |
|---|---|
| **仪表盘** | 整体统计:系统用户 / 模拟后端数 / 运行中端口 / 在线会话(管理员),模拟后端概览表、当前数据源、协议分布 |
| **模拟后端** | 左右布局:左侧服务列表(新建 / 启动 / 停止 / 删除,含协议、端口、归属、规则数),右侧「连接示例」+「规则管理」+「请求日志」两个标签页,日志可查看请求详情 |
| **模拟前端** | 独立请求库:存放「采集数据」页提交过来的请求(真实后端地址 + URL + 请求体),支持收藏、关键词过滤、只看收藏、自动刷新、查看详情、**每行重放**与**带二次确认的删除 / 清空**;**不挂在任何服务下,因此没有服务选择框** |
| **采集数据** | 左右布局:左侧采集服务列表(新建 / 启动 / 停止 / 删除 / 真实后端地址),右侧采集控制(启动 / 开始采集 / 停止采集 / 清空历史规则)+ 请求日志(前端请求与后端响应分开展示,可过滤、自动刷新、清空;每条可「提交到模拟前端」,后端记录还可「提交到模拟后端」) |
| **系统管理** | 用户管理、持久化管理、配置备份(导出 / 导入)(仅管理员可见) |

**顶部栏**:右侧除用户名与「修改密码 / 退出登录」外,还有一个语言切换(`中文` / `English`),点一下即时切换整个界面与接口错误消息,选择结果记在浏览器本地。

---

## 技术栈

| 层次 | 选型 |
|---|---|
| 语言 / 构建 | Java 8、Maven、Spring Boot 2.7.18(打成可执行 fat jar) |
| 管理端 Web | Spring Boot 内嵌 Tomcat,默认端口 `8080` |
| 动态服务端口 | Jetty `9.4.53.v20231009`(HTTP/1.1 + 明文 HTTP/2 `h2c` + WebSocket 升级);TCP 用原生 `ServerSocket`。启动前由 `util/PortProbe` 做真实占用探测(区分「被监听」与「仅 TIME_WAIT 残留」) |
| 数据访问 | `spring-boot-starter-jdbc` + `JdbcTemplate` + HikariCP |
| 数据库 | H2(默认,文件模式)/ MySQL 8(运行期可切换),驱动 `mysql-connector-java 8.0.33` |
| 鉴权 | 自研 `X-Token` 会话 + `AuthInterceptor`,密码散列用 `spring-security-crypto` 的 BCrypt |
| 文档解析 | `jackson-databind` 读 JSON、`snakeyaml` 的 `SafeConstructor` 读 YAML(均已在依赖里),用于从 OpenAPI / Swagger 导入规则 |
| 前端 | Vue `2.7.16` + Element UI `2.15.14`(均为本地静态资源,**无 npm / 无构建步骤**) |
| 国际化 | 前端自研 `js/i18n.js`(`Vue.observable` 驱动,语言包在 `js/locale/`);后端 `ResourceBundleMessageSource` + `LocaleFilter`(`X-Lang` → ThreadLocal),文案在 `resources/i18n/` |

---

## 快速开始

### 环境要求

- JDK **1.8+**
- Maven 3.6+
- 无需安装数据库(默认使用内嵌 H2)

### 构建与运行

```bash
# 1. 打包
mvn clean package -DskipTests

# 2. 启动(管理端端口)
java -jar target/mock-server-1.0.1.jar --server.port=8080
```

打开浏览器访问 <http://localhost:8080>,使用内置账号登录:

| 用户名 | 密码 | 角色 |
|---|---|---|
| `admin` | `admin123` | 管理员 |

> 💡 **请务必显式指定 `--server.port=8080`**。项目 `application.yml` 里虽然写了 `server.port: 8080`,但 Spring Boot 的宽松绑定会识别形如 `SERVER__PORT` 的环境变量并覆盖它;显式传参可以避免端口被环境变量意外改掉。

开发时也可以直接用 Maven 插件运行:

```bash
mvn spring-boot:run
```

首次启动会在工作目录下自动创建 `data/` 目录(H2 数据文件与数据源配置),**不要删除该目录,否则数据会丢失**。

---

## 使用指南

### 1. 创建一个模拟后端接口

1. 进入 **模拟后端** → 点击左上角 **新建**:填写名称、选择协议(一般选 `HTTP`)、指定一个未被占用的端口(如 `9101`)。
2. 在左侧列表中点击 **启动**,服务开始监听该端口。
3. 在右侧 **规则管理** 中点击 **新增规则**:填写方法、路径、状态码、响应头、响应体,保存。

现在请求该端口即可拿到模拟数据:

```bash
curl -i http://127.0.0.1:9101/api/user/1
```

```http
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8

{"code":0,"data":{"id":1,"name":"mock用户"}}
```

常见规则示例:

| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` | `/api/user/1` | 精确匹配,只有 `/api/user/1` 命中 |
| `GET` | `/api/user/{id}` | **按段匹配**,只吃一段,`{id}` 会被捕获供响应模板用 |
| `GET` | `/api/user/*` | 前缀匹配,`/api/user/1`、`/api/user/2/profile` 都命中 |
| `ANY` | `/api/ping` | 不区分请求方法,路径精确匹配 |
| `POST` | `/api/order` | 精确匹配,可同时设置「延迟毫秒」模拟慢接口 |

> 三者同时存在时按 **精确 > 按段 `{id}` > 前缀 `/*`** 的优先级取最匹配的一条,
> 所以手工加的 `/api/user/search` 不会被 `/api/user/{id}` 抢走。

### 2. 让同一条规则按请求返回不同数据(响应模板)

假设要模拟一个用户详情接口 `GET /users/{id}`,希望前端请求 `/users/7` 就拿回 `id = 7`:

1. 规则管理里 **新增规则**,路径填 `/users/{id}` —— **路径参数要带花括号**,这样它只匹配一段,参数才能被模板取到。
2. 响应体里写:

   ```json
   {
     "id": {{request.path.id}},
     "path": "{{request.path}}",
     "q": "{{request.query.q ?? 没传}}"
   }
   ```

3. 打开 **「响应模板」** 开关 —— 用「插入变量」下拉点变量的话会自动打开;如果忘了开,输入框下面会提示。
4. 展开 **「试渲染」**,路径填 `/users/7`、Query 填 `q=zhang`,点 **渲染**:
   应该看到 `{"id": 7, "path": "/users/7", "q": "zhang"}`。清空 Query 再渲染一次会得到 `"q": "没传"`。
5. 保存 → 启动服务 → 请求验证:

   ```bash
   curl 'http://127.0.0.1:9101/users/7'
   # => {"id":7,"path":"/users/7","q":"没传"}
   curl 'http://127.0.0.1:9101/users/42?q=li'
   # => {"id":42,"path":"/users/42","q":"li"}
   ```

   换一个 id 就返回另一个 id —— **规则没改,响应变了**。

补充两点:

- 响应**头**也走同一套模板,比如 `X-Request-Id: {{uuid}}`、`Location: /users/{{request.body.id}}`。
- 模拟前端的**请求历史里存的是渲染后的响应**(不是模板),所以重放一条带参数的请求,看到的和当时一致,便于对照排查。

### 3. 采集真实后端响应并固化成模拟数据

场景:真实后端已经跑在 `http://127.0.0.1:9100`,你不想手抄接口返回值。

1. 进入 **采集数据** → **新建**:填写名称、监听端口(如 `9001`)、**真实后端地址** `http://127.0.0.1:9100`。
   > ⚠️ 真实后端地址**只填到端口**。填写 `http://127.0.0.1:9100/api/user/1` 会被拒绝,因为转发拼接规则是 `真实后端地址 + 请求路径`,多填路径会拼成 `/api/user/1/api/user/1` 而 404。
2. 点击 **启动**,再点击 **开始采集**。
3. 让前端(或 Postman / curl)把请求发到采集端口:

   ```bash
   curl -i http://127.0.0.1:9001/api/user/1
   ```

   请求会被转发到真实后端,**返回的是真实数据**;同时这条请求与真实响应被**分开**记录进右侧**请求日志**(方向标签:前端 / 后端)。
4. 在请求日志里找到「后端」记录,点击 **提交到模拟后端**,选择一个目标模拟后端并确认 —— 这条真实响应就变成了该模拟后端 `GET /api/user/1` 的规则。
5. (可选)点击 **提交到模拟前端** —— 弹窗里会列出这条请求的**真实后端地址、URL 与请求体**,确认后即存入「模拟前端」请求库(**不需要选择目标模拟后端**),之后可在「模拟前端」页按原样重放。
6. 之后只要请求那个模拟后端的端口,就会返回这份(已经脱离真实后端的)固定数据。

> 采集开关是**内存状态**:服务 stop/start 不会丢,但**应用重启后会重置为关闭**。

### 4. 提交到模拟前端并重放(模拟前端)

「模拟前端」是一个**独立的请求库**,没有服务选择框,内容全部来自「采集数据」页的提交。

1. 在 **采集数据** 的请求日志里,对任意一条记录(「前端」请求或「后端」响应都可以)点击 **提交到模拟前端**。
2. 弹窗里核对 **真实后端地址**、**URL** 与 **请求体**,点 **提交**。
   > 真实后端地址取自该采集服务的上游配置,会随这条请求一起保存下来,作为重放时的默认目标。
3. 进入 **模拟前端**:列表按时间倒序列出请求库里的所有记录,每行操作里可以 **详情 / 重放 / 删除**。
   - **删除**会弹二次确认,确认后才真正删除;
   - 顶部还有 **清空(保留收藏)** 与 **全部清空**(同样带二次确认)。
4. 点击某一行的 **重放**,会打开重放面板并把这条设为重放对象:
   - **统一目标地址留空** → 请求发往它自己保存的真实后端地址(最常用);
   - **填写统一目标地址** → 发往该地址,可用于把线上采集到的流量回放到测试环境;
   - **重放范围**可切换成「仅收藏 / 全部」,即可从任意一行进入批量重放。
5. 重放报告会逐条给出目标地址、状态码、耗时与响应预览。

> 重放流量带 `X-Mock-Replay` 标记头,不会被重复记录;该标记头也不会透传给真实后端。
> 请求库**按用户隔离**:每个登录用户看到的是自己提交的请求。

### 5. 多协议模拟后端(gRPC / WebSocket / TCP)

创建服务时选择对应协议,启动后按协议特性配置规则:

| 协议 | 说明 | 规则扩展字段 |
|---|---|---|
| `HTTP` | HTTP/1.1 + 明文 HTTP/2(h2c) | 标准的 方法 / 路径 / 状态码 / 响应头 |
| `GRPC` | 客户端需以 **plaintext(insecure)** 方式连接;按路径匹配(POST) | `grpcStatus`、`grpcMessage` |
| `WEBSOCKET` | `ws://` 连接 | `wsPushOnOpen`(建连后主动推送)、`closeAfterReply` |
| `TCP` | 原始 TCP,按字节收发 | `responseMode`、`payloadEncoding`、`closeAfterReply` |

公共扩展字段:

- `responseMode`:`FIXED` 返回固定内容 / `ECHO` 原样回显收到的内容 / `NONE` 只记录不响应;
- `payloadEncoding`:响应体的编码方式 `TEXT`(UTF-8) / `BASE64` / `HEX`。

### 6. 用户、权限与数据源

- **用户管理**(管理员):新建用户、修改资料、启用/停用、重置密码、删除。
- **数据隔离**:普通用户只能看到自己创建的服务;管理员可见全部,并会把历史无归属的服务自动划归 `admin`。
- **持久化管理**(管理员):查看当前数据源、测试连接、在 H2 与 MySQL 之间切换(`overwrite` / `load`)。
- 任何用户可以修改自己的登录密码。

### 7. 从 OpenAPI / Swagger 导入规则

场景:后端已经维护着一份 Swagger / OpenAPI 文档,你不想手抄几十个接口。

1. 进入 **模拟后端** → 选中一个 **HTTP 协议** 的服务 → 右侧「规则管理」工具条点 **从 OpenAPI 导入**(gRPC / WebSocket / TCP 的服务没有这个按钮)。
2. 提供文档:**上传文件**(`.json` / `.yaml` / `.yml`)或切到 **粘贴文本** 直接粘进去。OpenAPI 3.x 与 Swagger 2.0、JSON 与 YAML 都认,格式自动识别。
3. 核对 **路径前缀**:默认取文档里声明的 `servers[0].url` 的 path(或 Swagger 2 的 `basePath`);如果后端是按网关路径发布的,改成 `/api` 这类前缀,或者清空表示不加。改了前缀要**重新点「解析文档」**。
4. 选 **导入方式**:**只新增**(默认,不动已有规则)或 **同步更新**(后端改了文档时用,见下文)。切换方式会自动重新解析一次。
5. 点 **解析文档**:这只是预览,不会写库。顶部会显示识别到的规范版本与格式(`JSON` / `YAML` 标签)。接口清单按 tag 分组,勾选要处理的接口;带 ⓘ 的行有提示(比如「路径参数 `{id}` 只匹配一段,可用 `{{request.path.id}}` 取值」「已被手工修改,勾选后会覆盖你的改动」),不可处理的行(已最新 / 被占用 / 已存在)会被禁用勾选。
6. 点 **导入 / 同步选中的 N 个接口**。

导入后规则列表里会多出带 **「导入」** 标签的规则,名称就是文档里的 `summary`,响应体是按 schema 生成的示例数据,可以直接手工再编辑。

> 同一份文档用 JSON 或 YAML 传进来,解析出的接口与生成的响应体完全一致;先导了 JSON 版,再导 YAML 版会全部判定为「已存在」而新增 0 条。

```bash
# 导入完成即可直接请求
curl -i http://127.0.0.1:9101/api/users/1
```

#### 后端改了文档之后:用「同步更新」增量更新

不用删规则重新导,直接切到 **同步更新** 再解析一次,预览会按每条接口告诉你它会怎么变:

| 预览状态 | 含义 | 会写库吗 |
| --- | --- | --- |
| **将新增** | 文档里有、本地还没有 | 会(默认勾选) |
| **将更新** | 配对上了导入来源的规则,且内容有变化(行内会列出改动点,如「改动:名称、响应体」) | 会(默认勾选) |
| **已最新** | 配对上了但内容一模一样 | 不会 |
| **已改过** | 配对上了,但这条规则被你手工改过(列表里有「已改」标记) | **不会**,要单独勾选它,或打开 `force` 批量强制 |
| **被占用** | 同「方法 + 路径」已被**手工规则**或文档里另一个接口占着 | 不会,任何情况下都不覆盖 |

更新是**就地改写**:规则 id 不变,**延迟、启用开关、额外响应头这些本地设置也保留**(指纹只看文档管得着的字段,所以调这些不会让规则变成「已改过」)。预览底部还会列出 **已不在文档里的导入规则**,方便你手工清理——它只是列表,不会自动删。

> 手工改过的规则默认被保护。如果确实要按文档覆盖它,两条路:在清单里**单独勾上那一行**(勾选本身就是「我同意覆盖」),或者调接口时带上 `"force": true` 一次性强制全部覆盖。

> ⚠️ 两个常见的语义差异:
> - **路径参数 `{id}` 会原样保留,并且只匹配一段**。`GET /users/{id}` 不会命中 `/users/search`、`/users/1/profile` —— 精确规则仍然优先,所以手工加的 `/users/search` 一定压得过它。捕获到的参数可以用 `{{request.path.id}}` 在响应模板里取值(需要在规则上打开「响应模板」)。解析预览对这类接口给出提示。
> - **文档里没写 2xx 响应的接口**会回退成一条状态码 200 的规则,内容取自 `default` 或最小的具体状态码,并提示来源。这是刻意的:Mock 的价值在于给前端一个能用的成功响应,直接跳过会让一份文档丢掉一大半接口。

### 8. 让服务随管理端自动启动(开机自启)

每个服务都有一个独立的「**开机自启**」开关(**默认开启**),新建时的弹窗里也能直接勾选。

- **服务列表里的「自启开 / 自启关」标签可以直接点**,点一下切换并立刻保存,不用进弹窗。
- 管理端启动时会把开着自启的服务逐个拉起来,日志里能看到完整结果:

  ```
  端口服务开机自启完成:成功 7,端口被占用跳过 1
    已自启 测试(HTTP :8887)
    端口被其它进程占用,已跳过 登录(HTTP :8888)
  ```

- **单个服务起不来不影响其它服务**:端口被别的进程占用时只跳过那一个并记警告,其余照常启动,管理端本身也不会启动失败。这里的「被占用」与手动点「启动」用的是**同一套真实探活**(见 [功能特性 1](#1-页面化多端口模拟后端)),区别只在于自启会把「端口被占」单独归类上报,不影响其余服务。
- 改完开关不想重启管理端,点列表头部的「**启动自启服务**」即可立即拉起(普通用户只会拉起自己可见的服务)。

> 开关与运行状态是**两件独立的事**:点「启动 / 停止」不会改动「自启」开关。所以「我想让它现在停一下,但重启后还要自己起来」这种诉求不需要额外操作。

### 9. 切换界面语言(中英文)

1. 登录后在**右上角**点语言按钮,选择 `中文` 或 `English`;
2. 整个界面**立即切换**,无需刷新页面(当前正在编辑的弹窗、表格、校验提示都会跟着变);
3. 选择结果存在浏览器 `localStorage`,下次打开还是这个语言。

接口返回的错误消息也一起切换:前端每次请求都会带 `X-Lang: zh|en`,后端按它挑选文案。可以这样验证:

```bash
# 英文
curl -s -H "X-Lang: en" http://localhost:8080/api/servers
# {"error":"Not signed in, or the session has expired. Please sign in again","code":401}

# 中文(不带头也等同中文)
curl -s http://localhost:8080/api/servers
# {"error":"未登录或登录已过期,请重新登录","code":401}
```

> **不跟随浏览器语言**是刻意的:这类联调工具的读者基本固定,跟随浏览器会出现「同事英文系统打开就是英文、排查问题时对不上截图」的麻烦。想换语言就点一下,选择会被记住。

### 10. 备份与迁移配置(导出 / 导入)

入口:**系统管理 → 配置备份**(仅管理员可见)。

**导出**

1. 选一个导出范围:全部服务 / 仅模拟后端 / 仅采集数据(等价于接口上的 `?kind=`);
2. 页面上会显示当前范围的**服务数与规则总数**,点「导出备份文件」即下载一份 `mock-config-20261001-201322.json`。

**导入**

1. 把备份文件拖进上传区(或切到「粘贴」页签直接贴 JSON)→ 点「**预演导入**」;
2. 预演结果会列出备份的头部信息与四项计数:**新增 / 覆盖 / 未变化 / 跳过**,以及被跳过的每条服务及其原因;
3. 确认无误后在弹窗里点「开始导入」,结果会保留在页面上(含「需重启生效」的服务名)。

导入只写「有变化」的服务,所以**重复导入同一份文件是空操作**,可以放心地把它写进流水线。

命令行等价写法:

```bash
TOKEN=$(curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"admin123"}' \
  http://localhost:8080/api/auth/login | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')

# 导出全部(缩进两格便于 diff)
curl -s -H "X-Token: $TOKEN" http://localhost:8080/api/servers/export | tee mock-config.json

# 先预演(不写库)
curl -s -X POST -H "X-Token: $TOKEN" -H 'Content-Type: application/json' \
  --data-binary @mock-config.json 'http://localhost:8080/api/servers/import?dryRun=true'

# 确认后真正导入
curl -s -X POST -H "X-Token: $TOKEN" -H 'Content-Type: application/json' \
  --data-binary @mock-config.json 'http://localhost:8080/api/servers/import?dryRun=false'
```

> 想把配置纳入 Git 做 review,推荐导出后用 `kind` 拆成两份分别提交(模拟后端与采集数据的变更节奏通常不同),并在提交信息里带上导出的 `exportedAt`。

---

## 规则匹配规则

假设请求为 `GET /api/user/1`,规则的匹配过程:

1. 先过滤掉**未启用**的规则,以及**协议不一致**的规则;
2. 路径匹配(`PathMatcher.match`):
   - `pattern` 含 `{name}` → 转成**按段正则**,`{name}` 匹配 `[^/]+`(只吃一段)并把值捕获下来;
     可与 `*` 混用,如 `/pet/{id}/uploadImage`;
   - `pattern` 以 `/*` 结尾 → 按**前缀**匹配(`/api/user/*` 命中 `/api/user/1`、`/api/user/1/detail`);
   - `pattern` 含有 `*` → 转成**通配正则**(`/api/*/detail`);
   - 否则 → **精确相等**;
3. HTTP 协议下还需方法匹配:规则方法为 `ANY` 时匹配任意方法;
4. **按精确度分层取最匹配的一条**(`PathMatcher.tier`):
   精确 `0` &gt; 按段 `{id}` `1` &gt; 前缀 `/*` `2`;同一层里取先声明的那条。
   所以手工加的 `/api/user/search` 一定压得过 `/api/user/{id}`;
5. 全部未命中 → 返回 `404`,响应体为 `{"error":"no mock rule matched", ...}`。

> 只含精确与 `/*` 的规则集,分层结果与早期版本完全一致,老规则的行为不会变。

---

## REST API 一览

所有接口以 `/api` 开头(除登录外),鉴权头为 `X-Token: <token>`(也支持 `Authorization: Bearer <token>`)。

### 认证

| 方法 | 路径 | 说明 |
|---|---|---|
| `POST` | `/api/auth/login` | 登录,返回 token 与用户信息 |
| `POST` | `/api/auth/logout` | 登出 |
| `GET` | `/api/auth/me` | 当前登录用户 |
| `POST` | `/api/auth/password` | 修改自己的密码 |

### 服务与规则

| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` | `/api/servers` | 服务列表,支持 `?kind=MOCK\|PROXY_BACK` 过滤 |
| `POST` | `/api/servers` | 创建服务 |
| `DELETE` | `/api/servers/{id}` | 删除服务(同时清理其规则、日志与采集数据) |
| `POST` | `/api/servers/{id}/start` | 启动服务 |
| `POST` | `/api/servers/{id}/stop` | 停止服务 |
| `PUT` | `/api/servers/{id}/auto-start` | 打开 / 关闭开机自启,body `{"autoStart":true}`。**只改设置**:关掉不会停掉正在跑的服务,打开也不会立刻拉起(用下面的接口或重启管理端) |
| `POST` | `/api/servers/auto-start` | 立即把当前用户可见、开着自启但未运行的服务拉一遍(等价于重启时那一轮)。返回 `started` / `portBusy` / `failed` / `alreadyRunning` / `summary`;单个失败不影响其它服务 |
| `GET` | `/api/servers/{id}/rules` | 规则列表 |
| `POST` | `/api/servers/{id}/rules` | 新增规则 |
| `PUT` | `/api/servers/{id}/rules/{ruleId}` | 更新规则 |
| `DELETE` | `/api/servers/{id}/rules/{ruleId}` | 删除规则 |
| `POST` | `/api/servers/{id}/openapi/parse` | 解析 OpenAPI / Swagger 文档并生成导入预览(**dry-run**,不写库),body `{"spec":"<文档全文>","prefix":"/api","mode":"merge","force":false}`。`prefix` 省略时用文档声明的 basePath,传 `""` 表示不加前缀;`mode` 省略按 `append`;预览返回 `counts`(create / update / unchanged / conflict / blocked / exists / skipped)、`items`(含 `changes` 改动点、`ruleId` 配对到的规则)、`orphans`(只报告,仅 merge 模式下有)|
| `POST` | `/api/servers/{id}/openapi/import` | 按勾选导入 / 同步成规则,body 同 `parse` 再加 `"keys":["GET /users/{id}"]`。`keys` 省略表示「全部可处理的项」(merge 下不含冲突项)。返回 `created` / `updated` / `unchanged` / `conflicts` / `blocked` / `skippedExisting` / `skippedUnusable` / `updatedIds` |
| `POST` | `/api/servers/{id}/template/preview` | **试渲染**响应模板(不写库),body `{"template":"...","pathTemplate":"/users/{id}","path":"/users/7","method":"GET","query":"page=2","headers":{...},"body":"{...}"}`。返回 `rendered` / `warnings` / `pathVars`。用的是和真实请求**同一套**渲染实现 |
| `GET` | `/api/servers/{id}/requests` | 请求日志 |
| `GET` | `/api/servers/export` | 导出配置备份(服务 + 规则,自包含 JSON),支持 `?kind=MOCK\|PROXY_BACK` 过滤。范围是**当前用户可见的服务** |
| `POST` | `/api/servers/import` | 导入配置备份,**按服务 id upsert**。`?dryRun=true` 只返回将要发生的变更(`created` / `updated` / `unchanged` / `reowned` / `skipped[]` / `needsRestart[]`)不写库。也接受裸的服务数组。校验不过或端口冲突的服务会被跳过并逐条返回原因;普通用户不能借 id 覆盖别人的服务 |

### 模拟前端 / 采集数据

| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` | `/api/servers/{id}/proxy` | 代理状态(是否采集、真实后端地址、各类记录数) |
| `POST` | `/api/servers/{id}/proxy/capture/start` | 开始采集(body 可带 `upstream`) |
| `POST` | `/api/servers/{id}/proxy/capture/stop` | 停止采集 |
| `GET` | `/api/servers/{id}/proxy/{direction}` | 记录列表,`direction` = `front` / `back` / `all`(合并倒序) |
| `GET` | `/api/servers/{id}/proxy/{direction}/items/{itemId}` | 单条详情(含请求/响应体) |
| `DELETE` | `/api/servers/{id}/proxy/{direction}/items/{itemId}` | 删除单条 |
| `PUT` | `/api/servers/{id}/proxy/{direction}/items/{itemId}/favorite` | 收藏 / 取消收藏 |
| `DELETE` | `/api/servers/{id}/proxy/{direction}` | 清空(`direction` 支持 `all`;`?includeFavorites=false` 时保留收藏) |
| `POST` | `/api/servers/{id}/proxy/front/replay` | 重放某个服务自己记录的请求历史(body `{baseUrl, ids}`)。页面上的「模拟前端」已改为使用下面的请求库接口,此接口保留给脚本调用 |
| `POST` | `/api/servers/{id}/proxy/{direction}/items/{itemId}/to-mock` | 把一条**后端**采集数据固化为目标模拟后端的规则(body `{targetServerId}`;`direction` 传 `BACK`/`back` 均可,传 `FRONT` 会被拒绝) |
| `POST` | `/api/servers/{id}/proxy/{direction}/items/{itemId}/to-front` | 把一条采集请求提交到「模拟前端」请求库,保存真实后端地址 + URL + 请求体(**无需 body / 无需选目标服务**) |

#### 模拟前端请求库

请求库按用户隔离,**路径里不再有服务 id**。

| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` | `/api/proxy/front` | 请求库列表(最新在前) |
| `GET` | `/api/proxy/front/items/{itemId}` | 单条详情(含请求头 / 请求体) |
| `DELETE` | `/api/proxy/front/items/{itemId}` | 删除单条 |
| `PUT` | `/api/proxy/front/items/{itemId}/favorite` | 收藏 / 取消收藏(body `{favorite}`) |
| `DELETE` | `/api/proxy/front` | 清空(`?includeFavorites=false` 时保留收藏) |
| `POST` | `/api/proxy/front/replay` | 重放。body `{baseUrl, ids}`;`baseUrl` 为空时**逐条发往记录自身保存的真实后端地址**,填了则整批发往该地址;`ids` 为空表示全部(单次最多 200 条) |

### 系统管理(仅管理员)

| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` `POST` | `/api/users` | 用户列表 / 新建用户 |
| `PUT` `DELETE` | `/api/users/{id}` | 修改 / 删除用户 |
| `POST` | `/api/users/{id}/enable`、`/disable` | 启用 / 停用 |
| `POST` | `/api/users/{id}/password` | 重置密码 |
| `GET` | `/api/persistence/current` | 当前数据源与统计 |
| `POST` | `/api/persistence/test`、`/check`、`/switch` | 测试连接 / 检查目标库 / 切换数据源 |

---

## 目录结构

```
mock-server/
├── pom.xml
├── LICENSE                             # Apache License 2.0
├── README.md                           # 中文文档(本文件)
├── README.en.md                        # English documentation
├── data/                               # 运行时自动生成:H2 数据文件 + 数据源配置
├── docs/screenshots/                   # README 界面预览截图(tools/shot.js 生成)
├── tools/                              # 开发期辅助脚本(非运行依赖)
│   ├── static-check.js                 # 前端静态校验:语法 + Vue 模板编译 + el-* 标签存在性 + 语言包 key 一致性
│   ├── front-library-e2e-test.py       # 端到端验证:采集 → 提交到模拟前端 → 请求库 → 重放
│   ├── openapi-e2e-test.py             # 端到端验证:OpenAPI / Swagger 导入(解析、生成、append、merge 同步、命中、权限、YAML)
│   ├── template-e2e-test.py            # 端到端验证:响应模板(取值、转义、现算函数、命中、匹配优先级)
│   ├── config-transaction-e2e-test.py   # 端到端验证:配置落库事务化(中途失败时整体回滚)
│   └── shot.js                         # 界面截图(无头 Edge/Chrome + CDP,无依赖)
└── src/main/
    ├── java/com/example/mock/
    │   ├── MockApplication.java          # 启动类
    │   ├── auth/                         # 鉴权:TokenStore / AuthInterceptor / @RequireAdmin
    │   ├── bootstrap/                    # 启动收尾:历史服务归属初始化 + 开机自启(MockServerAutoStarter)
    │   ├── core/                         # 运行时核心
    │   │   ├── MockServerManager.java    # 服务生命周期(启动前 PortProbe 预检、Jetty / TCP 启停、失败时回收半启动实例)
    │   │   ├── DynamicMockServlet.java   # 请求分发:规则匹配 / 模板渲染 / 采集转发 / 日志记录
    │   │   ├── MockWebSocketHandler.java # WebSocket 处理
    │   │   ├── TcpMockServer.java        # TCP 处理
    │   │   └── UpstreamForwarder.java    # 转发到真实后端(逐跳头处理、PATCH 等非标准方法、重定向不跟随)
    │   ├── db/                           # 数据源门面与配置(H2 / MySQL 动态切换)
    │   ├── i18n/                         # 后端国际化:LocaleFilter(读 X-Lang)
    │   │                                 #   + I18n(ThreadLocal 语言上下文与文案解析)+ LocaleConfig(消息源)
    │   ├── model/                        # MockServerConfig / MockRule / ProxyRecord / RequestLog / SysUser
    │   ├── openapi/                      # OpenAPI / Swagger 导入:OpenApiParser(2.0 与 3.x,JSON / YAML)
    │   │                                 #   + SchemaExampleGenerator(按 schema 生成示例数据)
    │   ├── service/                      # ProxyService(重放、固化规则)/ PersistenceService
    │   │                                 #   / OpenApiImportService(文档导入预览、append 与 merge 同步落库)
    │   │                                 #   / AutoStartService(按开机自启批量拉起,端口占用单独识别并跳过)
    │   │                                 #   / ConfigTransferService(配置备份导出 / 导入:按 id upsert、dryRun 预演)
    │   ├── store/                        # ConfigStore / ProxyStore / RequestLogStore / UserStore
    │   ├── template/                     # 响应模板:TemplateRenderer({{...}} 渲染与转义)
    │   │                                 #   + TemplateContext(请求上下文)+ PathMatcher({id} 按段匹配与优先级)
    │   ├── util/                         # PayloadUtil(TEXT / BASE64 / HEX 编解码)
    │   │                                 #   + PortProbe(端口真实占用探测:被监听 / 仅 TIME_WAIT 残留)
    │   │                                 #   + ConfigValidator(规则与「真实后端地址」校验,接口与导入共用同一套)
    │   └── web/                          # Controller 层
    │                                     #   + GlobalExceptionHandler(统一异常)与 ApiErrorController(统一 /error)
    └── resources/
        ├── application.yml
        ├── i18n/                         # 后端文案:messages_zh.properties / messages_en.properties
        └── static/                       # 前端(无构建)
            ├── index.html
            └── js/
                ├── api.js, app.js        # 请求封装、主布局与路由
                ├── i18n.js               # 国际化引擎(Vue.observable 驱动,零依赖)
                ├── locale/               # 语言包:zh.js / en.js
                ├── vue.min.js            # Vue 2.7.16
                ├── element-ui/           # Element UI 2.15.14(js + css + 字体)
                └── views/                # 各页面组件:dashboard / mock-servers /
                                          #   rule-manager / openapi-import / proxy-front /
                                          #   proxy-back / users / persistence /
                                          #   config-backup / login
```

---

## 数据与持久化

| 内容 | 位置 / 说明 |
|---|---|
| H2 数据文件 | `./data/mockdb.mv.db` |
| 数据源配置 | `./data/db-profile.properties`(密码做了 Base64 混淆) |
| 默认数据源 | H2(文件模式,零配置) |
| 可选数据源 | MySQL 8,可在「持久化管理」中运行期切换 |

数据库中的 5 张表:

| 表 | 说明 |
|---|---|
| `sys_user` | 用户(用户名、BCrypt 密码、角色、启用状态) |
| `mock_server` | 服务(名称、协议、端口、类型 kind、归属人、真实后端地址)。`config_json` 里另存**开机自启**开关 `autoStart`(默认 true) |
| `mock_rule` | 规则(方法、路径、状态码、响应头、响应体及多协议扩展字段),一条规则一行。另含:`template`(响应模板开关)、导入来源三件套 `source`(MANUAL / OPENAPI)、`spec_key`(文档里的接口标识)、`spec_hash` + `dirty`(增量同步用:上次导入时的内容指纹、是否被手工改过) |
| `mock_request_log` | 请求日志(每服务保留最近 1000 条) |
| `mock_proxy_record` | 模拟前端 / 采集数据记录(FRONT=前端请求、BACK=后端响应),含真实后端地址 `upstream_target`、收藏与来源标记。`server_id` 除了服务 id,还用 `__front__:<用户id>` 承载「模拟前端请求库」 |

内存态(**应用重启会重置**):采集开关、登录会话(token)。

**端口服务的运行状态本身仍是内存态,但可由「开机自启」开关持久化地恢复**:开着自启的服务会在管理端启动时自动拉起,关掉的则完全手动控制。这个开关存在 `mock_server.config_json` 里,点「启动 / 停止」不会改动它。

---

## 开发与验证

项目自带四个开发期脚本(不参与运行,仅用于自检):

| 脚本 | 作用 |
|---|---|
| `tools/static-check.js` | 前端静态校验:`node --check` 语法检查 + 用本地 `vue.min.js` 的 `Vue.compile()` 逐个编译所有页面组件模板 + 校验用到的 `el-*` 标签在 Element UI 里都存在 + 校验 `js/locale/zh.js` 与 `en.js` 的 key 集合一致 + 扫描源码里 `$t()` 用到的 key 是否都已定义(漏了会直接报 `文件:行号`)。**改完前端先跑它** |
| `tools/front-library-e2e-test.py` | 端到端验证:未登录 401 → 启动采集服务 → 采集 FRONT/BACK → 「提交到模拟前端」→ 请求库增删改查 → 「提交到模拟后端」固化规则并实请求校验 → 重放(含逐条发往各自真实后端地址)→ 清空 |
| `tools/openapi-e2e-test.py` | 端到端验证 OpenAPI 导入:dry-run 不写库 → 路径前缀与 `{id}` 保留 → 响应生成优先级 → 状态码回退 → append / 重复导入 → 导入的规则实请求命中(含 `{id}` 只吃一段)→ 权限与非法文档 → **YAML 文档**(格式识别、锚点别名、块标量、未加引号的标量、Java 标签不实例化、BOM)→ **merge 同步**(首次全新增、重复同步全「已最新」且不写库、文档变更后就地更新且保留规则 id 与本地设置、手工改过则冲突、`force` 强制覆盖、同路径被占用则 blocked、改前缀仍是同一条规则、orphans 只报告不删、非法 `mode` 400、append 回归);可选传 `--petstore3` / `--petstore2` / `--petstore3-yaml` / `--petstore2-yaml` 用真实文档冒烟,并断言同一份文档的 JSON 与 YAML 解析结果一致 |
| `tools/template-e2e-test.py` | 端到端验证响应模板:`request.*` 取值(含路径参数、同名参数下标、URL 解码、请求头大小写、`request.body` JSON 路径)→ `??` 默认值 → JSON 引号内自动转义 / 引号外不转义 / 非 JSON 不转义 → `\{{` 字面量 → 未知占位符原样保留并告警 → `now` / `uuid` / `random.*` → 真实端口命中(路径参数、翻页、回显请求体、响应头渲染、关模板时原样返回)→ 匹配优先级(精确 &gt; `{id}` &gt; `/*`)→ 请求历史存渲染后的响应 → 权限与负向 |
| `tools/config-transaction-e2e-test.py` | 端到端验证配置落库的事务边界:先加一条规则确认写入链路本身可用 → 再加一条 **id 相同**的规则触发主键冲突 → 断言接口失败、且**整份配置与失败前逐字节一致**(没有出现「删掉了但没写回去」的半个配置)→ 确认内存也已回退到库里的状态 → 清理探针 → 反过来确认新增 / 删除服务能正常**提交** → 最终配置回到基线 |
| `tools/shot.js` | 界面截图:调用本机 Edge/Chrome 的无头模式 + CDP,注入登录态后逐页截图到 `docs/screenshots/`(README 里的界面预览就来自它),无需安装任何依赖 |

```bash
node tools/static-check.js
python tools/front-library-e2e-test.py          # 需先在 8080 运行实例
python tools/openapi-e2e-test.py                # 同上,加 --petstore3/--petstore2 可跑真实文档
python tools/template-e2e-test.py               # 同上
python tools/config-transaction-e2e-test.py     # 同上(只读写配置,不动请求库与采集数据)
```

> ⚠️ 端到端脚本为验证「清空」接口会清空**模拟前端请求库**。为避免误删你的数据,脚本在库非空时**先备份到 `data/front-library-backup-<时间戳>.json` 并中止**,必须显式加 `--force` 才会继续。

### 打包与前端改动

```bash
# 1. 先停掉正在运行的实例(否则 repackage 会失败,jar 被截断成 thin jar)
# 2. 再打包
mvn clean package -DskipTests
# 3. 启动
java -jar target/mock-server-1.0.1.jar --server.port=8080
```

改动前端静态资源后**必须重新 `package`**:Maven 只在 `process-resources` 阶段拷贝 `static/`,打包完成后再改文件,jar 里仍是旧版本。

### 改文案时的两个坑

1. **新增界面文案要同时加进两份语言包**:`js/locale/zh.js` 与 `js/locale/en.js`。只加一份时 `static-check.js` 会报 key 不一致。
2. **Element UI 自带文案(分页、日期选择、MessageBox 等)也要自己维护**:本地这份 Element UI 打包版本把中文写死在内置语言包、也没有暴露 `ELEMENT.lang`,所以 `js/i18n.js` 里的 `ELEMENT_LANG` 手工维护了中英两份(结构照抄内置的 14 个 section,**漏 key 会让对应控件显示 `undefined`**),切换语言时调 `ELEMENT.locale()` 生效。

---

## 常见问题

**Q:8080 端口被占用怎么办?**

换一个管理端口启动即可:`java -jar target/mock-server-1.0.1.jar --server.port=8081`。注意「模拟后端」和「采集服务」的监听端口是**全局唯一**的,创建时会校验冲突。

**Q:点「启动」时提示「端口 xxx 已被其它进程占用」?**

说明系统里确实有另一个进程正在监听这个端口(另一个 Mock 实例、正在调试的真实后端、上次没退干净的旧进程都算)。先关掉它,或给这个服务换个端口再启动。

注意与另一条提示区分:「**已被其它服务占用**」是**配置冲突**(本系统内已有服务配了同一个端口,创建 / 修改时就会拦下),文案里带「服务」二字;「**已被其它进程占用**」是**运行期真实探测**的结果,文案里带「进程」二字。

这条提示不要绕过:Windows 的 `SO_REUSEADDR` 允许对已被监听的端口重复绑定,硬绑会「启动成功」但请求全被原来那个进程接走 —— 界面显示运行中,实际一个请求都收不到。

**Q:为什么我改了前端代码,重新打包后页面没变?**

静态资源带版本号(`?v=...`),浏览器可能缓存了旧文件。硬刷新(`Ctrl+F5`)即可;服务端对静态资源已配置 `no-cache`,正常会回源校验。

**Q:我写了 `{{request.query.page}}`,为什么响应里还是原样吐回来的?**

两种可能,规则编辑弹窗里都能直接看出来:

1. **「响应模板」开关没打开**。占位符只有在开关打开时才会被渲染,默认是关的(有些接口的报文本来就带花括号)。输入框下面会提示,用「插入变量」下拉点变量也会自动帮你打开。
2. **占位符名字拼错了**,或者规则路径里没有这个路径参数。这时占位符会被**原样保留**并给出告警 —— 用弹窗里的「试渲染」跑一遍就能看到告警内容。

另外注意「写法对、这次请求没带」的情况会替换成**空串**(属正常运行,不告警);想给它兜个默认值就写 `{{request.query.page ?? 1}}`。

**Q:采集数据为什么没有自动回放?**

这是设计如此。「采集服务」只负责采集;要让某个接口长期返回固定数据,请在请求日志里把后端数据**「提交到模拟后端」**,然后请求那个模拟后端的端口。

**Q:采集端口一直返回 404?**

两种可能:① **采集开关没打开**(采集服务未采集时一律 404);② 请求路径在目标模拟后端里没有对应规则。先点「开始采集」重试。

**Q:转发到真实后端 404 / 路径不对?**

检查「真实后端地址」是否**只填到了端口**。转发地址 = `真实后端地址 + 请求路径`,多填的路径会被重复拼接。

**Q:应用重启后,之前运行的服务会自己起来吗?**

会——只要该服务开着「开机自启」(**默认开启**)。管理端启动时会逐个把它拉起来,日志里会列出成功 / 跳过 / 失败的服务。关掉某个服务的「自启」后它就不会再被自动拉起,需要手动点「启动」。采集开关与登录会话仍是内存态,重启后需要重开 / 重新登录。

**Q:为什么打包时提示 jar 变小了 / 应用起不来?**

**不要在对运行中的应用执行 `mvn package`**。运行中的 JVM 占用 jar,Spring Boot 的 repackage 步骤会失败,导致 jar 被截断成几十 KB 的 thin jar。正确流程是:

```bash
# 先停止应用,再打包,最后启动
mvn clean package -DskipTests
java -jar target/mock-server-1.0.1.jar --server.port=8080
```

**Q:界面语言换成 English 后,某处还是中文 / 直接显示了 `server.xxx` 这样的 key?**

- 显示成 **key 本身**(如 `server.ruleCount`):这句话的 key 在 `js/locale/en.js` 里还没定义。按 [改文案时的两个坑](#改文案时的两个坑) 补齐即可,`node tools/static-check.js` 能直接把缺的 key 列出来。
- 仍然是**中文**:先确认它是不是「落库的数据」(规则名、日志明细等)——这类值按设计不随语言变,见 [已知限制](#已知限制)。如果是 Element UI 控件的内置文字(分页、日期选择、确认框按钮),那是 `ELEMENT_LANG` 里缺了 key。

**Q:接口返回的错误消息没跟着界面语言变?**

后端按请求头 `X-Lang` 选语言。用 `curl` 手工调接口时不会带这个头,返回中文是预期行为;浏览器里由前端统一加上。想手工验证就显式带上:`curl -H "X-Lang: en" ...`。

---

## 已知限制

- **明文传输**:模拟后端与采集端口只提供 HTTP / HTTP/2(h2c)/ ws,不含 TLS,请仅用于内网与本地联调。
- gRPC 仅支持明文(insecure)连接,未实现 proto 描述文件解析,请求/响应按帧字节透传与展示。
- 采集开关与登录会话为**内存态**,重启即失效;端口服务的运行状态由「开机自启」开关决定,但**启动动作本身仍是单机行为**。如需集群部署,需要额外改造(会话共享、采集开关持久化)。
- 端口资源是**单机**概念,同一台机器上不能启动两个占用相同端口的实例。
- **端口占用一律以真实探测为准**:手动「启动」与开机自启走的是同一套探测(`util/PortProbe`)—— 先尝试独占绑定,绑不上再看端口上是否真有进程在监听;确认被占用就**拒绝启动**(手动启动直接报错,自启则跳过该服务并记警告,其余服务继续启动、管理端本身不受影响)。之所以必须显式探测,是因为 Windows 上 `SO_REUSEADDR` 允许对已被监听的端口重复绑定 —— 直接绑会出现「显示已启动但收不到请求」的**静默失败**。探明状态后还会据此显式设置 `SO_REUSEADDR`(只有确认端口上只剩本机 `TIME_WAIT` 残留时才打开),从源头堵住抢占。
- **端口探活有两个边界**:① 探测与真正绑定之间仍存在极短的抢夺窗口(探测只是把窗口缩到最小,无法彻底消除,真正兜底的仍是绑定失败的 catch);② 判断「有没有活监听」是连本机**回环地址**得出,若对方只绑定在某个非回环地址上,理论上探测不到。另外「绑定了端口但还没开始 listen」这种状态在 Windows 上表现为丢弃 SYN,会被按「有进程在监听」**保守拒绝** —— 宁可误拒,也不去抢一个已有归属的端口。
- **配置备份只含「服务 + 规则」**:`mock_request_log`(请求日志)与 `mock_proxy_record`(采集数据 / 模拟前端请求库)不在导出范围内 —— 前者是运行痕迹、后者是采集素材,都不该进 Git。要整体搬运这些数据,用「持久化管理」切换数据源。
- **配置备份的界面入口仅管理员可见**,接口则按**归属**鉴权:普通用户能导出/导入自己可见的服务,但不能借 id 覆盖别人的服务。界面入口统一放在「系统管理」下只是为了不把两个入口散到多处。
- **导入不会自动重启正在运行的服务**:改动照常落库,但运行实例用的是启动那一刻的配置,所以结果里会列出「需重启生效」的服务名,需要手动点一次「停止 → 启动」。
- 单次导入上限 **500 个服务 / 5000 条规则**,界面上的单个文件不超过 5 MB。超限会明确报错而不是慢慢把内存吃掉。
- **配置落库是「全量重写 + 单事务」**:每次保存(哪怕只改一条规则)都会先清空 `mock_server` / `mock_rule` 再整体写回,整段包在**一个事务**里 —— 中途失败会整体回滚,不会留下「删掉了但没写回去」的半个配置;失败时内存也会按库里的内容重新载入,避免界面显示得比库「新」。写入用 `synchronized` 串行化,属于**单进程写**语义;量级适合几十个服务 / 几百条规则,需要更高并发或增量更新时再改造。
- **响应模板只对 HTTP 协议生效**:gRPC / WebSocket / TCP 的载荷是字节流(可能还是 Base64 / HEX 编码的),做文本模板没有意义,这些规则上的「响应模板」不会出现。
- **响应模板的 JSON 转义是启发式的**:判断「模板整体像 JSON」+ 跟踪字符串引号状态来决定要不要转义,而不是把模板当 JSON 解析后逐节点替换。好处是 `{"page": {{request.query.page}}}` 这种引号外的裸占位符也能用,代价是模板本身引号错位时可能判断不准。规则编辑弹窗里的「试渲染」用的是同一套实现,保存前先试一次就能发现。
- **渲染结果上限 4MB**:`{{request.body}}` 这类回显可能把响应撑得很大,超限会截断并告警。`random.string(n)` 的 n 最大 1024。
- **替换结果不会被二次解析**:占位符渲染出来的文本里如果再含 `{{...}}`,不会继续展开(避免自引用式递归)。
- **OpenAPI 导入的外部 `$ref` 不解析**:只展开文档内 `#/...` 引用(JSON Pointer),引用其它文件或 URL 的 `$ref` 会被当成空 schema 处理。单份文档上限 5MB。
- **同步更新(merge)只按 `spec_key` 配对,且不带「删除」语义**:文档里删掉的接口只会列进 `orphans` 提醒,不会自动删规则;同「方法 + 路径」被手工规则占用时一律 `blocked` 不覆盖(避免把手工规则顶掉或造出永远命中不到的重复规则)。
- **没有同步指纹的导入规则按「已改过」处理**:`spec_hash` 是随本功能一起加的,在那之前导入的规则没有指纹,无从判断有没有被手工改过,所以 merge 时会算作「已改过」——需要单独勾选或 `force` 才会覆盖。这是刻意的保守选择:宁可多点一次,也不要把别人的手工改动静默覆盖掉。
- **`spec_key` 一旦丢失就无法再配对**:规则编辑弹窗现在会带上 `source` / `spec_key` / `spec_hash`(后端也会在旧客户端不传时从原规则兜回来),所以正常编辑不会破坏同步关系;但如果直接用 API 建一条同名规则、或删掉再手工重建,它就不再与文档关联了。
- YAML 的**锚点别名会就地展开**,所以「别名被层层套用」的文档展开后节点数会远超文档体积。已加两道闸:snakeyaml 的别名数量上限(1000)与转成 JSON 树之前的节点预算(按文档长度缩放,每字符 2 个节点、下限 10 万),超限会明确报 400,而不是卡死或 OOM;自引用别名同理。带全局标签(如 `!!java.net.URL`)的内容走 `SafeConstructor`,**不会触发任何实例化**,会以「无法识别的标签」报 400。
- YAML 解析本身是递归下降的,嵌套上万层的畸形文档可能因栈溢出失败(正常文档远达不到这个量级)。
- **界面语言只管「界面与错误消息」,不管「落库的数据」**:程序生成后写进库里的文本(如从采集数据固化规则时自动起的规则名 `采集后端 GET /xxx`、请求日志里「未匹配到 TCP 规则」这类明细、内置管理员昵称「超级管理员」)不随语言变。这类值是**数据**而不是消息,若跟着请求语言写库,同一条记录会因为当时是谁在操作而变成不同语言。
- **后端日志一律中文**:`log.info` / `log.warn` 不参与国际化,grep 日志时不必在意界面语言。

---

## 说明

- 默认账号 `admin / admin123` 仅用于本地起步,**正式环境请立即修改密码**。
- 前端依赖(Vue、Element UI)以静态资源形式内置于仓库,无需联网安装,克隆即可构建运行。
- 项目基于 [Apache License 2.0](LICENSE) 开源。
相关推荐
小蒜学长1 小时前
基于Spring Boot+Vue的“禾源”农产品销售系统设计与实现(代码+数据库+LW)
java·spring boot·后端·农产品销售系统·产品溯源
弹简特1 小时前
【Java项目-企悦抽】14-活动管理模块01-创建活动的实现
java·开发语言·springboot
迅猛龙办公室1 小时前
Python实现求两个数字之和
java·开发语言·python
范桂飓1 小时前
AWS Agent Infra 架构分析
java·架构·aws
企业数字化笔记1 小时前
视频成片怎么稳定导出?编码选择、GPU/CPU降级与媒体交付验收
java·python·ffmpeg
Wx-bishekaifayuan2 小时前
springboot房屋租赁系统11574-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·sql·spring·课程设计
三8442 小时前
Fastjson 漏洞学习笔记 · 03 · 经典利用链:TemplatesImpl 与 JdbcRowSetImpl
java·web安全·fastjson
蜗牛互联网3 小时前
GPT-6.1 Sol迁移指南:从token单价转向每任务成本门禁
java·人工智能·后端·gpt
蜗牛互联网3 小时前
HSTU在Dynamo-Triton中的AOTI与KV缓存验收方法
java·人工智能·后端·缓存