diff --git a/.claude/agents/wechat-article-summarizer.md b/.claude/agents/wechat-article-summarizer.md
new file mode 100644
index 0000000..5feef58
--- /dev/null
+++ b/.claude/agents/wechat-article-summarizer.md
@@ -0,0 +1,103 @@
+---
+name: wechat-article-summarizer
+description: "Use this agent when the user wants to summarize WeChat official account articles (公众号文章). This includes when the user shares a WeChat article link, asks for a summary of an article, or wants key points extracted from WeChat content.\\n\\nExamples:\\n\\n\\nContext: User shares a WeChat article link and wants a summary.\\nuser: \"帮我总结一下这篇文章:https://mp.weixin.qq.com/s/xxxxx\"\\nassistant: \"我来使用公众号文章总结代理来帮你总结这篇文章。\"\\n\\nSince the user wants to summarize a WeChat article, use the Agent tool to launch the wechat-article-summarizer agent.\\n\\n\\n\\n\\nContext: User has pasted WeChat article content and wants key points.\\nuser: \"这篇文章讲了什么?[粘贴了公众号文章内容]\"\\nassistant: \"我来使用公众号文章总结代理帮你提取这篇文章的要点。\"\\n\\nThe user has provided WeChat article content and wants to understand the main points. Use the Agent tool to launch the wechat-article-summarizer agent.\\n\\n\\n\\n\\nContext: User wants a quick overview of a shared WeChat article.\\nuser: \"这个公众号文章太长了,帮我看看主要说什么\"\\nassistant: \"我来用公众号文章总结代理帮你快速了解文章主旨。\"\\n\\nThe user finds the article too long and wants a quick overview. Use the Agent tool to launch the wechat-article-summarizer agent.\\n\\n"
+model: sonnet
+color: blue
+memory: project
+---
+
+你是一位专业的公众号文章总结专家,擅长快速阅读和提炼中文文章的核心内容。你具备优秀的阅读理解能力和信息提取技巧,能够准确把握文章主旨并生成结构清晰的摘要并将总结结果生成rules。
+
+## 核心职责
+
+你的任务是为用户提供高质量的公众号文章总结,帮助他们快速理解文章内容并生成rules。
+
+## 工作流程
+
+1. **获取文章内容**
+ - 如果用户提供链接,使用可用的工具获取文章内容
+ - 如果用户直接粘贴内容,直接处理该内容
+
+2. **分析文章结构**
+ - 识别文章标题和主题
+ - 找出核心论点和关键信息
+ - 标记重要的数据、案例或引用
+ - 注意文章的写作目的(科普、观点、新闻等)
+
+3. **生成摘要**
+ 输出格式如下:
+
+ ### 📌 核心主旨
+ [一句话概括文章主题]
+
+ ### 📝 内容概要
+ [100-200字的文章概述]
+
+ ### 🔑 关键要点
+ - 要点1
+ - 要点2
+ - 要点3
+ - ...(通常3-6个要点)
+
+ ### 💡 金句摘录
+ > [文章中的精彩语句,如有]
+
+ ### ⚖️ 个人观点(可选)
+ [如果文章有争议性或值得讨论的观点,简要说明]
+
+## 质量标准
+
+- **准确性**:摘要必须忠实于原文,不能曲解或添加原文没有的信息
+- **简洁性**:去除冗余信息,保留核心内容
+- **完整性**:涵盖文章的主要观点和重要细节
+- **可读性**:使用清晰的语言,逻辑结构分明
+
+## 注意事项
+
+- 如果文章内容无法获取,明确告知用户并说明原因
+- 对于长文章,优先提取核心观点,次要内容可以略过
+- 如果文章包含专业术语,适当提供简单解释
+- 保持客观中立的立场,不在摘要中加入个人偏见
+- 使用中文进行总结
+- 在markdown文件中禁止使用emoji(根据用户配置要求,在最终输出时移除所有emoji)
+
+## 处理特殊情况
+
+- **付费文章**:告知用户无法访问付费内容
+- **已删除文章**:说明文章可能已被删除
+- **图片为主的内容**:说明文章以图片为主,总结可识别的文字部分
+
+# Persistent Agent Memory
+
+You have a persistent Persistent Agent Memory directory at `D:\CNWei\CNW\Rust\mock-server\.claude\agent-memory\wechat-article-summarizer\`. This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence). Its contents persist across conversations.
+
+As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
+
+Guidelines:
+- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise
+- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md
+- Update or remove memories that turn out to be wrong or outdated
+- Organize memory semantically by topic, not chronologically
+- Use the Write and Edit tools to update your memory files
+
+What to save:
+- Stable patterns and conventions confirmed across multiple interactions
+- Key architectural decisions, important file paths, and project structure
+- User preferences for workflow, tools, and communication style
+- Solutions to recurring problems and debugging insights
+
+What NOT to save:
+- Session-specific context (current task details, in-progress work, temporary state)
+- Information that might be incomplete — verify against project docs before writing
+- Anything that duplicates or contradicts existing CLAUDE.md instructions
+- Speculative or unverified conclusions from reading a single file
+
+Explicit user requests:
+- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
+- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
+- When the user corrects you on something you stated from memory, you MUST update or remove the incorrect entry. A correction means the stored memory is wrong — fix it at the source before continuing, so the same mistake does not repeat in future conversations.
+- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project
+
+## MEMORY.md
+
+Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.
diff --git a/.claude/commands/git-branch.md b/.claude/commands/git-branch.md
new file mode 100644
index 0000000..d934a6a
--- /dev/null
+++ b/.claude/commands/git-branch.md
@@ -0,0 +1,20 @@
+# Git 分支管理
+
+读取 `.claude/rules/git-branch-specification.md` 规范,帮助用户:
+
+1. **生成合规分支名** - 根据用户输入的项目名、版本号,自动生成符合规范的分支名
+2. **校验分支名** - 检查当前分支名是否符合规范
+3. **创建分支** - 按规范创建新分支
+
+## 使用示例
+
+- `/git-branch new feature 项目名 v1.0.0` - 创建功能分支
+- `/git-branch new hotfix 项目名 v1.0.1` - 创建热修复分支
+- `/git-branch check` - 校验当前分支名
+- `/git-branch help` - 显示命名规范
+
+## 执行步骤
+
+1. 读取规范文件 `.claude/rules/git-branch-specification.md`
+2. 根据用户指令执行对应操作
+3. 生成分支名时使用当天日期(格式:YYYYMMDD)
diff --git a/.claude/rules/git-branch-specification.md b/.claude/rules/git-branch-specification.md
new file mode 100644
index 0000000..620452e
--- /dev/null
+++ b/.claude/rules/git-branch-specification.md
@@ -0,0 +1,75 @@
+# Git分支管理规范
+
+## 分支类型
+
+| 类型 | 分支名称 | 用途 | 特点 |
+|------|----------|------|------|
+| 主分支 | master | 正式版本代码归档 | 受保护,仅运维可合并 |
+| 主分支 | develop | 日常开发主分支 | 团队开发基准 |
+| 主分支 | doc | 文档、SQL脚本、配置 | 文档管理 |
+| 辅助分支 | feature | 功能开发分支 | 临时性,合并后删除 |
+| 辅助分支 | hotfix | Bug紧急修复 | 临时性,合并后删除 |
+
+## 分支命名规则
+
+### 功能分支
+```
+feature-{项目名}-{版本号}-SNAPSHOT-{日期}
+```
+示例:`feature-javadog-v2.1.1-SNAPSHOT-20240703`
+
+### 个人分支
+```
+{功能分支}-{开发者姓名}
+```
+示例:`feature-javadog-v2.1.1-SNAPSHOT-20240703-zhangsan`
+
+### 预生产分支
+```
+feature-{项目名}-{版本号}-{日期}
+```
+示例:`feature-javadog-v2.1.1-20240703`(去除SNAPSHOT标识)
+
+### 热修复分支
+```
+hotfix-{项目名}-{版本号}-{日期}
+```
+示例:`hotfix-javadog-v2.1.2-20240705`
+
+## 分支权限规则
+
+1. master/develop/doc 分支受保护,仅运维可合并
+2. 辅助分支为临时分支,合并后询问开发人员是否需要删除
+
+## 开发流程规则
+
+### 功能开发流程(五阶段)
+
+| 阶段 | 操作 | 核心目的 |
+|------|------|----------|
+| 开发前 | 从develop拉取功能分支 | 保持团队起始点一致 |
+| 开发中 | 组员从功能分支拉取个人临时分支 | 保证开发灵活性 |
+| 提测中 | 个人分支合并到功能分支 | 流水线打包提测 |
+| 预生产 | 从功能分支拉取预生产分支(去SNAPSHOT标识) | 环境验证解耦 |
+| 上线 | 蓝绿部署,分支合并 | 无缝切换,稳定上线 |
+
+### 热修复流程
+
+1. 从 master 拉取 hotfix 分支
+2. 修复 Bug 后合并回 master 和 develop
+3. 合并后删除 hotfix 分支
+
+## 蓝绿部署策略
+
+- 定义:同时运行两个生产环境,通过切换实现无缝发布
+- 优势:新版本测试期间不影响线上环境
+- 流程:蓝线发布新版本 -> 验证通过 -> 蓝绿切换 -> 负载均衡
+
+## 命名示例汇总
+
+| 分支类型 | 命名格式 | 示例 |
+|---------|---------|------|
+| 功能分支 | `feature-{项目}-{版本}-SNAPSHOT-{日期}` | `feature-javadog-v2.1.1-SNAPSHOT-20240703` |
+| 个人分支 | `{功能分支}-{姓名}` | `feature-javadog-v2.1.1-SNAPSHOT-20240703-zhangsan` |
+| 预生产分支 | `feature-{项目}-{版本}-{日期}` | `feature-javadog-v2.1.1-20240703` |
+| 热修复分支 | `hotfix-{项目}-{版本}-{日期}` | `hotfix-javadog-v2.1.2-20240705` |
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index ea1c057..da7756b 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -2,7 +2,8 @@
"permissions": {
"allow": [
"Bash(find:*)",
- "Bash(cargo search:*)"
+ "Bash(cargo search:*)",
+ "WebSearch"
]
}
}
diff --git a/Cargo.lock b/Cargo.lock
index 607bd6e..939f909 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -412,6 +412,7 @@ dependencies = [
"tracing",
"tracing-subscriber",
"walkdir",
+ "winres",
]
[[package]]
@@ -772,6 +773,15 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "tower"
version = "0.5.2"
@@ -999,6 +1009,15 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+[[package]]
+name = "winres"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
+dependencies = [
+ "toml",
+]
+
[[package]]
name = "wit-bindgen"
version = "0.46.0"
diff --git a/Cargo.toml b/Cargo.toml
index f831bc6..9d5c71b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,4 +32,7 @@ notify-debouncer-mini = "0.6.0"
#pathdiff = "0.2.3"
[dev-dependencies]
-tempfile = "3.24.0"
\ No newline at end of file
+tempfile = "3.24.0"
+
+[build-dependencies]
+winres = "0.1"
\ No newline at end of file
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..be57857
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,29 @@
+fn main() {
+ // 仅在 Windows 平台上编译资源
+ if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
+ let mut res = winres::WindowsResource::new();
+
+ // 1. 设置图标路径 (请确保 icons 文件夹下有 icon.ico)
+ res.set_icon("icons/icon.ico");
+
+ // 2. 自动同步 Cargo.toml 中的元数据
+ // 使用 env! 宏在编译阶段获取 package 信息
+ res.set("FileDescription", env!("CARGO_PKG_DESCRIPTION"));
+ res.set("ProductName", "Mock Server");
+ res.set("ProductVersion", env!("CARGO_PKG_VERSION"));
+ res.set("FileVersion", env!("CARGO_PKG_VERSION"));
+ res.set("InternalName", &format!("{}.exe", env!("CARGO_PKG_NAME")));
+
+ // 3. 设置版权信息
+ res.set("LegalCopyright", "Copyright © 2026 Ways");
+
+ // 4. 设置语言为中文 (中国)
+ res.set_language(0x0804);
+
+ // 执行编译,如果失败则打印错误并停止
+ if let Err(e) = res.compile() {
+ eprintln!("资源编译失败: {}", e);
+ std::process::exit(1);
+ }
+ }
+}
\ No newline at end of file
diff --git a/icons/128x128.png b/icons/128x128.png
new file mode 100644
index 0000000..7ff4526
Binary files /dev/null and b/icons/128x128.png differ
diff --git a/icons/128x128@2x.png b/icons/128x128@2x.png
new file mode 100644
index 0000000..8e59eb7
Binary files /dev/null and b/icons/128x128@2x.png differ
diff --git a/icons/32x32.png b/icons/32x32.png
new file mode 100644
index 0000000..e20cfc6
Binary files /dev/null and b/icons/32x32.png differ
diff --git a/icons/64x64.png b/icons/64x64.png
new file mode 100644
index 0000000..1dcfef7
Binary files /dev/null and b/icons/64x64.png differ
diff --git a/icons/Square107x107Logo.png b/icons/Square107x107Logo.png
new file mode 100644
index 0000000..1fae436
Binary files /dev/null and b/icons/Square107x107Logo.png differ
diff --git a/icons/Square142x142Logo.png b/icons/Square142x142Logo.png
new file mode 100644
index 0000000..dd13a22
Binary files /dev/null and b/icons/Square142x142Logo.png differ
diff --git a/icons/Square150x150Logo.png b/icons/Square150x150Logo.png
new file mode 100644
index 0000000..521ab74
Binary files /dev/null and b/icons/Square150x150Logo.png differ
diff --git a/icons/Square284x284Logo.png b/icons/Square284x284Logo.png
new file mode 100644
index 0000000..f20a4f9
Binary files /dev/null and b/icons/Square284x284Logo.png differ
diff --git a/icons/Square30x30Logo.png b/icons/Square30x30Logo.png
new file mode 100644
index 0000000..fe400c6
Binary files /dev/null and b/icons/Square30x30Logo.png differ
diff --git a/icons/Square310x310Logo.png b/icons/Square310x310Logo.png
new file mode 100644
index 0000000..b3141e0
Binary files /dev/null and b/icons/Square310x310Logo.png differ
diff --git a/icons/Square44x44Logo.png b/icons/Square44x44Logo.png
new file mode 100644
index 0000000..38f4cdf
Binary files /dev/null and b/icons/Square44x44Logo.png differ
diff --git a/icons/Square71x71Logo.png b/icons/Square71x71Logo.png
new file mode 100644
index 0000000..7047da6
Binary files /dev/null and b/icons/Square71x71Logo.png differ
diff --git a/icons/Square89x89Logo.png b/icons/Square89x89Logo.png
new file mode 100644
index 0000000..f7dadcb
Binary files /dev/null and b/icons/Square89x89Logo.png differ
diff --git a/icons/StoreLogo.png b/icons/StoreLogo.png
new file mode 100644
index 0000000..527fcd3
Binary files /dev/null and b/icons/StoreLogo.png differ
diff --git a/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/icons/android/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..2ffbf24
--- /dev/null
+++ b/icons/android/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/icons/android/mipmap-hdpi/ic_launcher.png b/icons/android/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..3090c6b
Binary files /dev/null and b/icons/android/mipmap-hdpi/ic_launcher.png differ
diff --git a/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/icons/android/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..8b1796a
Binary files /dev/null and b/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/icons/android/mipmap-hdpi/ic_launcher_round.png b/icons/android/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..402bd00
Binary files /dev/null and b/icons/android/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/icons/android/mipmap-mdpi/ic_launcher.png b/icons/android/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..954fcbb
Binary files /dev/null and b/icons/android/mipmap-mdpi/ic_launcher.png differ
diff --git a/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/icons/android/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..c268ca5
Binary files /dev/null and b/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/icons/android/mipmap-mdpi/ic_launcher_round.png b/icons/android/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..de326fc
Binary files /dev/null and b/icons/android/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/icons/android/mipmap-xhdpi/ic_launcher.png b/icons/android/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..7b38a48
Binary files /dev/null and b/icons/android/mipmap-xhdpi/ic_launcher.png differ
diff --git a/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..a47469e
Binary files /dev/null and b/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/icons/android/mipmap-xhdpi/ic_launcher_round.png b/icons/android/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..314548e
Binary files /dev/null and b/icons/android/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/icons/android/mipmap-xxhdpi/ic_launcher.png b/icons/android/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..5c0ca48
Binary files /dev/null and b/icons/android/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..12db2fe
Binary files /dev/null and b/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/icons/android/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..11e16a9
Binary files /dev/null and b/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/icons/android/mipmap-xxxhdpi/ic_launcher.png b/icons/android/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d209722
Binary files /dev/null and b/icons/android/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..73512b1
Binary files /dev/null and b/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8b78c7e
Binary files /dev/null and b/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/icons/android/values/ic_launcher_background.xml b/icons/android/values/ic_launcher_background.xml
new file mode 100644
index 0000000..ea9c223
--- /dev/null
+++ b/icons/android/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #fff
+
\ No newline at end of file
diff --git a/icons/icon.icns b/icons/icon.icns
new file mode 100644
index 0000000..66157b9
Binary files /dev/null and b/icons/icon.icns differ
diff --git a/icons/icon.ico b/icons/icon.ico
new file mode 100644
index 0000000..24518e9
Binary files /dev/null and b/icons/icon.ico differ
diff --git a/icons/icon.png b/icons/icon.png
new file mode 100644
index 0000000..9044e88
Binary files /dev/null and b/icons/icon.png differ
diff --git a/icons/ios/AppIcon-20x20@1x.png b/icons/ios/AppIcon-20x20@1x.png
new file mode 100644
index 0000000..1624452
Binary files /dev/null and b/icons/ios/AppIcon-20x20@1x.png differ
diff --git a/icons/ios/AppIcon-20x20@2x-1.png b/icons/ios/AppIcon-20x20@2x-1.png
new file mode 100644
index 0000000..06f7063
Binary files /dev/null and b/icons/ios/AppIcon-20x20@2x-1.png differ
diff --git a/icons/ios/AppIcon-20x20@2x.png b/icons/ios/AppIcon-20x20@2x.png
new file mode 100644
index 0000000..06f7063
Binary files /dev/null and b/icons/ios/AppIcon-20x20@2x.png differ
diff --git a/icons/ios/AppIcon-20x20@3x.png b/icons/ios/AppIcon-20x20@3x.png
new file mode 100644
index 0000000..6cfa902
Binary files /dev/null and b/icons/ios/AppIcon-20x20@3x.png differ
diff --git a/icons/ios/AppIcon-29x29@1x.png b/icons/ios/AppIcon-29x29@1x.png
new file mode 100644
index 0000000..84a0153
Binary files /dev/null and b/icons/ios/AppIcon-29x29@1x.png differ
diff --git a/icons/ios/AppIcon-29x29@2x-1.png b/icons/ios/AppIcon-29x29@2x-1.png
new file mode 100644
index 0000000..c9361a1
Binary files /dev/null and b/icons/ios/AppIcon-29x29@2x-1.png differ
diff --git a/icons/ios/AppIcon-29x29@2x.png b/icons/ios/AppIcon-29x29@2x.png
new file mode 100644
index 0000000..c9361a1
Binary files /dev/null and b/icons/ios/AppIcon-29x29@2x.png differ
diff --git a/icons/ios/AppIcon-29x29@3x.png b/icons/ios/AppIcon-29x29@3x.png
new file mode 100644
index 0000000..2edb20f
Binary files /dev/null and b/icons/ios/AppIcon-29x29@3x.png differ
diff --git a/icons/ios/AppIcon-40x40@1x.png b/icons/ios/AppIcon-40x40@1x.png
new file mode 100644
index 0000000..06f7063
Binary files /dev/null and b/icons/ios/AppIcon-40x40@1x.png differ
diff --git a/icons/ios/AppIcon-40x40@2x-1.png b/icons/ios/AppIcon-40x40@2x-1.png
new file mode 100644
index 0000000..eb14789
Binary files /dev/null and b/icons/ios/AppIcon-40x40@2x-1.png differ
diff --git a/icons/ios/AppIcon-40x40@2x.png b/icons/ios/AppIcon-40x40@2x.png
new file mode 100644
index 0000000..eb14789
Binary files /dev/null and b/icons/ios/AppIcon-40x40@2x.png differ
diff --git a/icons/ios/AppIcon-40x40@3x.png b/icons/ios/AppIcon-40x40@3x.png
new file mode 100644
index 0000000..35d25bc
Binary files /dev/null and b/icons/ios/AppIcon-40x40@3x.png differ
diff --git a/icons/ios/AppIcon-512@2x.png b/icons/ios/AppIcon-512@2x.png
new file mode 100644
index 0000000..85ae0ae
Binary files /dev/null and b/icons/ios/AppIcon-512@2x.png differ
diff --git a/icons/ios/AppIcon-60x60@2x.png b/icons/ios/AppIcon-60x60@2x.png
new file mode 100644
index 0000000..35d25bc
Binary files /dev/null and b/icons/ios/AppIcon-60x60@2x.png differ
diff --git a/icons/ios/AppIcon-60x60@3x.png b/icons/ios/AppIcon-60x60@3x.png
new file mode 100644
index 0000000..dbf7d17
Binary files /dev/null and b/icons/ios/AppIcon-60x60@3x.png differ
diff --git a/icons/ios/AppIcon-76x76@1x.png b/icons/ios/AppIcon-76x76@1x.png
new file mode 100644
index 0000000..b7b9912
Binary files /dev/null and b/icons/ios/AppIcon-76x76@1x.png differ
diff --git a/icons/ios/AppIcon-76x76@2x.png b/icons/ios/AppIcon-76x76@2x.png
new file mode 100644
index 0000000..a265c4d
Binary files /dev/null and b/icons/ios/AppIcon-76x76@2x.png differ
diff --git a/icons/ios/AppIcon-83.5x83.5@2x.png b/icons/ios/AppIcon-83.5x83.5@2x.png
new file mode 100644
index 0000000..3942831
Binary files /dev/null and b/icons/ios/AppIcon-83.5x83.5@2x.png differ
diff --git a/plan.md b/plan.md
new file mode 100644
index 0000000..f984916
--- /dev/null
+++ b/plan.md
@@ -0,0 +1,239 @@
+# Body 匹配优化:基于 Content-Type 的智能解析
+
+## Context
+
+**问题**: 当前只支持 JSON body 匹配,非 JSON 请求无法正确匹配。
+
+**需求**: 根据配置和请求的 Content-Type 智能选择解析方式,支持 JSON、XML、Form、Text 等类型。
+
+---
+
+## 核心设计
+
+### Body 解析规则(优先级)
+
+| 优先级 | YAML Content-Type | 请求 Content-Type | 解析方式 |
+|--------|-------------------|-------------------|----------|
+| 1 | 有 | 任意 | 按 **YAML** 的类型解析 |
+| 2 | 无 | 有 | 按 **请求**的 Content-Type 解析 |
+| 3 | 无 | 无 | **字符串**比较 |
+
+### 支持的 Content-Type
+
+| Content-Type | 解析结果 |
+|--------------|----------|
+| `application/json` | `ParsedBody::Json(Value)` |
+| `application/xml`, `text/xml` | `ParsedBody::Xml(String)` |
+| `application/x-www-form-urlencoded` | `ParsedBody::Form(HashMap)` |
+| `multipart/form-data` | `ParsedBody::Multipart(Vec)` |
+| `text/plain` 或其他 | `ParsedBody::Text(String)` |
+
+---
+
+## 实现方案
+
+### Step 1: 新增数据结构 (model.rs)
+
+```rust
+/// 解析后的请求 Body
+#[derive(Debug, Clone)]
+pub enum ParsedBody {
+ Json(serde_json::Value),
+ Xml(String),
+ Form(HashMap),
+ Multipart(Vec), // 字段名列表
+ Text(String),
+ None,
+}
+
+impl ParsedBody {
+ /// 转换为字符串(用于兜底比较)
+ pub fn to_compare_string(&self) -> String {
+ match self {
+ ParsedBody::Json(v) => v.to_string(),
+ ParsedBody::Xml(s) | ParsedBody::Text(s) => s.clone(),
+ ParsedBody::Form(map) => {
+ let mut pairs: Vec<_> = map.iter().collect();
+ pairs.sort_by_key(|(k, _)| *k);
+ pairs.iter().map(|(k, v)| format!("{}={}", k, v)).join("&")
+ }
+ ParsedBody::Multipart(fields) => fields.join(","),
+ ParsedBody::None => String::new(),
+ }
+ }
+}
+```
+
+### Step 2: 解析函数 (handler.rs)
+
+```rust
+/// 提取 Content-Type(去掉参数部分)
+fn extract_content_type(headers: &HashMap) -> Option {
+ headers.iter()
+ .find(|(k, _)| k.to_lowercase() == "content-type")
+ .map(|(_, v)| v.split(';').next().unwrap_or(v).trim().to_lowercase())
+}
+
+/// 根据 Content-Type 解析 Body
+fn parse_body(content_type: Option<&str>, bytes: &[u8]) -> ParsedBody {
+ if bytes.is_empty() {
+ return ParsedBody::None;
+ }
+
+ match content_type {
+ Some(ct) if ct.contains("application/json") => {
+ serde_json::from_slice(bytes)
+ .map(ParsedBody::Json)
+ .unwrap_or_else(|_| ParsedBody::Text(String::from_utf8_lossy(bytes).to_string()))
+ }
+ Some(ct) if ct.contains("xml") => {
+ ParsedBody::Xml(String::from_utf8_lossy(bytes).to_string())
+ }
+ Some(ct) if ct.contains("form-urlencoded") => {
+ ParsedBody::Form(parse_urlencoded(bytes))
+ }
+ Some(ct) if ct.contains("multipart/form-data") => {
+ ParsedBody::Multipart(extract_multipart_fields(bytes))
+ }
+ _ => {
+ ParsedBody::Text(String::from_utf8_lossy(bytes).to_string())
+ }
+ }
+}
+```
+
+### Step 3: 确定解析类型 (handler.rs)
+
+```rust
+// 在 mock_handler 中:
+
+// 1. 提取请求的 Content-Type
+let req_content_type = extract_content_type(&req_headers);
+
+// 2. 读取请求 body
+let body_bytes = ...;
+let parsed_body = parse_body(req_content_type.as_deref(), &body_bytes);
+
+// 3. 匹配时传递 req_headers 和 parsed_body
+// router 会根据 YAML 中是否配置了 Content-Type 来决定使用哪个类型
+```
+
+### Step 4: 匹配逻辑 (router.rs)
+
+```rust
+fn match_body(
+ &self,
+ rule: &MockRule,
+ parsed_body: &ParsedBody,
+ req_content_type: Option<&str>,
+) -> bool {
+ let yaml_body = match &rule.request.body {
+ Some(b) => b,
+ None => return true, // YAML 没配置 body,跳过检查
+ };
+
+ // 确定用于解析/比较的 Content-Type
+ let effective_content_type = rule.request.headers
+ .as_ref()
+ .and_then(|h| h.iter()
+ .find(|(k, _)| k.to_lowercase() == "content-type")
+ .map(|(_, v)| v.as_str()))
+ .or(req_content_type); // YAML 优先,没有则用请求的
+
+ match effective_content_type {
+ Some(ct) if ct.contains("application/json") => {
+ // JSON 比较
+ match parsed_body {
+ ParsedBody::Json(actual) => yaml_body == actual,
+ _ => false,
+ }
+ }
+ Some(ct) if ct.contains("xml") => {
+ // XML 字符串比较
+ match parsed_body {
+ ParsedBody::Xml(actual) => yaml_body.as_str()
+ .map(|expected| expected.trim() == actual.trim())
+ .unwrap_or(false),
+ _ => false,
+ }
+ }
+ Some(ct) if ct.contains("form-urlencoded") => {
+ // Form 比较
+ match parsed_body {
+ ParsedBody::Form(actual) => compare_form(yaml_body, actual),
+ _ => false,
+ }
+ }
+ _ => {
+ // 无 Content-Type 或其他类型:字符串比较
+ let actual_str = parsed_body.to_compare_string();
+ let expected_str = yaml_body.to_string();
+ expected_str.trim() == actual_str.trim()
+ }
+ }
+}
+```
+
+---
+
+## 需要修改的文件
+
+| 文件 | 改动 |
+|------|------|
+| `src/model.rs` | 新增 `ParsedBody` 枚举和 `to_compare_string()` 方法 |
+| `src/handler.rs` | 新增 `extract_content_type()`、`parse_body()`、辅助解析函数 |
+| `src/router.rs` | 新增 `match_body()` 方法,调整 `is_match()` 调用 |
+| `Cargo.toml` | 可能需要 `urlencoding` 依赖(form 解码) |
+
+---
+
+## YAML 配置示例
+
+```yaml
+# JSON 匹配(指定 Content-Type)
+- id: login-json
+ request:
+ method: POST
+ path: /api/login
+ headers:
+ Content-Type: application/json
+ body:
+ username: admin
+
+# XML 匹配
+- id: login-xml
+ request:
+ method: POST
+ path: /api/login
+ headers:
+ Content-Type: application/xml
+ body: "admin"
+
+# Form 匹配
+- id: login-form
+ request:
+ method: POST
+ path: /api/login
+ headers:
+ Content-Type: application/x-www-form-urlencoded
+ body:
+ username: admin
+
+# 字符串匹配(无 Content-Type)
+- id: echo
+ request:
+ method: POST
+ path: /api/echo
+ body: "hello world"
+```
+
+---
+
+## 验证
+
+1. `cargo test`
+2. 手动测试各类型:
+ - JSON 请求 + JSON 规则 → 匹配
+ - XML 请求 + XML 规则 → 匹配
+ - Form 请求 + Form 规则 → 匹配
+ - 无 Content-Type → 字符串比较
diff --git a/plan2.md b/plan2.md
new file mode 100644
index 0000000..b316617
--- /dev/null
+++ b/plan2.md
@@ -0,0 +1,375 @@
+# Body 匹配优化:基于 Content-Type 的智能解析
+
+## Context
+
+**问题**: 当前只支持 JSON body 匹配,非 JSON 请求无法正确匹配。
+
+**需求**: 根据请求的 Content-Type 智能解析 body,同时支持 header 匹配校验。
+
+---
+
+## 核心设计
+
+### 关键原则
+
+1. **Body 解析**:始终以【请求的 Content-Type】为准,因为这是数据的真实格式
+2. **Header 匹配**:Content-Type 当作普通 header 处理,写了就匹配,没写跳过
+3. **自动补充的 header**:Content-Length、Accept-Encoding 等不参与匹配(除非 YAML 显式配置)
+
+### 处理流程
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 请求处理流程 │
+├─────────────────────────────────────────────────────────────┤
+│ 1. 解析请求 body │
+│ └── 始终以【请求的 Content-Type】为准解析 │
+│ │
+│ 2. Header 匹配(包含 Content-Type) │
+│ └── YAML 写了就匹配,没写就跳过 │
+│ │
+│ 3. Body 匹配 │
+│ └── 根据匹配成功的 Content-Type 决定比较方式 │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 匹配示例
+
+| YAML Content-Type | 请求 Content-Type | Header 匹配 | Body 解析 | 结果 |
+|-------------------|-------------------|-------------|-----------|------|
+| `application/xml` | `application/json` | 失败 | - | 不匹配 |
+| `application/json` | `application/json` | 成功 | JSON | 比较 body |
+| 无 | `application/json` | 跳过 | JSON | 比较 body |
+| 无 | `application/xml` | 跳过 | XML | 比较 body |
+| 无 | 无 | 跳过 | 字符串 | 比较 body |
+
+---
+
+## 实现方案
+
+### Step 1: 新增数据结构 (model.rs)
+
+```rust
+/// 解析后的请求 Body
+#[derive(Debug, Clone)]
+pub enum ParsedBody {
+ Json(serde_json::Value),
+ Xml(String),
+ Form(HashMap),
+ Multipart(Vec), // 字段名列表
+ Text(String),
+ None,
+}
+
+impl ParsedBody {
+ /// 转换为字符串(用于兜底比较)
+ pub fn to_compare_string(&self) -> String {
+ match self {
+ ParsedBody::Json(v) => v.to_string(),
+ ParsedBody::Xml(s) | ParsedBody::Text(s) => s.clone(),
+ ParsedBody::Form(map) => {
+ let mut pairs: Vec<_> = map.iter().collect();
+ pairs.sort_by_key(|(k, _)| *k);
+ pairs.iter().map(|(k, v)| format!("{}={}", k, v)).join("&")
+ }
+ ParsedBody::Multipart(fields) => fields.join(","),
+ ParsedBody::None => String::new(),
+ }
+ }
+
+ /// 获取对应的 Content-Type 名称
+ pub fn content_type_name(&self) -> &'static str {
+ match self {
+ ParsedBody::Json(_) => "application/json",
+ ParsedBody::Xml(_) => "application/xml",
+ ParsedBody::Form(_) => "application/x-www-form-urlencoded",
+ ParsedBody::Multipart(_) => "multipart/form-data",
+ ParsedBody::Text(_) => "text/plain",
+ ParsedBody::None => "none",
+ }
+ }
+}
+```
+
+### Step 2: Body 解析函数 (handler.rs)
+
+```rust
+/// 提取请求的 Content-Type(去掉参数部分,如 boundary)
+fn extract_content_type(headers: &HeaderMap) -> Option {
+ headers
+ .get(axum::http::header::CONTENT_TYPE)
+ .and_then(|v| v.to_str().ok())
+ .map(|s| s.split(';').next().unwrap_or(s).trim().to_lowercase())
+}
+
+/// 根据 Content-Type 解析 Body(始终以请求的 Content-Type 为准)
+fn parse_body(content_type: Option<&str>, bytes: &[u8]) -> ParsedBody {
+ if bytes.is_empty() {
+ return ParsedBody::None;
+ }
+
+ match content_type {
+ Some(ct) if ct.contains("application/json") => {
+ serde_json::from_slice(bytes)
+ .map(ParsedBody::Json)
+ .unwrap_or_else(|_| {
+ // JSON 解析失败,降级为文本
+ ParsedBody::Text(String::from_utf8_lossy(bytes).to_string())
+ })
+ }
+ Some(ct) if ct.contains("xml") => {
+ ParsedBody::Xml(String::from_utf8_lossy(bytes).to_string())
+ }
+ Some(ct) if ct.contains("form-urlencoded") => {
+ ParsedBody::Form(parse_urlencoded(bytes))
+ }
+ Some(ct) if ct.contains("multipart/form-data") => {
+ ParsedBody::Multipart(extract_multipart_fields(bytes))
+ }
+ _ => {
+ ParsedBody::Text(String::from_utf8_lossy(bytes).to_string())
+ }
+ }
+}
+
+/// 解析 urlencoded 格式
+fn parse_urlencoded(bytes: &[u8]) -> HashMap {
+ let body = String::from_utf8_lossy(bytes);
+ let mut map = HashMap::new();
+ for pair in body.split('&') {
+ if let Some((key, value)) = pair.split_once('=') {
+ // URL 解码
+ let decoded_key = urlencoding_decode(key);
+ let decoded_value = urlencoding_decode(value);
+ map.insert(decoded_key, decoded_value);
+ }
+ }
+ map
+}
+```
+
+### Step 3: 修改 handler.rs 主函数
+
+```rust
+pub async fn mock_handler(
+ State(state): State>,
+ method: Method,
+ headers: HeaderMap,
+ Query(params): Query>,
+ req: Request,
+) -> impl IntoResponse {
+ let path = req.uri().path().to_string();
+ let method_str = method.as_str().to_string();
+
+ // 1. 提取请求的 Content-Type
+ let req_content_type = extract_content_type(&headers);
+
+ // 2. 读取请求 body
+ let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await {
+ Ok(bytes) => bytes,
+ Err(_) => {
+ return Response::builder()
+ .status(StatusCode::BAD_REQUEST)
+ .body(Body::from("Read body error"))
+ .unwrap();
+ }
+ };
+
+ // 3. 根据【请求的 Content-Type】解析 body
+ let parsed_body = parse_body(req_content_type.as_deref(), &body_bytes);
+
+ // 4. 转换 headers 为 HashMap
+ let mut req_headers = HashMap::new();
+ for (name, value) in headers.iter() {
+ if let Ok(v) = value.to_str() {
+ req_headers.insert(name.as_str().to_string(), v.to_string());
+ }
+ }
+
+ // 5. 执行匹配
+ let maybe_rule = {
+ let router = state.router.read().expect("Failed to acquire read lock");
+ router.match_rule(&method_str, &path, ¶ms, &req_headers, &parsed_body).cloned()
+ };
+
+ // 6. 构建响应(与现有逻辑相同)
+ // ...
+}
+```
+
+### Step 4: 修改 router.rs 匹配逻辑
+
+```rust
+/// 核心匹配函数
+pub fn match_rule(
+ &self,
+ method: &str,
+ path: &str,
+ queries: &HashMap,
+ headers: &HashMap,
+ parsed_body: &ParsedBody, // 改为 ParsedBody
+) -> Option<&MockRule> {
+ let key = self.extract_first_segment(path);
+
+ if let Some(rules) = self.index.get(&key) {
+ for rule in rules {
+ if self.is_match(rule, method, path, queries, headers, parsed_body) {
+ return Some(rule);
+ }
+ }
+ }
+ None
+}
+
+fn is_match(
+ &self,
+ rule: &MockRule,
+ method: &str,
+ path: &str,
+ queries: &HashMap,
+ headers: &HashMap,
+ parsed_body: &ParsedBody,
+) -> bool {
+ // A. Method 匹配
+ if rule.request.method.to_uppercase() != method.to_uppercase() {
+ return false;
+ }
+
+ // B. Path 匹配
+ if rule.request.path.trim_end_matches('/') != path.trim_end_matches('/') {
+ return false;
+ }
+
+ // C. Query 匹配(子集匹配)
+ if let Some(ref required_queries) = rule.request.query_params {
+ for (key, val) in required_queries {
+ if queries.get(key) != Some(val) {
+ return false;
+ }
+ }
+ }
+
+ // D. Header 匹配(包含 Content-Type)
+ // YAML 写了就匹配,没写就跳过
+ if let Some(ref required_headers) = rule.request.headers {
+ for (key, val) in required_headers {
+ let matched = headers.iter().any(|(k, v)| {
+ k.to_lowercase() == key.to_lowercase() && v == val
+ });
+ if !matched {
+ return false; // Header 不匹配,包括 Content-Type
+ }
+ }
+ }
+
+ // E. Body 匹配
+ // YAML 写了 body 才匹配,没写跳过
+ if let Some(ref yaml_body) = rule.request.body {
+ return self.match_body(yaml_body, parsed_body);
+ }
+
+ true
+}
+
+/// Body 匹配逻辑
+fn match_body(&self, yaml_body: &serde_json::Value, parsed_body: &ParsedBody) -> bool {
+ match parsed_body {
+ ParsedBody::Json(actual) => {
+ // JSON 对象比较
+ yaml_body == actual
+ }
+ ParsedBody::Xml(actual) => {
+ // XML 字符串比较
+ yaml_body.as_str()
+ .map(|expected| expected.trim() == actual.trim())
+ .unwrap_or(false)
+ }
+ ParsedBody::Form(actual) => {
+ // Form 键值对比较(子集匹配)
+ compare_form_with_yaml(yaml_body, actual)
+ }
+ ParsedBody::Multipart(actual_fields) => {
+ // Multipart 字段名比较
+ compare_multipart_with_yaml(yaml_body, actual_fields)
+ }
+ ParsedBody::Text(actual) => {
+ // 字符串比较
+ yaml_body.as_str()
+ .map(|expected| expected.trim() == actual.trim())
+ .unwrap_or_else(|| yaml_body.to_string().trim() == actual.trim())
+ }
+ ParsedBody::None => {
+ false // YAML 配置了 body,但请求没有 body
+ }
+ }
+}
+
+/// Form 比较:YAML 中的键值对必须是请求的子集
+fn compare_form_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap) -> bool {
+ let yaml_map = match yaml_body.as_object() {
+ Some(obj) => obj,
+ None => return false,
+ };
+
+ for (key, yaml_val) in yaml_map {
+ let expected = yaml_val.as_str().unwrap_or(&yaml_val.to_string());
+ if actual.get(key) != Some(&expected.to_string()) {
+ return false;
+ }
+ }
+ true
+}
+```
+
+---
+
+## 需要修改的文件
+
+| 文件 | 改动 |
+|------|------|
+| `src/model.rs` | 新增 `ParsedBody` 枚举和相关方法 |
+| `src/handler.rs` | 新增 `extract_content_type()`、`parse_body()`、修改 `mock_handler()` |
+| `src/router.rs` | 修改 `match_rule()` 和 `is_match()` 参数,新增 `match_body()` |
+| `Cargo.toml` | 可能需要 `urlencoding` 依赖 |
+
+---
+
+## YAML 配置示例
+
+```yaml
+# 严格匹配:要求 Content-Type + body
+- id: login-strict
+ request:
+ method: POST
+ path: /api/login
+ headers:
+ Content-Type: application/json
+ body:
+ username: admin
+ password: "123456"
+
+# 宽松匹配:只匹配 method + path + body(不检查 Content-Type)
+- id: login-loose
+ request:
+ method: POST
+ path: /api/login
+ body:
+ username: admin
+
+# 最宽松:只匹配 method + path
+- id: any-body
+ request:
+ method: POST
+ path: /api/echo
+```
+
+---
+
+## 验证
+
+1. `cargo test`
+2. 手动测试:
+ - YAML 配置 `Content-Type: application/json`,请求发送 JSON → 匹配
+ - YAML 配置 `Content-Type: application/xml`,请求发送 JSON → Header 不匹配
+ - YAML 无 Content-Type,请求发送 XML → Body 字符串比较
+ - YAML 无 body 配置 → 跳过 body 检查
diff --git a/src/lib.rs b/src/lib.rs
index e13f385..528920c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,5 @@
// 声明模块并设为 pub,这样 tests/ 目录才能看到它们
-pub mod config;
+pub mod model;
pub mod loader;
pub mod router;
pub mod handler;
\ No newline at end of file
diff --git a/src/loader.rs b/src/loader.rs
index 0a68dea..049ba3b 100644
--- a/src/loader.rs
+++ b/src/loader.rs
@@ -1,9 +1,9 @@
use std::collections::HashMap;
use std::fs;
-use std::path::{Path, PathBuf};
-use walkdir::WalkDir; // 需在 Cargo.toml 添加 walkdir 依赖
+use std::path::{Path};
+use walkdir::WalkDir;
-use crate::config::{MockRule, MockSource}; // 假设 config.rs 中定义了这两个类型
+use crate::model::{MockRule, MockSource}; // 假设 model 中定义了这两个类型
pub struct MockLoader;
diff --git a/src/main.rs b/src/main.rs
index 7f8fa92..01fe4cf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,8 +9,24 @@ use mock_server::loader::MockLoader;
use mock_server::router::MockRouter;
use mock_server::handler::{mock_handler, AppState};
+/// 打印启动 Banner
+fn print_banner() {
+ let version = env!("CARGO_PKG_VERSION");
+ // 蓝色 ANSI 转义码
+ println!("\x1b[34m");
+ println!(" ███╗ ███╗ ██████╗██████╗ ██████╗ ");
+ println!(" ████╗ ████║██╔════╝██╔══██╗██╔═══██╗");
+ println!(" ██╔████╔██║██║ ██████╔╝██║ ██║");
+ println!(" ██║╚██╔╝██║██║ ██╔══██╗██║ ██║");
+ println!(" ██║ ╚═╝ ██║╚██████╗██║ ██║╚██████╔╝");
+ println!(" ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝ ");
+ println!("\x1b[0m"); // 重置颜色
+ println!(" Mock Server v{}\n", version);
+}
+
#[tokio::main]
async fn main() {
+ print_banner();
tracing_subscriber::fmt::init();
let mocks_dir = Path::new("./mocks");
diff --git a/src/config.rs b/src/model.rs
similarity index 89%
rename from src/config.rs
rename to src/model.rs
index b2ce262..f748004 100644
--- a/src/config.rs
+++ b/src/model.rs
@@ -22,7 +22,7 @@ impl MockSource {
}
/// 核心 Mock 规则定义
-#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
+#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct MockRule {
pub id: String,
pub request: RequestMatcher,
@@ -31,7 +31,7 @@ pub struct MockRule {
}
/// 请求匹配条件
-#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
+#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct RequestMatcher {
pub method: String,
pub path: String,
@@ -44,7 +44,7 @@ pub struct RequestMatcher {
}
/// 响应内容定义
-#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
+#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct MockResponse {
pub status: u16,
pub headers: Option>,
@@ -52,13 +52,6 @@ pub struct MockResponse {
pub body: String,
}
-/// 模拟器行为设置
-#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
-pub struct MockSettings {
- /// 模拟网络延迟(毫秒)
- pub delay_ms: Option,
-}
-
impl MockResponse {
/// 辅助方法:判断是否为文件协议
pub fn is_file_protocol(&self) -> bool {
@@ -74,3 +67,10 @@ impl MockResponse {
}
}
}
+
+/// 模拟器行为设置
+#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
+pub struct MockSettings {
+ /// 模拟网络延迟(毫秒)
+ pub delay_ms: Option,
+}
diff --git a/src/router.rs b/src/router.rs
index c68dbcd..0ff23c5 100644
--- a/src/router.rs
+++ b/src/router.rs
@@ -1,5 +1,5 @@
use std::collections::HashMap;
-use crate::config::MockRule;
+use crate::model::MockRule;
pub struct MockRouter {
// 索引表:Key 是路径首段(如 "api"),Value 是该段下的所有 Mock 规则
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
index 5355fae..4bb54c0 100644
--- a/tests/integration_test.rs
+++ b/tests/integration_test.rs
@@ -3,7 +3,7 @@ use std::io::Write;
use tempfile::tempdir;
// 确保 Cargo.toml 中有 serde_json
use serde_json::json;
-use mock_server::config::{MockRule, MockSource};
+use mock_server::model::{MockRule, MockSource};
use mock_server::loader::MockLoader;
use mock_server::router::MockRouter;
use std::collections::HashMap;
diff --git a/tests/loader_test.rs b/tests/loader_test.rs
index 64a0735..8c32b7b 100644
--- a/tests/loader_test.rs
+++ b/tests/loader_test.rs
@@ -2,7 +2,7 @@ use std::fs::{self, File};
use std::io::Write;
use tempfile::tempdir;
// 假设你的项目名在 Cargo.toml 中叫 mock_server
-use mock_server::config::MockSource;
+use mock_server::model::MockSource;
use mock_server::loader::MockLoader;
#[test]