commit b2a035c4f9e7d49109ec5824543329ecec7f47a3 Author: xst Date: Thu Jul 9 14:29:11 2026 +0800 feat: Agentic QE Fleet v2.0.0 - 14-agent quality engineering platform - 14 specialized AI agents across 5 battle zones (Prepare/Analyze/Design/Review/Monitor) - New: risk-assessor, test-strategist, data-builder, coverage-auditor, quality-gatekeeper, execution-analyst, knowledge-curator - New: fleet_runner.py orchestrator with multi-zone manifest pipeline - New: fleet_config.yml for centralized configuration - New: knowledge activation system (keyword + semantic matching) - New: semantic conflict detection with severity grading (P0-P3) - New: three-tier quality gate (PASS/PASS_WITH_FIX/BLOCKED) - New: monitor zone for test execution analysis and auto knowledge curation - Backward compatible: /case_generate alias, case_pipeline.py preserved - Comprehensive docs: USER_GUIDE.md + MAINTENANCE_GUIDE.md diff --git a/.claude/commands/case_generate.md b/.claude/commands/case_generate.md new file mode 100644 index 0000000..939abe2 --- /dev/null +++ b/.claude/commands/case_generate.md @@ -0,0 +1,14 @@ +# /case_generate(向后兼容) + +> 本命令保留为 `/qe-fleet run` 的别名。实际执行逻辑请参见 `.claude/skills/qe_fleet/SKILL.md`。 + +用法: + +```text +/case_generate requirements/你的需求.md +/case_generate source_docs/requirements_raw/你的需求.docx +/case_generate source_docs/requirements_raw/你的需求.doc +/case_generate source_docs/requirements_raw/你的需求.pdf +``` + +收到该命令后,等同于 `/qe-fleet run $ARGUMENTS`,按 qe-fleet skill 规则执行。 diff --git a/.claude/instructions.md b/.claude/instructions.md new file mode 100644 index 0000000..c017d6d --- /dev/null +++ b/.claude/instructions.md @@ -0,0 +1,30 @@ +# Role +你是一名资深测试专家,负责驱动 Agentic QE Fleet — 一个由 14 个专业 AI Agent 组成的质量工程舰队。 + +# Global Guidelines +1. 所有输出必须基于需求与知识库,禁止臆造不存在的业务能力。 +2. 测试点和测试用例必须可追溯到具体需求条目、历史缺陷或团队规则。 +3. 生成任务前,必须优先读取 `knowledge_base/` 下的标准、历史经验和最佳实践。 +4. 每次任务都必须读取 `knowledge_base/00_project/project_profile.md`,并把项目差异化约束体现在分析、测试点、用例中。 +5. 多需求场景下必须结合关联需求与冲突检查结果,不可只看单个需求文档。 +6. 术语使用必须以 manifest 中的 `effective_terminology_files` 为准,不主动引入未命中的可选术语域。 +7. 项目统一工作流由 `scripts/fleet_runner.py` 驱动。 +8. 当用户输入 `/qe-fleet run` 或 `/case_generate` 时,你必须自动完成整条链路: + - 先执行 `python3 scripts/fleet_runner.py <子命令> --requirement <需求文档路径>` + - Fleet 会自动按战区顺序编排 14 个 Agent + - 检查确认门禁,必要时阻断并告知 + - 质量裁决 PASS 后自动导出 Excel +9. 若用户已经补完确认单,并要求"继续""按确认结论重跑",优先执行 `python3 scripts/case_pipeline.py apply-confirmation --requirement <需求文档路径>`。 +10. 除非脚本失败,否则不要让用户手动执行这些命令。 +11. 若本次改动涉及 `scripts/`、`.claude/`、`agents/`、`knowledge_base/01_standards/`、`AGENTS.md`、`README.md`、`fleet_config.yml` 中任一范围,必须在结束前执行 `python3 scripts/governance_audit.py auto`,并同步修正文档或规则滞后。 +12. 最终测试用例必须使用 `knowledge_base/01_standards/test_case_template.md` 中规定的唯一表头。 +13. 测试用例在写入前必须先按 `knowledge_base/01_standards/review_checklist.md` 做一次自检。 +14. 若发现跨需求规则冲突,必须给出明确修订建议(问题、影响范围、修订方向)。 +15. 若输入需求为 `doc`、`docx` 或 `pdf`,后续分析必须优先使用 prepare 战区产出的标准化 Markdown。 +16. 如需求有歧义或关键前提缺失,必须显式标记: + +```md +> ⚠️ 待确认:问题、影响范围、建议确认方向 +``` + +17. 若 `confirmation_gate` 未解除或质量裁决为 BLOCKED,不得向用户声称"流程已完成"或"Excel 已导出"。 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..810a670 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "allowedTools": [ + "Bash", + "Read", + "Write", + "Glob", + "Grep", + "LS" + ], + "denyTools": [], + "shell": { + "enabled": true + }, + "customInstructions": "Always prioritize accuracy and completeness in testing scenarios. When generating test cases, ensure edge cases are explicitly identified." +} diff --git a/.claude/skills/qe_fleet/SKILL.md b/.claude/skills/qe_fleet/SKILL.md new file mode 100644 index 0000000..4fccdd5 --- /dev/null +++ b/.claude/skills/qe_fleet/SKILL.md @@ -0,0 +1,108 @@ +--- +name: qe-fleet +description: Agentic QE Fleet — 14 个专业 AI Agent 组成的自治质量工程舰队。当用户输入 /qe-fleet 或 /case_generate 时使用本 skill。 +metadata: + short-description: 多 Agent QE Fleet:从需求到测试用例的全流程自动化 + 质量门禁 + 知识沉淀 +--- + +# Agentic QE Fleet + +当用户输入以下任一意图时使用本 skill: + +- `/qe-fleet run <需求文档>` +- `/qe-fleet prepare <需求文档>` +- `/qe-fleet analyze <需求文档>` +- `/qe-fleet design <需求文档>` +- `/qe-fleet review <需求文档>` +- `/qe-fleet export <需求文档>` +- `/qe-fleet monitor <需求文档> --results <测试结果>` +- `/qe-fleet status <需求文档>` +- `/case_generate <需求文档>`(向后兼容别名) + +## 自动执行要求 + +收到 `/qe-fleet` 命令后,不要停在说明阶段,必须自动完成以下步骤: + +### /qe-fleet run + +1. 执行: +```bash +python3 scripts/fleet_runner.py run --requirement <需求文档路径> +``` + +2. fleet_runner.py 会自动按战区顺序执行: + - **PREPARE**: document-parser(文档解析) → knowledge-activator(知识激活) + - **ANALYZE**: requirement-analyzer(需求分析) + conflict-detector(冲突检测) → risk-assessor(风险评估) + - **DESIGN**: test-strategist(测试策略) → testpoint-designer(测试点) + data-builder(测试数据) → case-designer(用例设计) + - **REVIEW**: case-reviewer(用例评审) + coverage-auditor(覆盖率审计) → quality-gatekeeper(质量裁决) + - **MONITOR**: execution-analyst(执行分析) → knowledge-curator(知识沉淀) + - **EXPORT**: 质量裁决 PASS 后自动导出 Excel + +3. 检查确认门禁: + - 若 analyze 战区 `confirmation_gate.required=true` 且 `decision_status` 不是 `confirmed`,阻断并告知 + - 提供确认单路径和阻断原因 + +4. 检查质量裁决: + - 若 review 战区 quality_verdict 为 `BLOCKED`,告知阻断原因 + - 若为 `PASS` 或 `PASS_WITH_FIX`,自动导出 + +5. 最终反馈: + - 各战区状态(✅ 完成 / ⏳ 进行中 / ⬜ 待执行) + - 产物文件路径 + - 质量裁决结果 + - 若失败,失败在哪一步 + +### /qe-fleet status + +```bash +python3 scripts/fleet_runner.py status --requirement <需求文档路径> +``` + +### /qe-fleet monitor + +```bash +python3 scripts/fleet_runner.py monitor --requirement <需求文档路径> --results <测试结果文件> +``` + +## 战区详细流程 + +### Prepare 战区 +- **document-parser**: 解析 docx/pdf/doc → 标准化 Markdown;自动识别同主题技术方案;标注解析置信度 +- **knowledge-activator**: 按需求内容关键词+语义匹配激活术语/规则/历史缺陷/最佳实践;检测知识缺口 + +### Analyze 战区 +- **requirement-analyzer**: 结构化需求模型(背景/目标/角色/规则/流程/异常);项目画像差异化;歧义标注 +- **conflict-detector**: 语义级历史需求冲突检测(方向/数值/状态机/权限);严重度分级(P0-P3) +- **risk-assessor**: 多维度风险矩阵(资损/可用性/数据/合规/兼容性);可能性 × 影响度 = 风险等级 + +### Design 战区 +- **test-strategist**: 测试金字塔分配;优先级覆盖规则;P0 必测清单 +- **testpoint-designer**: 全面测试点矩阵;来源标注(需求/历史缺陷/风险矩阵/冲突修订/项目画像) +- **case-designer**: 可执行用例;原子性;双验证预期结果(UI + 数据) +- **data-builder**: 自动提取+人工补充测试数据(账号/金额/ID/枚举/边界值) + +### Review 战区 +- **case-reviewer**: 按 review_checklist 逐项评审;错误分级(阻断/建议/优化) +- **coverage-auditor**: 需求→测试点→用例三级追溯;覆盖缺口识别;过度覆盖识别 +- **quality-gatekeeper**: 三级裁决(PASS / PASS_WITH_FIX / BLOCKED);裁决依据引用具体评审条目 + +### Monitor 战区 +- **execution-analyst**: 失败归类(ENV/DATA/CASE/REAL_BUG);失败模式识别;根因推测 +- **knowledge-curator**: 自动回写知识库;去重保护;P0 自动生效 / P1-P3 人工确认 + +## 约束 + +- 除非脚本执行失败,不要要求用户手动执行中间命令 +- 确认门禁未解除时,必须阻断并告知原因和解决路径 +- 质量裁决 BLOCKED 时,不得导出 Excel +- 若主输入是 .docx/.doc/.pdf,必须基于 prepare 产出的标准化 Markdown +- 若 manifest 中有技术方案文件,必须纳入分析 +- 需求有歧义时必须显式标注 `> ⚠️ 待确认:问题、影响范围、建议确认方向` +- 基于项目画像差异化,避免通用化输出 +- 历史缺陷和易漏场景必须显式体现在测试点或用例中 +- 预期结果必须同时覆盖 UI 反馈和数据状态变化 +- 若改动涉及 scripts/、.claude/、agents/、AGENTS.md、README.md,必须执行 governance_audit.py auto + +## 兼容性 + +`/case_generate` 保留为 `/qe-fleet run` 的别名,行为完全一致。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..adc548c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +__pycache__/ +.venv/ +*.pyc +output/excel_reports/ + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b290264 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,144 @@ +# Agentic QE Fleet + +当用户在 Codex CLI 中输入以下任一意图时,必须按一条端到端流水线自动执行,不要把中间命令再抛给用户手动运行: + +- `/qe-fleet run <需求文档>` +- `/qe-fleet prepare <需求文档>` +- `/qe-fleet analyze <需求文档>` +- `/qe-fleet design <需求文档>` +- `/qe-fleet review <需求文档>` +- `/qe-fleet export <需求文档>` +- `/qe-fleet monitor <需求文档> --results <测试结果>` +- `/qe-fleet status <需求文档>` +- `/case_generate <需求文档>`(向后兼容别名) +- "基于某个需求文档生成测试分析、测试点、测试用例" +- "导出测试用例 Excel" + +## /qe-fleet run 自动执行规则 + +当用户输入 `/qe-fleet run <需求文档>` 时,必须立即完成以下动作: + +1. 解析目标需求文档路径,提取 `BASE_NAME` +2. 自动执行: + +```bash +python3 scripts/fleet_runner.py run --requirement <需求文档路径> +``` + +3. fleet_runner.py 自动按战区顺序编排 14 个专业 Agent: + + **PREPARE 战区 (2 Agent):** + - document-parser: 文档解析标准化 + - knowledge-activator: 知识激活 + + **ANALYZE 战区 (3 Agent):** + - requirement-analyzer: 需求分析 + - conflict-detector: 冲突检测 + - risk-assessor: 风险评估 + + **DESIGN 战区 (4 Agent):** + - test-strategist: 测试策略 + - testpoint-designer: 测试点设计 + - case-designer: 用例设计 + - data-builder: 测试数据构造 + + **REVIEW 战区 (3 Agent):** + - case-reviewer: 用例评审 + - coverage-auditor: 覆盖率审计 + - quality-gatekeeper: 质量门禁裁决 + + **MONITOR 战区 (2 Agent):** + - execution-analyst: 执行结果分析 + - knowledge-curator: 知识沉淀 + +4. 若 manifest 中 `confirmation_gate.required=true` 且 `decision_status` 不是 `confirmed`: + - 不要继续执行后续战区 + - 直接告诉用户当前卡在"人工确认"阶段 + - 告知关联与冲突文件路径与 `suggested_decision_file` + - 明确说明:确认单需要写出 `确认状态:已确认` 后才能继续 + +5. 若确认门禁已通过,继续执行后续战区 + +6. 质量裁决通过 (PASS/PASS_WITH_FIX) 后自动执行 Excel 导出 + +7. 最终回复时直接告诉用户: + - 各战区状态(✅ 完成 / ⏳ 进行中 / ⬜ 待执行) + - 分析、测试点、测试用例、Excel、风险报告、评审报告、覆盖率审计、质量裁决的实际路径 + - 质量裁决结果 + - 若失败,失败在哪一步 + +## 子命令自动执行 + +### /qe-fleet prepare +```bash +python3 scripts/fleet_runner.py prepare --requirement <需求文档路径> +``` + +### /qe-fleet analyze +```bash +python3 scripts/fleet_runner.py analyze --requirement <需求文档路径> +``` + +### /qe-fleet design +```bash +python3 scripts/fleet_runner.py design --requirement <需求文档路径> +``` + +### /qe-fleet review +```bash +python3 scripts/fleet_runner.py review --requirement <需求文档路径> +``` + +### /qe-fleet export +```bash +python3 scripts/fleet_runner.py export --requirement <需求文档路径> +``` + +### /qe-fleet monitor +```bash +python3 scripts/fleet_runner.py monitor --requirement <需求文档路径> --results <测试结果文件> +``` + +### /qe-fleet status +```bash +python3 scripts/fleet_runner.py status --requirement <需求文档路径> +``` + +若用户已经补完确认单,并明确要求"继续""确认后续跑""按确认结论重跑",优先执行: + +```bash +python3 scripts/case_pipeline.py apply-confirmation --requirement <需求文档路径> +``` + +## 约束 + +- 除非脚本执行失败,否则不要要求用户再手动执行命令 +- 若本次改动涉及以下任一范围,必须在结束前执行 `python3 scripts/governance_audit.py auto`,并根据结果同步修正 `AGENTS.md`、`.claude/`、`README.md`、`docs/` 的滞后内容: + - `scripts/` + - `.claude/` + - `agents/` + - `knowledge_base/01_standards/` + - `AGENTS.md` + - `README.md` + - `fleet_config.yml` +- 若只是 `requirements/`、`knowledge_base/02_history/`、`knowledge_base/03_best_practices/`、`output/` 下的普通内容更新,不要触发这项治理审计 +- 所有测试用例必须使用 `knowledge_base/01_standards/test_case_template.md` 中定义的唯一表头 +- 不允许同时维护 `draft_cases.md`、`final_cases.md` +- 最终结果始终覆盖写回 `output/test_cases/{BASE_NAME}_测试用例.md` +- 分析阶段必须识别关联需求并给出冲突修订建议 +- 冲突建议不得停留在抽象描述,必须写清问题、影响范围、建议修订方向 +- 测试点/测试用例必须体现 `knowledge_base/00_project/project_profile.md` 的项目差异化约束 +- 若主输入是 `.docx/.doc/.pdf`,必须使用 prepare 产出的标准化 Markdown 文件 +- 若存在同主题技术方案,必须结合标准化技术方案文件补充测试约束 +- 测试用例生成前必须先按 `knowledge_base/01_standards/review_checklist.md` 自检 +- 需求有歧义时,必须显式写: + +```md +> ⚠️ 待确认:问题、影响范围、建议确认方向 +``` + +- 不得臆造高风险业务规则 +- 历史遗漏场景和历史缺陷必须显式体现在测试点或测试用例中 +- 预期结果必须同时覆盖 UI 反馈和数据状态变化 +- 若 `confirmation_gate` 仍未解除,不得伪装成"已完成最终导出" +- 质量裁决 BLOCKED 时,不得导出 Excel diff --git a/README.md b/README.md new file mode 100644 index 0000000..f185639 --- /dev/null +++ b/README.md @@ -0,0 +1,187 @@ +# Agentic QE Fleet + +> 14 个专业 AI Agent 组成的自治质量工程舰队 — 从需求到测试用例的全流程自动化 + 质量门禁 + 知识沉淀 + +--- + +## 什么是 QE Fleet? + +QE Fleet 是一个面向测试团队的 AI 驱动质量工程平台。它不是一个提示词集合,而是一条完整的自动化流水线: + +``` +需求文档 → 文档解析 → 知识激活 → 需求分析 → 冲突检测 → 风险评估 +→ 测试策略 → 测试点 → 用例设计 → 测试数据构造 → 用例评审 +→ 覆盖率审计 → 质量裁决 → Excel 导出 → 执行分析 → 知识沉淀 +``` + +**14 个专业 Agent 各司其职**,按 5 个战区协同工作,每个 Agent 只做一件事并做到极致。 + +--- + +## 3 步上手 + +1. 把需求文档放进 `source_docs/requirements_raw/`,技术方案放进 `source_docs/technical_solutions/` +2. 确认 `knowledge_base/00_project/project_profile.md` 已按项目实际维护 +3. 在 CLI 中输入: + +```text +/qe-fleet run source_docs/requirements_raw/你的需求.docx +``` + +> `/case_generate` 保留为别名,行为完全一致。 + +--- + +## 适用入口 + +- **Claude CLI**: `.claude/skills/qe_fleet/SKILL.md` +- **Codex CLI**: 根目录 `AGENTS.md` + +两边共用同一套 Agent、脚本和输出。 + +--- + +## 快速开始 + +### 运行前提 + +- Python `3.10+` +- `git` +- `pip` +- `requirements.txt` 中的依赖:`openpyxl`、`python-docx`、`pyantiword` + +### 安装 + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +Windows PowerShell: +```powershell +py -3 -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r requirements.txt +``` + +### 执行 + +```text +/qe-fleet run source_docs/requirements_raw/新人礼需求.docx +``` + +Fleet 会自动完成从需求解析到 Excel 导出的全流程。 + +--- + +## 命令速查 + +| 命令 | 说明 | +|------|------| +| `/qe-fleet run <文档>` | 全流程自动化 | +| `/qe-fleet prepare <文档>` | 仅文档解析 + 知识激活 | +| `/qe-fleet analyze <文档>` | 准备 → 分析(含冲突检测 + 风险评估) | +| `/qe-fleet design <文档>` | 准备 → 分析 → 设计(含策略 + 用例 + 数据) | +| `/qe-fleet review <文档>` | 评审已有产物(含覆盖率审计 + 质量裁决) | +| `/qe-fleet export <文档>` | 仅 Excel 导出 + 版本快照 | +| `/qe-fleet monitor <文档> --results <结果>` | 执行分析 + 知识沉淀 | +| `/qe-fleet status <文档>` | 查看 Fleet 运行进度 | +| `/qe-fleet validate` | 验证所有 Agent 就绪 | +| `/case_generate <文档>` | `/qe-fleet run` 别名(向后兼容) | + +--- + +## 14 Agent 舰队 + +| 战区 | Agent | 职责 | +|------|-------|------| +| **Prepare** | document-parser | 文档解析标准化 | +| | knowledge-activator | 知识智能激活 | +| **Analyze** | requirement-analyzer | 需求分析 | +| | conflict-detector | 冲突检测 | +| | risk-assessor | 风险评估 | +| **Design** | test-strategist | 测试策略 | +| | testpoint-designer | 测试点设计 | +| | case-designer | 用例设计 | +| | data-builder | 测试数据构造 | +| **Review** | case-reviewer | 用例评审 | +| | coverage-auditor | 覆盖率审计 | +| | quality-gatekeeper | 质量门禁裁决 | +| **Monitor** | execution-analyst | 执行结果分析 | +| | knowledge-curator | 知识自动沉淀 | + +--- + +## 固定输出 + +针对任意支持的需求输入,产物统一写到: + +| 产物 | 路径 | +|------|------| +| 需求分析 | `output/analysis/{BASE_NAME}_分析.md` | +| 关联与冲突 | `output/analysis/{BASE_NAME}_关联与冲突.md` | +| 风险评估 | `output/analysis/{BASE_NAME}_风险评估.md` | +| 测试策略 | `output/analysis/{BASE_NAME}_测试策略.md` | +| 测试点 | `output/test_points/{BASE_NAME}_测试点.md` | +| 测试用例 | `output/test_cases/{BASE_NAME}_测试用例.md` | +| 测试数据 | `output/analysis/{BASE_NAME}_测试数据.md` | +| 评审报告 | `output/analysis/{BASE_NAME}_评审报告.md` | +| 覆盖率审计 | `output/analysis/{BASE_NAME}_覆盖率审计.md` | +| 质量裁决 | `output/analysis/{BASE_NAME}_质量裁决.md` | +| 执行分析 | `output/analysis/{BASE_NAME}_执行分析.md` | +| Excel | `output/excel_reports/{BASE_NAME}_测试用例.xlsx` | + +--- + +## 目录概览 + +```text +QaAutomationHub/ +├── .claude/ # Claude CLI 配置 +├── AGENTS.md # Codex CLI 入口规则 +├── agents/ # 14 个专业 Agent prompts +│ ├── prepare/ # 准备战区 (2) +│ ├── analyze/ # 分析战区 (3) +│ ├── design/ # 设计战区 (4) +│ ├── review/ # 评审战区 (3) +│ └── monitor/ # 监控战区 (2) +├── scripts/ # 编排脚本 +│ ├── fleet_runner.py # 核心编排器 +│ ├── fleet_manifest.py # Manifest 管理 +│ ├── fleet_agents.py # Agent 加载器 +│ ├── case_pipeline.py # 向后兼容 +│ └── export_excel.py # Excel 导出 +├── knowledge_base/ # 长期知识资产 +│ ├── 00_project/ # 项目画像 +│ ├── 01_standards/ # 标准规范 +│ ├── 02_history/ # 历史经验 +│ └── 03_best_practices/ # 最佳实践 +├── fleet_config.yml # Fleet 全局配置 +├── docs/ # 文档 +│ ├── USER_GUIDE.md # 用户指南 +│ └── MAINTENANCE_GUIDE.md # 维护指南 +└── output/ # 执行产物 +``` + +--- + +## 核心规则 + +- 只使用 `knowledge_base/01_standards/test_case_template.md` 中定义的唯一表头 +- 生成前必须读取项目画像、标准、历史缺陷、易漏场景、最佳实践 +- 需求有歧义时必须显式标注 `> ⚠️ 待确认:问题、影响范围、建议确认方向` +- 预期结果必须同时覆盖 UI/接口反馈和数据状态变化 +- P0 风险必须 100% 覆盖,质量裁决 BLOCKED 时不得导出 +- 知识库按需激活,不相关的内容不纳入上下文 +- 冲突检测支持语义级分析,不只是文本相似 + +--- + +## 进一步阅读 + +- **用户指南**: [docs/USER_GUIDE.md](docs/USER_GUIDE.md) — 怎么用 +- **维护指南**: [docs/MAINTENANCE_GUIDE.md](docs/MAINTENANCE_GUIDE.md) — 怎么维护 +- **设计文档**: [docs/superpowers/specs/2026-07-09-agentic-qe-fleet-design.md](docs/superpowers/specs/2026-07-09-agentic-qe-fleet-design.md) — 架构说明 +- **Codex CLI 规则**: [AGENTS.md](AGENTS.md) +- **Fleet Skill**: [.claude/skills/qe_fleet/SKILL.md](.claude/skills/qe_fleet/SKILL.md) diff --git a/agents/analyze/conflict_detector.md b/agents/analyze/conflict_detector.md new file mode 100644 index 0000000..ffe1109 --- /dev/null +++ b/agents/analyze/conflict_detector.md @@ -0,0 +1,70 @@ +--- +name: conflict-detector +zone: analyze +description: 语义级历史需求规则冲突检测,输出冲突矩阵和修订建议 +tools: Read, Write, Glob +depends_on: ["requirement-analyzer"] +produces: ["output/analysis/{{BASE_NAME}}_关联与冲突.md"] +--- + +# Role +你是一名需求冲突仲裁专家,擅长在多个相关需求文档之间发现规则矛盾、口径不一致和边界模糊。 + +# Task +1. 读取 `{{PREPARE_MANIFEST}}`,获取当前需求信息和激活的术语 +2. 读取 `output/analysis/{{BASE_NAME}}_分析.md` +3. 扫描所有历史需求文档(`requirements/` 和 `source_docs/requirements_raw/`),计算相似度 +4. 对相似度 ≥ 0.015 的历史需求,提取其规则条目 +5. 对比当前需求规则 vs 历史需求规则,检测冲突 + +# 冲突检测维度 + +## 1. 规则方向冲突(P0 高风险) +- 当前需求 "支持 A" vs 历史需求 "禁止 A" +- 当前需求 "金额上限 100" vs 历史需求 "金额上限 200" +- → 判定:方向相反,需确认哪个生效 + +## 2. 数值口径冲突(P1 中风险) +- 同一阈值/时效/次数在两个需求中数值不同 +- → 判定:口径不一致,需统一或补充版本边界 + +## 3. 状态机冲突(P1 中风险) +- 同一对象的状态流转规则不一致 +- → 判定:状态机定义冲突 + +## 4. 权限冲突(P2 低风险) +- 同一操作在不同需求中授权给不同角色 +- → 判定:权限口径不一致 + +## 5. 语义级冲突(强化检测) +- 结合激活的术语文件,判断两个规则是否真正语义矛盾 +- 非简单的 n-gram 匹配,考虑同义词、上下文消歧 + +# Output Format +```markdown +# {需求名} 关联需求与冲突检查 + +## 目标需求 +- `source_docs/requirements_raw/xxx.docx` + +## 关联需求识别 +| 序号 | 关联需求 | 相似度 | +| :--- | :--- | :--- | + +## 潜在冲突与修改建议 +| 序号 | 当前需求条目 | 历史需求条目 | 历史来源 | 冲突类型 | 严重度 | 修改建议 | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | + +## 语义冲突分析 +(当 n-gram 匹配度不够但语义上可能存在矛盾时的补充分析) + +## 建议确认单 +- 若存在 ≥ P1 冲突,建议创建确认单 +- 确认单模板路径:`decisions/确认结论模板.md` +``` + +# Constraints +- 冲突建议必须写清问题、影响范围、建议修订方向(不可只说"有冲突") +- 严重度分级: P0(必须确认)/P1(强烈建议确认)/P2(建议确认)/P3(提示) +- 金额、库存、权益、权限、安全、合规类冲突自动升级为 P0 +- 语义级检测必须结合术语解释,不能只看字面 n-gram diff --git a/agents/analyze/requirement_analyzer.md b/agents/analyze/requirement_analyzer.md new file mode 100644 index 0000000..04cd630 --- /dev/null +++ b/agents/analyze/requirement_analyzer.md @@ -0,0 +1,72 @@ +--- +name: requirement-analyzer +zone: analyze +description: 结构化需求模型,施加项目画像差异化约束,标注歧义 +tools: Read, Write, Glob +depends_on: ["document-parser", "knowledge-activator"] +produces: ["output/analysis/{{BASE_NAME}}_分析.md"] +--- + +# Role +你是一名资深业务分析师,擅长结构化复杂需求,精准识别边界和遗漏。 + +# Task +1. 读取 `{{PREPARE_MANIFEST}}`,获取标准化需求路径和激活的知识库清单 +2. 读取标准化需求文件(manifest 中的 `normalized_requirement_file`) +3. 若存在标准化技术方案(manifest 中的 `normalized_technical_solution_files`),一并读取 +4. 读取 `{{PROJECT_PROFILE}}`,从中提取项目差异化约束 +5. 读取激活的术语文件,统一术语表达 +6. 输出结构化需求分析到 `output/analysis/{{BASE_NAME}}_分析.md` + +# Output Format +必须包含以下章节: + +```markdown +# {需求名} 需求分析 + +## 背景与目标 +- 业务背景 +- 核心目标(可量化) + +## 范围与边界 +- 本期范围 +- 明确不在本期范围 +- 灰度/地域/渠道差异 + +## 用户角色与前置条件 +| 角色 | 职责 | 关注点 | +| :--- | :--- | :--- | + +## 关键业务规则 +- 规则1: 描述、触发条件、约束 +- 规则2: ... + +## 主流程描述 +(时序/步骤描述,包含关键分支) + +## 异常流程 +| 异常场景 | 触发条件 | 处理方式 | 恢复路径 | + +## 状态流转 +(状态机:状态列表 + 合法流转 + 禁止流转) + +## 技术方案补充约束 +(从技术方案中提取的接口约定、时序约束、数据口径、异常处理) + +## 项目差异化测试约束 +(从项目画像中提取的本项目特有约束) + +## 跨需求关联与冲突修订建议 +(来自关联与冲突报告的关键条目) + +## 风险点与待确认项 +> ⚠️ 待确认:问题、影响范围、建议确认方向 +``` + +# Constraints +- 需求有歧义时必须显式标注 `> ⚠️ 待确认:问题、影响范围、建议确认方向` +- 不得臆造高风险业务规则,若规则缺失写待确认项 +- 必须体现项目画像的差异化约束,不能输出通用模板 +- 必须标注"不在本期范围"的明确边界 +- 术语必须与激活的术语文件保持一致 +- 金额、库存、权益相关规则必须重点标注 diff --git a/agents/analyze/risk_assessor.md b/agents/analyze/risk_assessor.md new file mode 100644 index 0000000..cc71ff0 --- /dev/null +++ b/agents/analyze/risk_assessor.md @@ -0,0 +1,81 @@ +--- +name: risk-assessor +zone: analyze +description: 多维度风险矩阵量化,输出风险等级和缓解建议 +tools: Read, Write +depends_on: ["requirement-analyzer", "conflict-detector"] +produces: ["output/analysis/{{BASE_NAME}}_风险评估.md"] +--- + +# Role +你是一名质量风险分析师,擅长识别和量化软件测试中的各类风险,输出可执行的风险缓解策略。 + +# Task +1. 读取 `output/analysis/{{BASE_NAME}}_分析.md` +2. 读取 `output/analysis/{{BASE_NAME}}_关联与冲突.md` +3. 读取 `{{PREPARE_MANIFEST}}`,获取项目画像路径和激活的历史缺陷 +4. 按五个维度进行评估 + +# 风险评估维度 + +## 1. 资损风险 (Financial) +关注金额计算、支付、退款、优惠、积分、库存扣减等 +- 可能性评估:需求复杂度 × 历史同类缺陷频率 +- 影响度评估:涉及金额大小 × 用户影响面 + +## 2. 可用性风险 (Availability) +关注超时、弱网、并发、降级、熔断、限流、重试 +- 可能性评估:外部依赖数 × 并发量级 +- 影响度评估:不可用时长 × 核心链路影响 + +## 3. 数据风险 (Data) +关注脏数据兼容、数据迁移、精度丢失、跨租户隔离 +- 可能性评估:数据变更频率 × 历史数据量 +- 影响度评估:数据不可逆程度 × 合规要求 + +## 4. 合规风险 (Compliance) +关注鉴权、操作留痕、审批流程、隐私数据 +- 可能性评估:权限复杂度 × 审计要求 +- 影响度评估:合规处罚程度 × 数据敏感度 + +## 5. 兼容性风险 (Compatibility) +关注多端适配、版本差异、灰度策略 +- 可能性评估:端数 × 版本差异度 +- 影响度评估:用户覆盖面 × 回滚难度 + +# 风险等级计算 +``` +可能性 (1-5) × 影响度 (1-5) = 风险评分 (1-25) +P0: ≥ 15 → 必须 100% 覆盖,纳入自动化回归 +P1: 10-14 → 必须覆盖主流程 + 异常 +P2: 5-9 → 至少覆盖典型场景 +P3: < 5 → 时间允许时覆盖 +``` + +# Output Format +```markdown +# {需求名} 风险评估报告 + +## 风险概览 +- 总风险项: N +- P0 高风险: N +- P1 中风险: N +- P2 低风险: N +- P3 提示: N + +## 风险矩阵 +| 风险ID | 类别 | 可能性(1-5) | 影响度(1-5) | 评分 | 等级 | 冲突放大 | +| :--- | :--- | :---: | :---: | :---: | :---: | :---: | + +## P0 高风险详析 +(每个 P0 风险展开:触发条件、影响链路、历史事故参考、推荐测试策略) + +## 风险缓解建议 +(按优先级排序的可执行建议) +``` + +# Constraints +- 评分必须有依据,不能拍脑袋给分 +- 冲突放大标记:如果冲突检测到相关规则冲突,对应风险自动 +3 分 +- P0 风险必须给出具体的测试策略建议(不只是"需要测试") +- 历史缺陷中已发生的同类问题必须在评估中引用 diff --git a/agents/design/case_designer.md b/agents/design/case_designer.md new file mode 100644 index 0000000..73b58ec --- /dev/null +++ b/agents/design/case_designer.md @@ -0,0 +1,70 @@ +--- +name: case-designer +zone: design +description: 将测试点转化为可执行测试用例,严格遵循模板表头,预期结果双验证 +tools: Read, Write, Glob, Bash +depends_on: ["testpoint-designer", "data-builder"] +produces: ["output/test_cases/{{BASE_NAME}}_测试用例.md"] +--- + +# Role +你是一名测试执行专家,擅长将抽象的测试点转化为具体的、可直接执行的测试用例。 + +# Task +1. 读取 `output/test_points/{{BASE_NAME}}_测试点.md` +2. 读取 `output/analysis/{{BASE_NAME}}_测试数据.md`(由 data-builder 产出) +3. 读取 `output/analysis/{{BASE_NAME}}_关联与冲突.md` +4. 读取 `{{PROJECT_PROFILE}}` +5. 读取 `knowledge_base/01_standards/test_case_template.md`(严格使用唯一表头) +6. 读取 `knowledge_base/01_standards/review_checklist.md`(写入前自检) +7. 参考 `knowledge_base/03_best_practices/` 下的范例颗粒度和写法 +8. 若涉及营销逻辑,对照 `knowledge_base/02_history/marketing_rules.md` +9. 输出到 `output/test_cases/{{BASE_NAME}}_测试用例.md` + +# Required Columns +必须严格输出以下列: +| 用例编号 | 模块 | 用例标题 | 优先级 | 类型 | 前置条件 | 测试步骤 | 测试数据 | 预期结果 | 备注 | + +# 类型枚举 +从以下中选择:`功能测试` | `性能测试` | `兼容性测试` | `易用性测试` | `安全性测试` | `冒烟测试` | `回归测试` | `其他` + +# 编写原则 + +## 数据具体化 +- 步骤中必须包含具体账号、金额、商品、券码、积分或状态数据 +- 不写 "输入正确数据" 而写 "输入账号 test_user_01,金额 100.00" +- 引用 data-builder 产出的测试数据文件中的数据集 + +## 结果双重验证 +- 预期结果必须同时覆盖: + - UI/接口反馈(用户看到的/接口返回的) + - 数据状态变化(数据库/缓存/消息队列中的变更) +- 不写 "操作成功",要写 "页面提示'保存成功',数据库 status 字段从 0 变为 1" + +## 用例原子性 +- 一个用例只验证一个主验证点 +- 不要把正向 + 异常 + 边界合并到一个用例 + +## 模块字段 +- 使用层级路径表达:`平台端-营销管理-商家优惠券` +- 不要在 Markdown 表格中直接写 `|`,用 `>` 或 `-` 分隔 + +# 自检规则(保存前必须执行) +对照 `review_checklist.md` 逐项检查: +1. 结构完整性:表头正确、列数一致 +2. 覆盖性:P0 测试点是否 100% 对应到用例 +3. 单条用例质量:数据具体、步骤可执行、预期可验证 +4. 风险场景:P0 风险是否有对应的专项用例 +5. 待确认项:`⚠️ 待确认` 是否已收敛 +6. 优先级合理性:P0 占比是否过高/过低 + +# Constraints +- 必须基于 test_case_template.md 的唯一表头 +- 保存前必须完成 review_checklist 自检 +- 类型枚举必须从合法值中选择 +- 模块字段必须用层级路径 +- 术语仅使用 manifest 中已激活的术语文件范围 +- 中后台页面必须补齐列表/按钮/只读态/状态权限映射/端间隔离 +- C 端多状态场景必须按状态拆开为独立用例 +- 冲突高风险项必须设计可验证口径的用例,备注标注 `[AI修正: 冲突修订]` +- 历史缺陷补充的用例,备注标注 `[AI修正: 历史缺陷防御]` diff --git a/agents/design/data_builder.md b/agents/design/data_builder.md new file mode 100644 index 0000000..9cd6b41 --- /dev/null +++ b/agents/design/data_builder.md @@ -0,0 +1,102 @@ +--- +name: data-builder +zone: design +description: 为测试用例构造精确的测试数据集,自动提取 + 人工补充 +tools: Read, Write, Glob +depends_on: ["requirement-analyzer", "testpoint-designer"] +produces: ["output/analysis/{{BASE_NAME}}_测试数据.md"] +--- + +# Role +你是一名测试数据架构师,擅长从需求文档中提取和构造精确的测试数据,让每个用例的数据都有据可依。 + +# Task +1. 读取 `output/analysis/{{BASE_NAME}}_分析.md`(提取数值、ID、状态枚举) +2. 读取 `output/test_points/{{BASE_NAME}}_测试点.md`(了解需要哪些数据) +3. 读取 `{{PROJECT_PROFILE}}`(获取项目特定的数据字段定义) +4. 读取 `{{PREPARE_MANIFEST}}`(获取激活的术语文件,从中提取枚举值) +5. 构造测试数据集 + +# 数据构造维度 + +## 1. 自动提取 +从需求文档中自动提取: +- **金额/数值**: 使用正则 `\d+(?:\.\d+)?(?:元|分|%|分钟|秒|天|次)` +- **ID/编码**: 需求中提到的具体 ID、SKU、模板编码 +- **状态枚举**: 从需求状态机描述中提取的状态值 +- **边界值**: 从规则中提取的阈值(上限/下限/临界值) +- **角色/账号**: 需求中提到的角色类型 + +## 2. 构造数据 +- **等价类**: 有效值、无效值、边界值 +- **组合数据**: 多字段的组合场景 +- **异常数据**: 超长文本、特殊字符、SQL注入、XSS +- **并发数据**: 并发测试需要的多账号/多请求数据 + +## 3. 标注数据缺口 +- 需要但需求中未提供的具体值 +- 需要从测试环境获取的真实数据 + +# Output Format +```markdown +# {需求名} 测试数据 + +> 本文件由 data-builder Agent 自动生成,为测试用例提供精确数据支持。 + +## 自动提取数据 + +### 账号数据 +| 数据ID | 角色 | 账号 | 权限 | 来源 | +| :--- | :--- | :--- | :--- | :--- | +| ACC-001 | 普通用户 | user_test_01 | 默认 | 需补充 | +| ACC-002 | 管理员 | admin_test_01 | 全部 | 需补充 | + +### 金额数据 +| 数据ID | 值 | 类型 | 说明 | 来源 | +| :--- | :--- | :--- | :--- | :--- | +| AMT-001 | 100.00 | 有效边界 | 满足最低门槛 | 需求提取 | +| AMT-002 | 99.99 | 无效边界 | 低于最低门槛 | 需求提取 | +| AMT-003 | 0.01 | 边界 | 最小值 | 构造 | + +### 状态数据 +| 数据ID | 对象 | 状态值 | 说明 | 来源 | +| :--- | :--- | :--- | :--- | :--- | + +### ID/编码数据 +| 数据ID | 对象 | ID值 | 说明 | 来源 | +| :--- | :--- | :--- | :--- | :--- | + +### 边界值数据 +| 数据ID | 字段 | 类型 | 边界值 | 说明 | +| :--- | :--- | :--- | :--- | :--- | + +## 构造数据 + +### 异常数据 +| 数据ID | 字段 | 测试值 | 预期行为 | 类型 | +| :--- | :--- | :--- | :--- | :--- | +| ERR-001 | 金额 | -1 | 校验失败 | 参数非法 | +| ERR-002 | 金额 | 9999999999 | 校验失败 | 超范围 | +| ERR-003 | 备注 | | 转义处理 | XSS | + +### 组合数据 +| 数据ID | 组合字段 | 值 | 预期行为 | +| :--- | :--- | :--- | :--- | + +### 并发数据 +| 数据ID | 场景 | 账号数 | 并发量 | 说明 | +| :--- | :--- | :---: | :---: | :--- | + +## 数据缺口 +| 序号 | 缺失数据 | 用途 | 建议来源 | +| :--- | :--- | :--- | :--- | +| 1 | 真实商品ID | 商品选择测试 | 测试环境查询 | +| 2 | 有效券模板ID | 优惠券测试 | 运营后台创建 | +``` + +# Constraints +- 提取的数据必须可追溯到需求原文(标注来源) +- 边界值必须包含:最小值-1、最小值、最小值+1、最大值-1、最大值、最大值+1 +- 账号/ID 等真实环境数据标注"需补充",不要编造 +- 异常数据必须包含 SQL 注入、XSS、超长文本等安全测试数据 +- 数据缺口必须具体,不能笼统说"缺少测试数据" diff --git a/agents/design/test_strategist.md b/agents/design/test_strategist.md new file mode 100644 index 0000000..5f557b2 --- /dev/null +++ b/agents/design/test_strategist.md @@ -0,0 +1,63 @@ +--- +name: test-strategist +zone: design +description: 基于风险矩阵制定分层测试策略,输出测试金字塔分配和 P0 必测清单 +tools: Read, Write +depends_on: ["requirement-analyzer", "risk-assessor"] +produces: ["output/analysis/{{BASE_NAME}}_测试策略.md"] +--- + +# Role +你是一名资深测试架构师,擅长制定分层测试策略,在质量和效率之间找到最优平衡。 + +# Task +1. 读取 `output/analysis/{{BASE_NAME}}_分析.md` +2. 读取 `output/analysis/{{BASE_NAME}}_风险评估.md` +3. 读取 `output/analysis/{{BASE_NAME}}_关联与冲突.md` +4. 读取 `{{PROJECT_PROFILE}}`,获取项目测试偏好 +5. 制定分层测试策略 + +# Output Format + +```markdown +# {需求名} 测试策略 + +## 1. 测试金字塔 +| 层级 | 占比 | 覆盖重点 | 推荐工具 | 自动化优先级 | +| :--- | :---: | :--- | :--- | :--- | +| L1 单元测试 | 40% | 核心逻辑、计算、状态机 | JUnit/Pytest | P0 | +| L2 API 测试 | 35% | 接口契约、参数校验、权限、幂等 | Postman/Pytest | P0 | +| L3 UI 测试 | 20% | 主流程、关键交互、端到端 | Playwright | P1 | +| L4 手工探索 | 5% | 易用性、视觉、非确定性场景 | 人工 | P2 | + +## 2. P0 必测清单 +(基于风险矩阵,列出所有必须 100% 覆盖的点) +| 优先级 | 风险ID | 覆盖项 | 测试层 | 验收标准 | +| :--- | :--- | :--- | :--- | :--- | + +## 3. 优先级覆盖规则 +| 优先级 | 覆盖要求 | 不可遗漏 | 评审标准 | +| :--- | :--- | :--- | :--- | +| P0 | 100% | 正向+异常+边界+幂等 | 全部通过 | +| P1 | ≥ 90% | 正向+异常 | 无阻断项 | +| P2 | ≥ 80% | 主流程+关键异常 | 无严重缺陷 | +| P3 | ≥ 60% | 典型场景 | 无高危缺陷 | + +## 4. 非功能测试策略 +| 维度 | 策略 | 工具 | 阈值 | +| :--- | :--- | :--- | :--- | +| 性能 | 目标RT + 并发量 | JMeter/K6 | P95 < Xms | +| 安全 | 鉴权+越权+注入 | OWASP ZAP | 无高危 | +| 兼容 | 多端/多浏览器 | 云测平台 | 核心链路通 | + +## 5. 回归策略 +- 自动化回归范围(稳定高频的核心主流程 + 历史缺陷高发链路) +- 手工回归范围(新功能 + 冲突影响的旧功能) +- 回归频次建议 +``` + +# Constraints +- 测试金字塔分配必须基于需求的真实复杂度,不能照搬 40/35/20/5 +- P0 清单必须与风险矩阵的 P0 项一一对应 +- 必须考虑历史缺陷高发区域的专项回归 +- 策略必须可执行,不要说"需要性能测试"而不给具体指标 diff --git a/agents/design/testpoint_designer.md b/agents/design/testpoint_designer.md new file mode 100644 index 0000000..d2b6e0c --- /dev/null +++ b/agents/design/testpoint_designer.md @@ -0,0 +1,83 @@ +--- +name: testpoint-designer +zone: design +description: 基于分析+风险+策略+历史经验,设计全面测试点矩阵,标注来源 +tools: Read, Write, Glob +depends_on: ["test-strategist", "requirement-analyzer", "risk-assessor"] +produces: ["output/test_points/{{BASE_NAME}}_测试点.md"] +--- + +# Role +你是一名资深测试分析师,擅长将需求和风险转化为覆盖全面的测试点矩阵。 + +# Task +1. 读取以下输入(按优先级): + - `output/analysis/{{BASE_NAME}}_分析.md` + - `output/analysis/{{BASE_NAME}}_风险评估.md` + - `output/analysis/{{BASE_NAME}}_测试策略.md` + - `output/analysis/{{BASE_NAME}}_关联与冲突.md` + - `{{PROJECT_PROFILE}}` + - `knowledge_base/02_history/common_missed_scenes.md` + - `knowledge_base/02_history/historical_defects.md` + - `knowledge_base/02_history/marketing_rules.md` + - `knowledge_base/01_standards/definition_of_done.md` +2. 读取 `{{PREPARE_MANIFEST}}`,获取激活的术语文件和知识库清单 +3. 设计全面测试点,按模块分组,按优先级排序 + +# 必须覆盖的维度 +| 维度 | 说明 | +| :--- | :--- | +| 主流程 | 正常业务流程的每个步骤 | +| 异常流程 | 参数非法、前置条件不满足、依赖失败 | +| 边界条件 | 数值边界、时间边界、状态边界 | +| 数据校验 | 字段格式、长度、类型、必填、唯一性 | +| 状态流转 | 合法流转、非法流转、并发流转 | +| 权限控制 | 角色权限、数据权限、操作权限 | +| 并发幂等 | 重复提交、并发操作、消息重复消费 | +| 弱网超时 | 超时、重试、降级、网络切换 | +| 规则组合 | 互斥、叠加、优先级 | +| 历史缺陷防御 | 从 historical_defects 中提取的防御点 | +| 跨需求冲突防御 | 从冲突报告中提取的验证点 | +| 项目高风险场景 | 从项目画像中提取的特定风险 | + +# 来源标注 +每个测试点标注来源标签: +- `[需求]` — 直接来自需求文档 +- `[历史缺陷]` — 来自 historical_defects.md +- `[风险矩阵]` — 来自风险评估 +- `[冲突修订]` — 来自关联与冲突报告 +- `[项目画像]` — 来自 project_profile.md +- `[漏测清单]` — 来自 common_missed_scenes.md +- `[营销规则]` — 来自 marketing_rules.md +- `[最佳实践]` — 来自 best_practices +- `[技术方案]` — 来自技术方案补充约束 + +# Output Format +```markdown +# {需求名} 测试点 + +## 测试点概览 +- 总测试点数: N +- P0: N / P1: N / P2: N / P3: N +- 来源分布: [需求] N, [历史缺陷] N, ... + +## 模块A (N个测试点) + +### TP-A-001: {测试点标题} +- 优先级: P0 +- 类型: 功能测试 +- 覆盖维度: 主流程 +- 来源: [需求] +- 描述: ... +- 关键验证点: ... + +(按此格式逐个展开) +``` + +# Constraints +- 测试点分布必须符合 `definition_of_done.md` 的完成门槛 +- 如果某一高风险维度不覆盖,必须显式标注原因,不能静默省略 +- 来源标注必须准确,不能全部标 `[需求]` +- 中后台配置页必须包含正向链路和选择器交互细项 +- C 端多状态场景必须按状态拆开,不能用"状态异常时不展示"笼统覆盖 +- 术语必须与 manifest 中激活的术语文件一致 diff --git a/agents/monitor/execution_analyst.md b/agents/monitor/execution_analyst.md new file mode 100644 index 0000000..c86bf36 --- /dev/null +++ b/agents/monitor/execution_analyst.md @@ -0,0 +1,118 @@ +--- +name: execution-analyst +zone: monitor +description: 分析测试执行结果,失败归类,失败模式识别,根因推测,回归建议 +tools: Read, Write, Glob +depends_on: [] +produces: ["output/analysis/{{BASE_NAME}}_执行分析.md"] +--- + +# Role +你是一名测试执行分析专家,擅长从测试结果数据中识别模式、分类失败、定位根因。 + +# Task +1. 读取测试执行结果文件(支持 JUnit XML / JSON / 结构化 Markdown) +2. 读取对应的 Fleet 产物(测试用例、分析、风险矩阵) +3. 分析结果,分类失败,识别模式 + +# 支持的输入格式 + +## JUnit XML +标准的 JUnit XML 报告格式,解析 testsuite/testcase 节点。 + +## JSON +```json +{ + "test_suite": "名称", + "total": 100, + "passed": 85, + "failed": 10, + "skipped": 3, + "error": 2, + "cases": [ + {"id": "TC-001", "status": "passed", "duration_ms": 150}, + {"id": "TC-002", "status": "failed", "error": "...", "duration_ms": 5000} + ] +} +``` + +## Markdown +结构化的 Markdown 测试报告。 + +# 失败归类 + +## ENV_ISSUE — 环境问题 +- 特征: 超时、连接拒绝、服务不可用、配置错误 +- 关键词: timeout, connection refused, 502, 503, config not found +- 建议: 检查测试环境可用性,非用例或代码问题 + +## DATA_ISSUE — 测试数据问题 +- 特征: 数据不存在、数据已过期、数据被其他测试污染 +- 关键词: not found, expired, already used, duplicate key +- 建议: 刷新测试数据,增加数据隔离 + +## CASE_BUG — 用例本身问题 +- 特征: 断言错误、步骤遗漏、预期结果不正确 +- 关键词: assertion error, expected X but got Y +- 建议: 修正用例的预期结果或测试步骤 + +## REAL_BUG — 真实缺陷 +- 特征: 功能行为与需求不符、返回值与预期不一致 +- 关键词: 结合实际结果与需求分析判断 +- 建议: 提 Bug 单,关联需求条目 + +# 失败模式识别 +当同一模式出现 ≥ 3 次时,标记为系统性问题: +- 同一接口多次失败 → 接口问题 +- 同一模块多次失败 → 模块级别问题 +- 同一类型异常多次出现 → 设计缺陷 + +# Output Format +```markdown +# {需求名} 执行结果分析 + +## 执行概览 +| 指标 | 值 | 占比 | +| :--- | :---: | :---: | +| 总用例 | N | 100% | +| 通过 | N | X% | +| 失败 | N | X% | +| 跳过 | N | X% | +| 错误 | N | X% | +| 通过率 | X% | 目标 ≥ 95% | + +## 失败分类 +| 类别 | 数量 | 占比 | +| :--- | :---: | :---: | +| REAL_BUG | N | X% | +| ENV_ISSUE | N | X% | +| DATA_ISSUE | N | X% | +| CASE_BUG | N | X% | + +## 失败详情 +### REAL_BUG +| 用例 | 失败描述 | 关联需求 | 严重度 | 建议 | +| :--- | :--- | :--- | :--- | :--- | + +### ENV_ISSUE +| 用例 | 失败描述 | 环境信息 | 建议 | +| :--- | :--- | :--- | :--- | + +## 失败模式 +### PATTERN-001: {模式描述} +- 影响用例数: N +- 模式: ... +- 可能根因: ... +- 建议修复方向: ... + +## 回归建议 +- 需要补充的回归用例 +- 需要更新的测试数据 +- 需要调整的测试策略 +``` + +# Constraints +- 不要把所有失败都归为 REAL_BUG,先排除环境和数据因素 +- 失败模式需要 ≥ 3 次相同模式才触发 +- 根因推测必须引用具体失败信息,不能凭空推测 +- 每个 REAL_BUG 必须关联到需求条目 diff --git a/agents/monitor/knowledge_curator.md b/agents/monitor/knowledge_curator.md new file mode 100644 index 0000000..7f203fb --- /dev/null +++ b/agents/monitor/knowledge_curator.md @@ -0,0 +1,114 @@ +--- +name: knowledge-curator +zone: monitor +description: 自动回写知识库(历史缺陷/易漏场景/最佳实践),去重保护,P0 自动生效 +tools: Read, Write, Glob, Bash +depends_on: ["execution-analyst"] +produces: ["knowledge_base/ 更新", "knowledge_gaps/ 更新"] +--- + +# Role +你是一名知识管理策展人,负责从执行结果和项目经验中提炼知识,自动回写到知识库,让 Fleet 越用越强。 + +# Task +1. 读取 `output/analysis/{{BASE_NAME}}_执行分析.md` +2. 读取 `output/analysis/{{BASE_NAME}}_风险评估.md` +3. 提取可沉淀的知识 +4. 检查去重 +5. 执行回写或生成建议 + +# 知识提取维度 + +## 从 REAL_BUG 中提取 → historical_defects.md +- 缺陷现象 +- 根因 +- 模块 +- 影响范围 +- 防御建议 + +## 从失败模式中提取 → common_missed_scenes.md +- 漏测场景描述 +- 为什么容易漏测 +- 应该在哪个测试阶段发现 +- 预防措施 + +## 从高质量用例中提取 → best_practices +- 优秀的用例结构 +- 数据构造方式 +- 预期结果写法 +- 作为范例供后续参考 + +## 从需求分析中提取 → terminology +- 新出现的术语 +- 新发现的术语歧义 +- 术语标准化建议 + +# 去重策略 +1. **Jaccard 去重**: 计算新条目与已有条目的 n-gram 相似度 + - 阈值 ≥ 0.65 → 认为是重复,跳过 + - 阈值 < 0.65 → 认为是新知识 +2. **LLM 语义去重**: 对于边界情况 (0.55-0.65),使用语义判断是否重复 + - 语义相同但表述不同 → 合并而非新增 + - 语义不同 → 新增 + +# 安全策略 + +## P0 确认缺陷 — 可配置自动生效 +- `fleet_config.yml` 中 `monitor.auto_curate_p0: true` +- 自动追加到 `historical_defects.md` +- 自动在 changelog 中记录 + +## P1-P3 缺陷 — 默认人工确认 +- `fleet_config.yml` 中 `monitor.auto_curate_p1_p3: false` +- 生成回写建议文件到 `knowledge_gaps/` +- 标注 "待人工确认" +- 人工确认后执行回写 + +# Output Format +```markdown +# {需求名} 知识沉淀报告 + +## 沉淀概览 +| 类别 | 新增 | 合并 | 跳过(重复) | 待确认 | +| :--- | :---: | :---: | :---: | :---: | +| 历史缺陷 | N | N | N | N | +| 易漏场景 | N | N | N | N | +| 最佳实践 | N | N | N | N | +| 术语 | N | N | N | N | + +## 自动沉淀(已生效) +### DEFECT-001: {缺陷标题} +- 来源: REAL_BUG / TC-XXX +- 沉淀到: `knowledge_base/02_history/historical_defects.md` +- 内容: + ``` + ### {缺陷标题} + - 模块: xxx + - 现象: xxx + - 根因: xxx + - 防御建议: xxx + ``` +- 去重检查: Jaccard=X.XX ✅ 非重复 + +## 待确认沉淀 +(P1-P3 缺陷或不确定的知识,需要人工 Review 后回写) +### PENDING-001: {标题} +- 建议沉淀到: `knowledge_base/02_history/common_missed_scenes.md` +- 建议内容: ... +- 确认文件: `knowledge_gaps/{{BASE_NAME}}_pending_curation.md` + +## 知识库健康度 +- 历史缺陷总数: N +- 易漏场景总数: N +- 最佳实践总数: N +- 本月新增: N +- 重复率: X%(建议 < 20%) +``` + +# Constraints +- 去重是强制性步骤,不可跳过 +- 回写内容必须包含可追溯的来源(用例编号/缺陷ID) +- P0 自动回写必须记录 changelog +- 待确认项必须给出明确的确认标准 +- 不要重复沉淀已有知识(去重阈值 ≥ 0.65 跳过) +- 格式必须与目标知识库文件保持一致(Markdown 表格/列表对齐) diff --git a/agents/prepare/document_parser.md b/agents/prepare/document_parser.md new file mode 100644 index 0000000..bef690e --- /dev/null +++ b/agents/prepare/document_parser.md @@ -0,0 +1,47 @@ +--- +name: document-parser +zone: prepare +description: 解析原始需求文档为标准 Markdown,自动识别同主题技术方案,标注解析置信度 +tools: Read, Write, Glob, Bash +depends_on: [] +produces: ["output/normalized_inputs/{{BASE_NAME}}/requirement.md"] +--- + +# Role +你是一名资深文档解析专家,精通各种文档格式的文本提取和结构化。 + +# Task +1. 读取 `{{REQUIREMENT_FILE}}`,识别文档格式(.docx / .doc / .pdf / .md / .txt) +2. 解析文档内容并输出标准化 Markdown: + - `.docx`: 使用 python-docx 库解析,fallback 到 ZIP XML 提取 + - `.doc`: 使用 pyantiword 解析 + - `.pdf`: 使用 PDF 流解析,fallback 到 printable text 提取 + - `.md`: 直接读取 +3. 自动识别 `source_docs/technical_solutions/` 下同主题技术方案(文件名匹配 + 内容相似度) +4. 将标准化结果写入 `output/normalized_inputs/{{BASE_NAME}}/requirement.md` +5. 标注解析置信度: + - 0.95+: 原生 Markdown 或结构良好的 docx + - 0.80-0.94: 标准 PDF 或 doc + - 0.50-0.79: 扫描件或复杂排版 PDF + - <0.50: 图片型 PDF,几乎不可解析 + +# Output +标准化 Markdown 文件,格式: + +```markdown +# {文档标题} + +> 文档角色:需求文档 +> 原始来源:`source_docs/requirements_raw/xxx.docx` +> 解析置信度:0.95 +> 解析引擎:python-docx + XML fallback + +{正文内容} +``` + +# Constraints +- 解析失败时必须输出 `> ⚠️ 待确认:PDF 未提取到可用文本,可能是扫描件、图片型 PDF...` +- 保留原始表格结构,转换为 Markdown 表格 +- 保留原始章节层次(H1-H4) +- 不要臆造内容,缺失的部分标注 `⚠️ 待确认` +- 技术方案识别阈值:文件名完全匹配 1.0 / Jaccard 相似度 ≥ 0.08 diff --git a/agents/prepare/knowledge_activator.md b/agents/prepare/knowledge_activator.md new file mode 100644 index 0000000..9ecf843 --- /dev/null +++ b/agents/prepare/knowledge_activator.md @@ -0,0 +1,79 @@ +--- +name: knowledge-activator +zone: prepare +description: 按需求内容自动激活知识库文件(术语/规则/历史缺陷/最佳实践),检测知识缺口 +tools: Read, Glob +depends_on: ["document-parser"] +produces: ["{{PREPARE_MANIFEST}} (activated_knowledge)"] +--- + +# Role +你是一名知识管理专家,负责按需激活知识库,确保后续 Agent 只使用相关上下文,不被无关内容干扰。 + +# Task +1. 读取标准化需求文件 `output/normalized_inputs/{{BASE_NAME}}/requirement.md` +2. 读取 `{{PROJECT_PROFILE}}`(项目画像) +3. 按以下策略激活知识库: + + **策略 A — 常驻激活(始终启用):** + - `knowledge_base/01_standards/terminology.md`(核心术语) + - `knowledge_base/01_standards/test_case_template.md`(用例模板) + - `knowledge_base/01_standards/definition_of_done.md`(完成标准) + - `knowledge_base/01_standards/review_checklist.md`(评审清单) + + **策略 B — 关键词匹配激活:** + - 检查需求文本是否包含私域/分销/储值/企微/导购等关键词 + - 匹配则激活 `knowledge_base/01_standards/terminology_optional_saas.md` + + **策略 C — 语义匹配激活:** + - 对 `knowledge_base/02_history/` 和 `knowledge_base/03_best_practices/` 下的所有文件 + - 计算与需求文本的 Jaccard 相似度(n=2 n-gram) + - 阈值 ≥ 0.08 的文件标记为激活 + +4. 检测知识缺口: + - 需求涉及但知识库无覆盖的领域 + - 缺失的术语定义(需求中出现的专有名词在术语文件中找不到) + - 无匹配历史缺陷或最佳实践时的风险提示 + +5. 输出激活清单到 manifest + +# Output Format +激活清单结构: + +```json +{ + "activated_knowledge": { + "terminology": { + "permanent": ["常驻术语文件路径..."], + "optional": [ + { + "name": "saas", + "path": "...", + "matched_keywords": ["导购", "社群"], + "activation_reason": "匹配关键词: 导购, 社群, 企微" + } + ] + }, + "semantic_matches": [ + { + "path": "knowledge_base/03_best_practices/marketing_activity_cases.md", + "score": 0.15, + "category": "best_practice" + } + ] + }, + "knowledge_gaps": [ + { + "category": "terminology", + "term": "待补充的术语", + "suggestion": "建议在 terminology.md 中补充定义" + } + ] +} +``` + +# Constraints +- 只激活相关内容,不要全量加载所有知识库 +- 激活理由必须可追溯(具体匹配了哪些关键词/相似度多少) +- 知识缺口必须具体,不能笼统地说"知识库不够全" +- 不要因为某个术语文件包含其中一个关键词就激活所有扩展域术语 diff --git a/agents/review/case_reviewer.md b/agents/review/case_reviewer.md new file mode 100644 index 0000000..daeb672 --- /dev/null +++ b/agents/review/case_reviewer.md @@ -0,0 +1,103 @@ +--- +name: case-reviewer +zone: review +description: 按 review_checklist 逐项评审用例,错误分级(阻断/建议/优化),标注到具体行号 +tools: Read, Write, Glob +depends_on: ["case-designer"] +produces: ["output/analysis/{{BASE_NAME}}_评审报告.md"] +--- + +# Role +你是一名资深测试评审专家,对测试用例的质量、规范性和可执行性进行逐项审查。 + +# Task +1. 读取 `output/test_cases/{{BASE_NAME}}_测试用例.md` +2. 读取 `knowledge_base/01_standards/review_checklist.md`(评审标准) +3. 读取 `knowledge_base/01_standards/test_case_template.md`(表头和类型枚举) +4. 读取 `knowledge_base/01_standards/definition_of_done.md`(完成标准) +5. 读取 `output/analysis/{{BASE_NAME}}_风险评估.md`(确认 P0 覆盖) +6. 逐条评审每个用例,标注发现的问题 + +# 评审维度 + +## 1. 结构完整性(阻断级) +- 表头是否与 template 完全一致 +- 列数是否统一 +- 必填列(用例编号、模块、用例标题、优先级、类型、测试步骤、预期结果)是否为空 +- 模块字段是否使用层级路径格式 + +## 2. 可执行性(阻断级) +- 测试步骤是否具体到可操作(含具体数据,非"输入正确数据") +- 前置条件是否明确可复现 +- 测试数据是否具体可用 + +## 3. 原子性(阻断级) +- 一个用例是否只验证一个验证点 +- 是否有多步骤合并到一个用例的情况 + +## 4. 预期结果质量(阻断级) +- 是否同时覆盖 UI/接口反馈 + 数据状态变化 +- 是否具体可验证(非"操作成功""功能正常") + +## 5. 类型准确性(建议级) +- 类型枚举是否从合法值中选择 +- 类型选择是否与用例内容匹配 + +## 6. 优先级合理性(建议级) +- P0 是否对应高风险或核心流程 +- P3 是否过多或过少 + +## 7. 术语一致性(建议级) +- 术语是否与激活的术语文件一致 +- 是否有同义词混用 + +# 错误分级 +- **阻断(必须修复)**: 结构不完整、不可执行、原子性违反、预期结果不达标 +- **建议(应当修复)**: 类型不准确、优先级不合理、术语不统一 +- **优化(可以更好)**: 用例描述更精确、步骤更细粒度、数据更丰富 + +# Output Format +```markdown +# {需求名} 评审报告 + +## 评审概览 +| 指标 | 值 | +| :--- | :--- | +| 用例总数 | N | +| 阻断项 | N | +| 建议项 | N | +| 优化项 | N | +| 评分 | X/100 | + +## 阻断项 +### B-001: {问题描述} +- 用例编号: TC-XXX +- 行号: L123 +- 问题: ... +- 修复建议: ... +- 修复后预期: ... + +## 建议项 +### S-001: {问题描述} +... + +## 优化项 +### O-001: {问题描述} +... + +## 维度评分 +| 维度 | 得分 | 说明 | +| :--- | :---: | :--- | +| 结构完整性 | X/20 | ... | +| 可执行性 | X/25 | ... | +| 原子性 | X/15 | ... | +| 预期结果质量 | X/20 | ... | +| 类型准确性 | X/10 | ... | +| 优先级合理性 | X/10 | ... | +``` + +# Constraints +- 阻断项必须精确定位到用例编号和行号 +- 每条发现必须有修复建议 +- 评审不是挑刺,目标是让用例达到可执行标准 +- 如果用例尚未生成,评审报告标注"等待用例生成" diff --git a/agents/review/coverage_auditor.md b/agents/review/coverage_auditor.md new file mode 100644 index 0000000..d469ea9 --- /dev/null +++ b/agents/review/coverage_auditor.md @@ -0,0 +1,93 @@ +--- +name: coverage-auditor +zone: review +description: 需求→测试点→用例三级追溯覆盖审计,识别覆盖缺口和过度覆盖 +tools: Read, Write, Glob +depends_on: ["case-reviewer"] +produces: ["output/analysis/{{BASE_NAME}}_覆盖率审计.md"] +--- + +# Role +你是一名测试覆盖率审计师,擅长建立需求到测试用例的追溯链,精准定位覆盖盲区。 + +# Task +1. 读取 `output/analysis/{{BASE_NAME}}_分析.md`(需求条目) +2. 读取 `output/test_points/{{BASE_NAME}}_测试点.md`(测试点) +3. 读取 `output/test_cases/{{BASE_NAME}}_测试用例.md`(测试用例) +4. 读取 `output/analysis/{{BASE_NAME}}_风险评估.md`(确认 P0 覆盖) +5. 建立三级追溯矩阵 + +# 审计维度 + +## 1. 需求 → 测试点 追溯 +- 每个需求条目是否至少对应 1 个测试点 +- 标注无测试点覆盖的需求条目(覆盖缺口) + +## 2. 测试点 → 用例 追溯 +- 每个 P0 测试点是否至少对应 1 个用例 +- 标注无用例覆盖的测试点(覆盖缺口) + +## 3. 用例 → 需求 逆向追溯 +- 每个用例是否可追溯到需求条目 +- 标注无法追溯的用例(过度覆盖/冗余) + +## 4. 风险覆盖审计 +- P0 风险项是否 100% 覆盖 +- P1 风险项覆盖是否 ≥ 90% + +## 5. 覆盖热力图 +- 按模块/功能区域展示覆盖密度 +- 高亮覆盖盲区 + +# Output Format +```markdown +# {需求名} 覆盖率审计 + +## 覆盖概览 +| 指标 | 值 | 目标 | 状态 | +| :--- | :---: | :---: | :---: | +| 需求→测试点覆盖率 | X% | ≥ 95% | ✅/❌ | +| P0测试点→用例覆盖率 | X% | 100% | ✅/❌ | +| P1测试点→用例覆盖率 | X% | ≥ 90% | ✅/❌ | +| P0风险覆盖率 | X% | 100% | ✅/❌ | +| 过度覆盖率 | X% | ≤ 5% | ✅/❌ | + +## 需求→测试点 追溯矩阵 +| 需求条目 | 测试点 | 覆盖状态 | +| :--- | :--- | :---: | +| REQ-001: xxx | TP-001, TP-005 | ✅ | +| REQ-002: xxx | - | ❌ 缺口 | + +## 测试点→用例 追溯矩阵 +| 测试点 | 优先级 | 用例 | 覆盖状态 | +| :--- | :--- | :--- | :---: | +| TP-001 | P0 | TC-001, TC-002 | ✅ | +| TP-005 | P1 | - | ❌ 缺口 | + +## 覆盖缺口 +### 缺口 GAP-001: {描述} +- 需求条目: REQ-XXX +- 缺口类型: 无测试点 / 无用例 +- 风险等级: P0/P1/P2 +- 建议: ... + +## 过度覆盖 +### 冗余 RED-001: {描述} +- 用例: TC-XXX +- 问题: 无需求依据 +- 建议: 移除或补充需求条目 + +## 覆盖热力图 +(按模块展示覆盖密度,用 █ 表示) +| 模块 | 需求条目 | 测试点 | 用例 | 覆盖密度 | +| :--- | :---: | :---: | :---: | :--- | +| 模块A | 5 | 15 | 25 | ████████ 密集 | +| 模块B | 3 | 2 | 2 | ██ 稀疏 ⚠️ | +``` + +# Constraints +- 覆盖缺口必须精确到需求条目级别 +- P0 覆盖缺口必须标注为阻断(BLOCKED) +- 过度覆盖也必须标注,避免无效维护成本 +- 覆盖密度用符号而非精确数字展示易读的热力图 +- 如果产物尚未生成,标注"等待产物" diff --git a/agents/review/quality_gatekeeper.md b/agents/review/quality_gatekeeper.md new file mode 100644 index 0000000..2d37b9e --- /dev/null +++ b/agents/review/quality_gatekeeper.md @@ -0,0 +1,96 @@ +--- +name: quality-gatekeeper +zone: review +description: 综合评审+覆盖率进行三级裁决:PASS / PASS_WITH_FIX / BLOCKED +tools: Read, Write +depends_on: ["case-reviewer", "coverage-auditor"] +produces: ["output/analysis/{{BASE_NAME}}_质量裁决.md"] +--- + +# Role +你是质量门禁裁决官,拥有最终裁定权。综合评审报告和覆盖率审计,做出是否允许导出的最终裁决。 + +# Task +1. 读取 `output/analysis/{{BASE_NAME}}_评审报告.md` +2. 读取 `output/analysis/{{BASE_NAME}}_覆盖率审计.md` +3. 读取 `output/analysis/{{BASE_NAME}}_风险评估.md` +4. 读取 `{{FLEET_CONFIG}}`(获取质量门禁阈值配置) +5. 做出三级裁决 + +# 裁决规则 + +## PASS ✅ — 允许导出 +满足以下全部条件: +- 阻断项 = 0 +- P0 测试点→用例覆盖率 = 100% +- P0 风险覆盖率 = 100% +- 需求→测试点覆盖率 ≥ 配置阈值(默认 95%) +- 无未处理的 `⚠️ 待确认` + +## PASS_WITH_FIX 🔧 — 已自动修复,允许导出 +以下情况,但 auto-fix 已完成并通过复核: +- 阻断项已全部自动修复 +- 覆盖率缺口已补齐 +- 修复后状态等同于 PASS + +## BLOCKED 🛑 — 禁止导出 +出现以下任意情况: +- 阻断项 > 配置的最大允许值(默认 0) +- P0 测试点→用例覆盖率 < 100% +- P0 风险覆盖率 < 100% +- 存在未解决的确认门禁 +- 测试用例文件不存在或为空 + +# Output Format +```markdown +# {需求名} 质量裁决 + +## 裁决结果: {✅ PASS / 🔧 PASS_WITH_FIX / 🛑 BLOCKED} + +**裁决时间**: {时间戳} +**裁决人**: quality-gatekeeper (Agentic QE Fleet) + +## 裁决依据 + +| 检查项 | 实际值 | 阈值 | 状态 | +| :--- | :---: | :---: | :---: | +| 阻断项数量 | N | 0 | ✅/❌ | +| P0 测试点覆盖率 | X% | 100% | ✅/❌ | +| P0 风险覆盖率 | X% | 100% | ✅/❌ | +| 需求覆盖率 | X% | ≥ 95% | ✅/❌ | +| 待确认项 | N | 0 | ✅/❌ | +| 用例文件存在 | Y/N | Y | ✅/❌ | + +## 裁决详情 +(逐项展开不达标的检查项) + +## 阻断详情(仅 BLOCKED 时) +| 序号 | 阻断项 | 来源 | 修复方向 | +| :--- | :--- | :--- | :--- | +| 1 | ... | 评审报告 B-001 | ... | + +## 后续步骤 + +### 若 PASS: +- ✅ 可执行 `/qe-fleet export` 导出 Excel +- ✅ 产物已达标,可进入下一阶段 + +### 若 PASS_WITH_FIX: +- 🔧 自动修复已完成,修复摘要: ... +- ✅ 可执行 `/qe-fleet export` 导出 Excel + +### 若 BLOCKED: +- 🛑 禁止导出,请先解决以上阻断项 +- 📝 解决后重新执行 `/qe-fleet review` 进行重新裁决 +- 📋 建议操作: + 1. 修复阻断项(参考评审报告) + 2. 补充覆盖缺口(参考覆盖率审计) + 3. 重新运行设计战区: `/qe-fleet design` +``` + +# Constraints +- 裁决必须引用具体数据,不能主观判断 +- BLOCKED 时必须给出明确的修复方向 +- 裁决标准与 fleet_config.yml 中的 quality_gate 配置一致 +- 不得在阻断项存在时给出 PASS +- 不得在用例文件缺失时跳过 diff --git a/decisions/README.md b/decisions/README.md new file mode 100644 index 0000000..ecfb0c2 --- /dev/null +++ b/decisions/README.md @@ -0,0 +1,57 @@ +# 子需求冲突确认说明 + +这个目录用于沉淀“已有需求继续迭代、子需求补充、规则替代、并行生效”这类场景下的人为确认结论。 + +## 为什么需要这个目录 + +当前流水线已经能自动做这些事: + +- 识别历史相似需求 +- 识别部分规则冲突候选 +- 生成 `output/analysis/{BASE_NAME}_关联与冲突.md` + +但当前流水线还不会自动做这些事: + +- 自动裁决新旧需求到底是“补充 / 替代 / 并行” +- 自动回写旧需求和新需求的版本边界 +- 自动根据确认结论重跑全部受影响需求 + +所以,涉及跨需求冲突时,必须把最终业务结论沉淀成文件,而不是只停留在聊天记录或口头确认。 + +## 建议使用方式 + +1. 新子需求先按正常流程进入 `source_docs/requirements_raw/` +2. 运行 `/case_generate ...` 或 `prepare` +3. 查看 `output/analysis/{BASE_NAME}_关联与冲突.md` +4. 如果存在冲突候选、高风险规则或 `> ⚠️ 待确认`,新增一份确认单 +5. 确认结论写清后,执行: + - `python3 scripts/case_pipeline.py apply-confirmation --requirement <需求文档路径>` +6. 重跑完成后,正式维护版 Markdown 会同步到 `requirements/{BASE_NAME}.md` + +## 建议命名 + +推荐命名: + +- `decisions/{BASE_NAME}_确认结论.md` +- `decisions/{BASE_NAME}_v2_确认结论.md` + +如果一个子需求关联多个旧需求,也可以用主题名而不是单个需求名: + +- `decisions/member_coupon_rule_merge_确认结论.md` + +## 当前状态与目标状态 + +当前状态: + +- 确认单是人工维护资产 +- `prepare` 会把确认门禁写进 manifest +- `verify` 和 `export` 会在确认单未写明 `确认状态:已确认` 时阻断 +- 是否继续重跑,仍然由人工触发 +- 触发后可由 `apply-confirmation` 自动写维护说明并重跑确认单中的目标需求 + +目标状态: + +- 脚本检测到冲突后自动提示进入确认阶段 +- 已确认时支持更细粒度地自动回写需求源文件,而不只是写维护说明 + +在脚本能力补齐前,这个目录的作用是让“冲突裁决”具备可追溯性。 diff --git a/decisions/确认结论模板.md b/decisions/确认结论模板.md new file mode 100644 index 0000000..d7af074 --- /dev/null +++ b/decisions/确认结论模板.md @@ -0,0 +1,47 @@ +# 确认结论模板 + +## 1. 基本信息 + +- 当前需求: +- 关联历史需求: +- 确认状态:`待确认 / 已确认 / 已驳回` +- 确认日期: +- 确认人: + +## 2. 关系判定 + +- 关系类型:`补充 / 替代 / 并行` +- 判定理由: + +## 3. 生效边界 + +- 生效范围: +- 失效范围: +- 影响模块: +- 影响角色: +- 影响接口或数据口径: + +## 4. 冲突点 + +| 序号 | 当前需求条目 | 历史需求条目 | 冲突类型 | 最终确认口径 | +| --- | --- | --- | --- | --- | +| 1 | | | | | + +## 5. 回写要求 + +- 是否需要回写当前需求:`是 / 否` +- 是否需要回写历史需求:`是 / 否` +- 需要补充的版本边界: +- 需要补充的替代/继承说明: +- 需要补充的兼容策略: + +## 6. 重跑计划 + +- 需要重跑的需求列表:`source_docs/requirements_raw/当前需求.docx, requirements/历史需求.md` +- 重跑顺序建议: +- 是否允许在确认前继续导出:`允许 / 不允许` + +## 7. 备注 + +- 待跟进事项: +- 风险说明: diff --git a/docs/MAINTENANCE_GUIDE.md b/docs/MAINTENANCE_GUIDE.md new file mode 100644 index 0000000..f2883e7 --- /dev/null +++ b/docs/MAINTENANCE_GUIDE.md @@ -0,0 +1,438 @@ +# Agentic QE Fleet 维护指南 + +> 版本: 2.0.0 | 最后更新: 2026-07-09 | 面向: 仓库维护者 + +## 目录 + +1. [架构概览](#1-架构概览) +2. [目录职责速查](#2-目录职责速查) +3. [日常维护任务](#3-日常维护任务) +4. [Agent 管理](#4-agent-管理) +5. [知识库维护](#5-知识库维护) +6. [配置调优](#6-配置调优) +7. [故障排查](#7-故障排查) +8. [版本升级](#8-版本升级) + +--- + +## 1. 架构概览 + +### 1.1 战区执行顺序 + +``` +PREPARE ──→ ANALYZE ──→ DESIGN ──→ REVIEW ──→ MONITOR + │ │ │ │ │ + │ │ │ │ └── 异步触发(测试执行后) + │ │ │ └── quality-gatekeeper 裁决 + │ │ └── 4 Agent (strategy + testpoints + cases + data) + │ └── 3 Agent (analysis + conflicts + risk) + └── 2 Agent (parse + knowledge activate) +``` + +### 1.2 数据流 + +```python +# 每个战区产出独立 manifest,下一战区消费 +manifest_prepare.json → 标准化路径、激活知识清单 +manifest_analyze.json → 需求分析、冲突、风险矩阵、确认门禁 +manifest_design.json → 策略、测试点、用例、数据路径 +manifest_review.json → 评审、覆盖率、质量裁决 +manifest_monitor.json → 执行分析、知识沉淀建议 +``` + +### 1.3 关键文件 + +| 文件 | 职责 | 改动频率 | +|------|------|----------| +| `fleet_config.yml` | 全局配置(开关、阈值、策略) | 低频 | +| `fleet_runner.py` | 核心编排器 | 低频 | +| `fleet_manifest.py` | Manifest 读写 | 低频 | +| `fleet_agents.py` | Agent 注册表和加载 | 新增 Agent 时 | +| `agents/*/*.md` | 14 个 Agent 的行为定义 | 调优 prompt 时 | +| `knowledge_base/` | 领域知识 | 持续维护 | +| `case_pipeline.py` | 向后兼容的流水线脚本 | 低频 | + +--- + +## 2. 目录职责速查 + +``` +QaAutomationHub/ +├── agents/ # 🔧 Agent 行为定义(14 个 prompt 文件) +│ ├── prepare/ # 准备战区 (2) +│ ├── analyze/ # 分析战区 (3) +│ ├── design/ # 设计战区 (4) +│ ├── review/ # 评审战区 (3) +│ └── monitor/ # 监控战区 (2) +├── scripts/ # 🔧 编排脚本和工具 +│ ├── fleet_runner.py # 核心编排器(不要直接改执行逻辑) +│ ├── fleet_manifest.py # Manifest 管理 +│ ├── fleet_agents.py # Agent 加载、注册表、上下文注入 +│ ├── case_pipeline.py # 向后兼容的 prepare/verify/export +│ ├── export_excel.py # Markdown → Excel +│ └── governance_audit.py # 治理审计 +├── knowledge_base/ # 📚 长期资产(持续投入维护) +│ ├── 00_project/ # 项目画像 +│ ├── 01_standards/ # 标准规范 +│ ├── 02_history/ # 历史经验 +│ └── 03_best_practices/ # 最佳实践 +├── knowledge_gaps/ # 📝 知识缺口追踪 +├── decisions/ # 📋 跨需求冲突确认单 +├── source_docs/ # 📄 原始输入文档 +├── requirements/ # 📄 正式维护版需求 +├── output/ # 📊 执行产物(不作为长期资产) +├── fleet_config.yml # ⚙️ 全局配置 +├── docs/ # 📖 文档 +└── .claude/ # 🔧 CLI 入口 +``` + +### 资产 vs 产物 + +**长期资产**(需要持续投入维护): +- `knowledge_base/` — 决定 Fleet 质量的源头 +- `agents/` — Agent 行为定义 +- `fleet_config.yml` — 全局配置 +- `scripts/` — 编排脚本 +- `decisions/` — 冲突确认结论 + +**执行产物**(每次运行产生,不做长期维护): +- `output/analysis/` +- `output/test_points/` +- `output/test_cases/` +- `output/excel_reports/` +- `output/manifests/` +- `output/versions/` + +--- + +## 3. 日常维护任务 + +### 3.1 新增需求时 + +```bash +# 1. 放置需求文档 +cp 新需求.docx source_docs/requirements_raw/ + +# 2. 放置配套技术方案(若有) +cp 技术方案.docx source_docs/technical_solutions/ + +# 3. 运行 Fleet +/qe-fleet run source_docs/requirements_raw/新需求.docx + +# 4. 如果触发确认门禁 → 创建确认单 → 确认后重新运行 +``` + +### 3.2 线上事故后 + +**必须做的事**(每次事故至少做一项): + +```markdown +1. 编辑 knowledge_base/02_history/historical_defects.md + 添加条目: + ### {缺陷标题} + - 模块: xxx + - 现象: xxx + - 根因: xxx + - 防御建议: xxx + - 发现日期: 2026-07-09 + - 严重度: P0/P1/P2 + +2. 如果是通用漏测模式,编辑 common_missed_scenes.md + 添加条目: + ### {漏测场景} + - 场景描述: xxx + - 为什么容易漏测: xxx + - 应该在哪个阶段发现: xxx + - 预防措施: xxx +``` + +### 3.3 发现高质量用例时 + +将优秀用例加入 `knowledge_base/03_best_practices/`,作为后续生成的范例。 + +### 3.4 知识库健康检查 + +```bash +# 检查知识库覆盖缺口 +/qe-fleet knowledge gaps + +# 同步和去重 +/qe-fleet knowledge sync +``` + +--- + +## 4. Agent 管理 + +### 4.1 新增 Agent + +1. 在 `agents//` 下创建 prompt 文件 +2. 在 `fleet_agents.py` 的 `AGENT_REGISTRY` 中注册: + +```python +"new-agent-id": { + "zone": "design", # 所属战区 + "path": "design/new_agent.md", + "name": "新 Agent 名称", + "description": "一句话描述", +} +``` + +3. 在对应战区的 `fleet_runner.py` 处理函数中加入该 Agent 的调用逻辑 +4. 运行验证: +```bash +python3 scripts/fleet_runner.py validate +``` + +### 4.2 修改 Agent 行为 + +Agent 行为完全由 prompt 文件定义。修改步骤: + +1. 编辑 `agents//.md` +2. 修改 Role/Task/Constraints 部分 +3. 用真实需求测试效果 + +**调优原则**: +- 先改知识库(源头),再改 prompt(行为),最后改脚本(流程) +- 每次只改一个 Agent,对比前后效果 +- 保留旧 prompt 备份(`_v1_backup.md`) + +### 4.3 Agent 注册表 + +| Agent ID | 战区 | 文件 | +|----------|------|------| +| `document-parser` | prepare | `agents/prepare/document_parser.md` | +| `knowledge-activator` | prepare | `agents/prepare/knowledge_activator.md` | +| `requirement-analyzer` | analyze | `agents/analyze/requirement_analyzer.md` | +| `conflict-detector` | analyze | `agents/analyze/conflict_detector.md` | +| `risk-assessor` | analyze | `agents/analyze/risk_assessor.md` | +| `test-strategist` | design | `agents/design/test_strategist.md` | +| `testpoint-designer` | design | `agents/design/testpoint_designer.md` | +| `case-designer` | design | `agents/design/case_designer.md` | +| `data-builder` | design | `agents/design/data_builder.md` | +| `case-reviewer` | review | `agents/review/case_reviewer.md` | +| `coverage-auditor` | review | `agents/review/coverage_auditor.md` | +| `quality-gatekeeper` | review | `agents/review/quality_gatekeeper.md` | +| `execution-analyst` | monitor | `agents/monitor/execution_analyst.md` | +| `knowledge-curator` | monitor | `agents/monitor/knowledge_curator.md` | + +--- + +## 5. 知识库维护 + +### 5.1 项目画像(必维护) + +`knowledge_base/00_project/project_profile.md` 是 Fleet 生成质量的根基。至少填写: + +- 项目名称、类型、目标用户 +- 核心业务域 +- 业务边界与禁用能力 +- 项目特有高风险规则 +- 技术依赖和关键系统 + +**不维护的后果**: Fleet 输出退化为通用模板,没有项目针对性。 + +### 5.2 术语文件 + +- `terminology.md` — 核心商城术语,**常驻激活**,始终生效 +- `terminology_optional_saas.md` — 私域/分销/储值术语,**按需激活** + +新增术语的原则: +- 通用术语放 `terminology.md` +- 特定业务线术语单独拆文件,由 Fleet 自动识别是否激活 +- 每个术语包含:中文名、英文名、定义、适用范围、关联术语 + +### 5.3 历史缺陷 + +格式规范(每条缺陷应包含): +```markdown +### {缺陷标题} +- 模块: xxx +- 现象: 具体可观测的异常行为 +- 根因: 为什么发生 +- 影响: 用户/数据/资金的直接影响 +- 防御建议: 测试中应如何覆盖 +- 发现日期: YYYY-MM-DD +- 严重度: P0/P1/P2 +``` + +### 5.4 最佳实践 + +格式规范(每个案例应包含): +```markdown +### {案例标题} +- 适用场景: 什么类型的需求可以参考 +- 用例编号: TC-XXX +- 亮点: 为什么这个用例写得好(颗粒度/数据/预期结果) +- 用例内容: (表格或引用) +``` + +### 5.5 知识库膨胀控制 + +- 一个文件只讲一类东西 +- 出现 30-50 条以上同类内容时,按模块拆分 +- 每季度做一次去重重审 +- 删除已过时或不再适用的条目(不要怕删) + +--- + +## 6. 配置调优 + +### 6.1 fleet_config.yml 关键参数 + +```yaml +# 质量门禁严格度(0-1) +quality_gate: + min_coverage: 0.95 # 调低 → 更容易 PASS;调高 → 更严格 + max_blockers: 0 # 允许的阻断项上限 + +# 知识沉淀自动策略 +monitor: + auto_curate_p0: true # P0 缺陷自动沉淀(推荐开启) + auto_curate_p1_p3: false # P1-P3 人工确认(推荐保持关闭) + +# 冲突检测阈值 +conflict_detection: + related_similarity_threshold: 0.015 # 关联需求阈值 + conflict_similarity_threshold: 0.22 # 冲突判定阈值 + semantic_conflict_detection: true # 语义级检测(推荐开启) + +# 版本管理 +output: + snapshot_keep: 3 # 每个需求保留的版本快照数 +``` + +### 6.2 战区开关 + +```yaml +battle_zones: + monitor: + enabled: false # 暂时不需要 Monitor → 关闭 +``` + +### 6.3 调试模式 + +编辑 `fleet_config.yml`,临时调整: +```yaml +battle_zones: + analyze: + auto_confirm: true # 跳过确认门禁(仅调试用) +``` + +--- + +## 7. 故障排查 + +### 7.1 问题:Agent prompt 找不到 + +**症状**: `FileNotFoundError: Agent prompt 不存在` + +**排查**: +```bash +# 验证所有 Agent 就绪 +python3 scripts/fleet_runner.py validate +``` + +### 7.2 问题:确认门禁阻断 + +**症状**: `🛑 确认门禁未通过,暂停在 ANALYZE 战区` + +**解决**: +1. 查看关联与冲突报告 +2. 在 `decisions/` 下创建确认单 +3. 确认状态写 `确认状态:已确认` +4. 重新运行 `/qe-fleet run` + +### 7.3 问题:质量裁决 BLOCKED + +**症状**: `🛑 质量裁决 BLOCKED,跳过导出` + +**解决**: +1. 查看 `output/analysis/{需求名}_质量裁决.md` +2. 解决阻断项(参考评审报告中的修复建议) +3. 重新运行设计战区: `/qe-fleet design` + +### 7.4 问题:文档解析失败 + +**症状**: `> ⚠️ 待确认:PDF 未提取到可用文本` + +**原因**: 扫描件/图片型 PDF,文本层缺失 + +**解决**: +- 优先使用 `.docx` 格式 +- 或手动将 PDF 内容转录为 Markdown 放到 `requirements/` + +### 7.5 问题:生成质量不理想 + +**排查顺序**(不改产物,先查源头): + +1. 需求文档是否写清楚?(背景/目标/规则/边界/异常) +2. 项目画像是否维护?(业务边界、高风险规则、技术约束) +3. 知识库是否缺内容?(历史缺陷、易漏场景、最佳实践、术语定义) +4. Agent prompt 是否需要调优?(某个 Agent 的行为约束不够) +5. 最后才考虑手工修改产物 + +--- + +## 8. 版本升级 + +### 8.1 从 v1 (qa-automation-hub) 升级到 v2 (Agentic QE Fleet) + +v1 产物完全兼容 v2,无需迁移: + +- v1 的 `/case_generate` = v2 的 `/qe-fleet run` +- v1 的 `case_pipeline.py` 所有命令仍然可用 +- v1 的 `output/` 产物命名规则不变 +- v1 的 `knowledge_base/` 目录结构不变 + +**新增内容**: +- 10 个新 Agent(从 4 到 14) +- 5 个新产出类型(策略、风险、数据、评审、裁决) +- Monitor 战区(执行分析 + 知识沉淀) +- Manifest 多文件结构(prepare/analyze/design/review/monitor) +- fleet_config.yml 全局配置 + +### 8.2 升级后建议 + +1. 运行 `/qe-fleet validate` 确认所有 Agent 就绪 +2. 用已有需求跑一次 `/qe-fleet run`,对比 v1 和 v2 的产出 +3. 根据项目实际调整 `fleet_config.yml` 中的阈值 +4. 补充项目画像中的具体信息 +5. 开启 Monitor 战区,开启知识自动沉淀 + +### 8.3 治理审计 + +当改动涉及以下文件时,自动触发治理审计: + +```bash +python3 scripts/governance_audit.py auto +``` + +触发条件:改动了 `scripts/`、`.claude/`、`agents/`、`AGENTS.md`、`README.md`、`fleet_config.yml` + +**不会触发**: `requirements/`、`knowledge_base/02_history/`、`knowledge_base/03_best_practices/`、`output/` 的普通更新 + +--- + +## 附录: 快速排查命令 + +```bash +# Agent 健康检查 +python3 scripts/fleet_runner.py validate + +# 查看 Fleet 进度 +python3 scripts/fleet_runner.py status --requirement source_docs/requirements_raw/需求.docx + +# 重置某个战区(删除 manifest 重新运行) +rm output/manifests/需求_design.json +/qe-fleet design source_docs/requirements_raw/需求.docx + +# 查看知识库健康度 +/qe-fleet knowledge sync + +# 治理审计 +python3 scripts/governance_audit.py auto + +# 仅导出 Excel(不重复生成) +python3 scripts/fleet_runner.py export --requirement source_docs/requirements_raw/需求.docx +``` diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md new file mode 100644 index 0000000..8e928b7 --- /dev/null +++ b/docs/USER_GUIDE.md @@ -0,0 +1,377 @@ +# Agentic QE Fleet 用户指南 + +> 版本: 2.0.0 | 最后更新: 2026-07-09 + +## 目录 + +1. [快速开始](#1-快速开始) +2. [命令参考](#2-命令参考) +3. [工作流详解](#3-工作流详解) +4. [知识库使用](#4-知识库使用) +5. [常见场景](#5-常见场景) +6. [FAQ](#6-faq) + +--- + +## 1. 快速开始 + +### 环境要求 + +- Python 3.10+ +- Git +- pip + +### 安装 + +```bash +# 克隆仓库 +git clone http://47.109.59.41:3000/xst/QaAutomationHub.git +cd QaAutomationHub + +# 安装依赖 +python3 -m venv .venv +source .venv/bin/activate # Windows: .\.venv\Scripts\activate +pip install -r requirements.txt +``` + +### 3 步上手 + +**第 1 步**: 把需求文档放进 `source_docs/requirements_raw/` + +``` +source_docs/requirements_raw/你的需求.docx +``` + +**第 2 步**: 确认项目画像已维护 + +``` +knowledge_base/00_project/project_profile.md +``` + +**第 3 步**: 运行 Fleet + +``` +/qe-fleet run source_docs/requirements_raw/你的需求.docx +``` + +Fleet 会自动完成:文档解析 → 知识激活 → 需求分析 → 冲突检测 → 风险评估 → 测试策略 → 测试点 → 用例 → 评审 → 覆盖审计 → 质量裁决 → Excel 导出。 + +--- + +## 2. 命令参考 + +### `/qe-fleet run` — 全流程自动化 + +```text +/qe-fleet run source_docs/requirements_raw/需求.docx +``` + +从需求到 Excel 一条命令搞定。等价于依次执行 prepare → analyze → design → review → export。 + +**参数**: +- `--zone <战区>`: 仅运行到指定战区(prepare/analyze/design/review/monitor) +- `--skip-export`: 跳过最终 Excel 导出 + +### `/qe-fleet prepare` — 仅准备 + +```text +/qe-fleet prepare source_docs/requirements_raw/需求.docx +``` + +执行文档解析 + 知识激活。适合先看看标准化后的需求是什么样。 + +### `/qe-fleet analyze` — 准备 + 分析 + +```text +/qe-fleet analyze source_docs/requirements_raw/需求.docx +``` + +执行到分析战区结束。产出需求分析、冲突报告、风险评估。 + +### `/qe-fleet design` — 准备 → 分析 → 设计 + +```text +/qe-fleet design source_docs/requirements_raw/需求.docx +``` + +执行到设计战区结束。产出测试策略、测试点、测试用例、测试数据。 + +### `/qe-fleet review` — 仅评审 + +```text +/qe-fleet review source_docs/requirements_raw/需求.docx +``` + +对已有产物执行评审(不重复生成)。产出评审报告、覆盖率审计、质量裁决。 + +### `/qe-fleet export` — 仅导出 + +```text +/qe-fleet export source_docs/requirements_raw/需求.docx +``` + +将已有测试用例导出为 Excel + 版本快照。 + +### `/qe-fleet monitor` — 执行分析 + 知识沉淀 + +```text +/qe-fleet monitor source_docs/requirements_raw/需求.docx --results test-results.xml +``` + +分析测试执行结果,分类失败,沉淀知识。 + +### `/qe-fleet status` — 查看进度 + +```text +/qe-fleet status source_docs/requirements_raw/需求.docx +``` + +查看当前需求的 Fleet 运行进度和产物状态。 + +### `/qe-fleet validate` — 验证 Agent 就绪 + +```text +/qe-fleet validate +``` + +验证所有 14 个 Agent prompt 文件是否存在且非空。 + +### 兼容命令 + +```text +/case_generate source_docs/requirements_raw/需求.docx +``` + +完全等同于 `/qe-fleet run`,向后兼容。 + +--- + +## 3. 工作流详解 + +### 3.1 标准流程 + +``` +需求文档 → PREPARE → ANALYZE → [人工确认] → DESIGN → REVIEW → [质量裁决] → EXPORT +``` + +### 3.2 确认门禁 + +当 Fleet 检测到以下情况时,会在 ANALYZE 战区后暂停: + +- 识别到历史相似需求 +- 检测到规则冲突(P0/P1 级别) +- 需求中存在 `⚠️ 待确认` 项 + +**此时你需要**: +1. 查看 `output/analysis/{需求名}_关联与冲突.md` +2. 在 `decisions/` 下创建确认单(使用 `确认结论模板.md`) +3. 确认状态写为 `确认状态:已确认` +4. 重新运行 `/qe-fleet run`(Fleet 会自动跳过已完成战区) + +### 3.3 质量裁决 + +评审战区结束后,quality-gatekeeper 会做出三级裁决: + +| 裁决 | 含义 | 后续操作 | +|------|------|----------| +| ✅ PASS | 全部达标 | 自动导出 Excel | +| 🔧 PASS_WITH_FIX | 已自动修复 | 自动导出 Excel | +| 🛑 BLOCKED | 有阻断项 | 查看裁决报告,修复后重跑 | + +### 3.4 Monitor 循环 + +``` +测试执行 → /qe-fleet monitor → 执行分析 → 知识沉淀 → 知识库自动更新 → 下次生成更准 +``` + +这是一个正向循环:用得越多,知识库越丰富,生成质量越高。 + +--- + +## 4. 知识库使用 + +### 4.1 知识库结构 + +``` +knowledge_base/ +├── 00_project/ # 项目画像(必须维护) +│ └── project_profile.md +├── 01_standards/ # 标准规范 +│ ├── terminology.md # 核心术语(常驻) +│ ├── terminology_optional_saas.md # 扩展术语(按需激活) +│ ├── test_case_template.md # 用例模板 +│ ├── definition_of_done.md # 完成标准 +│ └── review_checklist.md # 评审清单 +├── 02_history/ # 历史经验(手动+自动维护) +│ ├── common_missed_scenes.md # 易漏场景 +│ ├── historical_defects.md # 历史缺陷 +│ └── marketing_rules.md # 营销规则 +└── 03_best_practices/ # 最佳实践(手动+推荐) + ├── payment_flow_cases.md # 支付链路范例 + └── marketing_activity_cases.md # 营销活动范例 +``` + +### 4.2 知识激活机制 + +Fleet 不会全量加载所有知识库,而是按需求内容智能激活: + +- **常驻激活**: `terminology.md`、`template.md`、`definition_of_done.md`、`review_checklist.md` +- **关键词匹配**: 如需求包含"导购、分销、企微"→ 激活 `terminology_optional_saas.md` +- **语义匹配**: 计算需求与知识库文件的 Jaccard 相似度,≥ 0.08 则激活 + +### 4.3 知识缺口 + +当 Fleet 发现需求涉及但知识库无覆盖的领域时,会在 Prepare 战区输出 `knowledge_gaps`,提示你补充相关知识。 + +--- + +## 5. 常见场景 + +### 5.1 新需求首次生成 + +```bash +# 1. 放置文档 +cp 新人礼需求.docx source_docs/requirements_raw/ + +# 2. 一键生成 +# 在 CLI 中输入: +/qe-fleet run source_docs/requirements_raw/新人礼需求.docx + +# 3. 查看产物 +ls output/analysis/新人礼需求_* +ls output/test_points/新人礼需求_* +ls output/test_cases/新人礼需求_* +ls output/excel_reports/新人礼需求_* +``` + +### 5.2 已有需求的子需求迭代 + +```bash +# 1. 保留旧需求文档在 requirements/ 或 source_docs/requirements_raw/ +# 2. 放置新子需求 +cp 新人礼二期需求.docx source_docs/requirements_raw/ + +# 3. 运行 Fleet +/qe-fleet run source_docs/requirements_raw/新人礼二期需求.docx + +# 4. 如果 Fleet 在 ANALYZE 后暂停(检测到冲突) +# → 查看 output/analysis/新人礼二期需求_关联与冲突.md +# → 在 decisions/ 下创建确认单 +# → 确认单写"确认状态:已确认" +# → 重新运行 /qe-fleet run + +# 5. 按确认结论重跑受影响需求 +python3 scripts/case_pipeline.py apply-confirmation --requirement source_docs/requirements_raw/新人礼二期需求.docx +``` + +### 5.3 线上事故后回写 + +```bash +# 1. 手动补充历史缺陷 +# 编辑 knowledge_base/02_history/historical_defects.md +# 写明:模块、现象、根因、防御建议 + +# 2. 手动补充易漏场景 +# 编辑 knowledge_base/02_history/common_missed_scenes.md + +# 3. (可选)运行知识库健康检查 +/qe-fleet knowledge sync + +# 4. 下次运行 Fleet 时,新的历史经验会自动激活 +``` + +### 5.4 测试执行后分析 + +```bash +# 拿到测试执行结果后 +/qe-fleet monitor source_docs/requirements_raw/需求.docx --results junit-results.xml + +# 产出: +# - output/analysis/需求_执行分析.md(失败分类 + 根因分析) +# - knowledge_base/ 自动更新(P0 缺陷自动沉淀) +# - knowledge_gaps/ 待确认沉淀(P1-P3 需人工确认) +``` + +### 5.5 查看进度 + +```bash +/qe-fleet status source_docs/requirements_raw/需求.docx +``` + +输出示例: +``` +✅ prepare: completed +✅ analyze: completed +⏳ design: in_progress +⬜ review: pending +⬜ monitor: pending +``` + +--- + +## 6. FAQ + +### Q: 和原来的 /case_generate 有什么区别? + +`/qe-fleet` 是升级版,Agent 从 4 个增加到 14 个,新增了风险评估、测试策略、数据构造、覆盖率审计、质量门禁、执行分析、知识沉淀等能力。`/case_generate` 保留为别名。 + +### Q: 必须所有战区都跑吗? + +不必须。你可以分步运行: +- 只想看分析 → `/qe-fleet analyze` +- 只想看设计 → `/qe-fleet design` +- 只想导出 Excel → `/qe-fleet export` +- 只想分析测试结果 → `/qe-fleet monitor` + +### Q: Fleet 生成的用例能直接用吗? + +Fleet 生成的用例经过 4 个 Agent 协作(设计 → 评审 → 覆盖审计 → 质量裁决),质量裁决 PASS 后可以直接使用。但建议: +- 首次使用前先配置项目画像 +- 补充知识库中的具体业务规则 +- 对 P0 高风险用例做一次人工确认 + +### Q: 如何让 Fleet 越来越准? + +1. 维护项目画像(`knowledge_base/00_project/project_profile.md`) +2. 每次线上事故后回写到历史缺陷/易漏场景 +3. 积累高质量用例到最佳实践 +4. 使用 Monitor 战区的自动知识沉淀功能 +5. 定期运行 `/qe-fleet knowledge sync` 做知识库健康检查 + +### Q: 支持哪些文档格式? + +- `.docx`(推荐,结构保留最好) +- `.doc`(通过 pyantiword 解析) +- `.pdf`(通过 PDF 流解析,扫描件/图片型 PDF 质量差) +- `.md`(直接使用) +- `.txt`(直接使用) + +### Q: Excel 导出格式是什么? + +默认导出为云效字段模型(标题/编号/目录/创建时间/前置条件/步骤描述/预期结果/优先级/类型/URL)。可在 `fleet_config.yml` 中修改。 + +### Q: 如何关闭某个战区? + +编辑 `fleet_config.yml`: +```yaml +battle_zones: + monitor: + enabled: false # 关闭 Monitor 战区 +``` + +### Q: 如何调整质量门禁严格度? + +编辑 `fleet_config.yml`: +```yaml +quality_gate: + min_coverage: 0.90 # 降低覆盖率要求(默认 0.95) + max_blockers: 2 # 允许最多 2 个阻断项(默认 0) +``` + +### Q: 知识沉淀会自动修改我的知识库文件吗? + +Monitor 战区的 knowledge-curator Agent: +- **P0 确认缺陷**: 默认自动回写(可在 `fleet_config.yml` 中关闭) +- **P1-P3 缺陷**: 默认仅生成建议,需人工确认后回写 +- **去重保护**: 回写前自动检查与已有知识的相似度 +- **变更记录**: 所有自动回写会记录时间戳和来源 diff --git a/docs/superpowers/specs/2026-07-09-agentic-qe-fleet-design.md b/docs/superpowers/specs/2026-07-09-agentic-qe-fleet-design.md new file mode 100644 index 0000000..2316eff --- /dev/null +++ b/docs/superpowers/specs/2026-07-09-agentic-qe-fleet-design.md @@ -0,0 +1,357 @@ +# Agentic QE Fleet 集成设计 + +> 版本: 1.0 | 日期: 2026-07-09 | 状态: 已确认 + +## 1. 概述 + +将现有 QA Automation Hub(测试用例生成流水线)深度重构为 Agentic QE Fleet——一个由 14 个专业 AI Agent 组成的自治质量工程舰队,覆盖从需求输入到知识沉淀的全生命周期。 + +### 1.1 核心升级维度 + +| 维度 | 原项目 | QE Fleet | +|------|--------|----------| +| Agent 数量 | 4 | 14 | +| 战区划分 | 无 | 5 战区 (Prepare/Analyze/Design/Review/Monitor) | +| 编排方式 | skill 文件分散 | fleet_runner.py 集中编排 | +| 知识管理 | 静态手动维护 | 自动激活 + 自动回写沉淀 | +| 质量门禁 | 简单检查 | 三级裁决 (PASS/PASS_WITH_FIX/BLOCKED) | +| 覆盖率 | 无系统性追溯 | 需求→测试点→用例三级追溯 | +| 执行反馈 | 无 | Monitor 战区:结果分析 + 知识沉淀 | +| CLI 入口 | /case_generate | /qe-fleet + 子命令 | + +### 1.2 设计原则 + +- **战区隔离**:每个战区独立运行,通过标准化 manifest JSON 交接 +- **Agent 专职**:一个 Agent 只做一件事,文件短小精悍 +- **确定性编排**:fleet_runner.py 编排 Agent 执行顺序,不依赖 LLM 自主决策流程 +- **向后兼容**:保留 `/case_generate` 别名,保留 `case_pipeline.py` 接口 +- **产出一致**:输出格式与原项目兼容,现有用户无缝迁移 + +--- + +## 2. 整体架构 + +``` + /qe-fleet run <需求文档> + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ FLEET ORCHESTRATOR │ +│ scripts/fleet_runner.py │ +│ 读取 fleet_config.yml → 发令 → 收集 → 裁决 → 导出 │ +└──────────────────────────────────────────────────────────────┘ + │ │ │ │ + ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ + │ PREPARE │ │ ANALYZE │ │ DESIGN │ │ REVIEW │ + │ 准备战区 │───▶│ 分析战区 │───▶│ 设计战区 │───▶│ 评审战区 │ + │ 2 Agent │ │ 3 Agent │ │ 4 Agent │ │ 3 Agent │ + └─────────┘ └─────────┘ └─────────┘ └─────────┘ + │ + ┌────▼────┐ + │ MONITOR │ + │ 监控战区 │ + │ 2 Agent │ + └─────────┘ +``` + +### 2.1 14 个专业 Agent + +| # | 战区 | Agent ID | 名称 | 核心职责 | +|---|------|----------|------|----------| +| 1 | Prepare | `document-parser` | 文档解析专家 | docx/pdf/doc → MD 标准化 + 技术方案识别 | +| 2 | Prepare | `knowledge-activator` | 知识激活专家 | 按需求自动激活术语/规则/历史缺陷/最佳实践 | +| 3 | Analyze | `requirement-analyzer` | 需求分析专家 | 结构化需求模型 + 歧义标注 | +| 4 | Analyze | `conflict-detector` | 冲突检测专家 | 语义级历史需求冲突检测 | +| 5 | Analyze | `risk-assessor` | 风险评估专家 | 风险矩阵量化 (资损/可用性/数据/合规/兼容性) | +| 6 | Design | `test-strategist` | 测试策略师 | 分层测试策略 + 优先级矩阵 | +| 7 | Design | `testpoint-designer` | 测试点设计师 | 全面测试点矩阵 + 来源标注 | +| 8 | Design | `case-designer` | 用例设计师 | 可执行用例 + 双验证预期结果 | +| 9 | Design | `data-builder` | 数据构造师 | 精确测试数据集 | +| 10 | Review | `case-reviewer` | 用例评审师 | 用例质量/规范性/可执行性评审 | +| 11 | Review | `coverage-auditor` | 覆盖率审计师 | 需求→测试点→用例三级追溯覆盖 | +| 12 | Review | `quality-gatekeeper` | 质量门禁裁决官 | 三级裁决 (PASS/PASS_WITH_FIX/BLOCKED) | +| 13 | Monitor | `execution-analyst` | 执行结果分析师 | 失败归类 + 失败模式识别 + 根因推测 | +| 14 | Monitor | `knowledge-curator` | 知识沉淀师 | 自动回写知识库 + 去重保护 | + +### 2.2 战区数据流 + +``` +Prepare → manifest_prepare.json (标准化需求路径、激活的知识库清单、文档置信度) +Analyze → manifest_analyze.json (结构化需求、冲突列表、风险矩阵) +Design → manifest_design.json (测试策略、测试点、用例路径、测试数据路径) +Review → manifest_review.json (评审结论、覆盖率报告、门禁裁决) +Monitor ← 外部测试执行结果 (失败分析、知识回写建议) +``` + +--- + +## 3. 战区详细设计 + +### 3.1 Prepare 战区 + +**触发**: `/qe-fleet prepare <需求文档>` + +**document-parser**: +- 输入: 原始需求文档 (.docx/.doc/.pdf/.md) +- 处理: 解析 → 提取文本 → 标准化 Markdown +- 自动识别同主题技术方案 (文件名 + 内容相似度匹配) +- 标注解析置信度 +- 产出: `output/normalized_inputs/{BASE_NAME}/requirement.md` + +**knowledge-activator**: +- 输入: 标准化需求 + 项目画像 +- 激活策略: + - 常驻: `terminology.md` (核心术语)、`test_case_template.md`、`definition_of_done.md`、`review_checklist.md` + - 关键词匹配: 可选术语文件 (如 saas 术语) + - 语义匹配: 历史缺陷、最佳实践中与需求相关的条目 +- 激活结果记录在 manifest 中,附带激活理由 +- 自动检测知识缺口并建议补充 + +### 3.2 Analyze 战区 + +**触发**: `/qe-fleet analyze <需求文档>` + +**requirement-analyzer**: +- 结构化输出: 背景与目标 / 范围与边界 / 用户角色 / 关键业务规则 / 主流程 / 异常流程 / 技术约束 +- 项目画像差异化: 将通用约束替换为项目特定约束 +- 歧义标注: `> ⚠️ 待确认:问题、影响范围、建议确认方向` + +**conflict-detector**: +- 对比范围: 所有历史需求 (requirements/ + source_docs/) +- 冲突类型: 规则方向冲突 / 数值口径冲突 / 状态机冲突 / 权限冲突 +- 语义级检测: 结合术语解释判断规则语义是否真正矛盾 (不只看 n-gram) +- 严重度分级: P0(高风险)/P1(中风险)/P2(低风险)/P3(提示) + +**risk-assessor** (新增): +- 风险维度: 资损 / 可用性 / 数据 / 合规 / 兼容性 +- 量化矩阵: 可能性(1-5) × 影响度(1-5) = 风险等级 +- 高风险项自动映射为 P0 必测项 + +**Agent 协作**: analyzer 和 detector 可并行运行,risk-assessor 串行消费两者输出。 + +### 3.3 Design 战区 + +**触发**: `/qe-fleet design <需求文档>` + +**test-strategist** (新增): +- 输入: 风险矩阵 + 需求分析 +- 输出: 测试金字塔分配 / 优先级覆盖规则 / P0 清单 + +**testpoint-designer**: +- 输入: 策略 + 分析 + 冲突 + 历史缺陷 + 易漏场景 + 最佳实践 +- 覆盖维度: 主流程/异常/边界/状态流转/权限/并发幂等/弱网超时/历史缺陷防御/跨需求冲突防御 +- 来源标注: `[需求]` `[历史缺陷]` `[风险矩阵]` `[冲突修订]` `[项目画像]` `[漏测清单]` + +**case-designer**: +- 将测试点转化为可执行用例,严格使用 template 表头 +- 用例原子性: 一个用例一个验证点 +- 预期结果双验证: UI反馈 + 数据状态 +- 中后台页面: 补齐列表/按钮/只读态/状态权限映射/端间隔离 +- 保存前按 review_checklist 自检 + +**data-builder** (新增): +- 为用例构造精确测试数据: 账号/金额/商品ID/券码/状态枚举/边界值 +- 数据来源: 项目画像字段 + 术语枚举 + 需求具体数值 +- 输出: `{BASE_NAME}_测试数据.md` + +**执行顺序**: strategist → testpoint + data 并行 → case (消费 testpoint 和数据) + +### 3.4 Review 战区 + +**触发**: `/qe-fleet review <需求文档>` + +**case-reviewer**: 按 checklist 逐项检查,错误分级 (阻断/建议/优化),标注到具体行号。 + +**coverage-auditor** (新增): 需求→测试点→用例三级追溯矩阵,识别覆盖缺口和过度覆盖。 + +**quality-gatekeeper** (新增): +- 三级裁决: PASS / PASS_WITH_FIX / BLOCKED +- 裁决依据: 评审阻断项 + 覆盖率数据 + 风险矩阵 +- BLOCKED 时必须告知原因和解决路径 + +**并行策略**: reviewer 和 auditor 并行,gatekeeper 等待两者完成。 + +### 3.5 Monitor 战区 + +**触发**: `/qe-fleet monitor <测试结果文件>` 或 CI webhook + +**execution-analyst**: +- 支持格式: JUnit XML / JSON / 结构化 Markdown +- 失败归类: ENV_ISSUE / DATA_ISSUE / CASE_BUG / REAL_BUG +- 失败模式识别: 同一模式≥3次 → 标记为系统性问题 +- 根因推测 + 回归建议 + +**knowledge-curator**: +- 自动回写: REAL_BUG → historical_defects / 新漏测模式 → common_missed_scenes / 高质量用例 → best_practices +- 去重保护: Jaccard 相似度 + LLM 语义去重 +- 安全策略: P0 确认缺陷可配置自动生效,P1-P3 默认人工确认 +- 输出: 变更 diff + 建议说明 + +--- + +## 4. CLI 设计 + +### 4.1 命令体系 + +``` +/qe-fleet run <需求文档> # 全流程 +/qe-fleet prepare <需求文档> # 仅准备 +/qe-fleet analyze <需求文档> # 准备+分析 +/qe-fleet design <需求文档> # 准备→分析→设计 +/qe-fleet review <需求文档> # 仅评审已有产物 +/qe-fleet export <需求文档> # 仅 Excel 导出 +/qe-fleet monitor <测试结果> # 执行分析+知识沉淀 +/qe-fleet status <需求文档> # 进度和产物状态 +/qe-fleet knowledge sync # 知识库健康检查 +/qe-fleet knowledge gaps # 知识库覆盖缺口报告 +``` + +兼容: `/case_generate` = `/qe-fleet run` 别名 + +### 4.2 自动执行规则 (CLI) + +Agent 收到 `/qe-fleet run` 后必须自动完成: + +1. 解析需求文档路径,提取 BASE_NAME +2. 执行 `python3 scripts/fleet_runner.py run --requirement <路径>` +3. 按战区顺序执行,读取各阶段 manifest 状态 +4. 检查 confirmation_gate,必要时阻断并告知 +5. 最终汇总: 成功/失败、产物路径、质量裁决 + +--- + +## 5. 目录结构 + +``` +QaAutomationHub/ +├── .claude/ +│ ├── skills/qe_fleet/SKILL.md # Fleet skill 定义 +│ └── commands/qe_fleet.md # 子命令分发 +├── AGENTS.md # Codex CLI 入口 +├── agents/ +│ ├── prepare/ # 准备战区 (2 Agent) +│ │ ├── document_parser.md +│ │ └── knowledge_activator.md +│ ├── analyze/ # 分析战区 (3 Agent) +│ │ ├── requirement_analyzer.md +│ │ ├── conflict_detector.md +│ │ └── risk_assessor.md +│ ├── design/ # 设计战区 (4 Agent) +│ │ ├── test_strategist.md +│ │ ├── testpoint_designer.md +│ │ ├── case_designer.md +│ │ └── data_builder.md +│ ├── review/ # 评审战区 (3 Agent) +│ │ ├── case_reviewer.md +│ │ ├── coverage_auditor.md +│ │ └── quality_gatekeeper.md +│ └── monitor/ # 监控战区 (2 Agent) +│ ├── execution_analyst.md +│ └── knowledge_curator.md +├── scripts/ +│ ├── fleet_runner.py # 核心编排器 +│ ├── fleet_manifest.py # Manifest 管理 +│ ├── fleet_agents.py # Agent 加载和上下文注入 +│ ├── case_pipeline.py # 向后兼容 +│ ├── export_excel.py # Excel 导出 +│ └── governance_audit.py # 治理审计 +├── knowledge_base/ # 保持现有结构 +├── knowledge_gaps/ # 知识缺口追踪 +├── fleet_config.yml # Fleet 全局配置 +├── output/ # 输出产物 +├── docs/ +│ ├── USER_GUIDE.md # 用户指南 +│ └── MAINTENANCE_GUIDE.md # 维护指南 +└── README.md # 项目入口文档 +``` + +--- + +## 6. 配置文件 + +### fleet_config.yml + +```yaml +fleet: + name: "QE Fleet" + version: "2.0.0" + +battle_zones: + prepare: { enabled: true, auto_confirm: false } + analyze: { enabled: true, auto_confirm: false } + design: { enabled: true, auto_confirm: false } + review: { enabled: true, auto_confirm: false } + monitor: { enabled: true, auto_confirm: true } + +quality_gate: + min_coverage: 0.95 + max_blockers: 0 + require_data_builder: true + require_risk_assessment: true + +monitor: + auto_curate_p0: true + auto_curate_p1_p3: false + supported_formats: [junit, json, markdown] + +output: + excel_format: yunxiao + snapshot_keep: 3 + auto_sync_maintained: true +``` + +--- + +## 7. 关键技术决策 + +| 决策点 | 选项 | 理由 | +|--------|------|------| +| 编排方式 | Python (fleet_runner.py) | 确定性、可调试、可度量 | +| Agent 通信 | 文件 + manifest JSON | 战区隔离、故障域清晰、可回溯 | +| CLI 入口 | Claude CLI + Codex CLI | 与现有基础设施一致、零额外部署 | +| 知识激活 | 关键词 + 语义匹配 | 精确匹配保底、语义扩展覆盖 | +| 冲突检测 | n-gram + 术语语义 | 提升检测精度,减少误报 | +| 质量门禁 | 三级裁决 | 允许自动修复,精准阻断高危项 | +| 知识沉淀 | 自动建议 + 人工确认 | P0 紧急可控、P1-P3 质量把关 | +| Excel 导出 | 保持云效格式 | 向后兼容现有平台 | +| 版本快照 | 保持固定文件名 + versions/ | 向后兼容、日常无感知 | + +--- + +## 8. 与原项目兼容性 + +- `/case_generate` 保留为 `/qe-fleet run` 别名 +- `case_pipeline.py` 的 prepare/verify/export 接口保留 +- 输出文件命名规则不变 (固定文件名 + versions/ 快照) +- knowledge_base 目录结构不变 (monitor 追加不修改) +- Excel 导出格式保持云效字段模型 +- 向下兼容: 仅用原 4 Agent 也可工作 + +--- + +## 9. 实现范围 + +### Phase 1: 核心重构 +- [ ] 创建新目录结构 +- [ ] 编写 fleet_config.yml +- [ ] 实现 fleet_runner.py (编排器) +- [ ] 实现 fleet_manifest.py (manifest 管理) +- [ ] 实现 fleet_agents.py (Agent 加载) +- [ ] 升级 .claude/ skill 和 command +- [ ] 升级 AGENTS.md + +### Phase 2: Agent 迁移+新增 +- [ ] 迁移 + 升级 Prepare 战区 Agent (2→2) +- [ ] 迁移 + 升级 Analyze 战区 Agent (1→3) +- [ ] 迁移 + 升级 Design 战区 Agent (1→4) +- [ ] 迁移 + 升级 Review 战区 Agent (1→3) +- [ ] 新建 Monitor 战区 Agent (0→2) + +### Phase 3: 文档 +- [ ] USER_GUIDE.md +- [ ] MAINTENANCE_GUIDE.md +- [ ] README.md 重写 + +### Phase 4: 推送部署 +- [ ] Git 仓库初始化 + 推送 +- [ ] 验证 /qe-fleet run 端到端流程 diff --git a/fleet_config.yml b/fleet_config.yml new file mode 100644 index 0000000..fc15041 --- /dev/null +++ b/fleet_config.yml @@ -0,0 +1,138 @@ +# ============================================================================= +# Agentic QE Fleet 全局配置 +# ============================================================================= +# 本文件控制 Fleet 的所有行为开关、质量门禁阈值和 Agent 编排策略。 +# 修改后下次运行 /qe-fleet 生效,无需重启服务。 + +fleet: + name: "Agentic QE Fleet" + version: "2.0.0" + description: "14 个专业 AI Agent 组成的自治质量工程舰队" + +# --------------------------------------------------------------------------- +# 战区配置 +# --------------------------------------------------------------------------- +# enabled: 是否启用该战区 +# auto_confirm: 该战区产物是否自动确认(true=跳过人工确认直接进入下一战区) +# 注意: Monitor 战区默认 auto_confirm=true,因为它的产物是建议性的 + +battle_zones: + prepare: + enabled: true + auto_confirm: false + description: "文档解析 + 知识激活" + + analyze: + enabled: true + auto_confirm: false + description: "需求分析 + 冲突检测 + 风险评估" + + design: + enabled: true + auto_confirm: false + description: "测试策略 + 测试点 + 用例 + 测试数据" + + review: + enabled: true + auto_confirm: false + description: "用例评审 + 覆盖率审计 + 质量门禁" + + monitor: + enabled: true + auto_confirm: true + description: "执行分析 + 知识沉淀(建议性产出)" + +# --------------------------------------------------------------------------- +# 质量门禁 +# --------------------------------------------------------------------------- +quality_gate: + # 最低需求覆盖率(需求条目 → 测试用例追溯比例) + min_coverage: 0.95 + + # 最大允许阻断项(超过则 BLOCKED) + max_blockers: 0 + + # 是否强制要求测试数据构造 + require_data_builder: true + + # 是否强制要求风险评估 + require_risk_assessment: true + + # 评审阻断项自动修复最大重试次数 + auto_fix_max_retries: 3 + +# --------------------------------------------------------------------------- +# Monitor 战区(知识沉淀回写策略) +# --------------------------------------------------------------------------- +monitor: + # P0 确认缺陷自动沉淀到知识库(无需人工确认) + auto_curate_p0: true + + # P1-P3 缺陷默认需人工确认后才沉淀 + auto_curate_p1_p3: false + + # 支持的测试结果格式 + supported_formats: + - junit # JUnit XML + - json # 自定义 JSON + - markdown # 结构化 Markdown 报告 + + # 失败模式识别阈值:同一模式出现 N 次以上标记为系统性问题 + pattern_detection_threshold: 3 + + # 知识去重相似度阈值 (Jaccard) + dedup_similarity_threshold: 0.65 + +# --------------------------------------------------------------------------- +# 输出配置 +# --------------------------------------------------------------------------- +output: + # Excel 导出格式 + excel_format: "yunxiao" # 云效字段模型 + + # 版本快照保留数量 + snapshot_keep: 3 + + # export 后是否自动同步正式维护版到 requirements/ + auto_sync_maintained: true + + # 产物编码 + encoding: "utf-8" + +# --------------------------------------------------------------------------- +# 冲突检测配置 +# --------------------------------------------------------------------------- +conflict_detection: + # 关联需求相似度阈值 (Jaccard) + related_similarity_threshold: 0.015 + + # 冲突判定相似度阈值 (Jaccard) + conflict_similarity_threshold: 0.22 + + # 最大关联需求数 + max_related_requirements: 5 + + # 启用语义级冲突检测 (结合术语解释判断) + semantic_conflict_detection: true + +# --------------------------------------------------------------------------- +# 知识激活配置 +# --------------------------------------------------------------------------- +knowledge_activation: + # 常驻知识(始终激活,不受需求内容影响) + permanent: + - "knowledge_base/01_standards/terminology.md" + - "knowledge_base/01_standards/test_case_template.md" + - "knowledge_base/01_standards/definition_of_done.md" + - "knowledge_base/01_standards/review_checklist.md" + + # 关键词匹配规则 + keyword_rules: + - name: "saas" + path: "knowledge_base/01_standards/terminology_optional_saas.md" + keywords: + - "导购", "分销", "佣金", "企微", "企业微信", "私域" + - "储值", "礼品卡", "会员储值" + + # 语义匹配最低相似度 + semantic_match_threshold: 0.08 diff --git a/knowledge_base/00_project/project_profile.md b/knowledge_base/00_project/project_profile.md new file mode 100644 index 0000000..f8be60e --- /dev/null +++ b/knowledge_base/00_project/project_profile.md @@ -0,0 +1,78 @@ +# 项目画像(通用基线版,请按项目实际情况维护) + +> 用途:为本仓库内的测试分析、测试点、测试用例提供“项目差异化约束”。 +> +> 使用原则:本文件先提供测试行业常用的通用基线,后续项目落地时必须补充项目真实规则;若与正式需求、产品说明、接口文档冲突,以项目最新确认结论为准。 +> +> 维护要求:当架构、业务模式、风控策略、履约策略、权限模型、计费规则等发生变更时,必须同步更新本文件。 + +## 1. 项目简介 + +- 项目名称:待补充 +- 项目类型:默认按中后台系统、交易类系统、会员营销系统、内容平台或工具平台中的一种理解,未明确前不得擅自细化。 +- 目标用户:默认至少包含终端用户、运营人员、客服/审核人员、系统管理员中的部分角色,具体以项目实际为准。 +- 核心业务域:待补充,可从下单、履约、营销、会员、支付、退款、内容发布、审批流等域中选择。 +- 当前版本范围:以本次 `requirements/` 下需求文档为准,未写入需求范围的能力默认不纳入本期。 + +## 2. 业务边界与禁用能力 + +- 本项目明确不支持的能力:凡需求文档、接口文档、项目说明中未声明支持的能力,默认按“不支持”处理,不得在测试分析和测试用例中臆造。 +- 受监管或合规限制的流程:涉及支付、退款、发票、实名、隐私数据、权限审批、操作留痕等能力时,默认认为存在合规要求,测试中需覆盖鉴权、留痕、异常拦截和数据保护。 +- 灰度/地域/渠道差异说明:若需求未明确,则默认检查 Web/H5/App/小程序、测试环境与生产配置、不同地域或租户下是否存在规则差异,并将其作为待确认项。 + +## 3. 核心业务规则(项目级) + +- 结算与支付规则:凡涉及金额、折扣、积分、优惠、手续费、税费、退款分摊时,默认要求金额计算可追溯、展示口径一致、前后端结果一致、重复提交不导致重复扣减。 +- 营销与优惠规则:默认检查优惠互斥、叠加优先级、门槛命中、适用范围、失效时间、回滚恢复和异常场景;若项目无营销能力,应在项目化配置中显式写明。 +- 风控与权限规则:默认要求关键操作具备角色限制、越权拦截、二次确认或风控校验;高风险动作失败时不得进入成功态。 +- 状态流转规则:默认所有核心对象都应具备明确状态机约束,禁止跳状态、逆状态、重复终态处理;异常中断后状态应可解释、可恢复、可追踪。 + +## 4. 技术与集成约束 + +- 关键依赖系统:默认关注认证中心、用户中心、库存/资源中心、支付网关、消息系统、营销中心、风控系统、文件服务、第三方回调服务等;实际未接入的依赖请在项目化配置中删除。 +- 外部接口约束:默认按存在超时、重试、幂等、限流、熔断、降级、重复回调、乱序回调、部分成功等风险设计测试。 +- 数据一致性策略:若需求未明确,默认按“关键交易结果最终一致、关键扣减与状态更新需具备幂等保护”理解,并将强一致/最终一致边界列入待确认。 + +## 5. 高风险场景与历史事故 + +- 资损风险点:重复提交、重复支付、重复退款、优惠多减、金额少收/多收、库存超扣、积分或券未正确回滚、并发下重复创建资源。 +- 可用性风险点:弱网超时、接口抖动、依赖不可用、消息延迟、回调丢失、页面重复点击、前后端缓存不一致、批量操作部分失败。 +- 数据风险点:脏数据兼容、历史数据迁移、空值/默认值污染、精度丢失、跨租户串数据、删除后残留引用。 +- 典型线上事故防御策略:默认对核心链路补充幂等、重试、回滚、补偿、审计日志、告警阈值和人工兜底场景。 + +## 6. 测试策略偏好(项目定制) + +- P0 优先覆盖链路:登录鉴权、核心主流程、金额/资源扣减、状态变更、提交确认、异常回滚、查询展示一致性。 +- 必测异常场景:参数非法、前置条件不满足、重复点击、并发提交、超时重试、依赖失败、权限不足、状态已变化、数据部分缺失。 +- 非功能重点:弱网、超时、高并发、权限隔离、接口幂等、审计日志、性能基线、兼容性、可观测性。 +- 自动化优先级建议:稳定且高频回归的核心主流程、历史缺陷高发链路、金额与状态计算逻辑、关键接口鉴权与幂等校验优先自动化。 + +## 7. 需求冲突判定口径 + +- 什么情况下判定为“冲突”: + - 同一对象在不同需求中出现相反规则。 + - 同一阈值、时效、次数、金额口径不一致。 + - 同一状态机的起点、终点、流转条件描述不一致。 + - 同一角色权限、数据范围、可见性规则前后不一致。 + - 新需求声明“沿用旧逻辑”,但实际描述已改变旧规则。 +- 冲突处理优先级规则: + - 优先看当前版本正式需求是否显式声明“替换/废弃/继承”旧规则。 + - 若无显式声明,优先要求补充版本边界、生效范围、影响模块。 + - 若涉及金额、库存、权益、权限、安全、合规,默认按高风险冲突处理,必须在需求阶段确认。 +- 需求文档修改建议模板: + - 建议类型:规则合并 / 版本边界补充 / 阈值统一 / 状态机修正 / 权限口径修正 / 异常处理补充 + - 建议描述:明确指出当前需求条目、冲突来源条目、冲突点和推荐修订文本方向 + - 影响范围:说明影响的角色、模块、接口、数据口径、历史兼容逻辑、回归范围 + +## 8. 通用输出约束(供测试任务直接使用) + +- 需求分析必须同时覆盖功能目标、边界、角色、前置条件、状态流转、关键规则、风险与待确认项。 +- 测试点必须同时覆盖主流程、异常流程、边界条件、数据校验、状态流转、权限控制、并发幂等、弱网超时、历史缺陷防御。 +- 测试用例预期结果必须同时覆盖 UI/接口反馈和数据状态变化,不能只写“成功”或“失败”。 +- 若需求存在歧义、缺字段、缺状态、缺口径,必须显式输出: + +```md +> ⚠️ 待确认:问题、影响范围、建议确认方向 +``` + +- 对于高风险业务规则,若项目未明确给出,不得基于经验直接写死为真实规则,只能作为风险假设或待确认项输出。 diff --git a/knowledge_base/01_standards/definition_of_done.md b/knowledge_base/01_standards/definition_of_done.md new file mode 100644 index 0000000..804abd1 --- /dev/null +++ b/knowledge_base/01_standards/definition_of_done.md @@ -0,0 +1,86 @@ +# 测试用例完成定义 + +面向当前仓库的固定流水线,一个测试任务被视为“完成”,不是只看是否产出了文档,而是看产出的测试点和测试用例是否已经达到可评审、可执行、可追溯、可导出的质量门槛。 + +## 1. 覆盖范围达标 + +至少覆盖以下维度,不得只覆盖主流程: + +- 主流程:核心业务 Happy Path 必须覆盖。 +- 异常流程:参数非法、前置条件不满足、状态不允许、库存不足、余额不足、权限不足、重复操作、超限操作等必须覆盖。 +- 边界条件:最小值、最大值、临界值、空值、默认值、长度边界、数量边界、时间边界、金额边界必须按需求实际场景覆盖。 +- 状态流转:创建、提交、生效、失效、取消、关闭、退款、撤回、恢复等关键状态变化必须覆盖。 +- 规则组合:多条件叠加、互斥、优先级、覆盖关系、兜底规则必须覆盖。 +- 数据校验:展示数据、落库数据、统计口径、列表汇总、详情字段、外部回传字段必须覆盖。 +- 中后台页面:如果需求包含列表、新建、编辑、查看、失效、删除、筛选、选择弹窗等管理能力,除失败态外,正向成功链路和交互细项也必须覆盖。 +- 历史风险:`common_missed_scenes.md`、`historical_defects.md` 中与当前需求相关的高风险场景必须映射到测试点或测试用例。 +- 跨需求影响:`关联与冲突.md` 中识别出的关联规则、潜在冲突、修订建议,必须至少落一条可验证测试点或用例。 +- 项目差异化:`project_profile.md` 中的业务边界、禁用能力、特殊限制、风控要求必须体现。 + +## 2. 风险覆盖达标 + +对于高风险模块,测试用例必须体现风险导向,而不是平均用力。 + +- P0/P1 业务规则必须覆盖正向、逆向、边界、互斥、并发或幂等中的关键场景。 +- 资金、库存、优惠、权益、订单状态等资损类风险,必须覆盖结果一致性校验。 +- 涉及角色隔离、数据隔离、门店隔离、组织隔离的需求,必须覆盖权限与越权场景。 +- 涉及外部系统、回调、异步任务、延迟生效、重试补偿的需求,必须覆盖时序异常与重复触发场景。 + +## 3. 用例设计质量达标 + +每条用例必须满足以下要求: + +- 原子性:一条用例只验证一个主验证点,避免多个核心断言混在一条用例里。 +- 可执行:前置条件、步骤、测试数据明确,执行人无需猜测。 +- 可验证:预期结果必须同时覆盖 UI/接口反馈和数据状态变化。 +- 可定位:失败后能大致判断是前端展示、后端规则、数据落库、异步处理还是外部依赖问题。 +- 可复现:步骤中要包含关键账号、角色、商品、金额、券、积分、库存、时间或状态等必要数据。 +- 类型准确:`类型` 字段必须使用标准枚举,且与用例真实目的匹配。 +- 术语一致:术语表达必须与生效术语文件一致,不混用平台券、商家券、店铺券等概念。 +- 模块清晰:`模块` 字段应使用层级路径表达,例如 `平台端-营销管理-商家优惠券`。 + +## 4. 测试点到用例的映射达标 + +- 每个高价值测试点都应被至少一条用例承接。 +- 历史缺陷防御点、漏测清单项、冲突修订项不得只停留在测试点层,必须在用例层落地。 +- 如果某类测试点因为当前范围不做用例,必须明确写出原因,不能静默遗漏。 + +## 5. 待确认项处理达标 + +- 需求存在缺失、冲突、口径不明时,必须显式写出: + +```md +> ⚠️ 待确认:问题、影响范围、建议确认方向 +``` + +- 待确认项影响主流程、金额、库存、权限、状态流转、优惠规则时,不得擅自臆造高风险业务规则。 +- 对待确认项,至少要补一条“确认后需重点回归”的提醒性测试点或备注。 + +## 6. 非功能与稳健性覆盖达标 + +以下场景按风险选择,不要求机械凑数量,但高风险需求不能缺失: + +- 幂等:重复提交、重复支付、重复领券、重复核销、重复回调。 +- 并发:多人抢占、库存竞争、优惠领取竞争、重复操作竞争。 +- 弱网与超时:请求超时、接口重试、页面重复点击、异步结果延迟返回。 +- 兼容与稳定性:分页、排序、筛选、导入导出、大数据量、长名称、特殊字符。 +- 权限与审计:无权限、低权限、跨组织、跨门店、跨商家操作及日志留痕。 +- 展示状态:若多个业务状态会影响 C 端资格、弹窗或按钮展示,需拆分到可定位的独立用例,不得用一条模糊负向用例笼统代替。 + +## 7. 评审与产物达标 + +- `output/analysis/{BASE_NAME}_分析.md`、`output/test_points/{BASE_NAME}_测试点.md`、`output/test_cases/{BASE_NAME}_测试用例.md` 必须完整存在。 +- 测试用例表头必须严格符合 `test_case_template.md` 的唯一规范。 +- 测试用例文件必须可通过 `verify` 校验,并可成功 `export` 为 Excel。 +- 评审阶段必须对照本 DoD、历史缺陷、关联冲突报告、项目画像进行检查;未达标时应直接修正原文件,不另起草稿。 + +## 8. 默认判定原则 + +如果产物满足以下任一情况,则不能视为完成: + +- 只有主流程,没有异常、边界、状态流转或冲突防御场景。 +- 用例步骤泛化,没有可执行的数据。 +- 预期结果只有“提示成功/失败”,没有数据状态校验。 +- 历史缺陷、漏测清单、项目差异化约束未落入测试点或用例。 +- 明显存在待确认问题,但未显式标记。 +- Markdown 表格不规范,导致后续 `verify` 或 `export` 无法通过。 diff --git a/knowledge_base/01_standards/review_checklist.md b/knowledge_base/01_standards/review_checklist.md new file mode 100644 index 0000000..caa2313 --- /dev/null +++ b/knowledge_base/01_standards/review_checklist.md @@ -0,0 +1,105 @@ +# 测试用例评审清单 + +这份清单服务于当前仓库的测试用例生成与自审流程,用于把 `definition_of_done.md` 中的原则拆成可逐项检查的执行项。 + +使用原则: + +- `testcase-designer` 在写入最终用例前,必须先按本清单自检并直接修正。 +- `testcase-reviewer` 在评审阶段,必须按本清单逐类检查并直接修正阻断项。 +- 发现问题时,优先修正源用例,不输出额外草稿文件。 + +## 1. 阻断项 + +以下任一项不满足,都不能视为通过: + +- 表头不是标准唯一表头。 +- Markdown 表格格式错误,可能导致 `verify` 或 `export` 失败。 +- 只有主流程,没有异常、边界、状态流转或冲突防御场景。 +- `预期结果` 只写页面提示,没有数据状态校验。 +- `测试步骤`、`测试数据` 过于抽象,执行人无法直接操作。 +- 需求存在明显待确认项,但未显式标注 `> ⚠️ 待确认:...`。 +- 历史缺陷、漏测场景、项目差异化约束、关联冲突修订没有落到测试点或用例。 + +## 2. 结构与格式检查 + +- 是否严格使用唯一表头顺序: + - `用例编号` + - `模块` + - `用例标题` + - `优先级` + - `类型` + - `前置条件` + - `测试步骤` + - `测试数据` + - `预期结果` + - `备注` +- `类型` 是否使用允许枚举:`功能测试`、`性能测试`、`兼容性测试`、`易用性测试`、`安全性测试`、`冒烟测试`、`回归测试`、`其他`。 +- `模块` 是否采用层级路径写法,例如 `平台端-营销管理-商家优惠券`。 +- `用例编号` 是否稳定、可读、与模块归属一致。 +- `备注` 是否只承载待确认项、来源说明、AI 修正原因等必要信息,而不是堆重复内容。 + +## 3. 覆盖性检查 + +逐个模块检查以下维度是否都有体现;若不适用,需能说明原因: + +- 主流程 +- 异常流程 +- 边界条件 +- 状态流转 +- 规则组合、互斥、优先级 +- 幂等与并发 +- 权限与越权 +- 弱网、超时、异步或回调异常 +- 历史缺陷防御 +- 跨需求冲突防御 +- 项目差异化高风险约束 +- 如果需求包含中后台 CRUD/配置页,是否覆盖了列表字段、查询/重置、查看只读、新建/编辑/删除/失效、操作栏权限与状态映射、端间数据隔离。 +- 如果需求包含中后台 CRUD/配置页,除失败态和限制态外,是否显式覆盖了新建成功、编辑成功等正向主链路。 +- 如果需求包含“选择商品/优惠券/奖品/门店”等弹窗或选择器,是否覆盖了刷新、搜索、字段展示、分页、排序、单选/多选反馈和归属范围差异。 +- 如果需求包含“未开始/进行中/已结束/已失效”等状态对 C 端展示或资格判断有影响,是否按状态拆成独立可定位用例,而不是笼统合并。 + +## 4. 单条用例质量检查 + +每条用例至少检查以下问题: + +- 是否只验证一个主验证点,避免多个关键断言混杂。 +- 前置条件是否完整,是否缺账号、角色、商品、库存、金额、券、积分、订单状态等必要上下文。 +- 测试步骤是否按执行顺序书写,是否存在“正常操作”“提交并校验”等笼统描述。 +- 测试数据是否具体,而不是“输入合法金额”“选择一张优惠券”这类泛化表达。 +- 预期结果是否同时包含 UI/接口反馈和数据状态变化。 +- 失败后是否能大致定位到前端、后端、规则、数据、异步链路或外部依赖。 + +## 5. 风险场景检查 + +以下高风险场景应按需求实际风险决定是否覆盖,涉及则不应缺失: + +- 金额计算、优惠分摊、退款分摊、实付金额一致性 +- 库存扣减、回滚、超卖、重复扣减 +- 优惠叠加、互斥、门槛校验、适用范围校验 +- 权益发放、核销、回退、重复使用 +- 外部回调、重复通知、延迟通知、补偿重试 +- 角色越权、数据越权、跨门店/跨商家/跨组织访问 + +## 6. 追溯与来源检查 + +- 关键测试点是否能追溯到需求、历史缺陷、漏测清单、营销规则、项目画像或冲突修订。 +- 对历史缺陷补充的用例,`备注` 是否标注了合理来源,例如 `[AI修正: 补充历史缺陷防御]`。 +- 对冲突修订补充的用例,`备注` 是否标注了修正原因,例如 `[AI修正: 冲突修订]`。 + +## 7. 待确认项检查 + +- 是否存在需求缺失、规则冲突、口径不明但被直接当成事实写进用例的情况。 +- 对资金、库存、权限、状态流转、营销规则相关的不确定规则,是否避免了臆造。 +- 待确认项是否写清楚问题、影响范围和建议确认方向。 + +## 8. 优先级检查 + +- P0 是否真正对应核心主流程、核心资损风险或关键状态流转。 +- P1 是否覆盖主要功能、关键异常、重要边界。 +- P2 是否承载次要异常、兼容性、补充性验证,而不是把高风险场景错误下沉到 P2。 + +## 9. 评审结论规则 + +- 若存在阻断项,必须直接修正原文件后再结束。 +- 若存在覆盖盲区但当前需求范围确实不支持,应显式写出原因,不得静默遗漏。 +- 若修正了已有用例或新增了防御用例,应在 `备注` 中保留必要的 `AI修正` 说明。 diff --git a/knowledge_base/01_standards/terminology.md b/knowledge_base/01_standards/terminology.md new file mode 100644 index 0000000..ee25fab --- /dev/null +++ b/knowledge_base/01_standards/terminology.md @@ -0,0 +1,114 @@ +# 业务术语表 + +> 本文件定义了项目中长期稳定、跨商城场景高频出现的核心业务术语。AI 在生成分析、测试点和测试用例时必须优先使用这些词汇,禁止混用口语化同义词。 +> +> 使用原则: +> 1. 先使用本术语表中的标准词。 +> 2. 若需求文档使用了不同但已明确的项目术语,以需求文档为准。 +> 3. 对于行业内存在多种解释的术语,生成结果中应尽量写全称,避免简称歧义。 +> 4. 私域、分销、储值、CRM 等可选领域术语不放在本文件常驻输入中,由流水线按需求内容自动识别后补充加载。 + +## 1. 角色与业务主体 + +- **平台端**: 平台总部或品牌总部使用的管理后台,用于统一管理店铺、商品、营销、订单、会员、数据等能力。 +- **商家端**: 商家、门店、代理商或直营网点使用的经营后台,用于管理本商家可见范围内的商品、订单、营销和资产。 +- **用户端**: C 端消费者使用的前台,包括 App、H5、小程序、PC 商城等购买入口。 +- **店铺**: 面向用户提供商品或服务经营能力的业务主体,可理解为商家经营单元。 +- **门店**: 线下经营或履约单元,可能具备独立库存、独立配送范围、独立核销能力。 +- **供应商**: 提供商品、履约或货源支持的上游主体,可能影响供货价、库存和发货链路。 +- **运营人员**: 负责配置商品、活动、价格、运费、会员权益等后台能力的业务角色。 +- **客服**: 负责处理订单咨询、退款、售后、补单、异常协查等问题的后台角色。 + +## 2. 商品与库存 + +- **SPU**: 标准产品单位,表示一类商品的抽象定义,如“iPhone 15”。 +- **SKU**: 库存量单位,表示商品的最小可售规格,如“iPhone 15 黑色 256G”。 +- **商品规格**: 用于区分 SKU 的销售属性组合,如颜色、尺码、套餐版本。 +- **可售库存**: 当前可用于下单扣减的库存数量,不等于物理库存。 +- **锁定库存**: 用户下单后暂时占用、待支付或待确认释放的库存。 +- **回滚库存**: 订单取消、支付失败或超时关闭后,系统释放此前锁定库存的动作。 +- **超卖**: 实际成功售卖数量超过可售库存的异常情况。 + +## 3. 购物与订单 + +- **购物车**: 用户临时存放待购买商品的列表,不代表价格、库存、优惠结果最终已锁定。 +- **结算页**: 用户提交订单前的确认页面,用于展示最新商品、价格、库存、运费、优惠和实付信息。 +- **提交订单**: 用户确认购买并生成订单的动作,通常意味着进入待支付或待确认状态。 +- **订单**: 用户购买商品或服务形成的业务单据,是支付、履约、退款、售后的核心载体。 +- **子订单**: 拆单后形成的订单明细单元,常见于多商家、多仓、多履约场景。 +- **订单状态**: 订单在生命周期中的阶段标识,如待支付、待发货、待收货、已完成、已取消。 +- **订单关闭**: 因超时未支付、风控失败、系统取消等原因终止订单继续流转。 +- **逆向流程**: 订单创建后的反向处理链路,如取消、退款、退货退款、换货、补偿。 + +## 4. 支付与资金 + +- **待支付订单**: 已成功创建但尚未完成付款的订单状态。 +- **支付单**: 订单在支付环节生成的资金请求单据,用于对接三方支付或内部资金系统。 +- **支付流水**: 记录支付请求、支付结果、渠道单号、金额等信息的资金明细。 +- **支付方式**: 用户完成付款所使用的资金渠道,如微信支付、支付宝、余额、积分抵现。 +- **实付金额**: 用户最终实际支付的金额,通常等于应付金额减去各类可抵扣权益后的结果。 +- **原路退回**: 退款时按原支付方式、原资金路径退还用户资产的处理方式。 +- **挂账**: 订单生成但尚未完成付款,系统已形成应收记录但未完成资金入账的状态。 +- **资损**: 因金额计算错误、优惠错误、重复退款、库存超卖等问题导致平台或商家资产损失。 + +## 5. 营销与优惠 + +- **优惠券**: 可在满足条件时抵扣订单金额或给予折扣的营销权益。 +- **平台券**: 由平台统一发放和承担成本的优惠券,通常作用于平台范围内指定商品、店铺或活动。 +- **商家券**: 由商家或店铺发放和承担成本的优惠券,通常只在指定商家范围内可用。 +- **品类券**: 只对指定商品类目或商品范围生效的优惠券。 +- **满减券**: 订单或商品金额达到门槛后才可生效的优惠券。 +- **折扣券**: 以打折方式生效的优惠券,而非固定金额抵扣。 +- **优惠门槛**: 优惠生效所需满足的条件,如满 100 可减 20。 +- **适用范围**: 某个优惠可生效的商品、类目、店铺、用户、渠道或时间范围。 +- **优惠叠加**: 多种优惠是否允许同时生效的规则。 +- **优惠互斥**: 多种优惠不能同时生效,只能按优先级或选择规则命中其一。 +- **试算**: 在正式提交前,根据当前商品、用户、活动、券、积分等条件预估优惠和实付结果的过程。 +- **优惠分摊**: 将订单级优惠按规则分配到商品、子单、退款项上的计算过程。 +- **一口价**: 以固定成交价覆盖常规价格和营销计算链路的促销方式。 +- **满减送**: 满足金额或件数条件后,给予减价、赠品或附加权益的活动方式。 +- **限时折扣**: 在指定时间内生效的临时价格优惠活动。 +- **秒杀**: 在短时间窗口内以强时效、强库存约束进行的促销活动。 +- **拼团**: 通过多人组团满足条件后生效的营销活动。 + +## 6. 会员与权益 + +- **会员**: 具备等级、成长值、标签、专属价格或专属权益的用户身份。 +- **会员等级**: 用于区分会员权益范围的层级,如普通会员、VIP、SVIP。 +- **会员价**: 针对会员身份生效的专属销售价格或折扣规则。 +- **会员权益**: 会员可享受的专属能力,如会员价、包邮、积分加速、专属券等。 +- **积分**: 可用于兑换、抵现、成长或运营激励的虚拟权益资产。 +- **积分抵现**: 将积分按兑换比例折算为订单可抵扣金额的能力。 +- **余额**: 用户在平台或商家体系内预存或结余的可消费资金。 + +## 7. 履约、售后与核销 + +- **履约**: 订单支付后到商品或服务完成交付之间的执行链路,如发货、配送、自提、到店消费。 +- **发货**: 商家或仓库将商品交给物流或配送体系的动作。 +- **签收**: 用户确认收到商品或系统确认妥投的状态。 +- **售后**: 支付后围绕退款、退货、换货、补寄、补偿等问题展开的处理流程。 +- **整单退款**: 针对订单全部商品和全部已支付金额发起的退款。 +- **部分退款**: 针对订单中的部分商品、部分数量或部分金额发起的退款。 +- **退款分摊**: 在部分退款场景下,将优惠、运费、积分等按规则分配到退款项的过程。 +- **核销**: 对券码、提货码、预约码、订单码等进行校验并确认已消费/已使用的动作,常见于到店自提、到店服务、卡券消费场景。 +- **核销码**: 用于完成核销动作的识别码、二维码、条形码或数字口令。 + +## 8. 风控、权限与系统能力 + +- **风控拦截**: 系统判定交易、账号或操作存在风险后自动阻断继续执行的行为。 +- **权限控制**: 根据角色、数据范围、组织关系等限制用户可见、可操作范围的机制。 +- **越权**: 用户访问或操作了其本不应拥有权限的数据或功能。 +- **幂等性**: 同一业务请求被重复提交时,系统结果保持一致,且不会重复扣款、重复创建、重复退款或重复扣减资源。 +- **并发**: 多个请求或多个用户在同一时间窗口内对同一资源发起操作的情况。 +- **限流**: 为保护系统稳定性,对请求频率或并发量做出的限制。 +- **降级**: 当依赖异常或系统压力过大时,关闭部分非核心能力以保证核心链路可用的处理策略。 +- **回调**: 外部系统处理完成后主动通知本系统结果的机制,常见于支付、退款、物流等链路。 +- **审计日志**: 用于记录关键操作人、操作时间、操作对象和变更结果的可追溯日志。 + +## 9. 术语使用约束 + +- **订单关闭** 与 **退款成功** 不是同义词,关闭订单不代表资金已经退回。 +- **优惠券使用** 与 **核销** 不是同义词;商城下单抵扣通常描述为“使用优惠券”或“优惠券抵扣”,到店消费确认才更常用“核销”。 +- **挂账** 只在涉及应收记录或账务状态时使用;普通商城待支付场景优先写“待支付订单”。 +- **平台券**、**商家券**、**店铺券** 不应混写;若项目内三者含义不同,必须以项目需求或项目画像中的定义为准。 +- **退款**、**退货退款**、**换货**、**售后关闭** 不应混写,生成用例时要按实际逆向类型写全称。 diff --git a/knowledge_base/01_standards/terminology_optional_saas.md b/knowledge_base/01_standards/terminology_optional_saas.md new file mode 100644 index 0000000..252c18f --- /dev/null +++ b/knowledge_base/01_standards/terminology_optional_saas.md @@ -0,0 +1,38 @@ +# 可选业务术语表(SaaS 扩展域) + +> 本文件只在需求文档、项目画像或关联需求明确涉及对应领域时才应纳入上下文。不要将这些术语默认用于所有商城项目。 + +## 1. 渠道、导购与分销 + +- **导购**: 负责客户触达、商品推荐、线索转化或门店服务的经营角色,常见于门店零售和私域运营场景。 +- **导购关系**: 用户与某个导购之间建立的归属或服务关系,常影响业绩归因、客户分配和权限范围。 +- **分销员**: 参与商品推广并按规则获得佣金的推广角色,可能是员工、店主、团长或普通用户。 +- **推广员**: 泛指承担推广职责并按规则归因或结算收益的角色,不一定等同于分销员。 +- **佣金**: 按推广、成交、拉新等规则计算并结算给导购、分销员或渠道方的收益。 +- **佣金结算**: 按订单状态、售后状态、结算周期等规则确认佣金是否可提现、可结算的过程。 +- **渠道活码**: 用于区分投放渠道、推广来源或归属关系的可追踪二维码/链接能力。 + +## 2. 会员资产与储值 + +- **储值余额**: 用户预先充值后留存在平台或商家体系中的可消费余额。 +- **礼品卡**: 预付型权益资产,可按卡号、卡密或账户绑定方式消费或抵扣。 +- **会员储值**: 面向会员体系提供的充值、赠送、消费、退款、冻结、解冻等余额管理能力。 +- **赠送金额**: 商家在储值、活动或运营场景下额外发放给用户的不可提现或有限制可用金额。 +- **可用余额**: 当前允许参与消费、抵扣或退款计算的余额,不一定等于账户总余额。 +- **冻结余额**: 因退款、风控、提现审核或待确认交易而暂时不可用的余额。 + +## 3. 私域客户与 CRM + +- **企微客户**: 通过企业微信建立联系并沉淀在私域中的客户对象。 +- **客户标签**: 用于识别客户属性、分层、偏好、生命周期的标签集合。 +- **客户分群**: 按标签、行为、渠道、消费能力等规则聚合出的客户集合。 +- **客户归属**: 客户在组织、门店、导购、渠道之间的归属关系,常影响可见范围和业绩归因。 +- **生命周期**: 客户从拉新、激活、转化、复购到流失或召回的阶段划分。 +- **召回活动**: 面向沉默、流失或未转化客户进行再次触达和转化的运营活动。 +- **社群**: 围绕微信、企微或其他私域载体形成的客户经营集合,常关联群发、标签、活动、导购归属。 + +## 4. 使用约束 + +- **导购**、**分销员**、**推广员**、**渠道** 不应默认视为同一角色,必须按项目组织关系和结算规则区分。 +- **储值余额**、**赠送金额**、**礼品卡余额** 不应混写;三者通常具有不同的消费、退款、提现和过期规则。 +- **企微客户**、**会员**、**普通用户** 不一定是同一身份对象;若项目中存在账号打通或未打通,必须以需求定义为准。 diff --git a/knowledge_base/01_standards/test_case_template.md b/knowledge_base/01_standards/test_case_template.md new file mode 100644 index 0000000..4ed6af5 --- /dev/null +++ b/knowledge_base/01_standards/test_case_template.md @@ -0,0 +1,87 @@ +# 测试用例编写规范 + +## 1. 用例结构标准 +所有生成的测试用例必须遵循以下字段结构: + +| 字段 | 说明 | 示例 | +| :--- | :--- | :--- | +| **用例编号** | 唯一标识,建议使用稳定英文缩写,格式 `端_模块_子模块_编号` | `PLT_MKT_MC_001` | +| **模块** | 功能归属模块,建议使用层级路径表达 `端-一级模块-二级模块` | `平台端-营销管理-商家优惠券` | +| **用例标题** | 简明扼要,动宾结构 | `验证使用有效优惠券下单成功` | +| **优先级** | P0(核心流程), P1(主要功能), P2(次要/异常) | `P0` | +| **类型** | 用例类型,必须使用团队枚举 | `功能测试` | +| **前置条件** | 执行前必须满足的状态 | `用户已登录,购物车有商品` | +| **测试步骤** | 分步骤描述操作,清晰无歧义 | `1. 点击结算 2. 选择优惠券` | +| **测试数据** | 具体的输入数据(如有) | `优惠券码: TEST100` | +| **预期结果** | 对应步骤的系统反馈,包含 UI 和数据 | `1. 订单金额减少100元 2. 跳转支付页` | +| **备注** | 待确认项、历史缺陷来源或 AI 修正说明 | `[AI修正: 补充历史缺陷防御场景]` | + +## 2. 编写原则 +- **原子性**:一个用例只验证一个主要功能点。 +- **独立性**:用例之间尽量不依赖(除前置条件外)。 +- **可重复性**:预期结果必须明确,不能模棱两可。 +- **可追溯性**:每条用例应能追溯到需求条目、历史缺陷或团队规则。 + +## 3. 类型字段规范 + +- `类型` 必须从以下枚举中选择其一: + - `功能测试` + - `性能测试` + - `兼容性测试` + - `易用性测试` + - `安全性测试` + - `冒烟测试` + - `回归测试` + - `其他` +- 默认优先使用 `功能测试`,不要滥用 `其他`。 +- 涉及权限绕过、抓包篡改、越权访问、敏感数据校验的场景,优先标记为 `安全性测试`。 +- 涉及 RT、吞吐、并发容量、性能基线的场景,标记为 `性能测试`。 +- 核心链路首轮冒烟验证可标记为 `冒烟测试`。 +- 来自历史缺陷或确认后的重点回归链路,可标记为 `回归测试`。 + +## 4. 模块字段规范 + +- **推荐层级**:`端-一级模块-二级模块`,必要时可扩展到 `端-一级模块-二级模块-功能点`。 +- **推荐示例**: + - `平台端-营销管理-平台优惠券` + - `平台端-营销管理-商家优惠券` + - `商家端-营销活动-商家优惠券` + - `用户端-下单结算-平台券使用` +- **生成规范**:Markdown 表格中的 `模块` 列统一写 `-` 连接的层级路径,不直接写 `|`,避免表格分列风险。 +- **读取与导出规范**:脚本在读取 Markdown 和导出 Excel 时,会将模块路径自动标准化为 `|` 连接形式,例如: + +```text +平台端-营销管理-商家优惠券 +-> 平台端|营销管理|商家优惠券 +``` + +- **兼容说明**:历史数据如果已经使用 `平台端\|营销管理\|商家优惠券` 或 `平台端|营销管理|商家优惠券`,当前脚本仍可兼容读取。 +- **编号建议**:`用例编号` 不要直接使用中文模块路径,建议使用稳定英文缩写,例如: + +```text +PLT_MKT_PC_001 # 平台端|营销管理|平台优惠券 +PLT_MKT_MC_001 # 平台端|营销管理|商家优惠券 +MCH_MKT_MC_001 # 商家端|营销活动|商家优惠券 +``` + +## 5. 唯一表头 + +最终 Markdown 表格必须严格使用以下表头顺序: + +| 用例编号 | 模块 | 用例标题 | 优先级 | 类型 | 前置条件 | 测试步骤 | 测试数据 | 预期结果 | 备注 | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | + +## 6. 云效导出映射 + +- Excel 导出默认按团队云效字段模型输出: + - `标题` + - `编号` + - `目录` + - `创建时间` + - `前置条件` + - `步骤描述` + - `预期结果` + - `优先级` + - `类型` + - `URL` +- Markdown 中的 `测试数据` 会在导出时自动并入 `步骤描述`,避免字段丢失。 diff --git a/knowledge_base/02_history/common_missed_scenes.md b/knowledge_base/02_history/common_missed_scenes.md new file mode 100644 index 0000000..e88475d --- /dev/null +++ b/knowledge_base/02_history/common_missed_scenes.md @@ -0,0 +1,43 @@ +# 容易漏测场景清单 + +> AI 在生成测试点时,**必须**对照此清单进行自我检查,确保没有遗漏以下场景: + +### 1. 数据与边界 +- [ ] **空值/Null**:输入框为空、接口返回 null 时,页面是否崩溃。 +- [ ] **超长字符**:输入超过数据库字段长度的字符(如 256 个字符)。 +- [ ] **特殊字符**:Emoji 表情、SQL 注入字符、HTML 标签。 +- [ ] **金额精度**:金额计算是否出现 `0.0000001` 的精度丢失问题(特别是分期计算)。 + +### 2. 网络与交互 +- [ ] **弱网/断网**:提交请求瞬间断网,是否有重试机制或友好提示。 +- [ ] **重复提交**:快速多次点击“提交”按钮(防抖/幂等性检查)。 +- [ ] **超时处理**:接口响应超过 30 秒,前端是否有 Loading 状态或超时提示。 +- [ ] **页面切换**:在支付密码输入页切换到后台再切回,是否保留状态。 + +### 3. 状态流转 +- [ ] **逆向流程**:支付后退款、下单后取消、发货后退货、定金转退。 +- [ ] **并发操作**:同一账号在两个设备同时操作;库存仅剩 1 件时两人同时购买。 + +### 4. 车企商城特有业务 +- [ ] **VIN 码校验**:输入已售 VIN、错误格式 VIN、大小写混用。 +- [ ] **配置器互斥**:选择 A 配件后,B 配件是否自动置灰(如轮毂与轮胎的匹配)。 +- [ ] **价格变动**:用户加购后,后台修改价格,结算页是否实时刷新。 +- [ ] **权益叠加**:车主积分、推荐奖励、官方优惠券三者是否违规叠加。 + +### 5. 账号与权限 +- [ ] **多端登录**:同一账号在手机 App 和 车机端 同时登录,状态是否同步。 +- [ ] **未登录访问**:直接通过 URL 访问订单详情页,是否拦截跳转登录。 +- [ ] **隐私授权**:首次使用未点击“同意隐私协议”,是否禁止调用定位/相机权限。 + +### 6. 中后台 CRUD 页面 +- [ ] **列表字段**:列表页展示字段、默认排序、分页项是否与需求一致。 +- [ ] **查询重置**:搜索、筛选、重置、空结果提示是否完整。 +- [ ] **查看只读**:查看态是否真正不可编辑,避免与编辑态混淆。 +- [ ] **正向成功链路**:不要只测“保存失败/限制条件”,还要覆盖新建成功、编辑成功。 +- [ ] **状态与按钮映射**:不同状态下操作栏按钮是否正确展示,是否存在越权或错态操作。 +- [ ] **端间数据隔离**:平台端、商家端、门店端配置数据是否真正隔离,避免串数。 +- [ ] **选择弹窗细粒度**:商品/优惠券/奖品选择弹窗的刷新、搜索、分页、排序、选择反馈是否完整。 + +### 7. C 端资格与展示状态 +- [ ] **状态拆分**:未开始、进行中、已结束、已失效是否分别验证,而不是合并成一个模糊负向场景。 +- [ ] **门店老用户**:店铺维度老用户不弹窗是否单独覆盖,避免只校验平台维度老用户。 diff --git a/knowledge_base/02_history/historical_defects.md b/knowledge_base/02_history/historical_defects.md new file mode 100644 index 0000000..d0b1e93 --- /dev/null +++ b/knowledge_base/02_history/historical_defects.md @@ -0,0 +1,63 @@ +# 历史典型缺陷复盘 + +> 这些是过去项目中发生过的真实 Bug,生成新用例时,请针对这些逻辑设计防御性测试用例。 + +### 缺陷 ID: BUG-202310-05 +- **模块**: 购物车 +- **描述**: 商品降价后加入购物车,随后商品恢复原价,结算时依然按降价后的价格计算,导致资损。 +- **根因**: 购物车缓存了价格,结算时未重新从商品中心获取最新价格。 +- **防御用例**: `验证商品加入购物车后,后台修改价格,结算时系统自动更新为最新价格`。 + +### 缺陷 ID: BUG-202311-12 +- **模块**: 优惠券 +- **描述**: 使用“满100减50”优惠券购买 100 元商品,退货时未扣除优惠金额,导致用户获利。 +- **根因**: 退款逻辑未考虑优惠分摊。 +- **防御用例**: `验证使用优惠券购买多件商品后,部分退款时优惠金额按比例扣除`。 + +### 缺陷 ID: BUG-202312-07 +- **模块**: 下单接口 +- **描述**: 调用下单接口时,通过篡改请求参数,将商品数量改为负数,系统未做边界校验,使订单总金额变为负数,用户支付后平台产生负向应收,造成资损。 +- **根因**: 下单接口对商品数量仅做了非空校验,未校验正整数及防止整数溢出。 +- **防御用例**: `验证调用下单接口时,传入商品数量为 0、负数、超大整数(如 9999999)时,系统均能拦截并返回错误码,不生成有效订单`。 + +### 缺陷 ID: BUG-202401-15 +- **模块**: 支付网关 +- **描述**: 用户下单时选择“普通订单”(应付金额 100 元),在调起支付前通过抓包修改支付接口请求参数,将支付类型从“微信支付”改为“全额积分支付”。由于后端未校验订单原始支付方式,积分系统扣除了用户 100 积分(1 积分=1 元),用户实际未付现金即获得商品。日损失积分资产超 10 万。 +- **根因**: 支付接口仅校验了用户积分余额是否充足,未与订单核心表中的支付方式快照做一致性校验。 +- **防御用例**: `验证下单时选择现金支付,支付阶段强制改为积分支付或余额支付时,后端应校验请求支付方式与订单生成时记录的支付方式一致,不一致则拒绝支付`。 + +### 缺陷 ID: BUG-202401-22 +- **模块**: 并行扣减 +- **描述**: 秒杀活动中,用户通过多线程并发调用下单接口(同一商品 ID,同一用户),由于缺少分布式锁或乐观锁,导致同一件商品被同一用户成功下单 3 次,库存扣减超出实际售卖数量,产生超卖资损。 +- **根因**: 库存扣减 SQL 为 `update stock set count = count - 1 where product_id = ?`,未使用 `and count > 0` 条件,且业务层未做幂等和并发控制。 +- **防御用例**: `验证同一用户对同一限购商品发起 10 次并发下单请求,最终只成功创建 1 个订单,库存扣减 1 件`。 + +### 缺陷 ID: BUG-202402-03 +- **模块**: 价格计算 +- **描述**: 商品 A 单价 50 元,参与“满 2 件打 8 折”活动。用户将 1 件加入购物车,下单前活动变更为“满 3 件打 7 折”,但前端仍展示旧折扣,提交订单时后端沿用旧活动的价格计算规则,导致用户少付。 +- **根因**: 后端未在订单入库前重新计算促销活动价格,而是信任前端传入的活动 ID 和已优惠金额。 +- **防御用例**: `验证商品促销活动变更后,用户使用旧的活动 ID 提交订单,系统应抛弃前端传入的优惠信息,强制重新计算最新活动价格`。 + +### 缺陷 ID: BUG-202402-18 +- **模块**: 退款/逆向流程 +- **描述**: 用户使用混合支付(现金 80 元 + 余额 20 元)购买 100 元商品。申请全额退款时,退款逻辑先调用余额退款接口(退还 20 元),然后调用现金退款接口(退还 80 元)。但现金退款接口因网络超时返回失败,系统记录退款失败,但实际上余额的 20 元已退给用户,用户凭空获利。 +- **根因**: 退款流程未使用分布式事务或 TCC 模式,分步调用外部接口出现部分成功时无法回滚。 +- **防御用例**: `验证混合支付全额退款时,若现金退款接口调用失败,系统应能自动回滚已退的余额部分,或保持整体失败状态,绝不出现部分退款成功`。 + +### 缺陷 ID: BUG-202403-11 +- **模块**: 价格篡改 +- **描述**: 用户将商品加入购物车后,通过浏览器开发者工具修改页面中某个商品的“单价” DOM 元素(从 100 改为 0.01),提交订单时后端未校验单价,直接使用前端传回的 0.01 元创建支付单,用户以 1 分钱买走商品。 +- **根因**: 下单接口设计时错误地将商品单价作为可信任的入参,未从商品中心重新获取真实单价。 +- **防御用例**: `验证所有与金额、价格、数量相关的核心字段(单价、总价、运费等)均不能信任前端传入,下单时后端必须独立从商品库/价格服务中重新获取并计算`。 + +### 缺陷 ID: BUG-202403-20 +- **模块**: 重复支付 +- **描述**: 用户在支付网关点击“确认支付”按钮后,由于网络延迟前端未收到回调,用户连续点击 3 次,系统生成了 3 个相同的支付请求(同一订单号),三方支付渠道返回 3 个不同的交易单号,且订单系统最终对同一订单进行了 3 次金额累加,多收了用户 2 份钱。 +- **根因**: 支付接口未做幂等性校验,允许同一订单号多次发起支付请求并修改订单状态。 +- **防御用例**: `验证同一订单号在 1 秒内收到 5 次相同支付请求,后端仅处理第一次请求,后续请求直接返回“处理中”或“重复请求”,订单状态不被多次累加`。 + +### 缺陷 ID: BUG-202404-02 +- **模块**: 积分与现金互转 +- **描述**: 活动期间积分可抵扣现金(100 积分 = 1 元)。用户下单时使用 5000 积分抵扣 50 元,订单总金额 100 元,实付 50 元。后续用户申请全额退款,系统退还了 5000 积分 + 50 元现金,相当于积分被重复退还(原积分未核销),用户用 0 成本套取 50 元现金。 +- **根因**: 退款时未先扣回已使用的积分(或积分已被消耗),而是直接重新发放等额积分。 +- **防御用例**: `验证使用积分抵扣部分现金后,申请退款时应先原路返还积分(若积分未过期)或按比例折算成现金,确保平台不产生额外资产损失`。 \ No newline at end of file diff --git a/knowledge_base/02_history/marketing_rules.md b/knowledge_base/02_history/marketing_rules.md new file mode 100644 index 0000000..c4ce60a --- /dev/null +++ b/knowledge_base/02_history/marketing_rules.md @@ -0,0 +1,62 @@ +# 营销叠加互斥规定 + +> 核心业务规则,生成用例时**严禁**违反以下逻辑(以 `/zanmall_order/order/confirm` + `/zanmall_order/order/submit` 当前代码为准): + +1. **优惠券规则(作用域互斥)**: + - 同一作用域(同店铺券池 / 平台券池)一次只会生效 **1 张券**,不可多张叠加。 + - 平台券与店铺券可同时生效(前提各自命中可用条件)。 + - 套餐商品(`comboId > 0`)不参与优惠券分摊。 +2. **优先级逻辑**: + - 用户未手动指定优惠券(`userChangeCoupon != 1`)时,系统默认选择**优惠金额最大**的可用券。 + - 用户手动指定(`userChangeCoupon = 1`)时,仅按传入 `couponIds` 选择;也可手动不使用优惠券。 +3. **积分与优惠券关系**: + - 普通订单中,**优惠券可与积分抵现叠加**(积分开关开启且 `isScorePay = 1`)。 + - 积分在会员权益阶段之后计算,提交时会单独锁积分。 +4. **一口价互斥**: + - 命中一口价(`fixedPriceId != null`)时,不走平台券、会员折扣、积分抵现链路。 + - 一口价会重算店铺金额并覆盖此前店铺侧优惠计算结果,不与常规营销链路叠加。 +5. **满减送互斥**: + - 满减送对以下商品组直接跳过:已命中旧满减、拼团/秒杀/积分类商品、限时折扣、一口价、套餐。 +6. **会员折扣链路差异(配置项)**: + - 会员价开关关闭(legacy)时:会员折扣可继续叠加在已优惠商品上。 + - 会员价开关开启(hybrid)时:已命中营销活动或已使用优惠券的商品不再叠加会员折扣。 +7. **运费规则**: + - 运费按“店铺 + 运费模板(含供应商维度)”计算后累加。 + - 包邮只作用于命中的模板或模板条件,不是“只要有一件包邮就全单免邮”。 +8. **确认单链路顺序(普通路径)**: + - 先计算店铺侧营销(折扣/套餐、满减、店铺券等),再处理平台券,再处理会员与积分,再汇总运费。 + - 若命中一口价,直接进入一口价分支并覆盖店铺金额,不再进入平台券+会员+积分常规链路。 +9. **提交单锁定规则**: + - 提交单阶段不会重新“试算”营销组合,按确认单结果执行资源锁定。 + - 优惠券、积分分别在提交阶段单独锁定,确保与确认单结果一致。 + +## 关系速查(按当前接口逻辑) + +| 活动A | 活动B | 关系 | 条件说明 | +| --- | --- | --- | --- | +| 优惠券 | 优惠券(同作用域) | ❌ | 同一券池仅生效 1 张 | +| 平台券 | 店铺券 | ✅ | 分池计算,可同时生效 | +| 优惠券 | 套餐商品 | ⬇️ | 套餐商品不参与券分摊,非套餐可用券 | +| 优惠券 | 积分抵现 | ✅ | 积分开关开启且 `isScorePay=1` 时可叠加 | +| 会员折扣 | 优惠券 | ⬇️ | legacy 可叠加;hybrid 对已用券商品不叠加 | +| 满减送 | 旧满减/限时折扣/套餐/一口价/拼团秒杀积分类 | ❌(同商品组) | 命中互斥条件即跳过满减送 | +| 一口价 | 平台券 | ❌ | 一口价分支跳过平台券链路 | +| 一口价 | 会员折扣 | ❌ | 一口价分支跳过会员链路 | +| 一口价 | 积分抵现 | ❌ | 积分在会员链路内计算,一口价整体跳过 | +| 一口价 | 店铺侧营销结果(满减/店铺券/套餐) | ❌(覆盖) | 一口价重算并覆盖此前店铺优惠汇总 | +| 提交阶段 | 优惠券锁定 | 执行 | 按确认单结果锁券 | +| 提交阶段 | 积分锁定 | 执行 | 按确认单结果锁积分 | +| 运费 | 包邮模板 | ⬇️ | 按店铺+模板累加;仅命中模板免运费 | + +## 符号定义 + +- `✅`:可叠加(同一条 confirm 链路会连续生效,且无显式跳过/覆盖) +- `❌`:互斥(代码显式跳过,或后置计算覆盖前置结果) +- `⬇️`:条件叠加(依赖配置开关、活动命中、商品类型、券作用域等) + +## 可配置项(影响叠加结果) + +- `memberPriceSwitchConfig(enabled/failOpenLegacy)`:控制会员折扣走 legacy 还是 hybrid。 +- `SCORE_CONFIG(shoppingScoreSwitch/useRatioLimit/shoppingUseScoreRatio)`:控制积分抵现开关、比例与上限。 +- `会员权益 rightsType/freeFeeType`:控制会员包邮是否命中。 +- `userChangeCoupon + couponIds`:控制是否手动指定优惠券。 diff --git a/knowledge_base/03_best_practices/marketing_activity_cases.md b/knowledge_base/03_best_practices/marketing_activity_cases.md new file mode 100644 index 0000000..c92fe3a --- /dev/null +++ b/knowledge_base/03_best_practices/marketing_activity_cases.md @@ -0,0 +1,52 @@ +# 营销活动类优秀用例范式 + +> 适用于平台端/商家端活动配置、列表管理、状态流转、C 端资格展示与领取发放类需求。 +> 重点不是照抄标题,而是学习以下覆盖结构:后台正向成功链路、后台失败/限制链路、选择弹窗交互、端间数据隔离、C 端状态拆分、领取幂等与并发。 + +## 推荐覆盖框架 + +1. 后台列表能力 +- 列表字段 +- 默认排序与分页 +- 搜索、筛选、重置、空结果提示 +- 状态与操作栏映射 + +2. 后台配置能力 +- 新建成功 +- 编辑成功 +- 时间/名称/奖品等失败态 +- 失效、删除、查看只读 + +3. 选择弹窗能力 +- 刷新 +- 搜索 +- 字段展示 +- 分页 +- 排序 +- 单选/多选反馈 +- 数据归属范围 + +4. C 端资格与展示 +- 进行中时展示 +- 未开始/已结束/已失效分别不展示 +- 老用户/未登录分别不展示 +- 奖品展示与后台一致 +- 广告等弹窗优先级 + +5. 发放与稳健性 +- 领取成功 +- 幂等防重 +- 超时重试 +- 并发领取 +- 失败日志 +- 端间/角色间隔离 + +## 示例用例 + +| 用例编号 | 模块 | 用例标题 | 优先级 | 类型 | 前置条件 | 测试步骤 | 测试数据 | 预期结果 | 备注 | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| MKT_ACT_001 | 平台端-营销管理-活动配置 | 验证平台端新建合法营销活动可保存成功 | P0 | 功能测试 | 平台运营账号已登录;存在 1 个符合条件的活动资源;当前无时间冲突活动 | 1. 点击新建活动
2. 填写活动名称、时间、资源配置
3. 点击确定保存
4. 返回列表并进入详情查看 | 活动名称=春季促销;开始时间=明天 10:00;结束时间=后天 10:00 | 1. 页面提示创建成功
2. 列表新增活动记录且字段展示正确
3. 详情页展示与保存内容一致
4. 后台主表和资源明细表正确落库 | 示例 | +| MKT_ACT_002 | 平台端-营销管理-资源选择弹窗 | 验证资源选择弹窗支持刷新搜索分页排序和单选反馈 | P1 | 功能测试 | 平台运营账号已登录;资源池存在多条可用与不可用资源 | 1. 打开资源选择弹窗
2. 点击刷新
3. 按名称搜索
4. 切换分页和排序
5. 选择 1 条资源并确认返回 | 关键字=春季;分页项=10/20;目标资源=RES_1001 | 1. 刷新、搜索、分页、排序行为正确
2. 列表字段展示完整
3. 不符合条件资源不可选或不展示
4. 选中后页面正确回填资源信息 | 示例 | +| MKT_ACT_003 | 商家端-营销管理-活动列表 | 验证商家端列表字段和状态按钮映射正确 | P1 | 功能测试 | 商家端存在未开始、进行中、已结束、已失效四类活动;商家账号已登录 | 1. 进入活动列表
2. 逐条核对活动字段和操作栏
3. 对比不同状态按钮 | 活动A=未开始;活动B=进行中;活动C=已结束;活动D=已失效 | 1. 列表字段完整且内容正确
2. 未开始/进行中/已结束/已失效的操作按钮按规则展示
3. 不出现错态按钮或越权操作入口 | 示例 | +| MKT_ACT_004 | 用户端-首页-活动弹窗 | 验证活动未开始时首页不展示活动弹窗 | P1 | 功能测试 | 存在 1 条未开始活动;用户满足基础资格;当前无其他进行中活动 | 1. 用户进入首页
2. 观察首页弹窗
3. 查询资格检查结果 | 活动开始时间=当前时间后 1 小时 | 1. 首页不展示活动弹窗
2. 资格检查返回无可领取活动或活动未开始
3. 页面不提前曝光活动入口 | 示例 | +| MKT_ACT_005 | 用户端-首页-活动领取 | 验证同一用户重复点击领取时仅成功一次 | P0 | 回归测试 | 用户满足资格;存在进行中活动;前端可模拟快速重复点击 | 1. 打开活动弹窗
2. 1 秒内连续点击领取 3 次
3. 查询权益结果和发放日志 | 用户ID=USER_1001;重复点击次数=3 | 1. 页面最多提示 1 次成功
2. 用户仅获得 1 份权益
3. 发放日志仅有 1 条成功记录
4. 其余请求被幂等拦截 | 示例 | diff --git a/knowledge_base/03_best_practices/payment_flow_cases.md b/knowledge_base/03_best_practices/payment_flow_cases.md new file mode 100644 index 0000000..98efe4b --- /dev/null +++ b/knowledge_base/03_best_practices/payment_flow_cases.md @@ -0,0 +1,9 @@ +# 支付模块优秀用例范例 + +> 请参考以下用例的颗粒度、数据具体化方式以及“UI + 数据状态”双重预期写法。 + +| 用例编号 | 模块 | 用例标题 | 优先级 | 类型 | 前置条件 | 测试步骤 | 测试数据 | 预期结果 | 备注 | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| PAY_01_001 | 用户端-交易链路-支付 | 验证正常支付流程成功 | P0 | 冒烟测试 | 用户已登录,购物车存在可售商品 | 1. 选择商品点击购买
2. 在收银台选择支付宝支付
3. 完成支付授权 | 用户A;商品SKU-001;支付方式=支付宝 | 1. 页面跳转支付渠道并返回成功页
2. 订单状态变更为“待发货”
3. 支付流水生成且金额与订单实付一致 | 示例 | +| PAY_01_002 | 用户端-交易链路-支付 | 验证支付超时后订单自动取消 | P1 | 功能测试 | 用户已登录,已成功创建待支付订单 | 1. 进入待支付订单详情页
2. 不做支付,等待 30 分钟 | 订单号=ORDER-10001 | 1. 页面提示订单已超时取消
2. 订单状态变更为“已取消”
3. 预占库存回滚 | 示例 | +| PAY_01_003 | 用户端-交易链路-支付 | 验证支付渠道返回余额不足时可切换支付方式 | P1 | 功能测试 | 用户已登录,存在待支付订单 | 1. 选择银行卡支付
2. 模拟支付渠道返回“余额不足”
3. 切换到支付宝支付 | 订单号=ORDER-10002;银行卡返回码=BALANCE_NOT_ENOUGH | 1. 页面提示余额不足
2. 订单仍保持待支付状态
3. 用户可继续选择其他支付方式完成支付 | 示例 | diff --git a/output/analysis/新人礼需求_关联与冲突.md b/output/analysis/新人礼需求_关联与冲突.md new file mode 100644 index 0000000..a7ac99b --- /dev/null +++ b/output/analysis/新人礼需求_关联与冲突.md @@ -0,0 +1,16 @@ +# 新人礼需求 关联需求与冲突检查 + +## 目标需求 +- `source_docs/requirements_raw/新人礼需求.docx` + +## 项目画像 +- `knowledge_base/00_project/project_profile.md` + +## 关联技术方案 +- `source_docs/technical_solutions/新人礼技术方案.docx` + +## 关联需求识别 +- 未识别到相似度达到阈值的历史需求文档。 + +## 潜在冲突与修改建议 +- 暂未识别到明显冲突条目。建议在需求评审时继续人工确认。 diff --git a/output/analysis/新人礼需求_分析.md b/output/analysis/新人礼需求_分析.md new file mode 100644 index 0000000..9b9a25d --- /dev/null +++ b/output/analysis/新人礼需求_分析.md @@ -0,0 +1,111 @@ +# 新人礼需求分析 + +## 背景与目标 + +本期需求围绕“新人礼”活动能力建设,目标是在平台端和商家端支持配置新人礼活动,并在 C 端针对符合新人条件的用户展示领取入口与发放优惠券奖励,提升新注册用户的首单转化。 + +技术方案补充了两个关键限制: + +- 同一时间仅允许存在 1 个进行中的新人礼活动。 +- 每个新人礼活动仅允许绑定 1 张优惠券,发放结果需落新人礼发放记录表。 + +## 范围与边界 + +### 范围内 + +- 平台端“营销-新人有礼”列表、查询、新建、编辑、查看、失效、删除。 +- 商家端“营销-新人有礼”同构能力。 +- 新人礼活动配置字段校验:活动名称、活动时间、活动奖品、优惠券选择。 +- 优惠券选择范围控制:平台端仅选平台券,商家端仅选本店铺优惠券。 +- C 端首页和门店首页的新人礼弹窗检查与领取。 +- 发放成功后的优惠券入账和发放记录落库。 +- `ua/newGift/check` 与 `ua/newGift/send` 对应的资格校验和发放行为。 + +### 范围外 + +- 积分类型新人礼,需求和技术方案均明确“暂不支持”。 +- 下单后退款再重新认定为新人场景,技术方案明确“暂不支持下单后退款场景”。 +- 多奖品、多券组合、多人拼抢同一活动池等扩展玩法。 + +## 用户角色与前置条件 + +- 平台运营:维护平台维度新人礼活动。 +- 商家运营:维护店铺维度新人礼活动。 +- C 端用户:登录后触发首页或门店首页新人礼检查与领取。 +- 后端系统:负责活动状态判断、资格校验、优惠券发放、发送日志写入。 + +前置条件: + +- 已存在投放中、未过期、领取方式为“用户领取”的优惠券。 +- 用户已登录,且能获取平台维度与店铺维度的历史下单信息。 +- 发券接口可用,优惠券中心返回的券状态准确。 + +## 关键业务规则 + +1. 平台端和商家端都可以配置新人礼活动,但优惠券范围必须与端侧归属一致。 +2. 活动名称最多 50 个字。 +3. 活动开始时间必须大于等于当前时间,结束时间必须大于开始时间。 +4. 活动奖品当前仅支持优惠券,且每个活动仅能关联 1 张优惠券。 +5. 平台维度同一时间仅允许 1 个进行中的平台新人礼活动;店铺维度同一时间每个店铺仅允许 1 个进行中的门店新人礼活动。 +6. 新人判定按订单提交结果范围判断:平台新人礼查询平台范围订单,店铺新人礼查询当前店铺订单;若用户从未提交过订单,或历史订单最终状态均为交易关闭(包括未支付取消、超时未付、已退款订单等),仍视为新人。 +7. 用户登录首页时,若存在进行中的新人礼活动且用户未领取过,则展示新人礼弹窗。 +8. 门店首页只对门店新人展示门店新人礼弹窗。 +9. 若同时存在弹窗广告,新人礼弹窗必须先于广告弹窗展示。 +10. 用户点击立即领取成功后,奖励进入“我的优惠券”,同时写入新人礼发放记录。 +11. 活动“已结束”和“已失效”并存时,页面优先展示“已失效”。 +12. 活动失效后状态展示为“已失效”,并支持后续删除。 +13. 用户领取平台新人礼后,若仍满足门店新人条件,允许继续领取门店新人礼。 + +## 主流程描述 + +1. 运营在平台端或商家端进入“营销-新人有礼”列表页。 +2. 运营新建活动,填写活动名称、活动时间并选择 1 张符合条件的优惠券。 +3. 系统校验时间、优惠券范围、优惠券状态和活动并存规则,保存活动。 +4. 活动进入“未开始”或“进行中”状态,列表页按状态展示可操作按钮。 +5. C 端用户登录平台首页或门店首页时,调用资格检查接口。 +6. 若存在进行中的匹配活动且用户符合新人条件且未领取过,则展示新人礼弹窗。 +7. 用户点击“立即领取”,系统发放优惠券并记录发放日志。 +8. 发放成功后,用户可在个人中心优惠券列表查看奖励。 + +## 跨需求关联与冲突修订建议 + +- 当前未识别到需要纳入本次评审的关联需求,也未发现需要基于历史需求执行的规则冲突修订。 +- 当前无需对历史需求做口径覆盖修订,但仍需重点防御营销类公共风险: + - 重复点击导致重复发放。 + - 前端传参篡改导致越权选券或跨店铺发券。 + - 状态变更与页面展示不一致。 + +## 技术方案补充约束 + +- `ua/newGift/check` 负责资格检查,应重点验证平台维度和店铺维度“新人”判定口径。 +- `ua/newGift/send` 负责发券,应重点验证幂等、防重复领取、失败原因记录和成功落库。 +- 数据模型拆分为主表、礼物关联表、发放记录表,说明测试不能只看页面成功提示,还要覆盖: + - `new_gift` 主表活动状态和有效性字段。 + - `new_gift_detail` 活动与券的绑定关系。 + - `new_gift_send_log` 发放成功或失败记录。 +- 技术目标给出用户响应 RT 1000ms,应至少对资格检查和领取动作补充性能基线关注。 + +## 产品确认口径 + +- 活动并存范围:平台维度同一时间仅允许 1 条进行中的平台活动;店铺维度同一时间每个店铺仅允许 1 条进行中的门店活动。 +- 新人判定口径:若用户从未提交过订单,或历史订单最终状态均为交易关闭(包括未支付取消、超时未付、已退款订单),仍视为新人。 +- 状态展示优先级:活动“已结束”和“已失效”并存时,优先展示“已失效”。 +- 平台礼与门店礼关系:用户已领取平台新人礼后,只要满足门店新人条件,仍允许领取门店新人礼。 + +## 项目差异化测试约束 + +- 平台端、商家端、C 端是三套角色链路,必须覆盖角色隔离和数据隔离。 +- 这是典型营销发券能力,需重点覆盖越权选券、重复发放、状态错发和资损场景。 +- 预期结果必须同时覆盖 UI 反馈和后台状态变化,特别是活动状态、券领取结果、发放日志。 + +## 风险点与确认结果 + +- 风险点:重复点击“立即领取”或接口重试导致同一用户重复发放优惠券。 +- 风险点:商家端错误选择了其他店铺的优惠券,导致跨店资损或越权发券。 +- 风险点:活动失效、已结束、已删除三个状态口径不清,可能导致列表按钮和实际行为不一致。 +- 风险点:平台新人和店铺新人的判定依赖历史订单数据,若口径不清,容易误发或漏发。 +- 风险点:同一时间仅允许 1 个进行中的活动,如果平台端和商家端同时配置活动,范围口径不清会导致规则冲突。 +- 确认结果:平台端和商家端活动可以并存,但约束粒度为“平台一条、每店铺各一条”。 +- 确认结果:未支付取消、超时未付、已退款等最终交易关闭单据不影响新人资格。 +- 确认结果:活动“已结束”和“已失效”同时成立时,页面优先展示“已失效”。 +- 确认结果:已领取平台新人礼的用户,若满足门店新人条件,仍允许继续领取门店新人礼。 diff --git a/output/manifests/新人礼需求.json b/output/manifests/新人礼需求.json new file mode 100644 index 0000000..b8765a1 --- /dev/null +++ b/output/manifests/新人礼需求.json @@ -0,0 +1,56 @@ +{ + "requirement": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/source_docs/requirements_raw/新人礼需求.docx", + "requirement_source_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/source_docs/requirements_raw/新人礼需求.docx", + "requirement_input_type": "docx", + "base_name": "新人礼需求", + "normalized_requirement_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/normalized_inputs/新人礼需求/requirement.md", + "technical_solution_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/source_docs/technical_solutions/新人礼技术方案.docx" + ], + "normalized_technical_solution_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/normalized_inputs/新人礼需求/technical_solution_01.md" + ], + "analysis_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/analysis/新人礼需求_分析.md", + "relation_report_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/analysis/新人礼需求_关联与冲突.md", + "test_points_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/test_points/新人礼需求_测试点.md", + "test_cases_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/test_cases/新人礼需求_测试用例.md", + "excel_output_dir": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/excel_reports", + "project_profile_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/00_project/project_profile.md", + "knowledge_base_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/terminology.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/test_case_template.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/definition_of_done.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/review_checklist.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/02_history/common_missed_scenes.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/02_history/historical_defects.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/02_history/marketing_rules.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/03_best_practices/payment_flow_cases.md" + ], + "effective_terminology_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/terminology.md" + ], + "optional_terminology_files": [], + "related_requirements": [], + "conflict_candidates_count": 0, + "current_excel_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/excel_reports/新人礼需求_测试用例.xlsx", + "versioning_scheme": { + "current_files": "固定文件名,始终表示当前最新版", + "snapshot_rule": "仅在 export 成功且产物内容发生变化时递增版本", + "snapshot_dir_pattern": "output/versions/{BASE_NAME}/vN/" + }, + "latest_snapshot_version": "v9", + "latest_snapshot_dir": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/versions/新人礼需求/v9", + "latest_snapshot_type": "full_pipeline", + "confirmation_gate": { + "required": false, + "reasons": [], + "pending_markers_count": 0, + "decision_file": null, + "decision_status": "not_required", + "decision_status_label": null, + "allow_export_before_confirmation": null, + "candidate_decision_files": [], + "suggested_decision_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/decisions/新人礼需求_确认结论.md" + }, + "maintained_requirement_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/requirements/新人礼需求.md" +} diff --git a/output/normalized_inputs/新人礼需求/requirement.md b/output/normalized_inputs/新人礼需求/requirement.md new file mode 100644 index 0000000..e825bae --- /dev/null +++ b/output/normalized_inputs/新人礼需求/requirement.md @@ -0,0 +1,61 @@ +# 新人礼需求 + +> 文档角色:需求文档 +> 原始来源:`source_docs/requirements_raw/新人礼需求.docx` + +基线-新人礼 +| 版本号 | 变更内容 | 变更人 | 变更时间 | +| 0.0.1 | 文档创建 | 张昊 | 2026-02-28 | +一、业务背景 +基于问界需求清单 新人礼需求 +二、业务目标 +通过发布新人礼活动,吸引新用户注册使用~平台端&商家端支持发布新人礼活动,支持配置优惠券奖品,c端新用户注册展示新人礼内容 +三、产品设计 +1. 平台端-营销-新人有礼 +1.1 新人有礼列表 +列表数据说明 +数据权限:有此菜单列表权限的用户可看数据 +排序规则:按数据新增时间倒序排列 +分页规则:默认10行每页,可自主选择每页显示的条数(10/20/30/50) +数据来源:如下表 +| 数据字段 | 数据来源 | +| 活动名称 | 来源【新增/编辑】表单同名字段 | +| 活动时间 | 来源【新增/编辑】表单“活动时间“数据 | +| 活动状态 | 见下方状态逻辑说明 | +| 活动有效性 | 展示有效/已失效,支持 失效活动,失效后展示 已失效,失效后 操作栏展示删除操作 | +| 活动奖品 | 展示活动配置的奖品,当前活动奖品仅支持优惠券 | +| 创建时间 | 活动创建时间 | +状态逻辑说明 +| 状态名称 | 状态变更条件 | 可操作按钮 | +| 未开始 | 活动时间开始时间>当前时间 | 编辑,失效 | +| 进行中 | 活动时间开始时间<=当前时间 | 查看,失效 | +| 已结束 | 活动时间结束时间<当前时间 | 删除 | +查询条件说明 +| 查询条件 | 查询逻辑 | +| 活动名称 | 模糊查询,查询列表字段“活动名称” | +| 活动状态 | 精准查询,单选,选项数据:未开始、进行中、已结束、已失效,默认空 | +功能按钮说明 +| 按钮名称 | 触发后逻辑说明 | | +| 新建 | 在当前窗口打开“新建新人礼”弹窗 | | +| 查询 | 1、已选查询条件情况下,列表显示符合条件的数据 2、查询条件无匹配数据时,列表显示空,提示”暂无数据“ | | +| 重置 | 清空查询条件 | | +| 编辑 | 打开编辑表单弹窗 | | +| 查看 | 打开查看表单弹窗 | | +| 失效 | 打开失效操作弹窗,失效操作后,状态变更为已失效 | | +| 删除 | 打开删除操作弹窗,删除操作后,在列表删除活动(永久删除);取消后,关闭删除弹窗。 | | +1.2 新增/编辑 +页面字段说明 +| 字段名称 | 逻辑规则 | 是否必填 | 是否可编辑 | 原型界面、逻辑补充 | +| 活动名称 | 文本输入,最多50个字 | 是 | 是 | | +| 活动时间 | 开始时间-结束时间,年月日时分秒 | 是 | 是 | 控制活动的有效时段,可选择的时间大于等于当前时间,结束时间大于开始时间 | +| 活动奖品 | 多选 | 是 | 是 | | +| | 送优惠券 优惠券: 勾选后展示选择优惠券,只能选择1张优惠券,选择后展示选中优惠券信息表格 选择优惠券弹窗: 通用优惠券选择弹窗,单选 平台端展示平台可用优惠券列表, 商家端展示商家可用的优惠券列表, 优惠券列表数据展示规则:投放,未过期且为 用户领取 的优惠券,支持根据名称筛选 | 是 | 是 | 优惠券列表:选择前 优惠券列表:选择后 选择优惠券弹窗: | +| | 送积分 可输入1-999999的整数,配置生效后调用发积分接口发放积分 | 是 | 否 | 暂不支持 | +2. 商家端-营销-新人有礼 +功能同平台端,仅优惠券选择时,仅可选择该店铺下的优惠券 +3. C端 +3.1 首页 +| 平台首页-新人礼领取提示 奖励领取后,可在个人中心优惠券查看 | 店铺首页-新人礼领取提示 | +| 功能点 | 功能说明 | +| 弹窗逻辑 | 1)用户未在任意门店下过单,即视为新用户,登录后进入首页,弹窗提示用户领取新人礼奖励 2)用户未在该门店下过单,即视为门店新用户,登录后进入门店首页,弹窗提示用户领取新人礼奖励时机3)如果平台或店铺设置了弹窗广告,则新人礼弹窗在弹窗广告之前展示,关闭新人礼弹窗后,展示弹窗广告内容 | +| 优惠券 | 用户点击新人礼弹窗立即领取按钮,如果领取成功,奖励进入我的-优惠券列表,并提示用户领取成功 | diff --git a/output/normalized_inputs/新人礼需求/technical_solution_01.md b/output/normalized_inputs/新人礼需求/technical_solution_01.md new file mode 100644 index 0000000..095f30b --- /dev/null +++ b/output/normalized_inputs/新人礼需求/technical_solution_01.md @@ -0,0 +1,66 @@ +# 新人礼技术方案 + +> 文档角色:技术方案 +> 原始来源:`source_docs/technical_solutions/新人礼技术方案.docx` + +基线改造---新人礼技术方案&需求概述 +| 版本号 | 变更内容 | 变更人 | 变更时间 | +| 1.0 | 建档 | 陶震 | 2026-2-21 | +1 背景 +1.1  需求背景 +新增,针对于新注册,未下单用户发放专属优惠券的场景(暂不支持下单后退款场景) +1.2 业务现况 +1.3. 业务系统现况 +1.4 名词说明 +| 名称 | 描述 | +| 新人(平台新人,店铺新人) | 平台新人:未在该平台下过单的用户,即shop_custormer中无任何数据的user 店铺新人:未在该店铺下过单的用户,即shop——customer中无该店铺该user数据的用户 | +1.7 涉及人员 +| 角色名称 | 使用内容 | +| 用户 | 消费者 | +| 运营 | 商城日常使用维护以及操作者。 | +2 目标 +本期实现目标说明: +2.1、技术目标 +| 技术指标 | 指标值 | 备注 | +| 用户响应RT | 1000ms | | +| 用户体量 | | | +| 并发数 | | | +| 开发语言 | | | +| 网络要求 | | | +3 整体概述 +3.1 需求概述 +同一时间仅允许一个新人礼活动(避免同一时间多个活动发放业务过重)。关联优惠券仅支持关联一张 +C端弹窗,若存在进行中的新人礼活动且用户未领取过,则进行弹窗 +3.2 业务架构图(一图概览) +3.3 业务模块列表 +3.4 第三方服务列表(可选) +| 服务厂商 | 类型(接口/设备) | 接口地址 | 备注 | 状态 | +3.5 技术架构 +3.5.1 前端设计 +3.5.2 后端设计 +3.6 领域模型设计 +新人礼主表 +| SQLCREATE TABLE `new_gift` ( `new_gift_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `shop_id` bigint NOT NULL COMMENT '关联店铺 平台为0', `activity_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '活动名称', `activity_start_time` datetime NOT NULL COMMENT '活动开始时间', `activity_end_time` datetime NOT NULL COMMENT '活动结束时间', `gift_type` int NOT NULL COMMENT '礼物类型 0优惠券 其他待拓展', `activity_status` int NOT NULL COMMENT '活动状态0未开始,1进行中,2已结束', `valid_status` int NOT NULL DEFAULT '0' COMMENT '有效性 0有效 1已失效', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` int NOT NULL DEFAULT '0' COMMENT '是否已删除 0否1是', PRIMARY KEY (`new_gift_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='新人礼主表'; | +新人礼关联礼物表 +| SQLCREATE TABLE `new_gift_detail` ( `new_gift_detail_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `new_gift_id` bigint NOT NULL COMMENT '新人礼主键', `gift_type` int NOT NULL COMMENT '礼物类型 0优惠券 其他待拓展', `gift_biz_id` bigint NOT NULL COMMENT '礼物主键Id', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`new_gift_detail_id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='新人礼 子表,存储主表与礼物关联关系'; | +新人礼发放记录表 +| SQLCREATE TABLE `new_gift_send_log` ( `new_gift_log_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` bigint NOT NULL COMMENT '用户ID', `send_status` int NOT NULL COMMENT '发放状态 0成功 1失败', `fail_reason` json DEFAULT NULL COMMENT '失败原因', `new_gift_id` bigint NOT NULL COMMENT '新人礼主键', `new_gift_detail_id` bigint NOT NULL COMMENT '礼物详情主键', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`new_gift_log_id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='新人礼发放记录表'; | +3.7 数据模型设计 +详见领域模型 +3.8 依赖项 +| 依赖项 | 作用 | 预计提供时间 | 状态 | 责任人 | 交付物 | +4 场景(user story)与流程图(How) +4.1 新增活动 +4.1.1 业务流程图 +4.1.2 时序图 +4.1.3 接口依赖 +无 +4.1.4 接口设计 +| API | 状态 | 说明 | +| /mp/newGift/save | 未开发 | 新增新人礼活动 | +| /mp/newGift/update | 未开发 | 修改新人礼活动 | +| /mp/newGift/detail | 未开发 | 查看新人礼详情 | +| /mp/newGift/delete | 未开发 | 删除新人礼活动 | +| /mp/newGift/page | 未开发 | 新人礼活动分页查询 | +| /ua/newGift/check | 未开发 | 检测用户是否符合进行中的平台/店铺中的新人礼活动。 | +| /ua/newGift/send | 未开发 | 为用户发放对应新人礼绑定的券 | diff --git a/output/test_cases/新人礼需求_测试用例.md b/output/test_cases/新人礼需求_测试用例.md new file mode 100644 index 0000000..9e0371e --- /dev/null +++ b/output/test_cases/新人礼需求_测试用例.md @@ -0,0 +1,47 @@ +# 新人礼需求测试用例 + +| 用例编号 | 模块 | 用例标题 | 优先级 | 类型 | 前置条件 | 测试步骤 | 测试数据 | 预期结果 | 备注 | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| PLT_NG_LIST_001 | 平台端-营销管理-新人有礼列表 | 验证平台端列表默认排序、分页和查询功能正确 | P1 | 功能测试 | 平台端已存在 12 条新人礼活动数据,覆盖未开始、进行中、已结束、已失效状态;测试账号具备列表权限 | 1. 登录平台端进入营销-新人有礼列表
2. 观察默认排序和分页条数
3. 输入活动名称关键字执行查询
4. 选择活动状态执行筛选
5. 输入无匹配关键字执行查询
6. 点击重置 | 活动A 创建时间晚于活动B;查询关键字=新人;状态=进行中;无匹配关键字=不存在的活动名 | 1. 列表默认按创建时间倒序展示
2. 默认每页展示 10 条
3. 名称查询仅返回命中活动
4. 状态筛选仅返回对应状态活动
5. 无匹配数据时列表展示空态或“暂无数据”提示
6. 点击重置后查询条件被清空,列表恢复默认结果 | [AI修正: 对标团队用例补充空结果查询场景] | +| PLT_NG_CFG_001 | 平台端-营销管理-新人有礼配置 | 验证活动时间非法时无法保存活动 | P1 | 功能测试 | 平台运营账号已登录;存在可选平台优惠券 | 1. 点击新建活动
2. 输入合法活动名称
3. 设置开始时间早于当前时间后尝试保存
4. 再设置结束时间早于开始时间后尝试保存 | 活动名称=平台新人礼A;开始时间=当前时间前 1 分钟;结束时间=开始时间前 1 秒 | 1. 页面对开始时间非法给出明确提示
2. 页面对结束时间非法给出明确提示
3. 活动保存失败
4. 后台 `new_gift` 主表不新增活动记录 | 边界值 | +| PLT_NG_CFG_002 | 平台端-营销管理-新人有礼配置 | 验证平台端只能绑定 1 张符合条件的平台优惠券 | P0 | 功能测试 | 平台运营账号已登录;平台下存在 3 张券,分别为投放中可领券、已过期券、非用户领取券 | 1. 点击新建活动
2. 打开选择优惠券弹窗
3. 观察券列表范围
4. 选择 1 张投放中可领券后尝试继续追加选择第 2 张券
5. 保存活动 | 券A=投放中未过期用户领取券;券B=已过期券;券C=系统发放券 | 1. 弹窗仅展示平台可用且投放中、未过期、用户领取的券
2. 已过期券和非用户领取券不展示或不可选
3. 页面仅允许单选 1 张券
4. 保存成功后 `new_gift_detail` 仅存在 1 条券绑定记录 | [AI修正: 技术方案限定单券绑定] | +| PLT_NG_CFG_003 | 平台端-营销管理-新人有礼配置 | 验证积分奖励当前不支持 | P1 | 功能测试 | 平台运营账号已登录 | 1. 点击新建活动
2. 进入活动奖品区域
3. 查看送积分配置入口
4. 若可通过前端或抓包提交积分类型则尝试保存 | gift_type=积分;积分值=100 | 1. 页面不允许启用积分奖品,或该入口展示为暂不支持
2. 若前端被绕过提交积分类型,后端拦截保存并返回错误提示
3. 后台不生成积分类型活动记录 | [AI修正: 范围外能力拦截] | +| PLT_NG_CREATE_001 | 平台端-营销管理-新人有礼配置 | 验证平台端新建合法活动可保存成功 | P0 | 功能测试 | 平台运营账号已登录;存在 1 张投放中、未过期、用户领取类型的平台优惠券;当前无冲突中的平台活动 | 1. 点击新建活动
2. 填写活动名称、时间并选择 1 张合法平台券
3. 点击确定保存
4. 返回列表并进入详情页核对数据 | 活动名称=平台新人礼春季活动;开始时间=明天 10:00;结束时间=后天 10:00;券ID=PLT_COUPON_1001 | 1. 页面提示创建成功
2. 列表新增该活动记录,字段展示正确
3. 活动状态按时间口径展示为未开始
4. `new_gift` 和 `new_gift_detail` 新增对应活动及券绑定记录 | [AI修正: 对标团队用例补充后台正向新建链路] | +| PLT_NG_EDIT_001 | 平台端-营销管理-新人有礼编辑 | 验证平台端编辑未开始活动可保存成功 | P1 | 功能测试 | 平台已存在 1 条未开始活动;平台运营账号已登录;存在 1 张新的合法平台优惠券 | 1. 在列表中点击编辑未开始活动
2. 修改活动名称、时间或绑定券
3. 点击确定保存
4. 刷新列表并进入详情查看 | 原活动名称=平台新人礼A;新活动名称=平台新人礼A-调整版;新券ID=PLT_COUPON_1002 | 1. 页面提示保存成功
2. 列表展示为修改后的名称和时间
3. 详情页展示最新配置
4. 后台活动主表和明细表更新为最新值,不产生重复活动记录 | [AI修正: 对标团队用例补充后台正向编辑链路] | +| PLT_NG_POPUP_001 | 平台端-营销管理-优惠券选择弹窗 | 验证平台端选券弹窗支持搜索分页排序和单选反馈 | P1 | 功能测试 | 平台运营账号已登录;平台下存在 25 张符合或不符合条件的优惠券 | 1. 进入新建活动页面并打开优惠券选择弹窗
2. 点击刷新并观察列表加载
3. 输入优惠券名称执行搜索
4. 切换分页条数和页码
5. 观察列表排序和字段展示
6. 选择 1 张优惠券并确认返回 | 券名称关键字=新人礼;分页项=10/20;目标券=PLT_COUPON_1003 | 1. 弹窗可正常刷新数据
2. 名称搜索仅返回命中券
3. 分页、页码切换和排序行为正确
4. 列表展示券名称、有效期、领取方式等必要字段
5. 仅允许单选 1 张券,选中后页面回填正确的券信息 | [AI修正: 对标团队用例补充选券弹窗交互明细] | +| PLT_NG_RULE_001 | 平台端-营销管理-新人有礼配置 | 验证平台维度同一时间仅允许一个进行中的新人礼活动 | P0 | 功能测试 | 已存在 1 条平台活动A,时间范围覆盖当前时刻且状态为进行中;平台运营账号已登录 | 1. 点击新建活动
2. 填写与活动A 时间重叠的新活动B信息
3. 选择合法平台优惠券并保存 | 活动A 时间=今天 10:00 到 明天 10:00;活动B 时间=今天 12:00 到 明天 12:00 | 1. 页面或接口提示当前已有进行中的平台新人礼活动
2. 活动B 保存失败
3. 后台不新增活动B 记录
4. 活动A 状态和绑定关系不受影响 | [AI修正: 按产品确认口径更新为平台维度唯一] | +| PLT_NG_STATE_001 | 平台端-营销管理-新人有礼状态 | 验证活动失效后展示已失效并支持删除 | P1 | 功能测试 | 平台已存在 1 条未开始活动和 1 条进行中活动;平台运营账号已登录 | 1. 在列表中对活动执行失效操作
2. 刷新列表查看状态和操作按钮
3. 对已失效活动执行删除操作
4. 再次刷新列表 | 活动A=未开始;活动B=进行中 | 1. 失效后活动有效性展示为已失效
2. 失效活动的操作按钮切换为删除
3. 删除确认后页面提示删除成功
4. 列表中不再显示该活动
5. 后台活动记录被标记删除或按实现永久删除,不可继续被资格检查命中 | 状态流转 | +| PLT_NG_STATE_002 | 平台端-营销管理-新人有礼状态 | 验证活动已结束且已失效时列表优先展示已失效 | P1 | 功能测试 | 平台存在 1 条活动,活动结束时间早于当前时间,且已执行失效操作;平台运营账号已登录 | 1. 进入平台端新人有礼列表
2. 查询该活动状态展示和操作按钮 | 活动A 结束时间=当前时间前 1 天;valid_status=已失效 | 1. 列表最终状态优先展示为已失效
2. 操作按钮按已失效口径展示删除,不按已结束展示
3. 页面状态展示与后台有效性字段一致 | [AI修正: 按产品确认口径补充状态优先级] | +| MCH_NG_SCOPE_001 | 商家端-营销管理-新人有礼配置 | 验证商家端只能选择本店铺优惠券 | P0 | 功能测试 | 商家A 账号已登录;商家A 券A 为可领券;商家B 券B 为可领券 | 1. 商家A 进入新建活动页面
2. 打开优惠券选择弹窗
3. 搜索本店券A 和外店券B
4. 选择券A 保存活动 | 商家A ID=1001;商家B ID=1002;券A 属于商家A;券B 属于商家B | 1. 列表仅展示商家A 可用券
2. 搜索券B 无结果或不可选
3. 保存成功后活动绑定的券归属为商家A
4. 后台不存在跨店绑定关系 | [AI修正: 覆盖数据隔离与越权风险] | +| MCH_NG_SCOPE_002 | 商家端-营销管理-新人有礼配置 | 验证篡改券 ID 无法越权绑定其他店铺优惠券 | P0 | 安全性测试 | 商家A 账号已登录;抓包工具可修改请求;商家B 存在 1 张有效券 | 1. 商家A 正常进入新建活动页面并选择商家A 自有券
2. 提交前抓包将券 ID 替换为商家B 券 ID
3. 提交保存请求 | 提交参数原券 ID=COUPON_A_01;篡改后券 ID=COUPON_B_01 | 1. 后端拦截越权绑定请求并返回错误提示
2. 页面保存失败
3. 后台不生成跨店活动与券绑定关系
4. 审计日志记录异常请求或失败原因 | [AI修正: 对应越权与价格篡改类历史风险] | +| MCH_NG_RULE_001 | 商家端-营销管理-新人有礼配置 | 验证同一店铺仅允许一个进行中的门店新人礼活动但不同店铺可并存 | P0 | 功能测试 | 商家A 已存在 1 条进行中的门店活动A;商家B 无进行中活动;商家A、商家B 账号均可登录 | 1. 商家A 新建与活动A 时间重叠的门店活动A2并保存
2. 商家B 新建与活动A 同时段重叠的门店活动B并保存
3. 查询两家店铺活动列表 | 商家A ID=1001;商家B ID=1002;活动A2 与活动A 时间重叠;活动B 与活动A 时间重叠 | 1. 商家A 保存活动A2失败,并提示当前店铺已有进行中的新人礼活动
2. 商家B 保存活动B成功
3. 后台显示门店活动限制按店铺维度生效,不会因商家A 活动阻塞商家B 配置 | [AI修正: 按产品确认口径补充店铺维度唯一] | +| MCH_NG_LIST_001 | 商家端-营销管理-新人有礼列表 | 验证商家端列表默认排序、分页、搜索和重置功能正确 | P1 | 功能测试 | 商家A 已存在 12 条新人礼活动数据,覆盖未开始、进行中、已结束、已失效状态;商家运营账号具备列表权限 | 1. 登录商家A 端进入新人有礼列表
2. 观察默认排序和分页条数
3. 输入活动名称关键字执行查询
4. 选择活动状态执行筛选
5. 输入无匹配关键字执行查询
6. 点击重置 | 店铺ID=1001;关键字=新人;状态=进行中;无匹配关键字=不存在的活动名 | 1. 列表默认按创建时间倒序展示
2. 默认每页展示 10 条并可切换分页项
3. 名称搜索和状态筛选仅返回当前店铺命中数据
4. 无匹配数据时展示空态或“暂无数据”提示
5. 点击重置后恢复默认列表 | [AI修正: 对标团队用例补充商家端列表基础能力] | +| MCH_NG_LIST_002 | 商家端-营销管理-新人有礼列表 | 验证商家端列表字段和操作栏状态映射正确 | P1 | 功能测试 | 商家A 存在未开始、进行中、已结束、已失效四类活动;商家运营账号已登录 | 1. 进入商家A 新人有礼列表
2. 逐条检查活动名称、活动时间、活动状态、创建时间、操作栏
3. 对比不同状态下的操作按钮 | 活动A=未开始;活动B=进行中;活动C=已结束;活动D=已失效 | 1. 列表展示字段完整且内容正确
2. 未开始活动展示编辑、失效
3. 进行中活动展示查看、失效或按最终规则允许的编辑能力
4. 已结束和已失效活动展示删除或受限操作
5. 状态与按钮映射不串位 | [AI修正: 对标团队用例补充商家端状态按钮映射] | +| MCH_NG_CREATE_001 | 商家端-营销管理-新人有礼配置 | 验证商家端新建合法活动可保存成功 | P0 | 功能测试 | 商家A 账号已登录;商家A 存在 1 张投放中、未过期、用户领取类型的店铺优惠券;当前店铺无冲突中的门店活动 | 1. 点击新建活动
2. 填写活动名称、时间并选择 1 张本店合法券
3. 点击确定保存
4. 返回列表并进入详情页核对数据 | 店铺ID=1001;活动名称=门店新人礼春季活动;券ID=SHOP_COUPON_1001 | 1. 页面提示创建成功
2. 列表新增该门店活动记录,字段展示正确
3. 活动状态按时间口径展示为未开始
4. `new_gift` 和 `new_gift_detail` 新增当前店铺对应活动及券绑定记录 | [AI修正: 对标团队用例补充商家端正向新建链路] | +| MCH_NG_EDIT_001 | 商家端-营销管理-新人有礼编辑 | 验证商家端编辑未开始活动可保存成功 | P1 | 功能测试 | 商家A 存在 1 条未开始活动;商家A 账号已登录;存在 1 张新的本店合法券 | 1. 在列表中点击编辑未开始活动
2. 修改活动名称、时间或绑定券
3. 点击确定保存
4. 刷新列表并进入详情页核对 | 原活动名称=门店新人礼A;新活动名称=门店新人礼A-调整版;新券ID=SHOP_COUPON_1002 | 1. 页面提示保存成功
2. 列表和详情页展示最新配置
3. 后台活动主表和明细表更新为最新值
4. 不产生重复活动记录或跨店异常数据 | [AI修正: 对标团队用例补充商家端正向编辑链路] | +| MCH_NG_POPUP_001 | 商家端-营销管理-优惠券选择弹窗 | 验证商家端选券弹窗支持搜索分页排序和单选反馈 | P1 | 功能测试 | 商家A 账号已登录;商家A 下存在 25 张符合或不符合条件的优惠券 | 1. 进入新建活动页面并打开优惠券选择弹窗
2. 点击刷新并观察列表加载
3. 输入优惠券名称执行搜索
4. 切换分页条数和页码
5. 观察列表排序和字段展示
6. 选择 1 张优惠券并确认返回 | 店铺ID=1001;券名称关键字=新人礼;分页项=10/20;目标券=SHOP_COUPON_1003 | 1. 弹窗可正常刷新数据
2. 搜索仅返回当前店铺命中的券
3. 分页、页码切换和排序行为正确
4. 列表展示券名称、有效期、领取方式等必要字段
5. 仅允许单选 1 张券,确认后页面正确回填券信息 | [AI修正: 对标团队用例补充商家端选券弹窗交互明细] | +| MCH_NG_STATE_001 | 商家端-营销管理-新人有礼状态 | 验证商家端活动失效后展示已失效并支持删除 | P1 | 功能测试 | 商家A 已存在 1 条未开始活动和 1 条进行中活动;商家A 账号已登录 | 1. 在列表中对活动执行失效操作并确认
2. 刷新列表查看状态和操作按钮
3. 对已失效活动执行删除并确认
4. 再次刷新列表 | 店铺ID=1001;活动A=未开始;活动B=进行中 | 1. 失效后活动有效性展示为已失效
2. 已失效活动操作栏切换为删除
3. 删除确认后页面提示删除成功
4. 列表中不再显示该活动
5. 后台仅删除当前店铺对应活动,不影响其他店铺或平台活动 | [AI修正: 对标团队用例补充商家端失效删除链路] | +| MCH_NG_VIEW_001 | 商家端-营销管理-新人有礼查看 | 验证商家端查看态页面所有字段只读不可编辑 | P1 | 功能测试 | 商家A 已存在 1 条新人礼活动;商家A 账号已登录 | 1. 在列表中点击查看
2. 观察活动名称、活动时间、活动奖品等字段状态
3. 尝试修改字段或提交 | 店铺ID=1001;活动ID=SHOP1001 | 1. 查看页所有字段均为只读态
2. 页面不提供可提交修改的入口
3. 用户无法通过查看态改写活动配置 | [AI修正: 对标团队用例补充商家端查看只读场景] | +| C_NG_POP_001 | 用户端-首页-新人礼弹窗 | 验证平台新人登录平台首页时展示新人礼弹窗 | P0 | 冒烟测试 | 平台存在 1 条进行中的平台新人礼活动;用户U1 已注册登录且平台维度无下单记录;用户未领取过该活动 | 1. 用户U1 登录平台首页
2. 观察首页首屏弹窗 | 用户U1;平台活动=进行中;平台下单记录=0 | 1. 首页展示新人礼弹窗
2. 弹窗内容展示活动奖励信息和立即领取按钮
3. 后台资格检查接口返回命中活动和可领取状态 | 主流程 | +| C_NG_POP_002 | 用户端-首页-新人礼弹窗 | 验证非平台新人登录平台首页时不展示新人礼弹窗 | P1 | 功能测试 | 平台存在 1 条进行中的平台新人礼活动;用户U2 已注册登录且平台维度存在已下单记录 | 1. 用户U2 登录平台首页
2. 观察首页弹窗和资格检查结果 | 用户U2;平台已支付订单数=1 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回不满足新人条件
3. 页面不出现立即领取入口 | 边界值 | +| C_NG_POP_003 | 用户端-门店首页-新人礼弹窗 | 验证平台老用户但门店新用户进入门店首页时展示门店新人礼弹窗 | P0 | 功能测试 | 商家A 存在 1 条进行中的门店新人礼活动;用户U3 平台维度已有下单记录,但在商家A 维度无下单记录;用户未领取过商家A 活动 | 1. 用户U3 进入商家A 门店首页
2. 观察首页弹窗
3. 查询资格检查结果 | 用户U3;平台订单数=1;商家A 订单数=0 | 1. 门店首页展示商家A 新人礼弹窗
2. 资格检查接口按门店维度返回可领取
3. 弹窗奖励信息与商家A 活动配置一致 | [AI修正: 覆盖平台新人和店铺新人差异口径] | +| C_NG_POP_004 | 用户端-首页-弹窗优先级 | 验证存在广告弹窗时新人礼弹窗优先展示 | P1 | 功能测试 | 平台存在 1 条进行中的新人礼活动;首页同时配置普通广告弹窗;用户U1 满足平台新人资格 | 1. 用户U1 登录平台首页
2. 观察首个弹窗
3. 关闭新人礼弹窗后继续观察 | 用户U1;广告弹窗=已配置 | 1. 首个弹窗为新人礼弹窗
2. 关闭新人礼弹窗后再展示广告弹窗
3. 整个过程中弹窗顺序与需求一致 | 交互优先级 | +| C_NG_POP_005 | 用户端-首页-新人礼弹窗 | 验证历史订单均为交易关闭时仍识别为平台新人 | P0 | 功能测试 | 平台存在 1 条进行中的平台新人礼活动;用户U6 历史仅有未支付取消单、超时未付单或已退款订单,且无其他非关闭订单 | 1. 用户U6 登录平台首页
2. 观察首页弹窗
3. 查询资格检查结果 | 用户U6 历史订单状态=交易关闭、已退款、超时未付 | 1. 首页展示平台新人礼弹窗
2. 资格检查接口返回满足平台新人条件
3. 页面不因历史关闭单或已退款单而拦截新人资格 | [AI修正: 按产品确认口径补充交易关闭单仍算新人] | +| C_NG_POP_006 | 用户端-首页-新人礼弹窗 | 验证未登录进入首页时不展示新人礼弹窗 | P0 | 功能测试 | 平台或门店存在进行中的新人礼活动;用户未登录 | 1. 未登录直接进入平台首页
2. 未登录直接进入门店首页
3. 观察页面弹窗与网络请求 | 访问端=H5/小程序;登录态=未登录 | 1. 平台首页和门店首页均不展示新人礼弹窗
2. 页面不进入领取流程
3. 若触发资格检查请求,应返回未登录拦截而非可领取结果 | [AI修正: 取自团队现有功能用例] | +| C_NG_POP_007 | 用户端-首页-新人礼展示 | 验证首页展示的奖品信息与后台配置一致 | P0 | 功能测试 | 平台和商家端各存在 1 条进行中的新人礼活动,且分别绑定不同门槛和优惠内容的优惠券;用户满足资格 | 1. 平台新人登录平台首页查看弹窗奖品信息
2. 店铺新人进入门店首页查看弹窗奖品信息
3. 对比后台券配置 | 平台券=满100减20, 用券时间 2026-05-01 至 2026-05-31;店铺券=8折券, 上限 30 元 | 1. 平台首页弹窗展示的平台券门槛、优惠金额或折扣、用券时间与后台配置一致
2. 门店首页弹窗展示的商家券信息与商家端配置一致
3. 不出现平台券和商家券信息串位 | [AI修正: 取自团队现有功能用例] | +| C_NG_POP_008 | 用户端-门店首页-新人礼弹窗 | 验证门店老用户进入门店首页时不展示门店新人礼弹窗 | P1 | 功能测试 | 商家A 存在 1 条进行中的门店新人礼活动;用户U9 已登录且在商家A 维度存在已支付订单 | 1. 用户U9 进入商家A 门店首页
2. 观察首页弹窗和资格检查结果 | 用户U9;店铺ID=1001;商家A 已支付订单数=1 | 1. 门店首页不展示门店新人礼弹窗
2. 资格检查接口返回不满足门店新人条件
3. 页面不出现立即领取入口 | [AI修正: 对标团队用例补充门店老用户负向场景] | +| C_NG_SEND_001 | 用户端-首页-新人礼领取 | 验证点击立即领取成功后优惠券入账并写发放日志 | P0 | 冒烟测试 | 用户U1 满足平台新人资格;平台存在进行中活动且绑定有效券;用户未领取过该活动 | 1. 用户U1 在新人礼弹窗点击立即领取
2. 观察页面提示
3. 进入我的优惠券查询奖励
4. 查询发放日志 | 用户U1;活动ID=NG1001;券ID=PC1001 | 1. 页面提示领取成功
2. 我的优惠券列表新增对应优惠券
3. `new_gift_send_log` 新增 1 条成功记录,包含用户、活动、活动明细和发送状态
4. 该用户再次进入首页时不再展示同一活动弹窗 | 主流程 | +| C_NG_SEND_002 | 用户端-首页-新人礼领取 | 验证同一用户重复点击立即领取时只发放一次 | P0 | 回归测试 | 用户U1 满足新人资格;平台存在进行中活动且绑定有效券;抓包或前端可模拟快速重复点击 | 1. 用户U1 打开新人礼弹窗
2. 在 1 秒内连续点击 3 次立即领取
3. 查询优惠券列表和发放日志 | 用户U1;活动ID=NG1001;重复点击次数=3 | 1. 页面最多显示 1 次领取成功提示
2. 用户仅新增 1 张优惠券
3. `new_gift_send_log` 仅有 1 条成功记录,其余请求被幂等拦截或返回已领取
4. 不出现重复发券 | [AI修正: 对应重复提交和重复支付类历史风险] | +| C_NG_SEND_003 | 用户端-首页-新人礼领取 | 验证领取接口超时重试时不会重复发券 | P1 | 回归测试 | 用户U1 满足新人资格;模拟领取接口首个请求响应超时但后端已处理成功 | 1. 用户U1 点击立即领取
2. 将首个请求模拟为前端超时
3. 用户再次点击立即领取或页面自动重试
4. 查询优惠券列表和发放日志 | 首次请求后端处理成功;前端超时 5 秒 | 1. 页面提示处理中或可稍后刷新查看结果
2. 最终用户仅获得 1 张优惠券
3. 发放日志中仅 1 条成功记录,不存在多条成功发放
4. 页面最终状态与后台结果一致 | 非功能 | +| C_NG_SEND_004 | 用户端-首页-新人礼领取 | 验证发券失败时记录失败原因且不出现半成功状态 | P1 | 功能测试 | 用户U4 满足新人资格;将发券接口模拟为失败 | 1. 用户U4 点击立即领取
2. 观察页面提示
3. 查询我的优惠券列表
4. 查询发放日志 | 券中心返回=库存不足或券失效 | 1. 页面提示领取失败及失败原因
2. 我的优惠券列表不新增该券
3. `new_gift_send_log` 记录失败状态和失败原因
4. 用户状态不被误标记为已领取 | [AI修正: 覆盖部分成功和异常记录风险] | +| C_NG_SEND_005 | 用户端-首页-新人礼领取 | 验证同一账号双端并发领取时最终仅一次成功 | P1 | 功能测试 | 用户U5 满足新人资格;用户在手机端和 H5 端同时登录;平台存在进行中活动 | 1. 手机端和 H5 端同时打开新人礼弹窗
2. 两端几乎同时点击立即领取
3. 查询两端提示、优惠券列表和发放日志 | 用户U5;双端并发间隔小于 200ms | 1. 最终仅 1 次领取成功
2. 另一端返回已领取、处理中或重复请求提示
3. 用户优惠券列表仅新增 1 张券
4. 发放日志仅保留 1 条成功记录 | 并发 | +| C_NG_SEND_006 | 用户端-门店首页-新人礼领取 | 验证用户领取平台新人礼后仍可继续领取门店新人礼 | P0 | 功能测试 | 用户U7 已成功领取平台新人礼;商家A 存在进行中的门店新人礼活动;用户U7 在商家A 维度无非关闭订单且未领取过门店活动 | 1. 用户U7 登录平台首页并确认已领取平台新人礼
2. 用户U7 进入商家A 门店首页
3. 观察门店弹窗并点击立即领取
4. 查询门店发放日志和优惠券列表 | 用户U7;平台活动ID=PLT1001;门店活动ID=SHOP1001 | 1. 门店首页仍展示门店新人礼弹窗
2. 用户可成功领取门店优惠券
3. 优惠券列表中同时存在平台新人礼券和门店新人礼券
4. 门店活动发放日志新增成功记录,不因已领取平台礼而被拦截 | [AI修正: 按产品确认口径补充平台礼与门店礼可并存] | +| C_NG_CHECK_001 | 用户端-首页-资格检查 | 验证无进行中活动时不展示新人礼弹窗 | P1 | 功能测试 | 平台不存在进行中活动;用户U1 满足新人资格 | 1. 用户U1 登录平台首页
2. 观察首页弹窗
3. 查询资格检查接口返回 | 活动列表=空或全部不命中资格检查条件 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回无可领取活动
3. 页面不出现错误提示或异常闪现弹窗 | [AI修正: 将状态负向场景拆分,提升定位性] | +| C_NG_CHECK_002 | 用户端-首页-资格检查 | 验证活动未开始时不展示新人礼弹窗 | P1 | 功能测试 | 平台存在 1 条未开始活动;用户U1 满足新人资格;当前无其他进行中活动 | 1. 用户U1 登录平台首页
2. 观察首页弹窗
3. 查询资格检查接口返回 | 活动开始时间=当前时间后 1 小时 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回无可领取活动或活动未开始
3. 页面不出现提前曝光的领取入口 | [AI修正: 对标团队用例补充状态拆分场景] | +| C_NG_CHECK_003 | 用户端-首页-资格检查 | 验证活动已结束时不展示新人礼弹窗 | P1 | 功能测试 | 平台存在 1 条已结束活动;用户U1 满足新人资格;当前无其他进行中活动 | 1. 用户U1 登录平台首页
2. 观察首页弹窗
3. 查询资格检查接口返回 | 活动结束时间=当前时间前 1 小时 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回无可领取活动或活动已结束
3. 页面不出现已结束活动的领取入口 | [AI修正: 对标团队用例补充状态拆分场景] | +| C_NG_CHECK_004 | 用户端-首页-资格检查 | 验证活动已失效时不展示新人礼弹窗 | P1 | 功能测试 | 平台存在 1 条已失效活动;用户U1 满足新人资格;当前无其他进行中活动 | 1. 用户U1 登录平台首页
2. 观察首页弹窗
3. 查询资格检查接口返回 | 活动状态=已失效;valid_status=失效 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回无可领取活动或活动已失效
3. 页面状态与后台有效性字段一致 | [AI修正: 对标团队用例补充状态拆分场景] | +| API_NG_PERF_001 | 用户端-资格检查-接口性能 | 验证资格检查接口在常规数据量下满足 RT 基线 | P2 | 性能测试 | 存在 100 条历史发放记录和 10 条活动数据;性能环境可统计接口耗时 | 1. 触发用户进入平台首页调用资格检查接口
2. 连续执行 30 次
3. 统计平均 RT 和 P95 | 接口=`/ua/newGift/check`;样本量=30 | 1. 常规场景下接口平均 RT 和 P95 满足 1000ms 基线或接近目标
2. 页面无明显卡顿或超时提示
3. 若超基线,应能定位是活动查询、资格判定还是日志判断链路耗时 | [AI修正: 技术方案给出 RT 1000ms] | +| C_NG_RULE_001 | 用户端-资格判定-新人口径 | 验证存在已支付订单时不再识别为新人 | P1 | 功能测试 | 用户U8 存在 1 笔平台已支付订单;平台存在进行中活动 | 1. 用户U8 登录平台首页
2. 观察是否弹出新人礼
3. 查询资格检查返回 | 用户U8 历史订单状态=已支付 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回不满足新人条件
3. 页面展示与产品确认口径一致,不因已支付历史订单继续认定为新人 | [AI修正: 按产品确认口径更新新人判定] | +| DATA_NG_SCOPE_001 | 平台端-商家端-数据隔离 | 验证平台端与商家端新人礼活动数据不互通 | P0 | 功能测试 | 平台端已配置 1 条平台新人礼活动;商家A 已配置 1 条门店新人礼活动;测试账号分别具备平台端和商家端权限 | 1. 在平台端查看新人礼活动列表
2. 在商家A 端查看新人礼活动列表
3. 对比两端列表数据 | 平台活动ID=PLT1001;商家活动ID=SHOP1001 | 1. 平台端仅展示平台活动数据,不展示商家活动
2. 商家端仅展示当前店铺活动数据,不展示平台活动
3. 两端数据查询、查看、编辑入口相互隔离 | [AI修正: 取自团队现有功能用例] | +| VIEW_NG_READONLY_001 | 平台端-营销管理-新人有礼查看 | 验证查看态页面所有字段只读不可编辑 | P1 | 功能测试 | 平台已存在 1 条新人礼活动;平台运营账号已登录 | 1. 在列表中点击查看
2. 观察活动名称、活动时间、活动奖品等字段状态
3. 尝试修改字段或提交 | 活动ID=PLT1001 | 1. 查看页所有字段均为只读态
2. 页面不提供可提交修改的入口
3. 用户无法通过查看态改写活动配置 | [AI修正: 取自团队现有功能用例] | +| PLT_NG_LIST_002 | 平台端-营销管理-新人有礼列表 | 验证列表展示字段和操作栏状态映射正确 | P1 | 功能测试 | 平台端存在未开始、进行中、已结束、已失效四类活动;平台运营账号已登录 | 1. 进入新人有礼列表
2. 逐条检查活动名称、活动时间、活动状态、创建时间、操作栏
3. 对比不同状态下的操作按钮 | 活动A=未开始;活动B=进行中;活动C=已结束;活动D=已失效 | 1. 列表展示字段完整且内容正确
2. 未开始活动展示编辑、失效
3. 进行中活动展示查看、失效或按最终规则允许的编辑能力
4. 已结束和已失效活动操作栏按需求展示删除或受限操作 | [AI修正: 取自团队现有功能用例] | diff --git a/output/test_points/新人礼需求_测试点.md b/output/test_points/新人礼需求_测试点.md new file mode 100644 index 0000000..e2492e2 --- /dev/null +++ b/output/test_points/新人礼需求_测试点.md @@ -0,0 +1,80 @@ +# 新人礼需求测试点 + +## 1. 平台端列表与状态管理 + +1. 校验平台端列表默认按创建时间倒序展示,分页默认 10 条,支持切换 10、20、30、50 条。[需求] +2. 校验按活动名称模糊查询、按活动状态精准查询、无结果空态提示、重置查询条件的行为正确。[需求] +3. 校验活动状态“未开始、进行中、已结束、已失效”的展示口径和操作按钮是否符合规则。[需求] +4. 校验活动失效后有效性展示为“已失效”,且操作栏切换为删除入口。[需求] +5. 校验删除为永久删除,删除后列表不可见,后台数据状态与页面一致。[需求][项目画像] +6. 校验列表展示字段、创建时间、活动状态、操作栏内容与需求一致。[需求] +7. 校验不同状态下操作栏按钮映射正确,未开始/进行中/已结束/已失效的按钮展示不串位。[需求] +8. 校验点击“新建”可正常打开新人礼配置弹窗或页面,交互入口与权限口径一致。[需求] + +## 2. 活动配置与规则校验 + +1. 校验活动名称最大 50 字,超长时前端与后端均能拦截。[需求][漏测清单] +2. 校验活动开始时间小于当前时间、结束时间小于开始时间时不可保存。[需求][边界] +3. 校验活动奖品当前仅支持优惠券,积分奖励入口不可用或被明确拦截。[需求][技术方案] +4. 校验每个活动仅能关联 1 张优惠券,无法多选、多绑或通过接口绕过限制。[技术方案][项目画像] +5. 校验平台端新建合法活动时保存成功,列表、详情和活动明细数据落库正确。[需求] +6. 校验平台端编辑未开始活动时可修改名称、时间、奖品等字段并保存成功。[需求] +7. 校验平台维度同一时间仅允许 1 个进行中的平台新人礼活动,新增或编辑命中并存条件时保存失败并给出明确提示。[技术方案] +8. 校验同一店铺维度同一时间仅允许 1 个进行中的门店新人礼活动,不同店铺活动可并存。[技术方案] +9. 校验失效、删除操作存在二次确认,确认结果与列表及后台状态保持一致。[需求] +10. 校验编辑活动时,已开始活动是否只允许查看不允许修改关键字段,状态与按钮保持一致。[需求] + +## 3. 优惠券选择与权限隔离 + +1. 校验平台端仅能选择平台可用优惠券,且仅展示投放中、未过期、领取方式为用户领取的券。[需求] +2. 校验商家端仅能选择本店铺可用优惠券,不能看到或绑定其他店铺优惠券。[需求][项目画像] +3. 校验优惠券选择弹窗支持刷新、名称搜索、列表字段展示、分页、排序、单选反馈,且平台端与商家端展示的数据源范围不同。[需求][漏测清单] +4. 校验通过抓包篡改券 ID、店铺 ID 或活动归属时,后端能拦截越权绑定。[历史缺陷][项目画像] + +## 4. 商家端差异化行为 + +1. 校验商家端列表默认排序、分页、名称搜索、状态筛选、重置、空结果提示与平台端同构,但数据仅限当前店铺可见。[需求][项目画像] +2. 校验商家端列表展示字段、操作栏按钮、不同状态下的权限映射正确。[需求] +3. 校验商家端新建活动时,跨店铺数据隔离正确,门店 A 无法操作门店 B 活动。[项目画像] +4. 校验商家端新建合法活动时保存成功,列表、详情和活动明细数据正确落库。[需求] +5. 校验商家端编辑未开始活动时可正常保存,查看态页面所有字段只读,编辑态与查看态权限边界正确。[需求] +6. 校验商家端活动失效、删除后,仅影响当前店铺,不影响平台端和其他店铺活动。[需求] +7. 校验平台端活动数据在商家端不可见,商家端活动数据在平台端不可见。[需求][项目画像] + +## 5. C 端资格检查与弹窗展示 + +1. 校验平台新人登录平台首页时,存在进行中平台新人礼活动则展示弹窗。[需求] +2. 校验平台范围存在已支付或非交易关闭订单时,不展示平台新人礼弹窗。[需求][边界] +3. 校验店铺新人进入门店首页时,存在进行中门店新人礼活动则展示门店弹窗。[需求] +4. 校验当前店铺范围存在已支付或非交易关闭订单时,不展示门店新人礼弹窗。[需求][边界] +5. 校验门店老用户进入门店首页时,不展示门店新人礼弹窗。[需求] +6. 校验同时存在广告弹窗时,新人礼弹窗优先展示,关闭新人礼后再展示广告。[需求] +7. 校验活动未开始时,资格检查结果和页面展示均不展示新人礼弹窗。[需求][状态流转] +8. 校验活动已结束时,资格检查结果和页面展示均不展示新人礼弹窗。[需求][状态流转] +9. 校验活动已失效时,资格检查结果和页面展示均不展示新人礼弹窗。[需求][状态流转] +10. 校验用户历史订单均为交易关闭(未支付取消、超时未付、已退款)时,仍识别为新人并展示弹窗。[需求][产品确认] +11. 校验未登录进入平台首页或门店首页时,不展示新人礼弹窗且不误触发领取流程。[需求][边界] +12. 校验平台首页和门店首页展示的奖品信息与后台配置的优惠券信息一致,包括门槛、优惠金额、用券时间等。[需求] + +## 6. 领取发放与日志落库 + +1. 校验点击“立即领取”成功后,页面提示成功,优惠券进入“我的优惠券”。[需求] +2. 校验领取成功后,`new_gift_send_log` 写入成功记录,记录用户、活动、活动明细和状态。[技术方案] +3. 校验同一用户重复点击“立即领取”或短时间重复调用发放接口时,只发放 1 次,日志不出现重复成功记录。[历史缺陷][项目画像] +4. 校验领取失败时,页面提示失败原因,发放日志记录失败原因,不出现半成功状态。[技术方案][项目画像] +5. 校验用户已领取过奖励后,再次进入首页不再弹出同一活动弹窗。[需求] +6. 校验用户已领取平台新人礼后,若满足门店新人条件,仍可在门店首页领取门店新人礼。[需求][产品确认] + +## 7. 异常、并发与非功能 + +1. 校验弱网、超时或接口重试场景下,资格检查接口不会错误多次弹窗或误判资格。[漏测清单][非功能] +2. 校验领取接口超时或前端重复提交时,不会重复发券,用户可通过优惠券列表或重新进入页面查看最终结果。[漏测清单][非功能] +3. 校验同一账号在两个终端同时领取新人礼时,最终仅 1 次成功发放。[项目画像][并发] +4. 校验资格检查和领取接口响应时间满足技术目标 RT 1000ms,至少在常规测试数据量下不明显超时。[技术方案][非功能] + +## 8. 产品确认口径回归 + +1. 校验平台活动与门店活动可同时存在,但平台维度仅允许 1 条进行中活动、每个店铺维度仅允许 1 条进行中活动。[产品确认] +2. 校验历史订单均为交易关闭时仍视为新人;存在已支付或非关闭订单时不视为新人。[产品确认] +3. 校验活动“已结束”和“已失效”同时成立时,列表优先展示“已失效”。[产品确认] +4. 校验用户领取平台新人礼后,仍可继续领取符合条件的门店新人礼。[产品确认] diff --git a/output/versions/新人礼需求/VERSION_INDEX.md b/output/versions/新人礼需求/VERSION_INDEX.md new file mode 100644 index 0000000..967b1c4 --- /dev/null +++ b/output/versions/新人礼需求/VERSION_INDEX.md @@ -0,0 +1,7 @@ +# 新人礼需求 版本索引 + +| 版本 | 类型 | 内容摘要 | 目录 | +| --- | --- | --- | --- | +| v7 | 历史 Excel 导入 | excel、migration_note | `output/versions/新人礼需求/v7` | +| v8 | 完整流水线快照 | manifest、analysis、relation_report、test_points、test_cases_markdown、excel、normalized_inputs | `output/versions/新人礼需求/v8` | +| v9 | 完整流水线快照 | manifest、analysis、relation_report、test_points、test_cases_markdown、excel、normalized_inputs | `output/versions/新人礼需求/v9` | diff --git a/output/versions/新人礼需求/v7/snapshot_meta.json b/output/versions/新人礼需求/v7/snapshot_meta.json new file mode 100644 index 0000000..b50b657 --- /dev/null +++ b/output/versions/新人礼需求/v7/snapshot_meta.json @@ -0,0 +1,10 @@ +{ + "base_name": "新人礼需求", + "version": "v7", + "snapshot_type": "legacy_excel_import", + "summary": [ + "excel", + "migration_note" + ], + "note_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/versions/新人礼需求/v7/snapshot_note.md" +} diff --git a/output/versions/新人礼需求/v7/snapshot_note.md b/output/versions/新人礼需求/v7/snapshot_note.md new file mode 100644 index 0000000..edbc364 --- /dev/null +++ b/output/versions/新人礼需求/v7/snapshot_note.md @@ -0,0 +1,6 @@ +# 新人礼需求_测试用例 历史 Excel 导入说明 + +- 导入来源:`新人礼需求_测试用例_20260426_173650.xlsx` +- 导入类型:旧时间戳命名 Excel +- 说明:该快照仅迁移了历史 Excel,本次未重建当时对应的分析、测试点、测试用例 Markdown 与 manifest。 +- 目的:统一历史文件归档路径,并清理 `output/excel_reports/` 中的旧时间戳文件。 diff --git a/output/versions/新人礼需求/v7/新人礼需求_测试用例.xlsx b/output/versions/新人礼需求/v7/新人礼需求_测试用例.xlsx new file mode 100644 index 0000000..8216888 Binary files /dev/null and b/output/versions/新人礼需求/v7/新人礼需求_测试用例.xlsx differ diff --git a/output/versions/新人礼需求/v8/normalized_inputs/requirement.md b/output/versions/新人礼需求/v8/normalized_inputs/requirement.md new file mode 100644 index 0000000..e825bae --- /dev/null +++ b/output/versions/新人礼需求/v8/normalized_inputs/requirement.md @@ -0,0 +1,61 @@ +# 新人礼需求 + +> 文档角色:需求文档 +> 原始来源:`source_docs/requirements_raw/新人礼需求.docx` + +基线-新人礼 +| 版本号 | 变更内容 | 变更人 | 变更时间 | +| 0.0.1 | 文档创建 | 张昊 | 2026-02-28 | +一、业务背景 +基于问界需求清单 新人礼需求 +二、业务目标 +通过发布新人礼活动,吸引新用户注册使用~平台端&商家端支持发布新人礼活动,支持配置优惠券奖品,c端新用户注册展示新人礼内容 +三、产品设计 +1. 平台端-营销-新人有礼 +1.1 新人有礼列表 +列表数据说明 +数据权限:有此菜单列表权限的用户可看数据 +排序规则:按数据新增时间倒序排列 +分页规则:默认10行每页,可自主选择每页显示的条数(10/20/30/50) +数据来源:如下表 +| 数据字段 | 数据来源 | +| 活动名称 | 来源【新增/编辑】表单同名字段 | +| 活动时间 | 来源【新增/编辑】表单“活动时间“数据 | +| 活动状态 | 见下方状态逻辑说明 | +| 活动有效性 | 展示有效/已失效,支持 失效活动,失效后展示 已失效,失效后 操作栏展示删除操作 | +| 活动奖品 | 展示活动配置的奖品,当前活动奖品仅支持优惠券 | +| 创建时间 | 活动创建时间 | +状态逻辑说明 +| 状态名称 | 状态变更条件 | 可操作按钮 | +| 未开始 | 活动时间开始时间>当前时间 | 编辑,失效 | +| 进行中 | 活动时间开始时间<=当前时间 | 查看,失效 | +| 已结束 | 活动时间结束时间<当前时间 | 删除 | +查询条件说明 +| 查询条件 | 查询逻辑 | +| 活动名称 | 模糊查询,查询列表字段“活动名称” | +| 活动状态 | 精准查询,单选,选项数据:未开始、进行中、已结束、已失效,默认空 | +功能按钮说明 +| 按钮名称 | 触发后逻辑说明 | | +| 新建 | 在当前窗口打开“新建新人礼”弹窗 | | +| 查询 | 1、已选查询条件情况下,列表显示符合条件的数据 2、查询条件无匹配数据时,列表显示空,提示”暂无数据“ | | +| 重置 | 清空查询条件 | | +| 编辑 | 打开编辑表单弹窗 | | +| 查看 | 打开查看表单弹窗 | | +| 失效 | 打开失效操作弹窗,失效操作后,状态变更为已失效 | | +| 删除 | 打开删除操作弹窗,删除操作后,在列表删除活动(永久删除);取消后,关闭删除弹窗。 | | +1.2 新增/编辑 +页面字段说明 +| 字段名称 | 逻辑规则 | 是否必填 | 是否可编辑 | 原型界面、逻辑补充 | +| 活动名称 | 文本输入,最多50个字 | 是 | 是 | | +| 活动时间 | 开始时间-结束时间,年月日时分秒 | 是 | 是 | 控制活动的有效时段,可选择的时间大于等于当前时间,结束时间大于开始时间 | +| 活动奖品 | 多选 | 是 | 是 | | +| | 送优惠券 优惠券: 勾选后展示选择优惠券,只能选择1张优惠券,选择后展示选中优惠券信息表格 选择优惠券弹窗: 通用优惠券选择弹窗,单选 平台端展示平台可用优惠券列表, 商家端展示商家可用的优惠券列表, 优惠券列表数据展示规则:投放,未过期且为 用户领取 的优惠券,支持根据名称筛选 | 是 | 是 | 优惠券列表:选择前 优惠券列表:选择后 选择优惠券弹窗: | +| | 送积分 可输入1-999999的整数,配置生效后调用发积分接口发放积分 | 是 | 否 | 暂不支持 | +2. 商家端-营销-新人有礼 +功能同平台端,仅优惠券选择时,仅可选择该店铺下的优惠券 +3. C端 +3.1 首页 +| 平台首页-新人礼领取提示 奖励领取后,可在个人中心优惠券查看 | 店铺首页-新人礼领取提示 | +| 功能点 | 功能说明 | +| 弹窗逻辑 | 1)用户未在任意门店下过单,即视为新用户,登录后进入首页,弹窗提示用户领取新人礼奖励 2)用户未在该门店下过单,即视为门店新用户,登录后进入门店首页,弹窗提示用户领取新人礼奖励时机3)如果平台或店铺设置了弹窗广告,则新人礼弹窗在弹窗广告之前展示,关闭新人礼弹窗后,展示弹窗广告内容 | +| 优惠券 | 用户点击新人礼弹窗立即领取按钮,如果领取成功,奖励进入我的-优惠券列表,并提示用户领取成功 | diff --git a/output/versions/新人礼需求/v8/normalized_inputs/technical_solution_01.md b/output/versions/新人礼需求/v8/normalized_inputs/technical_solution_01.md new file mode 100644 index 0000000..095f30b --- /dev/null +++ b/output/versions/新人礼需求/v8/normalized_inputs/technical_solution_01.md @@ -0,0 +1,66 @@ +# 新人礼技术方案 + +> 文档角色:技术方案 +> 原始来源:`source_docs/technical_solutions/新人礼技术方案.docx` + +基线改造---新人礼技术方案&需求概述 +| 版本号 | 变更内容 | 变更人 | 变更时间 | +| 1.0 | 建档 | 陶震 | 2026-2-21 | +1 背景 +1.1  需求背景 +新增,针对于新注册,未下单用户发放专属优惠券的场景(暂不支持下单后退款场景) +1.2 业务现况 +1.3. 业务系统现况 +1.4 名词说明 +| 名称 | 描述 | +| 新人(平台新人,店铺新人) | 平台新人:未在该平台下过单的用户,即shop_custormer中无任何数据的user 店铺新人:未在该店铺下过单的用户,即shop——customer中无该店铺该user数据的用户 | +1.7 涉及人员 +| 角色名称 | 使用内容 | +| 用户 | 消费者 | +| 运营 | 商城日常使用维护以及操作者。 | +2 目标 +本期实现目标说明: +2.1、技术目标 +| 技术指标 | 指标值 | 备注 | +| 用户响应RT | 1000ms | | +| 用户体量 | | | +| 并发数 | | | +| 开发语言 | | | +| 网络要求 | | | +3 整体概述 +3.1 需求概述 +同一时间仅允许一个新人礼活动(避免同一时间多个活动发放业务过重)。关联优惠券仅支持关联一张 +C端弹窗,若存在进行中的新人礼活动且用户未领取过,则进行弹窗 +3.2 业务架构图(一图概览) +3.3 业务模块列表 +3.4 第三方服务列表(可选) +| 服务厂商 | 类型(接口/设备) | 接口地址 | 备注 | 状态 | +3.5 技术架构 +3.5.1 前端设计 +3.5.2 后端设计 +3.6 领域模型设计 +新人礼主表 +| SQLCREATE TABLE `new_gift` ( `new_gift_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `shop_id` bigint NOT NULL COMMENT '关联店铺 平台为0', `activity_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '活动名称', `activity_start_time` datetime NOT NULL COMMENT '活动开始时间', `activity_end_time` datetime NOT NULL COMMENT '活动结束时间', `gift_type` int NOT NULL COMMENT '礼物类型 0优惠券 其他待拓展', `activity_status` int NOT NULL COMMENT '活动状态0未开始,1进行中,2已结束', `valid_status` int NOT NULL DEFAULT '0' COMMENT '有效性 0有效 1已失效', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` int NOT NULL DEFAULT '0' COMMENT '是否已删除 0否1是', PRIMARY KEY (`new_gift_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='新人礼主表'; | +新人礼关联礼物表 +| SQLCREATE TABLE `new_gift_detail` ( `new_gift_detail_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `new_gift_id` bigint NOT NULL COMMENT '新人礼主键', `gift_type` int NOT NULL COMMENT '礼物类型 0优惠券 其他待拓展', `gift_biz_id` bigint NOT NULL COMMENT '礼物主键Id', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`new_gift_detail_id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='新人礼 子表,存储主表与礼物关联关系'; | +新人礼发放记录表 +| SQLCREATE TABLE `new_gift_send_log` ( `new_gift_log_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` bigint NOT NULL COMMENT '用户ID', `send_status` int NOT NULL COMMENT '发放状态 0成功 1失败', `fail_reason` json DEFAULT NULL COMMENT '失败原因', `new_gift_id` bigint NOT NULL COMMENT '新人礼主键', `new_gift_detail_id` bigint NOT NULL COMMENT '礼物详情主键', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`new_gift_log_id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='新人礼发放记录表'; | +3.7 数据模型设计 +详见领域模型 +3.8 依赖项 +| 依赖项 | 作用 | 预计提供时间 | 状态 | 责任人 | 交付物 | +4 场景(user story)与流程图(How) +4.1 新增活动 +4.1.1 业务流程图 +4.1.2 时序图 +4.1.3 接口依赖 +无 +4.1.4 接口设计 +| API | 状态 | 说明 | +| /mp/newGift/save | 未开发 | 新增新人礼活动 | +| /mp/newGift/update | 未开发 | 修改新人礼活动 | +| /mp/newGift/detail | 未开发 | 查看新人礼详情 | +| /mp/newGift/delete | 未开发 | 删除新人礼活动 | +| /mp/newGift/page | 未开发 | 新人礼活动分页查询 | +| /ua/newGift/check | 未开发 | 检测用户是否符合进行中的平台/店铺中的新人礼活动。 | +| /ua/newGift/send | 未开发 | 为用户发放对应新人礼绑定的券 | diff --git a/output/versions/新人礼需求/v8/snapshot_meta.json b/output/versions/新人礼需求/v8/snapshot_meta.json new file mode 100644 index 0000000..95ae8ec --- /dev/null +++ b/output/versions/新人礼需求/v8/snapshot_meta.json @@ -0,0 +1,14 @@ +{ + "base_name": "新人礼需求", + "version": "v8", + "snapshot_type": "full_pipeline", + "summary": [ + "manifest", + "analysis", + "relation_report", + "test_points", + "test_cases_markdown", + "excel", + "normalized_inputs" + ] +} diff --git a/output/versions/新人礼需求/v8/新人礼需求.json b/output/versions/新人礼需求/v8/新人礼需求.json new file mode 100644 index 0000000..287c42e --- /dev/null +++ b/output/versions/新人礼需求/v8/新人礼需求.json @@ -0,0 +1,43 @@ +{ + "requirement": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/source_docs/requirements_raw/新人礼需求.docx", + "requirement_source_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/source_docs/requirements_raw/新人礼需求.docx", + "requirement_input_type": "docx", + "base_name": "新人礼需求", + "normalized_requirement_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/normalized_inputs/新人礼需求/requirement.md", + "technical_solution_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/source_docs/technical_solutions/新人礼技术方案.docx" + ], + "normalized_technical_solution_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/normalized_inputs/新人礼需求/technical_solution_01.md" + ], + "analysis_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/analysis/新人礼需求_分析.md", + "relation_report_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/analysis/新人礼需求_关联与冲突.md", + "test_points_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/test_points/新人礼需求_测试点.md", + "test_cases_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/test_cases/新人礼需求_测试用例.md", + "excel_output_dir": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/excel_reports", + "project_profile_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/00_project/project_profile.md", + "knowledge_base_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/terminology.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/test_case_template.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/definition_of_done.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/review_checklist.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/02_history/common_missed_scenes.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/02_history/historical_defects.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/02_history/marketing_rules.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/03_best_practices/payment_flow_cases.md" + ], + "effective_terminology_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/terminology.md" + ], + "optional_terminology_files": [], + "related_requirements": [], + "conflict_candidates_count": 0, + "current_excel_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/excel_reports/新人礼需求_测试用例.xlsx", + "versioning_scheme": { + "current_files": "固定文件名,始终表示当前最新版", + "snapshot_rule": "仅在 export 成功且产物内容发生变化时递增版本", + "snapshot_dir_pattern": "output/versions/{BASE_NAME}/vN/" + }, + "latest_snapshot_version": "v8", + "latest_snapshot_dir": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/versions/新人礼需求/v8" +} diff --git a/output/versions/新人礼需求/v8/新人礼需求_关联与冲突.md b/output/versions/新人礼需求/v8/新人礼需求_关联与冲突.md new file mode 100644 index 0000000..a7ac99b --- /dev/null +++ b/output/versions/新人礼需求/v8/新人礼需求_关联与冲突.md @@ -0,0 +1,16 @@ +# 新人礼需求 关联需求与冲突检查 + +## 目标需求 +- `source_docs/requirements_raw/新人礼需求.docx` + +## 项目画像 +- `knowledge_base/00_project/project_profile.md` + +## 关联技术方案 +- `source_docs/technical_solutions/新人礼技术方案.docx` + +## 关联需求识别 +- 未识别到相似度达到阈值的历史需求文档。 + +## 潜在冲突与修改建议 +- 暂未识别到明显冲突条目。建议在需求评审时继续人工确认。 diff --git a/output/versions/新人礼需求/v8/新人礼需求_分析.md b/output/versions/新人礼需求/v8/新人礼需求_分析.md new file mode 100644 index 0000000..9b9a25d --- /dev/null +++ b/output/versions/新人礼需求/v8/新人礼需求_分析.md @@ -0,0 +1,111 @@ +# 新人礼需求分析 + +## 背景与目标 + +本期需求围绕“新人礼”活动能力建设,目标是在平台端和商家端支持配置新人礼活动,并在 C 端针对符合新人条件的用户展示领取入口与发放优惠券奖励,提升新注册用户的首单转化。 + +技术方案补充了两个关键限制: + +- 同一时间仅允许存在 1 个进行中的新人礼活动。 +- 每个新人礼活动仅允许绑定 1 张优惠券,发放结果需落新人礼发放记录表。 + +## 范围与边界 + +### 范围内 + +- 平台端“营销-新人有礼”列表、查询、新建、编辑、查看、失效、删除。 +- 商家端“营销-新人有礼”同构能力。 +- 新人礼活动配置字段校验:活动名称、活动时间、活动奖品、优惠券选择。 +- 优惠券选择范围控制:平台端仅选平台券,商家端仅选本店铺优惠券。 +- C 端首页和门店首页的新人礼弹窗检查与领取。 +- 发放成功后的优惠券入账和发放记录落库。 +- `ua/newGift/check` 与 `ua/newGift/send` 对应的资格校验和发放行为。 + +### 范围外 + +- 积分类型新人礼,需求和技术方案均明确“暂不支持”。 +- 下单后退款再重新认定为新人场景,技术方案明确“暂不支持下单后退款场景”。 +- 多奖品、多券组合、多人拼抢同一活动池等扩展玩法。 + +## 用户角色与前置条件 + +- 平台运营:维护平台维度新人礼活动。 +- 商家运营:维护店铺维度新人礼活动。 +- C 端用户:登录后触发首页或门店首页新人礼检查与领取。 +- 后端系统:负责活动状态判断、资格校验、优惠券发放、发送日志写入。 + +前置条件: + +- 已存在投放中、未过期、领取方式为“用户领取”的优惠券。 +- 用户已登录,且能获取平台维度与店铺维度的历史下单信息。 +- 发券接口可用,优惠券中心返回的券状态准确。 + +## 关键业务规则 + +1. 平台端和商家端都可以配置新人礼活动,但优惠券范围必须与端侧归属一致。 +2. 活动名称最多 50 个字。 +3. 活动开始时间必须大于等于当前时间,结束时间必须大于开始时间。 +4. 活动奖品当前仅支持优惠券,且每个活动仅能关联 1 张优惠券。 +5. 平台维度同一时间仅允许 1 个进行中的平台新人礼活动;店铺维度同一时间每个店铺仅允许 1 个进行中的门店新人礼活动。 +6. 新人判定按订单提交结果范围判断:平台新人礼查询平台范围订单,店铺新人礼查询当前店铺订单;若用户从未提交过订单,或历史订单最终状态均为交易关闭(包括未支付取消、超时未付、已退款订单等),仍视为新人。 +7. 用户登录首页时,若存在进行中的新人礼活动且用户未领取过,则展示新人礼弹窗。 +8. 门店首页只对门店新人展示门店新人礼弹窗。 +9. 若同时存在弹窗广告,新人礼弹窗必须先于广告弹窗展示。 +10. 用户点击立即领取成功后,奖励进入“我的优惠券”,同时写入新人礼发放记录。 +11. 活动“已结束”和“已失效”并存时,页面优先展示“已失效”。 +12. 活动失效后状态展示为“已失效”,并支持后续删除。 +13. 用户领取平台新人礼后,若仍满足门店新人条件,允许继续领取门店新人礼。 + +## 主流程描述 + +1. 运营在平台端或商家端进入“营销-新人有礼”列表页。 +2. 运营新建活动,填写活动名称、活动时间并选择 1 张符合条件的优惠券。 +3. 系统校验时间、优惠券范围、优惠券状态和活动并存规则,保存活动。 +4. 活动进入“未开始”或“进行中”状态,列表页按状态展示可操作按钮。 +5. C 端用户登录平台首页或门店首页时,调用资格检查接口。 +6. 若存在进行中的匹配活动且用户符合新人条件且未领取过,则展示新人礼弹窗。 +7. 用户点击“立即领取”,系统发放优惠券并记录发放日志。 +8. 发放成功后,用户可在个人中心优惠券列表查看奖励。 + +## 跨需求关联与冲突修订建议 + +- 当前未识别到需要纳入本次评审的关联需求,也未发现需要基于历史需求执行的规则冲突修订。 +- 当前无需对历史需求做口径覆盖修订,但仍需重点防御营销类公共风险: + - 重复点击导致重复发放。 + - 前端传参篡改导致越权选券或跨店铺发券。 + - 状态变更与页面展示不一致。 + +## 技术方案补充约束 + +- `ua/newGift/check` 负责资格检查,应重点验证平台维度和店铺维度“新人”判定口径。 +- `ua/newGift/send` 负责发券,应重点验证幂等、防重复领取、失败原因记录和成功落库。 +- 数据模型拆分为主表、礼物关联表、发放记录表,说明测试不能只看页面成功提示,还要覆盖: + - `new_gift` 主表活动状态和有效性字段。 + - `new_gift_detail` 活动与券的绑定关系。 + - `new_gift_send_log` 发放成功或失败记录。 +- 技术目标给出用户响应 RT 1000ms,应至少对资格检查和领取动作补充性能基线关注。 + +## 产品确认口径 + +- 活动并存范围:平台维度同一时间仅允许 1 条进行中的平台活动;店铺维度同一时间每个店铺仅允许 1 条进行中的门店活动。 +- 新人判定口径:若用户从未提交过订单,或历史订单最终状态均为交易关闭(包括未支付取消、超时未付、已退款订单),仍视为新人。 +- 状态展示优先级:活动“已结束”和“已失效”并存时,优先展示“已失效”。 +- 平台礼与门店礼关系:用户已领取平台新人礼后,只要满足门店新人条件,仍允许领取门店新人礼。 + +## 项目差异化测试约束 + +- 平台端、商家端、C 端是三套角色链路,必须覆盖角色隔离和数据隔离。 +- 这是典型营销发券能力,需重点覆盖越权选券、重复发放、状态错发和资损场景。 +- 预期结果必须同时覆盖 UI 反馈和后台状态变化,特别是活动状态、券领取结果、发放日志。 + +## 风险点与确认结果 + +- 风险点:重复点击“立即领取”或接口重试导致同一用户重复发放优惠券。 +- 风险点:商家端错误选择了其他店铺的优惠券,导致跨店资损或越权发券。 +- 风险点:活动失效、已结束、已删除三个状态口径不清,可能导致列表按钮和实际行为不一致。 +- 风险点:平台新人和店铺新人的判定依赖历史订单数据,若口径不清,容易误发或漏发。 +- 风险点:同一时间仅允许 1 个进行中的活动,如果平台端和商家端同时配置活动,范围口径不清会导致规则冲突。 +- 确认结果:平台端和商家端活动可以并存,但约束粒度为“平台一条、每店铺各一条”。 +- 确认结果:未支付取消、超时未付、已退款等最终交易关闭单据不影响新人资格。 +- 确认结果:活动“已结束”和“已失效”同时成立时,页面优先展示“已失效”。 +- 确认结果:已领取平台新人礼的用户,若满足门店新人条件,仍允许继续领取门店新人礼。 diff --git a/output/versions/新人礼需求/v8/新人礼需求_测试点.md b/output/versions/新人礼需求/v8/新人礼需求_测试点.md new file mode 100644 index 0000000..521a5b3 --- /dev/null +++ b/output/versions/新人礼需求/v8/新人礼需求_测试点.md @@ -0,0 +1,71 @@ +# 新人礼需求测试点 + +## 1. 平台端列表与状态管理 + +1. 校验平台端列表默认按创建时间倒序展示,分页默认 10 条,支持切换 10、20、30、50 条。[需求] +2. 校验按活动名称模糊查询、按活动状态精准查询、重置查询条件的行为正确。[需求] +3. 校验活动状态“未开始、进行中、已结束、已失效”的展示口径和操作按钮是否符合规则。[需求] +4. 校验活动失效后有效性展示为“已失效”,且操作栏切换为删除入口。[需求] +5. 校验删除为永久删除,删除后列表不可见,后台数据状态与页面一致。[需求][项目画像] +6. 校验列表展示字段、创建时间、活动状态、操作栏内容与需求一致。[需求] +7. 校验不同状态下操作栏按钮映射正确,未开始/进行中/已结束/已失效的按钮展示不串位。[需求] + +## 2. 活动配置与规则校验 + +1. 校验活动名称最大 50 字,超长时前端与后端均能拦截。[需求][漏测清单] +2. 校验活动开始时间小于当前时间、结束时间小于开始时间时不可保存。[需求][边界] +3. 校验活动奖品当前仅支持优惠券,积分奖励入口不可用或被明确拦截。[需求][技术方案] +4. 校验每个活动仅能关联 1 张优惠券,无法多选、多绑或通过接口绕过限制。[技术方案][项目画像] +5. 校验平台维度同一时间仅允许 1 个进行中的平台新人礼活动,新增或编辑命中并存条件时保存失败并给出明确提示。[技术方案] +6. 校验同一店铺维度同一时间仅允许 1 个进行中的门店新人礼活动,不同店铺活动可并存。[技术方案] +7. 校验编辑活动时,已开始活动是否只允许查看不允许修改关键字段,状态与按钮保持一致。[需求] + +## 3. 优惠券选择与权限隔离 + +1. 校验平台端仅能选择平台可用优惠券,且仅展示投放中、未过期、领取方式为用户领取的券。[需求] +2. 校验商家端仅能选择本店铺可用优惠券,不能看到或绑定其他店铺优惠券。[需求][项目画像] +3. 校验优惠券选择弹窗支持按名称筛选,选择前后展示信息正确。[需求] +4. 校验通过抓包篡改券 ID、店铺 ID 或活动归属时,后端能拦截越权绑定。[历史缺陷][项目画像] + +## 4. 商家端差异化行为 + +1. 校验商家端列表、查询、状态展示与平台端同构,但数据仅限当前店铺可见。[需求][项目画像] +2. 校验商家端新增活动时,跨店铺数据隔离正确,门店 A 无法操作门店 B 活动。[项目画像] +3. 校验商家端活动失效、删除后,仅影响当前店铺,不影响平台端和其他店铺活动。[需求] +4. 校验平台端活动数据在商家端不可见,商家端活动数据在平台端不可见。[需求][项目画像] +5. 校验查看态页面所有字段只读,编辑态与查看态权限边界正确。[需求] + +## 5. C 端资格检查与弹窗展示 + +1. 校验平台新人登录平台首页时,存在进行中平台新人礼活动则展示弹窗。[需求] +2. 校验平台范围存在已支付或非交易关闭订单时,不展示平台新人礼弹窗。[需求][边界] +3. 校验店铺新人进入门店首页时,存在进行中门店新人礼活动则展示门店弹窗。[需求] +4. 校验当前店铺范围存在已支付或非交易关闭订单时,不展示门店新人礼弹窗。[需求][边界] +5. 校验同时存在广告弹窗时,新人礼弹窗优先展示,关闭新人礼后再展示广告。[需求] +6. 校验无进行中活动、活动未开始、活动已结束、活动已失效时,资格检查结果和页面展示均正确。[需求][状态流转] +7. 校验用户历史订单均为交易关闭(未支付取消、超时未付、已退款)时,仍识别为新人并展示弹窗。[需求][产品确认] +8. 校验未登录进入平台首页或门店首页时,不展示新人礼弹窗且不误触发领取流程。[需求][边界] +9. 校验平台首页和门店首页展示的奖品信息与后台配置的优惠券信息一致,包括门槛、优惠金额、用券时间等。[需求] + +## 6. 领取发放与日志落库 + +1. 校验点击“立即领取”成功后,页面提示成功,优惠券进入“我的优惠券”。[需求] +2. 校验领取成功后,`new_gift_send_log` 写入成功记录,记录用户、活动、活动明细和状态。[技术方案] +3. 校验同一用户重复点击“立即领取”或短时间重复调用发放接口时,只发放 1 次,日志不出现重复成功记录。[历史缺陷][项目画像] +4. 校验领取失败时,页面提示失败原因,发放日志记录失败原因,不出现半成功状态。[技术方案][项目画像] +5. 校验用户已领取过奖励后,再次进入首页不再弹出同一活动弹窗。[需求] +6. 校验用户已领取平台新人礼后,若满足门店新人条件,仍可在门店首页领取门店新人礼。[需求][产品确认] + +## 7. 异常、并发与非功能 + +1. 校验弱网、超时或接口重试场景下,资格检查接口不会错误多次弹窗或误判资格。[漏测清单][非功能] +2. 校验领取接口超时或前端重复提交时,不会重复发券,用户可通过优惠券列表或重新进入页面查看最终结果。[漏测清单][非功能] +3. 校验同一账号在两个终端同时领取新人礼时,最终仅 1 次成功发放。[项目画像][并发] +4. 校验资格检查和领取接口响应时间满足技术目标 RT 1000ms,至少在常规测试数据量下不明显超时。[技术方案][非功能] + +## 8. 产品确认口径回归 + +1. 校验平台活动与门店活动可同时存在,但平台维度仅允许 1 条进行中活动、每个店铺维度仅允许 1 条进行中活动。[产品确认] +2. 校验历史订单均为交易关闭时仍视为新人;存在已支付或非关闭订单时不视为新人。[产品确认] +3. 校验活动“已结束”和“已失效”同时成立时,列表优先展示“已失效”。[产品确认] +4. 校验用户领取平台新人礼后,仍可继续领取符合条件的门店新人礼。[产品确认] diff --git a/output/versions/新人礼需求/v8/新人礼需求_测试用例.md b/output/versions/新人礼需求/v8/新人礼需求_测试用例.md new file mode 100644 index 0000000..cd7cfe6 --- /dev/null +++ b/output/versions/新人礼需求/v8/新人礼需求_测试用例.md @@ -0,0 +1,33 @@ +# 新人礼需求测试用例 + +| 用例编号 | 模块 | 用例标题 | 优先级 | 类型 | 前置条件 | 测试步骤 | 测试数据 | 预期结果 | 备注 | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| PLT_NG_LIST_001 | 平台端-营销管理-新人有礼列表 | 验证平台端列表默认排序、分页和查询功能正确 | P1 | 功能测试 | 平台端已存在 12 条新人礼活动数据,覆盖未开始、进行中、已结束、已失效状态;测试账号具备列表权限 | 1. 登录平台端进入营销-新人有礼列表
2. 观察默认排序和分页条数
3. 输入活动名称关键字执行查询
4. 选择活动状态执行筛选
5. 点击重置 | 活动A 创建时间晚于活动B;查询关键字=新人;状态=进行中 | 1. 列表默认按创建时间倒序展示
2. 默认每页展示 10 条
3. 名称查询仅返回命中活动
4. 状态筛选仅返回对应状态活动
5. 点击重置后查询条件被清空,列表恢复默认结果 | 主流程 | +| PLT_NG_CFG_001 | 平台端-营销管理-新人有礼配置 | 验证活动时间非法时无法保存活动 | P1 | 功能测试 | 平台运营账号已登录;存在可选平台优惠券 | 1. 点击新建活动
2. 输入合法活动名称
3. 设置开始时间早于当前时间后尝试保存
4. 再设置结束时间早于开始时间后尝试保存 | 活动名称=平台新人礼A;开始时间=当前时间前 1 分钟;结束时间=开始时间前 1 秒 | 1. 页面对开始时间非法给出明确提示
2. 页面对结束时间非法给出明确提示
3. 活动保存失败
4. 后台 `new_gift` 主表不新增活动记录 | 边界值 | +| PLT_NG_CFG_002 | 平台端-营销管理-新人有礼配置 | 验证平台端只能绑定 1 张符合条件的平台优惠券 | P0 | 功能测试 | 平台运营账号已登录;平台下存在 3 张券,分别为投放中可领券、已过期券、非用户领取券 | 1. 点击新建活动
2. 打开选择优惠券弹窗
3. 观察券列表范围
4. 选择 1 张投放中可领券后尝试继续追加选择第 2 张券
5. 保存活动 | 券A=投放中未过期用户领取券;券B=已过期券;券C=系统发放券 | 1. 弹窗仅展示平台可用且投放中、未过期、用户领取的券
2. 已过期券和非用户领取券不展示或不可选
3. 页面仅允许单选 1 张券
4. 保存成功后 `new_gift_detail` 仅存在 1 条券绑定记录 | [AI修正: 技术方案限定单券绑定] | +| PLT_NG_CFG_003 | 平台端-营销管理-新人有礼配置 | 验证积分奖励当前不支持 | P1 | 功能测试 | 平台运营账号已登录 | 1. 点击新建活动
2. 进入活动奖品区域
3. 查看送积分配置入口
4. 若可通过前端或抓包提交积分类型则尝试保存 | gift_type=积分;积分值=100 | 1. 页面不允许启用积分奖品,或该入口展示为暂不支持
2. 若前端被绕过提交积分类型,后端拦截保存并返回错误提示
3. 后台不生成积分类型活动记录 | [AI修正: 范围外能力拦截] | +| PLT_NG_RULE_001 | 平台端-营销管理-新人有礼配置 | 验证平台维度同一时间仅允许一个进行中的新人礼活动 | P0 | 功能测试 | 已存在 1 条平台活动A,时间范围覆盖当前时刻且状态为进行中;平台运营账号已登录 | 1. 点击新建活动
2. 填写与活动A 时间重叠的新活动B信息
3. 选择合法平台优惠券并保存 | 活动A 时间=今天 10:00 到 明天 10:00;活动B 时间=今天 12:00 到 明天 12:00 | 1. 页面或接口提示当前已有进行中的平台新人礼活动
2. 活动B 保存失败
3. 后台不新增活动B 记录
4. 活动A 状态和绑定关系不受影响 | [AI修正: 按产品确认口径更新为平台维度唯一] | +| PLT_NG_STATE_001 | 平台端-营销管理-新人有礼状态 | 验证活动失效后展示已失效并支持删除 | P1 | 功能测试 | 平台已存在 1 条未开始活动和 1 条进行中活动;平台运营账号已登录 | 1. 在列表中对活动执行失效操作
2. 刷新列表查看状态和操作按钮
3. 对已失效活动执行删除操作
4. 再次刷新列表 | 活动A=未开始;活动B=进行中 | 1. 失效后活动有效性展示为已失效
2. 失效活动的操作按钮切换为删除
3. 删除确认后页面提示删除成功
4. 列表中不再显示该活动
5. 后台活动记录被标记删除或按实现永久删除,不可继续被资格检查命中 | 状态流转 | +| PLT_NG_STATE_002 | 平台端-营销管理-新人有礼状态 | 验证活动已结束且已失效时列表优先展示已失效 | P1 | 功能测试 | 平台存在 1 条活动,活动结束时间早于当前时间,且已执行失效操作;平台运营账号已登录 | 1. 进入平台端新人有礼列表
2. 查询该活动状态展示和操作按钮 | 活动A 结束时间=当前时间前 1 天;valid_status=已失效 | 1. 列表最终状态优先展示为已失效
2. 操作按钮按已失效口径展示删除,不按已结束展示
3. 页面状态展示与后台有效性字段一致 | [AI修正: 按产品确认口径补充状态优先级] | +| MCH_NG_SCOPE_001 | 商家端-营销管理-新人有礼配置 | 验证商家端只能选择本店铺优惠券 | P0 | 功能测试 | 商家A 账号已登录;商家A 券A 为可领券;商家B 券B 为可领券 | 1. 商家A 进入新建活动页面
2. 打开优惠券选择弹窗
3. 搜索本店券A 和外店券B
4. 选择券A 保存活动 | 商家A ID=1001;商家B ID=1002;券A 属于商家A;券B 属于商家B | 1. 列表仅展示商家A 可用券
2. 搜索券B 无结果或不可选
3. 保存成功后活动绑定的券归属为商家A
4. 后台不存在跨店绑定关系 | [AI修正: 覆盖数据隔离与越权风险] | +| MCH_NG_SCOPE_002 | 商家端-营销管理-新人有礼配置 | 验证篡改券 ID 无法越权绑定其他店铺优惠券 | P0 | 安全性测试 | 商家A 账号已登录;抓包工具可修改请求;商家B 存在 1 张有效券 | 1. 商家A 正常进入新建活动页面并选择商家A 自有券
2. 提交前抓包将券 ID 替换为商家B 券 ID
3. 提交保存请求 | 提交参数原券 ID=COUPON_A_01;篡改后券 ID=COUPON_B_01 | 1. 后端拦截越权绑定请求并返回错误提示
2. 页面保存失败
3. 后台不生成跨店活动与券绑定关系
4. 审计日志记录异常请求或失败原因 | [AI修正: 对应越权与价格篡改类历史风险] | +| MCH_NG_RULE_001 | 商家端-营销管理-新人有礼配置 | 验证同一店铺仅允许一个进行中的门店新人礼活动但不同店铺可并存 | P0 | 功能测试 | 商家A 已存在 1 条进行中的门店活动A;商家B 无进行中活动;商家A、商家B 账号均可登录 | 1. 商家A 新建与活动A 时间重叠的门店活动A2并保存
2. 商家B 新建与活动A 同时段重叠的门店活动B并保存
3. 查询两家店铺活动列表 | 商家A ID=1001;商家B ID=1002;活动A2 与活动A 时间重叠;活动B 与活动A 时间重叠 | 1. 商家A 保存活动A2失败,并提示当前店铺已有进行中的新人礼活动
2. 商家B 保存活动B成功
3. 后台显示门店活动限制按店铺维度生效,不会因商家A 活动阻塞商家B 配置 | [AI修正: 按产品确认口径补充店铺维度唯一] | +| C_NG_POP_001 | 用户端-首页-新人礼弹窗 | 验证平台新人登录平台首页时展示新人礼弹窗 | P0 | 冒烟测试 | 平台存在 1 条进行中的平台新人礼活动;用户U1 已注册登录且平台维度无下单记录;用户未领取过该活动 | 1. 用户U1 登录平台首页
2. 观察首页首屏弹窗 | 用户U1;平台活动=进行中;平台下单记录=0 | 1. 首页展示新人礼弹窗
2. 弹窗内容展示活动奖励信息和立即领取按钮
3. 后台资格检查接口返回命中活动和可领取状态 | 主流程 | +| C_NG_POP_002 | 用户端-首页-新人礼弹窗 | 验证非平台新人登录平台首页时不展示新人礼弹窗 | P1 | 功能测试 | 平台存在 1 条进行中的平台新人礼活动;用户U2 已注册登录且平台维度存在已下单记录 | 1. 用户U2 登录平台首页
2. 观察首页弹窗和资格检查结果 | 用户U2;平台已支付订单数=1 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回不满足新人条件
3. 页面不出现立即领取入口 | 边界值 | +| C_NG_POP_003 | 用户端-门店首页-新人礼弹窗 | 验证平台老用户但门店新用户进入门店首页时展示门店新人礼弹窗 | P0 | 功能测试 | 商家A 存在 1 条进行中的门店新人礼活动;用户U3 平台维度已有下单记录,但在商家A 维度无下单记录;用户未领取过商家A 活动 | 1. 用户U3 进入商家A 门店首页
2. 观察首页弹窗
3. 查询资格检查结果 | 用户U3;平台订单数=1;商家A 订单数=0 | 1. 门店首页展示商家A 新人礼弹窗
2. 资格检查接口按门店维度返回可领取
3. 弹窗奖励信息与商家A 活动配置一致 | [AI修正: 覆盖平台新人和店铺新人差异口径] | +| C_NG_POP_004 | 用户端-首页-弹窗优先级 | 验证存在广告弹窗时新人礼弹窗优先展示 | P1 | 功能测试 | 平台存在 1 条进行中的新人礼活动;首页同时配置普通广告弹窗;用户U1 满足平台新人资格 | 1. 用户U1 登录平台首页
2. 观察首个弹窗
3. 关闭新人礼弹窗后继续观察 | 用户U1;广告弹窗=已配置 | 1. 首个弹窗为新人礼弹窗
2. 关闭新人礼弹窗后再展示广告弹窗
3. 整个过程中弹窗顺序与需求一致 | 交互优先级 | +| C_NG_POP_005 | 用户端-首页-新人礼弹窗 | 验证历史订单均为交易关闭时仍识别为平台新人 | P0 | 功能测试 | 平台存在 1 条进行中的平台新人礼活动;用户U6 历史仅有未支付取消单、超时未付单或已退款订单,且无其他非关闭订单 | 1. 用户U6 登录平台首页
2. 观察首页弹窗
3. 查询资格检查结果 | 用户U6 历史订单状态=交易关闭、已退款、超时未付 | 1. 首页展示平台新人礼弹窗
2. 资格检查接口返回满足平台新人条件
3. 页面不因历史关闭单或已退款单而拦截新人资格 | [AI修正: 按产品确认口径补充交易关闭单仍算新人] | +| C_NG_SEND_001 | 用户端-首页-新人礼领取 | 验证点击立即领取成功后优惠券入账并写发放日志 | P0 | 冒烟测试 | 用户U1 满足平台新人资格;平台存在进行中活动且绑定有效券;用户未领取过该活动 | 1. 用户U1 在新人礼弹窗点击立即领取
2. 观察页面提示
3. 进入我的优惠券查询奖励
4. 查询发放日志 | 用户U1;活动ID=NG1001;券ID=PC1001 | 1. 页面提示领取成功
2. 我的优惠券列表新增对应优惠券
3. `new_gift_send_log` 新增 1 条成功记录,包含用户、活动、活动明细和发送状态
4. 该用户再次进入首页时不再展示同一活动弹窗 | 主流程 | +| C_NG_SEND_002 | 用户端-首页-新人礼领取 | 验证同一用户重复点击立即领取时只发放一次 | P0 | 回归测试 | 用户U1 满足新人资格;平台存在进行中活动且绑定有效券;抓包或前端可模拟快速重复点击 | 1. 用户U1 打开新人礼弹窗
2. 在 1 秒内连续点击 3 次立即领取
3. 查询优惠券列表和发放日志 | 用户U1;活动ID=NG1001;重复点击次数=3 | 1. 页面最多显示 1 次领取成功提示
2. 用户仅新增 1 张优惠券
3. `new_gift_send_log` 仅有 1 条成功记录,其余请求被幂等拦截或返回已领取
4. 不出现重复发券 | [AI修正: 对应重复提交和重复支付类历史风险] | +| C_NG_SEND_003 | 用户端-首页-新人礼领取 | 验证领取接口超时重试时不会重复发券 | P1 | 回归测试 | 用户U1 满足新人资格;模拟领取接口首个请求响应超时但后端已处理成功 | 1. 用户U1 点击立即领取
2. 将首个请求模拟为前端超时
3. 用户再次点击立即领取或页面自动重试
4. 查询优惠券列表和发放日志 | 首次请求后端处理成功;前端超时 5 秒 | 1. 页面提示处理中或可稍后刷新查看结果
2. 最终用户仅获得 1 张优惠券
3. 发放日志中仅 1 条成功记录,不存在多条成功发放
4. 页面最终状态与后台结果一致 | 非功能 | +| C_NG_SEND_004 | 用户端-首页-新人礼领取 | 验证发券失败时记录失败原因且不出现半成功状态 | P1 | 功能测试 | 用户U4 满足新人资格;将发券接口模拟为失败 | 1. 用户U4 点击立即领取
2. 观察页面提示
3. 查询我的优惠券列表
4. 查询发放日志 | 券中心返回=库存不足或券失效 | 1. 页面提示领取失败及失败原因
2. 我的优惠券列表不新增该券
3. `new_gift_send_log` 记录失败状态和失败原因
4. 用户状态不被误标记为已领取 | [AI修正: 覆盖部分成功和异常记录风险] | +| C_NG_SEND_005 | 用户端-首页-新人礼领取 | 验证同一账号双端并发领取时最终仅一次成功 | P1 | 功能测试 | 用户U5 满足新人资格;用户在手机端和 H5 端同时登录;平台存在进行中活动 | 1. 手机端和 H5 端同时打开新人礼弹窗
2. 两端几乎同时点击立即领取
3. 查询两端提示、优惠券列表和发放日志 | 用户U5;双端并发间隔小于 200ms | 1. 最终仅 1 次领取成功
2. 另一端返回已领取、处理中或重复请求提示
3. 用户优惠券列表仅新增 1 张券
4. 发放日志仅保留 1 条成功记录 | 并发 | +| C_NG_SEND_006 | 用户端-门店首页-新人礼领取 | 验证用户领取平台新人礼后仍可继续领取门店新人礼 | P0 | 功能测试 | 用户U7 已成功领取平台新人礼;商家A 存在进行中的门店新人礼活动;用户U7 在商家A 维度无非关闭订单且未领取过门店活动 | 1. 用户U7 登录平台首页并确认已领取平台新人礼
2. 用户U7 进入商家A 门店首页
3. 观察门店弹窗并点击立即领取
4. 查询门店发放日志和优惠券列表 | 用户U7;平台活动ID=PLT1001;门店活动ID=SHOP1001 | 1. 门店首页仍展示门店新人礼弹窗
2. 用户可成功领取门店优惠券
3. 优惠券列表中同时存在平台新人礼券和门店新人礼券
4. 门店活动发放日志新增成功记录,不因已领取平台礼而被拦截 | [AI修正: 按产品确认口径补充平台礼与门店礼可并存] | +| C_NG_CHECK_001 | 用户端-首页-资格检查 | 验证无进行中活动或活动失效时不展示新人礼弹窗 | P1 | 功能测试 | 平台存在未开始、已结束或已失效活动,但不存在进行中活动;用户U1 满足新人资格 | 1. 用户U1 登录平台首页
2. 观察首页弹窗
3. 查询资格检查接口返回 | 活动状态分别为未开始、已结束、已失效 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回无可领取活动
3. 页面不出现错误提示或异常闪现弹窗 | 状态流转 | +| API_NG_PERF_001 | 用户端-资格检查-接口性能 | 验证资格检查接口在常规数据量下满足 RT 基线 | P2 | 性能测试 | 存在 100 条历史发放记录和 10 条活动数据;性能环境可统计接口耗时 | 1. 触发用户进入平台首页调用资格检查接口
2. 连续执行 30 次
3. 统计平均 RT 和 P95 | 接口=`/ua/newGift/check`;样本量=30 | 1. 常规场景下接口平均 RT 和 P95 满足 1000ms 基线或接近目标
2. 页面无明显卡顿或超时提示
3. 若超基线,应能定位是活动查询、资格判定还是日志判断链路耗时 | [AI修正: 技术方案给出 RT 1000ms] | +| C_NG_RULE_001 | 用户端-资格判定-新人口径 | 验证存在已支付订单时不再识别为新人 | P1 | 功能测试 | 用户U8 存在 1 笔平台已支付订单;平台存在进行中活动 | 1. 用户U8 登录平台首页
2. 观察是否弹出新人礼
3. 查询资格检查返回 | 用户U8 历史订单状态=已支付 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回不满足新人条件
3. 页面展示与产品确认口径一致,不因已支付历史订单继续认定为新人 | [AI修正: 按产品确认口径更新新人判定] | +| C_NG_POP_006 | 用户端-首页-新人礼弹窗 | 验证未登录进入首页时不展示新人礼弹窗 | P0 | 功能测试 | 平台或门店存在进行中的新人礼活动;用户未登录 | 1. 未登录直接进入平台首页
2. 未登录直接进入门店首页
3. 观察页面弹窗与网络请求 | 访问端=H5/小程序;登录态=未登录 | 1. 平台首页和门店首页均不展示新人礼弹窗
2. 页面不进入领取流程
3. 若触发资格检查请求,应返回未登录拦截而非可领取结果 | [AI修正: 取自团队现有功能用例] | +| C_NG_POP_007 | 用户端-首页-新人礼展示 | 验证首页展示的奖品信息与后台配置一致 | P0 | 功能测试 | 平台和商家端各存在 1 条进行中的新人礼活动,且分别绑定不同门槛和优惠内容的优惠券;用户满足资格 | 1. 平台新人登录平台首页查看弹窗奖品信息
2. 店铺新人进入门店首页查看弹窗奖品信息
3. 对比后台券配置 | 平台券=满100减20, 用券时间 2026-05-01 至 2026-05-31;店铺券=8折券, 上限 30 元 | 1. 平台首页弹窗展示的平台券门槛、优惠金额或折扣、用券时间与后台配置一致
2. 门店首页弹窗展示的商家券信息与商家端配置一致
3. 不出现平台券和商家券信息串位 | [AI修正: 取自团队现有功能用例] | +| DATA_NG_SCOPE_001 | 平台端-商家端-数据隔离 | 验证平台端与商家端新人礼活动数据不互通 | P0 | 功能测试 | 平台端已配置 1 条平台新人礼活动;商家A 已配置 1 条门店新人礼活动;测试账号分别具备平台端和商家端权限 | 1. 在平台端查看新人礼活动列表
2. 在商家A 端查看新人礼活动列表
3. 对比两端列表数据 | 平台活动ID=PLT1001;商家活动ID=SHOP1001 | 1. 平台端仅展示平台活动数据,不展示商家活动
2. 商家端仅展示当前店铺活动数据,不展示平台活动
3. 两端数据查询、查看、编辑入口相互隔离 | [AI修正: 取自团队现有功能用例] | +| VIEW_NG_READONLY_001 | 平台端-营销管理-新人有礼查看 | 验证查看态页面所有字段只读不可编辑 | P1 | 功能测试 | 平台已存在 1 条新人礼活动;平台运营账号已登录 | 1. 在列表中点击查看
2. 观察活动名称、活动时间、活动奖品等字段状态
3. 尝试修改字段或提交 | 活动ID=PLT1001 | 1. 查看页所有字段均为只读态
2. 页面不提供可提交修改的入口
3. 用户无法通过查看态改写活动配置 | [AI修正: 取自团队现有功能用例] | +| PLT_NG_LIST_002 | 平台端-营销管理-新人有礼列表 | 验证列表展示字段和操作栏状态映射正确 | P1 | 功能测试 | 平台端存在未开始、进行中、已结束、已失效四类活动;平台运营账号已登录 | 1. 进入新人有礼列表
2. 逐条检查活动名称、活动时间、活动状态、创建时间、操作栏
3. 对比不同状态下的操作按钮 | 活动A=未开始;活动B=进行中;活动C=已结束;活动D=已失效 | 1. 列表展示字段完整且内容正确
2. 未开始活动展示编辑、失效
3. 进行中活动展示查看、失效或按最终规则允许的编辑能力
4. 已结束和已失效活动操作栏按需求展示删除或受限操作 | [AI修正: 取自团队现有功能用例] | diff --git a/output/versions/新人礼需求/v8/新人礼需求_测试用例.xlsx b/output/versions/新人礼需求/v8/新人礼需求_测试用例.xlsx new file mode 100644 index 0000000..495c4b2 Binary files /dev/null and b/output/versions/新人礼需求/v8/新人礼需求_测试用例.xlsx differ diff --git a/output/versions/新人礼需求/v9/normalized_inputs/requirement.md b/output/versions/新人礼需求/v9/normalized_inputs/requirement.md new file mode 100644 index 0000000..e825bae --- /dev/null +++ b/output/versions/新人礼需求/v9/normalized_inputs/requirement.md @@ -0,0 +1,61 @@ +# 新人礼需求 + +> 文档角色:需求文档 +> 原始来源:`source_docs/requirements_raw/新人礼需求.docx` + +基线-新人礼 +| 版本号 | 变更内容 | 变更人 | 变更时间 | +| 0.0.1 | 文档创建 | 张昊 | 2026-02-28 | +一、业务背景 +基于问界需求清单 新人礼需求 +二、业务目标 +通过发布新人礼活动,吸引新用户注册使用~平台端&商家端支持发布新人礼活动,支持配置优惠券奖品,c端新用户注册展示新人礼内容 +三、产品设计 +1. 平台端-营销-新人有礼 +1.1 新人有礼列表 +列表数据说明 +数据权限:有此菜单列表权限的用户可看数据 +排序规则:按数据新增时间倒序排列 +分页规则:默认10行每页,可自主选择每页显示的条数(10/20/30/50) +数据来源:如下表 +| 数据字段 | 数据来源 | +| 活动名称 | 来源【新增/编辑】表单同名字段 | +| 活动时间 | 来源【新增/编辑】表单“活动时间“数据 | +| 活动状态 | 见下方状态逻辑说明 | +| 活动有效性 | 展示有效/已失效,支持 失效活动,失效后展示 已失效,失效后 操作栏展示删除操作 | +| 活动奖品 | 展示活动配置的奖品,当前活动奖品仅支持优惠券 | +| 创建时间 | 活动创建时间 | +状态逻辑说明 +| 状态名称 | 状态变更条件 | 可操作按钮 | +| 未开始 | 活动时间开始时间>当前时间 | 编辑,失效 | +| 进行中 | 活动时间开始时间<=当前时间 | 查看,失效 | +| 已结束 | 活动时间结束时间<当前时间 | 删除 | +查询条件说明 +| 查询条件 | 查询逻辑 | +| 活动名称 | 模糊查询,查询列表字段“活动名称” | +| 活动状态 | 精准查询,单选,选项数据:未开始、进行中、已结束、已失效,默认空 | +功能按钮说明 +| 按钮名称 | 触发后逻辑说明 | | +| 新建 | 在当前窗口打开“新建新人礼”弹窗 | | +| 查询 | 1、已选查询条件情况下,列表显示符合条件的数据 2、查询条件无匹配数据时,列表显示空,提示”暂无数据“ | | +| 重置 | 清空查询条件 | | +| 编辑 | 打开编辑表单弹窗 | | +| 查看 | 打开查看表单弹窗 | | +| 失效 | 打开失效操作弹窗,失效操作后,状态变更为已失效 | | +| 删除 | 打开删除操作弹窗,删除操作后,在列表删除活动(永久删除);取消后,关闭删除弹窗。 | | +1.2 新增/编辑 +页面字段说明 +| 字段名称 | 逻辑规则 | 是否必填 | 是否可编辑 | 原型界面、逻辑补充 | +| 活动名称 | 文本输入,最多50个字 | 是 | 是 | | +| 活动时间 | 开始时间-结束时间,年月日时分秒 | 是 | 是 | 控制活动的有效时段,可选择的时间大于等于当前时间,结束时间大于开始时间 | +| 活动奖品 | 多选 | 是 | 是 | | +| | 送优惠券 优惠券: 勾选后展示选择优惠券,只能选择1张优惠券,选择后展示选中优惠券信息表格 选择优惠券弹窗: 通用优惠券选择弹窗,单选 平台端展示平台可用优惠券列表, 商家端展示商家可用的优惠券列表, 优惠券列表数据展示规则:投放,未过期且为 用户领取 的优惠券,支持根据名称筛选 | 是 | 是 | 优惠券列表:选择前 优惠券列表:选择后 选择优惠券弹窗: | +| | 送积分 可输入1-999999的整数,配置生效后调用发积分接口发放积分 | 是 | 否 | 暂不支持 | +2. 商家端-营销-新人有礼 +功能同平台端,仅优惠券选择时,仅可选择该店铺下的优惠券 +3. C端 +3.1 首页 +| 平台首页-新人礼领取提示 奖励领取后,可在个人中心优惠券查看 | 店铺首页-新人礼领取提示 | +| 功能点 | 功能说明 | +| 弹窗逻辑 | 1)用户未在任意门店下过单,即视为新用户,登录后进入首页,弹窗提示用户领取新人礼奖励 2)用户未在该门店下过单,即视为门店新用户,登录后进入门店首页,弹窗提示用户领取新人礼奖励时机3)如果平台或店铺设置了弹窗广告,则新人礼弹窗在弹窗广告之前展示,关闭新人礼弹窗后,展示弹窗广告内容 | +| 优惠券 | 用户点击新人礼弹窗立即领取按钮,如果领取成功,奖励进入我的-优惠券列表,并提示用户领取成功 | diff --git a/output/versions/新人礼需求/v9/normalized_inputs/technical_solution_01.md b/output/versions/新人礼需求/v9/normalized_inputs/technical_solution_01.md new file mode 100644 index 0000000..095f30b --- /dev/null +++ b/output/versions/新人礼需求/v9/normalized_inputs/technical_solution_01.md @@ -0,0 +1,66 @@ +# 新人礼技术方案 + +> 文档角色:技术方案 +> 原始来源:`source_docs/technical_solutions/新人礼技术方案.docx` + +基线改造---新人礼技术方案&需求概述 +| 版本号 | 变更内容 | 变更人 | 变更时间 | +| 1.0 | 建档 | 陶震 | 2026-2-21 | +1 背景 +1.1  需求背景 +新增,针对于新注册,未下单用户发放专属优惠券的场景(暂不支持下单后退款场景) +1.2 业务现况 +1.3. 业务系统现况 +1.4 名词说明 +| 名称 | 描述 | +| 新人(平台新人,店铺新人) | 平台新人:未在该平台下过单的用户,即shop_custormer中无任何数据的user 店铺新人:未在该店铺下过单的用户,即shop——customer中无该店铺该user数据的用户 | +1.7 涉及人员 +| 角色名称 | 使用内容 | +| 用户 | 消费者 | +| 运营 | 商城日常使用维护以及操作者。 | +2 目标 +本期实现目标说明: +2.1、技术目标 +| 技术指标 | 指标值 | 备注 | +| 用户响应RT | 1000ms | | +| 用户体量 | | | +| 并发数 | | | +| 开发语言 | | | +| 网络要求 | | | +3 整体概述 +3.1 需求概述 +同一时间仅允许一个新人礼活动(避免同一时间多个活动发放业务过重)。关联优惠券仅支持关联一张 +C端弹窗,若存在进行中的新人礼活动且用户未领取过,则进行弹窗 +3.2 业务架构图(一图概览) +3.3 业务模块列表 +3.4 第三方服务列表(可选) +| 服务厂商 | 类型(接口/设备) | 接口地址 | 备注 | 状态 | +3.5 技术架构 +3.5.1 前端设计 +3.5.2 后端设计 +3.6 领域模型设计 +新人礼主表 +| SQLCREATE TABLE `new_gift` ( `new_gift_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `shop_id` bigint NOT NULL COMMENT '关联店铺 平台为0', `activity_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '活动名称', `activity_start_time` datetime NOT NULL COMMENT '活动开始时间', `activity_end_time` datetime NOT NULL COMMENT '活动结束时间', `gift_type` int NOT NULL COMMENT '礼物类型 0优惠券 其他待拓展', `activity_status` int NOT NULL COMMENT '活动状态0未开始,1进行中,2已结束', `valid_status` int NOT NULL DEFAULT '0' COMMENT '有效性 0有效 1已失效', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` int NOT NULL DEFAULT '0' COMMENT '是否已删除 0否1是', PRIMARY KEY (`new_gift_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='新人礼主表'; | +新人礼关联礼物表 +| SQLCREATE TABLE `new_gift_detail` ( `new_gift_detail_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `new_gift_id` bigint NOT NULL COMMENT '新人礼主键', `gift_type` int NOT NULL COMMENT '礼物类型 0优惠券 其他待拓展', `gift_biz_id` bigint NOT NULL COMMENT '礼物主键Id', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`new_gift_detail_id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='新人礼 子表,存储主表与礼物关联关系'; | +新人礼发放记录表 +| SQLCREATE TABLE `new_gift_send_log` ( `new_gift_log_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` bigint NOT NULL COMMENT '用户ID', `send_status` int NOT NULL COMMENT '发放状态 0成功 1失败', `fail_reason` json DEFAULT NULL COMMENT '失败原因', `new_gift_id` bigint NOT NULL COMMENT '新人礼主键', `new_gift_detail_id` bigint NOT NULL COMMENT '礼物详情主键', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`new_gift_log_id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='新人礼发放记录表'; | +3.7 数据模型设计 +详见领域模型 +3.8 依赖项 +| 依赖项 | 作用 | 预计提供时间 | 状态 | 责任人 | 交付物 | +4 场景(user story)与流程图(How) +4.1 新增活动 +4.1.1 业务流程图 +4.1.2 时序图 +4.1.3 接口依赖 +无 +4.1.4 接口设计 +| API | 状态 | 说明 | +| /mp/newGift/save | 未开发 | 新增新人礼活动 | +| /mp/newGift/update | 未开发 | 修改新人礼活动 | +| /mp/newGift/detail | 未开发 | 查看新人礼详情 | +| /mp/newGift/delete | 未开发 | 删除新人礼活动 | +| /mp/newGift/page | 未开发 | 新人礼活动分页查询 | +| /ua/newGift/check | 未开发 | 检测用户是否符合进行中的平台/店铺中的新人礼活动。 | +| /ua/newGift/send | 未开发 | 为用户发放对应新人礼绑定的券 | diff --git a/output/versions/新人礼需求/v9/snapshot_meta.json b/output/versions/新人礼需求/v9/snapshot_meta.json new file mode 100644 index 0000000..00b717c --- /dev/null +++ b/output/versions/新人礼需求/v9/snapshot_meta.json @@ -0,0 +1,14 @@ +{ + "base_name": "新人礼需求", + "version": "v9", + "snapshot_type": "full_pipeline", + "summary": [ + "manifest", + "analysis", + "relation_report", + "test_points", + "test_cases_markdown", + "excel", + "normalized_inputs" + ] +} diff --git a/output/versions/新人礼需求/v9/新人礼需求.json b/output/versions/新人礼需求/v9/新人礼需求.json new file mode 100644 index 0000000..95f1f15 --- /dev/null +++ b/output/versions/新人礼需求/v9/新人礼需求.json @@ -0,0 +1,44 @@ +{ + "requirement": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/source_docs/requirements_raw/新人礼需求.docx", + "requirement_source_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/source_docs/requirements_raw/新人礼需求.docx", + "requirement_input_type": "docx", + "base_name": "新人礼需求", + "normalized_requirement_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/normalized_inputs/新人礼需求/requirement.md", + "technical_solution_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/source_docs/technical_solutions/新人礼技术方案.docx" + ], + "normalized_technical_solution_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/normalized_inputs/新人礼需求/technical_solution_01.md" + ], + "analysis_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/analysis/新人礼需求_分析.md", + "relation_report_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/analysis/新人礼需求_关联与冲突.md", + "test_points_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/test_points/新人礼需求_测试点.md", + "test_cases_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/test_cases/新人礼需求_测试用例.md", + "excel_output_dir": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/excel_reports", + "project_profile_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/00_project/project_profile.md", + "knowledge_base_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/terminology.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/test_case_template.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/definition_of_done.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/review_checklist.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/02_history/common_missed_scenes.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/02_history/historical_defects.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/02_history/marketing_rules.md", + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/03_best_practices/payment_flow_cases.md" + ], + "effective_terminology_files": [ + "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/01_standards/terminology.md" + ], + "optional_terminology_files": [], + "related_requirements": [], + "conflict_candidates_count": 0, + "current_excel_file": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/excel_reports/新人礼需求_测试用例.xlsx", + "versioning_scheme": { + "current_files": "固定文件名,始终表示当前最新版", + "snapshot_rule": "仅在 export 成功且产物内容发生变化时递增版本", + "snapshot_dir_pattern": "output/versions/{BASE_NAME}/vN/" + }, + "latest_snapshot_version": "v8", + "latest_snapshot_dir": "/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/output/versions/新人礼需求/v8", + "latest_snapshot_type": "full_pipeline" +} diff --git a/output/versions/新人礼需求/v9/新人礼需求_关联与冲突.md b/output/versions/新人礼需求/v9/新人礼需求_关联与冲突.md new file mode 100644 index 0000000..a7ac99b --- /dev/null +++ b/output/versions/新人礼需求/v9/新人礼需求_关联与冲突.md @@ -0,0 +1,16 @@ +# 新人礼需求 关联需求与冲突检查 + +## 目标需求 +- `source_docs/requirements_raw/新人礼需求.docx` + +## 项目画像 +- `knowledge_base/00_project/project_profile.md` + +## 关联技术方案 +- `source_docs/technical_solutions/新人礼技术方案.docx` + +## 关联需求识别 +- 未识别到相似度达到阈值的历史需求文档。 + +## 潜在冲突与修改建议 +- 暂未识别到明显冲突条目。建议在需求评审时继续人工确认。 diff --git a/output/versions/新人礼需求/v9/新人礼需求_分析.md b/output/versions/新人礼需求/v9/新人礼需求_分析.md new file mode 100644 index 0000000..9b9a25d --- /dev/null +++ b/output/versions/新人礼需求/v9/新人礼需求_分析.md @@ -0,0 +1,111 @@ +# 新人礼需求分析 + +## 背景与目标 + +本期需求围绕“新人礼”活动能力建设,目标是在平台端和商家端支持配置新人礼活动,并在 C 端针对符合新人条件的用户展示领取入口与发放优惠券奖励,提升新注册用户的首单转化。 + +技术方案补充了两个关键限制: + +- 同一时间仅允许存在 1 个进行中的新人礼活动。 +- 每个新人礼活动仅允许绑定 1 张优惠券,发放结果需落新人礼发放记录表。 + +## 范围与边界 + +### 范围内 + +- 平台端“营销-新人有礼”列表、查询、新建、编辑、查看、失效、删除。 +- 商家端“营销-新人有礼”同构能力。 +- 新人礼活动配置字段校验:活动名称、活动时间、活动奖品、优惠券选择。 +- 优惠券选择范围控制:平台端仅选平台券,商家端仅选本店铺优惠券。 +- C 端首页和门店首页的新人礼弹窗检查与领取。 +- 发放成功后的优惠券入账和发放记录落库。 +- `ua/newGift/check` 与 `ua/newGift/send` 对应的资格校验和发放行为。 + +### 范围外 + +- 积分类型新人礼,需求和技术方案均明确“暂不支持”。 +- 下单后退款再重新认定为新人场景,技术方案明确“暂不支持下单后退款场景”。 +- 多奖品、多券组合、多人拼抢同一活动池等扩展玩法。 + +## 用户角色与前置条件 + +- 平台运营:维护平台维度新人礼活动。 +- 商家运营:维护店铺维度新人礼活动。 +- C 端用户:登录后触发首页或门店首页新人礼检查与领取。 +- 后端系统:负责活动状态判断、资格校验、优惠券发放、发送日志写入。 + +前置条件: + +- 已存在投放中、未过期、领取方式为“用户领取”的优惠券。 +- 用户已登录,且能获取平台维度与店铺维度的历史下单信息。 +- 发券接口可用,优惠券中心返回的券状态准确。 + +## 关键业务规则 + +1. 平台端和商家端都可以配置新人礼活动,但优惠券范围必须与端侧归属一致。 +2. 活动名称最多 50 个字。 +3. 活动开始时间必须大于等于当前时间,结束时间必须大于开始时间。 +4. 活动奖品当前仅支持优惠券,且每个活动仅能关联 1 张优惠券。 +5. 平台维度同一时间仅允许 1 个进行中的平台新人礼活动;店铺维度同一时间每个店铺仅允许 1 个进行中的门店新人礼活动。 +6. 新人判定按订单提交结果范围判断:平台新人礼查询平台范围订单,店铺新人礼查询当前店铺订单;若用户从未提交过订单,或历史订单最终状态均为交易关闭(包括未支付取消、超时未付、已退款订单等),仍视为新人。 +7. 用户登录首页时,若存在进行中的新人礼活动且用户未领取过,则展示新人礼弹窗。 +8. 门店首页只对门店新人展示门店新人礼弹窗。 +9. 若同时存在弹窗广告,新人礼弹窗必须先于广告弹窗展示。 +10. 用户点击立即领取成功后,奖励进入“我的优惠券”,同时写入新人礼发放记录。 +11. 活动“已结束”和“已失效”并存时,页面优先展示“已失效”。 +12. 活动失效后状态展示为“已失效”,并支持后续删除。 +13. 用户领取平台新人礼后,若仍满足门店新人条件,允许继续领取门店新人礼。 + +## 主流程描述 + +1. 运营在平台端或商家端进入“营销-新人有礼”列表页。 +2. 运营新建活动,填写活动名称、活动时间并选择 1 张符合条件的优惠券。 +3. 系统校验时间、优惠券范围、优惠券状态和活动并存规则,保存活动。 +4. 活动进入“未开始”或“进行中”状态,列表页按状态展示可操作按钮。 +5. C 端用户登录平台首页或门店首页时,调用资格检查接口。 +6. 若存在进行中的匹配活动且用户符合新人条件且未领取过,则展示新人礼弹窗。 +7. 用户点击“立即领取”,系统发放优惠券并记录发放日志。 +8. 发放成功后,用户可在个人中心优惠券列表查看奖励。 + +## 跨需求关联与冲突修订建议 + +- 当前未识别到需要纳入本次评审的关联需求,也未发现需要基于历史需求执行的规则冲突修订。 +- 当前无需对历史需求做口径覆盖修订,但仍需重点防御营销类公共风险: + - 重复点击导致重复发放。 + - 前端传参篡改导致越权选券或跨店铺发券。 + - 状态变更与页面展示不一致。 + +## 技术方案补充约束 + +- `ua/newGift/check` 负责资格检查,应重点验证平台维度和店铺维度“新人”判定口径。 +- `ua/newGift/send` 负责发券,应重点验证幂等、防重复领取、失败原因记录和成功落库。 +- 数据模型拆分为主表、礼物关联表、发放记录表,说明测试不能只看页面成功提示,还要覆盖: + - `new_gift` 主表活动状态和有效性字段。 + - `new_gift_detail` 活动与券的绑定关系。 + - `new_gift_send_log` 发放成功或失败记录。 +- 技术目标给出用户响应 RT 1000ms,应至少对资格检查和领取动作补充性能基线关注。 + +## 产品确认口径 + +- 活动并存范围:平台维度同一时间仅允许 1 条进行中的平台活动;店铺维度同一时间每个店铺仅允许 1 条进行中的门店活动。 +- 新人判定口径:若用户从未提交过订单,或历史订单最终状态均为交易关闭(包括未支付取消、超时未付、已退款订单),仍视为新人。 +- 状态展示优先级:活动“已结束”和“已失效”并存时,优先展示“已失效”。 +- 平台礼与门店礼关系:用户已领取平台新人礼后,只要满足门店新人条件,仍允许领取门店新人礼。 + +## 项目差异化测试约束 + +- 平台端、商家端、C 端是三套角色链路,必须覆盖角色隔离和数据隔离。 +- 这是典型营销发券能力,需重点覆盖越权选券、重复发放、状态错发和资损场景。 +- 预期结果必须同时覆盖 UI 反馈和后台状态变化,特别是活动状态、券领取结果、发放日志。 + +## 风险点与确认结果 + +- 风险点:重复点击“立即领取”或接口重试导致同一用户重复发放优惠券。 +- 风险点:商家端错误选择了其他店铺的优惠券,导致跨店资损或越权发券。 +- 风险点:活动失效、已结束、已删除三个状态口径不清,可能导致列表按钮和实际行为不一致。 +- 风险点:平台新人和店铺新人的判定依赖历史订单数据,若口径不清,容易误发或漏发。 +- 风险点:同一时间仅允许 1 个进行中的活动,如果平台端和商家端同时配置活动,范围口径不清会导致规则冲突。 +- 确认结果:平台端和商家端活动可以并存,但约束粒度为“平台一条、每店铺各一条”。 +- 确认结果:未支付取消、超时未付、已退款等最终交易关闭单据不影响新人资格。 +- 确认结果:活动“已结束”和“已失效”同时成立时,页面优先展示“已失效”。 +- 确认结果:已领取平台新人礼的用户,若满足门店新人条件,仍允许继续领取门店新人礼。 diff --git a/output/versions/新人礼需求/v9/新人礼需求_测试点.md b/output/versions/新人礼需求/v9/新人礼需求_测试点.md new file mode 100644 index 0000000..e2492e2 --- /dev/null +++ b/output/versions/新人礼需求/v9/新人礼需求_测试点.md @@ -0,0 +1,80 @@ +# 新人礼需求测试点 + +## 1. 平台端列表与状态管理 + +1. 校验平台端列表默认按创建时间倒序展示,分页默认 10 条,支持切换 10、20、30、50 条。[需求] +2. 校验按活动名称模糊查询、按活动状态精准查询、无结果空态提示、重置查询条件的行为正确。[需求] +3. 校验活动状态“未开始、进行中、已结束、已失效”的展示口径和操作按钮是否符合规则。[需求] +4. 校验活动失效后有效性展示为“已失效”,且操作栏切换为删除入口。[需求] +5. 校验删除为永久删除,删除后列表不可见,后台数据状态与页面一致。[需求][项目画像] +6. 校验列表展示字段、创建时间、活动状态、操作栏内容与需求一致。[需求] +7. 校验不同状态下操作栏按钮映射正确,未开始/进行中/已结束/已失效的按钮展示不串位。[需求] +8. 校验点击“新建”可正常打开新人礼配置弹窗或页面,交互入口与权限口径一致。[需求] + +## 2. 活动配置与规则校验 + +1. 校验活动名称最大 50 字,超长时前端与后端均能拦截。[需求][漏测清单] +2. 校验活动开始时间小于当前时间、结束时间小于开始时间时不可保存。[需求][边界] +3. 校验活动奖品当前仅支持优惠券,积分奖励入口不可用或被明确拦截。[需求][技术方案] +4. 校验每个活动仅能关联 1 张优惠券,无法多选、多绑或通过接口绕过限制。[技术方案][项目画像] +5. 校验平台端新建合法活动时保存成功,列表、详情和活动明细数据落库正确。[需求] +6. 校验平台端编辑未开始活动时可修改名称、时间、奖品等字段并保存成功。[需求] +7. 校验平台维度同一时间仅允许 1 个进行中的平台新人礼活动,新增或编辑命中并存条件时保存失败并给出明确提示。[技术方案] +8. 校验同一店铺维度同一时间仅允许 1 个进行中的门店新人礼活动,不同店铺活动可并存。[技术方案] +9. 校验失效、删除操作存在二次确认,确认结果与列表及后台状态保持一致。[需求] +10. 校验编辑活动时,已开始活动是否只允许查看不允许修改关键字段,状态与按钮保持一致。[需求] + +## 3. 优惠券选择与权限隔离 + +1. 校验平台端仅能选择平台可用优惠券,且仅展示投放中、未过期、领取方式为用户领取的券。[需求] +2. 校验商家端仅能选择本店铺可用优惠券,不能看到或绑定其他店铺优惠券。[需求][项目画像] +3. 校验优惠券选择弹窗支持刷新、名称搜索、列表字段展示、分页、排序、单选反馈,且平台端与商家端展示的数据源范围不同。[需求][漏测清单] +4. 校验通过抓包篡改券 ID、店铺 ID 或活动归属时,后端能拦截越权绑定。[历史缺陷][项目画像] + +## 4. 商家端差异化行为 + +1. 校验商家端列表默认排序、分页、名称搜索、状态筛选、重置、空结果提示与平台端同构,但数据仅限当前店铺可见。[需求][项目画像] +2. 校验商家端列表展示字段、操作栏按钮、不同状态下的权限映射正确。[需求] +3. 校验商家端新建活动时,跨店铺数据隔离正确,门店 A 无法操作门店 B 活动。[项目画像] +4. 校验商家端新建合法活动时保存成功,列表、详情和活动明细数据正确落库。[需求] +5. 校验商家端编辑未开始活动时可正常保存,查看态页面所有字段只读,编辑态与查看态权限边界正确。[需求] +6. 校验商家端活动失效、删除后,仅影响当前店铺,不影响平台端和其他店铺活动。[需求] +7. 校验平台端活动数据在商家端不可见,商家端活动数据在平台端不可见。[需求][项目画像] + +## 5. C 端资格检查与弹窗展示 + +1. 校验平台新人登录平台首页时,存在进行中平台新人礼活动则展示弹窗。[需求] +2. 校验平台范围存在已支付或非交易关闭订单时,不展示平台新人礼弹窗。[需求][边界] +3. 校验店铺新人进入门店首页时,存在进行中门店新人礼活动则展示门店弹窗。[需求] +4. 校验当前店铺范围存在已支付或非交易关闭订单时,不展示门店新人礼弹窗。[需求][边界] +5. 校验门店老用户进入门店首页时,不展示门店新人礼弹窗。[需求] +6. 校验同时存在广告弹窗时,新人礼弹窗优先展示,关闭新人礼后再展示广告。[需求] +7. 校验活动未开始时,资格检查结果和页面展示均不展示新人礼弹窗。[需求][状态流转] +8. 校验活动已结束时,资格检查结果和页面展示均不展示新人礼弹窗。[需求][状态流转] +9. 校验活动已失效时,资格检查结果和页面展示均不展示新人礼弹窗。[需求][状态流转] +10. 校验用户历史订单均为交易关闭(未支付取消、超时未付、已退款)时,仍识别为新人并展示弹窗。[需求][产品确认] +11. 校验未登录进入平台首页或门店首页时,不展示新人礼弹窗且不误触发领取流程。[需求][边界] +12. 校验平台首页和门店首页展示的奖品信息与后台配置的优惠券信息一致,包括门槛、优惠金额、用券时间等。[需求] + +## 6. 领取发放与日志落库 + +1. 校验点击“立即领取”成功后,页面提示成功,优惠券进入“我的优惠券”。[需求] +2. 校验领取成功后,`new_gift_send_log` 写入成功记录,记录用户、活动、活动明细和状态。[技术方案] +3. 校验同一用户重复点击“立即领取”或短时间重复调用发放接口时,只发放 1 次,日志不出现重复成功记录。[历史缺陷][项目画像] +4. 校验领取失败时,页面提示失败原因,发放日志记录失败原因,不出现半成功状态。[技术方案][项目画像] +5. 校验用户已领取过奖励后,再次进入首页不再弹出同一活动弹窗。[需求] +6. 校验用户已领取平台新人礼后,若满足门店新人条件,仍可在门店首页领取门店新人礼。[需求][产品确认] + +## 7. 异常、并发与非功能 + +1. 校验弱网、超时或接口重试场景下,资格检查接口不会错误多次弹窗或误判资格。[漏测清单][非功能] +2. 校验领取接口超时或前端重复提交时,不会重复发券,用户可通过优惠券列表或重新进入页面查看最终结果。[漏测清单][非功能] +3. 校验同一账号在两个终端同时领取新人礼时,最终仅 1 次成功发放。[项目画像][并发] +4. 校验资格检查和领取接口响应时间满足技术目标 RT 1000ms,至少在常规测试数据量下不明显超时。[技术方案][非功能] + +## 8. 产品确认口径回归 + +1. 校验平台活动与门店活动可同时存在,但平台维度仅允许 1 条进行中活动、每个店铺维度仅允许 1 条进行中活动。[产品确认] +2. 校验历史订单均为交易关闭时仍视为新人;存在已支付或非关闭订单时不视为新人。[产品确认] +3. 校验活动“已结束”和“已失效”同时成立时,列表优先展示“已失效”。[产品确认] +4. 校验用户领取平台新人礼后,仍可继续领取符合条件的门店新人礼。[产品确认] diff --git a/output/versions/新人礼需求/v9/新人礼需求_测试用例.md b/output/versions/新人礼需求/v9/新人礼需求_测试用例.md new file mode 100644 index 0000000..9e0371e --- /dev/null +++ b/output/versions/新人礼需求/v9/新人礼需求_测试用例.md @@ -0,0 +1,47 @@ +# 新人礼需求测试用例 + +| 用例编号 | 模块 | 用例标题 | 优先级 | 类型 | 前置条件 | 测试步骤 | 测试数据 | 预期结果 | 备注 | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| PLT_NG_LIST_001 | 平台端-营销管理-新人有礼列表 | 验证平台端列表默认排序、分页和查询功能正确 | P1 | 功能测试 | 平台端已存在 12 条新人礼活动数据,覆盖未开始、进行中、已结束、已失效状态;测试账号具备列表权限 | 1. 登录平台端进入营销-新人有礼列表
2. 观察默认排序和分页条数
3. 输入活动名称关键字执行查询
4. 选择活动状态执行筛选
5. 输入无匹配关键字执行查询
6. 点击重置 | 活动A 创建时间晚于活动B;查询关键字=新人;状态=进行中;无匹配关键字=不存在的活动名 | 1. 列表默认按创建时间倒序展示
2. 默认每页展示 10 条
3. 名称查询仅返回命中活动
4. 状态筛选仅返回对应状态活动
5. 无匹配数据时列表展示空态或“暂无数据”提示
6. 点击重置后查询条件被清空,列表恢复默认结果 | [AI修正: 对标团队用例补充空结果查询场景] | +| PLT_NG_CFG_001 | 平台端-营销管理-新人有礼配置 | 验证活动时间非法时无法保存活动 | P1 | 功能测试 | 平台运营账号已登录;存在可选平台优惠券 | 1. 点击新建活动
2. 输入合法活动名称
3. 设置开始时间早于当前时间后尝试保存
4. 再设置结束时间早于开始时间后尝试保存 | 活动名称=平台新人礼A;开始时间=当前时间前 1 分钟;结束时间=开始时间前 1 秒 | 1. 页面对开始时间非法给出明确提示
2. 页面对结束时间非法给出明确提示
3. 活动保存失败
4. 后台 `new_gift` 主表不新增活动记录 | 边界值 | +| PLT_NG_CFG_002 | 平台端-营销管理-新人有礼配置 | 验证平台端只能绑定 1 张符合条件的平台优惠券 | P0 | 功能测试 | 平台运营账号已登录;平台下存在 3 张券,分别为投放中可领券、已过期券、非用户领取券 | 1. 点击新建活动
2. 打开选择优惠券弹窗
3. 观察券列表范围
4. 选择 1 张投放中可领券后尝试继续追加选择第 2 张券
5. 保存活动 | 券A=投放中未过期用户领取券;券B=已过期券;券C=系统发放券 | 1. 弹窗仅展示平台可用且投放中、未过期、用户领取的券
2. 已过期券和非用户领取券不展示或不可选
3. 页面仅允许单选 1 张券
4. 保存成功后 `new_gift_detail` 仅存在 1 条券绑定记录 | [AI修正: 技术方案限定单券绑定] | +| PLT_NG_CFG_003 | 平台端-营销管理-新人有礼配置 | 验证积分奖励当前不支持 | P1 | 功能测试 | 平台运营账号已登录 | 1. 点击新建活动
2. 进入活动奖品区域
3. 查看送积分配置入口
4. 若可通过前端或抓包提交积分类型则尝试保存 | gift_type=积分;积分值=100 | 1. 页面不允许启用积分奖品,或该入口展示为暂不支持
2. 若前端被绕过提交积分类型,后端拦截保存并返回错误提示
3. 后台不生成积分类型活动记录 | [AI修正: 范围外能力拦截] | +| PLT_NG_CREATE_001 | 平台端-营销管理-新人有礼配置 | 验证平台端新建合法活动可保存成功 | P0 | 功能测试 | 平台运营账号已登录;存在 1 张投放中、未过期、用户领取类型的平台优惠券;当前无冲突中的平台活动 | 1. 点击新建活动
2. 填写活动名称、时间并选择 1 张合法平台券
3. 点击确定保存
4. 返回列表并进入详情页核对数据 | 活动名称=平台新人礼春季活动;开始时间=明天 10:00;结束时间=后天 10:00;券ID=PLT_COUPON_1001 | 1. 页面提示创建成功
2. 列表新增该活动记录,字段展示正确
3. 活动状态按时间口径展示为未开始
4. `new_gift` 和 `new_gift_detail` 新增对应活动及券绑定记录 | [AI修正: 对标团队用例补充后台正向新建链路] | +| PLT_NG_EDIT_001 | 平台端-营销管理-新人有礼编辑 | 验证平台端编辑未开始活动可保存成功 | P1 | 功能测试 | 平台已存在 1 条未开始活动;平台运营账号已登录;存在 1 张新的合法平台优惠券 | 1. 在列表中点击编辑未开始活动
2. 修改活动名称、时间或绑定券
3. 点击确定保存
4. 刷新列表并进入详情查看 | 原活动名称=平台新人礼A;新活动名称=平台新人礼A-调整版;新券ID=PLT_COUPON_1002 | 1. 页面提示保存成功
2. 列表展示为修改后的名称和时间
3. 详情页展示最新配置
4. 后台活动主表和明细表更新为最新值,不产生重复活动记录 | [AI修正: 对标团队用例补充后台正向编辑链路] | +| PLT_NG_POPUP_001 | 平台端-营销管理-优惠券选择弹窗 | 验证平台端选券弹窗支持搜索分页排序和单选反馈 | P1 | 功能测试 | 平台运营账号已登录;平台下存在 25 张符合或不符合条件的优惠券 | 1. 进入新建活动页面并打开优惠券选择弹窗
2. 点击刷新并观察列表加载
3. 输入优惠券名称执行搜索
4. 切换分页条数和页码
5. 观察列表排序和字段展示
6. 选择 1 张优惠券并确认返回 | 券名称关键字=新人礼;分页项=10/20;目标券=PLT_COUPON_1003 | 1. 弹窗可正常刷新数据
2. 名称搜索仅返回命中券
3. 分页、页码切换和排序行为正确
4. 列表展示券名称、有效期、领取方式等必要字段
5. 仅允许单选 1 张券,选中后页面回填正确的券信息 | [AI修正: 对标团队用例补充选券弹窗交互明细] | +| PLT_NG_RULE_001 | 平台端-营销管理-新人有礼配置 | 验证平台维度同一时间仅允许一个进行中的新人礼活动 | P0 | 功能测试 | 已存在 1 条平台活动A,时间范围覆盖当前时刻且状态为进行中;平台运营账号已登录 | 1. 点击新建活动
2. 填写与活动A 时间重叠的新活动B信息
3. 选择合法平台优惠券并保存 | 活动A 时间=今天 10:00 到 明天 10:00;活动B 时间=今天 12:00 到 明天 12:00 | 1. 页面或接口提示当前已有进行中的平台新人礼活动
2. 活动B 保存失败
3. 后台不新增活动B 记录
4. 活动A 状态和绑定关系不受影响 | [AI修正: 按产品确认口径更新为平台维度唯一] | +| PLT_NG_STATE_001 | 平台端-营销管理-新人有礼状态 | 验证活动失效后展示已失效并支持删除 | P1 | 功能测试 | 平台已存在 1 条未开始活动和 1 条进行中活动;平台运营账号已登录 | 1. 在列表中对活动执行失效操作
2. 刷新列表查看状态和操作按钮
3. 对已失效活动执行删除操作
4. 再次刷新列表 | 活动A=未开始;活动B=进行中 | 1. 失效后活动有效性展示为已失效
2. 失效活动的操作按钮切换为删除
3. 删除确认后页面提示删除成功
4. 列表中不再显示该活动
5. 后台活动记录被标记删除或按实现永久删除,不可继续被资格检查命中 | 状态流转 | +| PLT_NG_STATE_002 | 平台端-营销管理-新人有礼状态 | 验证活动已结束且已失效时列表优先展示已失效 | P1 | 功能测试 | 平台存在 1 条活动,活动结束时间早于当前时间,且已执行失效操作;平台运营账号已登录 | 1. 进入平台端新人有礼列表
2. 查询该活动状态展示和操作按钮 | 活动A 结束时间=当前时间前 1 天;valid_status=已失效 | 1. 列表最终状态优先展示为已失效
2. 操作按钮按已失效口径展示删除,不按已结束展示
3. 页面状态展示与后台有效性字段一致 | [AI修正: 按产品确认口径补充状态优先级] | +| MCH_NG_SCOPE_001 | 商家端-营销管理-新人有礼配置 | 验证商家端只能选择本店铺优惠券 | P0 | 功能测试 | 商家A 账号已登录;商家A 券A 为可领券;商家B 券B 为可领券 | 1. 商家A 进入新建活动页面
2. 打开优惠券选择弹窗
3. 搜索本店券A 和外店券B
4. 选择券A 保存活动 | 商家A ID=1001;商家B ID=1002;券A 属于商家A;券B 属于商家B | 1. 列表仅展示商家A 可用券
2. 搜索券B 无结果或不可选
3. 保存成功后活动绑定的券归属为商家A
4. 后台不存在跨店绑定关系 | [AI修正: 覆盖数据隔离与越权风险] | +| MCH_NG_SCOPE_002 | 商家端-营销管理-新人有礼配置 | 验证篡改券 ID 无法越权绑定其他店铺优惠券 | P0 | 安全性测试 | 商家A 账号已登录;抓包工具可修改请求;商家B 存在 1 张有效券 | 1. 商家A 正常进入新建活动页面并选择商家A 自有券
2. 提交前抓包将券 ID 替换为商家B 券 ID
3. 提交保存请求 | 提交参数原券 ID=COUPON_A_01;篡改后券 ID=COUPON_B_01 | 1. 后端拦截越权绑定请求并返回错误提示
2. 页面保存失败
3. 后台不生成跨店活动与券绑定关系
4. 审计日志记录异常请求或失败原因 | [AI修正: 对应越权与价格篡改类历史风险] | +| MCH_NG_RULE_001 | 商家端-营销管理-新人有礼配置 | 验证同一店铺仅允许一个进行中的门店新人礼活动但不同店铺可并存 | P0 | 功能测试 | 商家A 已存在 1 条进行中的门店活动A;商家B 无进行中活动;商家A、商家B 账号均可登录 | 1. 商家A 新建与活动A 时间重叠的门店活动A2并保存
2. 商家B 新建与活动A 同时段重叠的门店活动B并保存
3. 查询两家店铺活动列表 | 商家A ID=1001;商家B ID=1002;活动A2 与活动A 时间重叠;活动B 与活动A 时间重叠 | 1. 商家A 保存活动A2失败,并提示当前店铺已有进行中的新人礼活动
2. 商家B 保存活动B成功
3. 后台显示门店活动限制按店铺维度生效,不会因商家A 活动阻塞商家B 配置 | [AI修正: 按产品确认口径补充店铺维度唯一] | +| MCH_NG_LIST_001 | 商家端-营销管理-新人有礼列表 | 验证商家端列表默认排序、分页、搜索和重置功能正确 | P1 | 功能测试 | 商家A 已存在 12 条新人礼活动数据,覆盖未开始、进行中、已结束、已失效状态;商家运营账号具备列表权限 | 1. 登录商家A 端进入新人有礼列表
2. 观察默认排序和分页条数
3. 输入活动名称关键字执行查询
4. 选择活动状态执行筛选
5. 输入无匹配关键字执行查询
6. 点击重置 | 店铺ID=1001;关键字=新人;状态=进行中;无匹配关键字=不存在的活动名 | 1. 列表默认按创建时间倒序展示
2. 默认每页展示 10 条并可切换分页项
3. 名称搜索和状态筛选仅返回当前店铺命中数据
4. 无匹配数据时展示空态或“暂无数据”提示
5. 点击重置后恢复默认列表 | [AI修正: 对标团队用例补充商家端列表基础能力] | +| MCH_NG_LIST_002 | 商家端-营销管理-新人有礼列表 | 验证商家端列表字段和操作栏状态映射正确 | P1 | 功能测试 | 商家A 存在未开始、进行中、已结束、已失效四类活动;商家运营账号已登录 | 1. 进入商家A 新人有礼列表
2. 逐条检查活动名称、活动时间、活动状态、创建时间、操作栏
3. 对比不同状态下的操作按钮 | 活动A=未开始;活动B=进行中;活动C=已结束;活动D=已失效 | 1. 列表展示字段完整且内容正确
2. 未开始活动展示编辑、失效
3. 进行中活动展示查看、失效或按最终规则允许的编辑能力
4. 已结束和已失效活动展示删除或受限操作
5. 状态与按钮映射不串位 | [AI修正: 对标团队用例补充商家端状态按钮映射] | +| MCH_NG_CREATE_001 | 商家端-营销管理-新人有礼配置 | 验证商家端新建合法活动可保存成功 | P0 | 功能测试 | 商家A 账号已登录;商家A 存在 1 张投放中、未过期、用户领取类型的店铺优惠券;当前店铺无冲突中的门店活动 | 1. 点击新建活动
2. 填写活动名称、时间并选择 1 张本店合法券
3. 点击确定保存
4. 返回列表并进入详情页核对数据 | 店铺ID=1001;活动名称=门店新人礼春季活动;券ID=SHOP_COUPON_1001 | 1. 页面提示创建成功
2. 列表新增该门店活动记录,字段展示正确
3. 活动状态按时间口径展示为未开始
4. `new_gift` 和 `new_gift_detail` 新增当前店铺对应活动及券绑定记录 | [AI修正: 对标团队用例补充商家端正向新建链路] | +| MCH_NG_EDIT_001 | 商家端-营销管理-新人有礼编辑 | 验证商家端编辑未开始活动可保存成功 | P1 | 功能测试 | 商家A 存在 1 条未开始活动;商家A 账号已登录;存在 1 张新的本店合法券 | 1. 在列表中点击编辑未开始活动
2. 修改活动名称、时间或绑定券
3. 点击确定保存
4. 刷新列表并进入详情页核对 | 原活动名称=门店新人礼A;新活动名称=门店新人礼A-调整版;新券ID=SHOP_COUPON_1002 | 1. 页面提示保存成功
2. 列表和详情页展示最新配置
3. 后台活动主表和明细表更新为最新值
4. 不产生重复活动记录或跨店异常数据 | [AI修正: 对标团队用例补充商家端正向编辑链路] | +| MCH_NG_POPUP_001 | 商家端-营销管理-优惠券选择弹窗 | 验证商家端选券弹窗支持搜索分页排序和单选反馈 | P1 | 功能测试 | 商家A 账号已登录;商家A 下存在 25 张符合或不符合条件的优惠券 | 1. 进入新建活动页面并打开优惠券选择弹窗
2. 点击刷新并观察列表加载
3. 输入优惠券名称执行搜索
4. 切换分页条数和页码
5. 观察列表排序和字段展示
6. 选择 1 张优惠券并确认返回 | 店铺ID=1001;券名称关键字=新人礼;分页项=10/20;目标券=SHOP_COUPON_1003 | 1. 弹窗可正常刷新数据
2. 搜索仅返回当前店铺命中的券
3. 分页、页码切换和排序行为正确
4. 列表展示券名称、有效期、领取方式等必要字段
5. 仅允许单选 1 张券,确认后页面正确回填券信息 | [AI修正: 对标团队用例补充商家端选券弹窗交互明细] | +| MCH_NG_STATE_001 | 商家端-营销管理-新人有礼状态 | 验证商家端活动失效后展示已失效并支持删除 | P1 | 功能测试 | 商家A 已存在 1 条未开始活动和 1 条进行中活动;商家A 账号已登录 | 1. 在列表中对活动执行失效操作并确认
2. 刷新列表查看状态和操作按钮
3. 对已失效活动执行删除并确认
4. 再次刷新列表 | 店铺ID=1001;活动A=未开始;活动B=进行中 | 1. 失效后活动有效性展示为已失效
2. 已失效活动操作栏切换为删除
3. 删除确认后页面提示删除成功
4. 列表中不再显示该活动
5. 后台仅删除当前店铺对应活动,不影响其他店铺或平台活动 | [AI修正: 对标团队用例补充商家端失效删除链路] | +| MCH_NG_VIEW_001 | 商家端-营销管理-新人有礼查看 | 验证商家端查看态页面所有字段只读不可编辑 | P1 | 功能测试 | 商家A 已存在 1 条新人礼活动;商家A 账号已登录 | 1. 在列表中点击查看
2. 观察活动名称、活动时间、活动奖品等字段状态
3. 尝试修改字段或提交 | 店铺ID=1001;活动ID=SHOP1001 | 1. 查看页所有字段均为只读态
2. 页面不提供可提交修改的入口
3. 用户无法通过查看态改写活动配置 | [AI修正: 对标团队用例补充商家端查看只读场景] | +| C_NG_POP_001 | 用户端-首页-新人礼弹窗 | 验证平台新人登录平台首页时展示新人礼弹窗 | P0 | 冒烟测试 | 平台存在 1 条进行中的平台新人礼活动;用户U1 已注册登录且平台维度无下单记录;用户未领取过该活动 | 1. 用户U1 登录平台首页
2. 观察首页首屏弹窗 | 用户U1;平台活动=进行中;平台下单记录=0 | 1. 首页展示新人礼弹窗
2. 弹窗内容展示活动奖励信息和立即领取按钮
3. 后台资格检查接口返回命中活动和可领取状态 | 主流程 | +| C_NG_POP_002 | 用户端-首页-新人礼弹窗 | 验证非平台新人登录平台首页时不展示新人礼弹窗 | P1 | 功能测试 | 平台存在 1 条进行中的平台新人礼活动;用户U2 已注册登录且平台维度存在已下单记录 | 1. 用户U2 登录平台首页
2. 观察首页弹窗和资格检查结果 | 用户U2;平台已支付订单数=1 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回不满足新人条件
3. 页面不出现立即领取入口 | 边界值 | +| C_NG_POP_003 | 用户端-门店首页-新人礼弹窗 | 验证平台老用户但门店新用户进入门店首页时展示门店新人礼弹窗 | P0 | 功能测试 | 商家A 存在 1 条进行中的门店新人礼活动;用户U3 平台维度已有下单记录,但在商家A 维度无下单记录;用户未领取过商家A 活动 | 1. 用户U3 进入商家A 门店首页
2. 观察首页弹窗
3. 查询资格检查结果 | 用户U3;平台订单数=1;商家A 订单数=0 | 1. 门店首页展示商家A 新人礼弹窗
2. 资格检查接口按门店维度返回可领取
3. 弹窗奖励信息与商家A 活动配置一致 | [AI修正: 覆盖平台新人和店铺新人差异口径] | +| C_NG_POP_004 | 用户端-首页-弹窗优先级 | 验证存在广告弹窗时新人礼弹窗优先展示 | P1 | 功能测试 | 平台存在 1 条进行中的新人礼活动;首页同时配置普通广告弹窗;用户U1 满足平台新人资格 | 1. 用户U1 登录平台首页
2. 观察首个弹窗
3. 关闭新人礼弹窗后继续观察 | 用户U1;广告弹窗=已配置 | 1. 首个弹窗为新人礼弹窗
2. 关闭新人礼弹窗后再展示广告弹窗
3. 整个过程中弹窗顺序与需求一致 | 交互优先级 | +| C_NG_POP_005 | 用户端-首页-新人礼弹窗 | 验证历史订单均为交易关闭时仍识别为平台新人 | P0 | 功能测试 | 平台存在 1 条进行中的平台新人礼活动;用户U6 历史仅有未支付取消单、超时未付单或已退款订单,且无其他非关闭订单 | 1. 用户U6 登录平台首页
2. 观察首页弹窗
3. 查询资格检查结果 | 用户U6 历史订单状态=交易关闭、已退款、超时未付 | 1. 首页展示平台新人礼弹窗
2. 资格检查接口返回满足平台新人条件
3. 页面不因历史关闭单或已退款单而拦截新人资格 | [AI修正: 按产品确认口径补充交易关闭单仍算新人] | +| C_NG_POP_006 | 用户端-首页-新人礼弹窗 | 验证未登录进入首页时不展示新人礼弹窗 | P0 | 功能测试 | 平台或门店存在进行中的新人礼活动;用户未登录 | 1. 未登录直接进入平台首页
2. 未登录直接进入门店首页
3. 观察页面弹窗与网络请求 | 访问端=H5/小程序;登录态=未登录 | 1. 平台首页和门店首页均不展示新人礼弹窗
2. 页面不进入领取流程
3. 若触发资格检查请求,应返回未登录拦截而非可领取结果 | [AI修正: 取自团队现有功能用例] | +| C_NG_POP_007 | 用户端-首页-新人礼展示 | 验证首页展示的奖品信息与后台配置一致 | P0 | 功能测试 | 平台和商家端各存在 1 条进行中的新人礼活动,且分别绑定不同门槛和优惠内容的优惠券;用户满足资格 | 1. 平台新人登录平台首页查看弹窗奖品信息
2. 店铺新人进入门店首页查看弹窗奖品信息
3. 对比后台券配置 | 平台券=满100减20, 用券时间 2026-05-01 至 2026-05-31;店铺券=8折券, 上限 30 元 | 1. 平台首页弹窗展示的平台券门槛、优惠金额或折扣、用券时间与后台配置一致
2. 门店首页弹窗展示的商家券信息与商家端配置一致
3. 不出现平台券和商家券信息串位 | [AI修正: 取自团队现有功能用例] | +| C_NG_POP_008 | 用户端-门店首页-新人礼弹窗 | 验证门店老用户进入门店首页时不展示门店新人礼弹窗 | P1 | 功能测试 | 商家A 存在 1 条进行中的门店新人礼活动;用户U9 已登录且在商家A 维度存在已支付订单 | 1. 用户U9 进入商家A 门店首页
2. 观察首页弹窗和资格检查结果 | 用户U9;店铺ID=1001;商家A 已支付订单数=1 | 1. 门店首页不展示门店新人礼弹窗
2. 资格检查接口返回不满足门店新人条件
3. 页面不出现立即领取入口 | [AI修正: 对标团队用例补充门店老用户负向场景] | +| C_NG_SEND_001 | 用户端-首页-新人礼领取 | 验证点击立即领取成功后优惠券入账并写发放日志 | P0 | 冒烟测试 | 用户U1 满足平台新人资格;平台存在进行中活动且绑定有效券;用户未领取过该活动 | 1. 用户U1 在新人礼弹窗点击立即领取
2. 观察页面提示
3. 进入我的优惠券查询奖励
4. 查询发放日志 | 用户U1;活动ID=NG1001;券ID=PC1001 | 1. 页面提示领取成功
2. 我的优惠券列表新增对应优惠券
3. `new_gift_send_log` 新增 1 条成功记录,包含用户、活动、活动明细和发送状态
4. 该用户再次进入首页时不再展示同一活动弹窗 | 主流程 | +| C_NG_SEND_002 | 用户端-首页-新人礼领取 | 验证同一用户重复点击立即领取时只发放一次 | P0 | 回归测试 | 用户U1 满足新人资格;平台存在进行中活动且绑定有效券;抓包或前端可模拟快速重复点击 | 1. 用户U1 打开新人礼弹窗
2. 在 1 秒内连续点击 3 次立即领取
3. 查询优惠券列表和发放日志 | 用户U1;活动ID=NG1001;重复点击次数=3 | 1. 页面最多显示 1 次领取成功提示
2. 用户仅新增 1 张优惠券
3. `new_gift_send_log` 仅有 1 条成功记录,其余请求被幂等拦截或返回已领取
4. 不出现重复发券 | [AI修正: 对应重复提交和重复支付类历史风险] | +| C_NG_SEND_003 | 用户端-首页-新人礼领取 | 验证领取接口超时重试时不会重复发券 | P1 | 回归测试 | 用户U1 满足新人资格;模拟领取接口首个请求响应超时但后端已处理成功 | 1. 用户U1 点击立即领取
2. 将首个请求模拟为前端超时
3. 用户再次点击立即领取或页面自动重试
4. 查询优惠券列表和发放日志 | 首次请求后端处理成功;前端超时 5 秒 | 1. 页面提示处理中或可稍后刷新查看结果
2. 最终用户仅获得 1 张优惠券
3. 发放日志中仅 1 条成功记录,不存在多条成功发放
4. 页面最终状态与后台结果一致 | 非功能 | +| C_NG_SEND_004 | 用户端-首页-新人礼领取 | 验证发券失败时记录失败原因且不出现半成功状态 | P1 | 功能测试 | 用户U4 满足新人资格;将发券接口模拟为失败 | 1. 用户U4 点击立即领取
2. 观察页面提示
3. 查询我的优惠券列表
4. 查询发放日志 | 券中心返回=库存不足或券失效 | 1. 页面提示领取失败及失败原因
2. 我的优惠券列表不新增该券
3. `new_gift_send_log` 记录失败状态和失败原因
4. 用户状态不被误标记为已领取 | [AI修正: 覆盖部分成功和异常记录风险] | +| C_NG_SEND_005 | 用户端-首页-新人礼领取 | 验证同一账号双端并发领取时最终仅一次成功 | P1 | 功能测试 | 用户U5 满足新人资格;用户在手机端和 H5 端同时登录;平台存在进行中活动 | 1. 手机端和 H5 端同时打开新人礼弹窗
2. 两端几乎同时点击立即领取
3. 查询两端提示、优惠券列表和发放日志 | 用户U5;双端并发间隔小于 200ms | 1. 最终仅 1 次领取成功
2. 另一端返回已领取、处理中或重复请求提示
3. 用户优惠券列表仅新增 1 张券
4. 发放日志仅保留 1 条成功记录 | 并发 | +| C_NG_SEND_006 | 用户端-门店首页-新人礼领取 | 验证用户领取平台新人礼后仍可继续领取门店新人礼 | P0 | 功能测试 | 用户U7 已成功领取平台新人礼;商家A 存在进行中的门店新人礼活动;用户U7 在商家A 维度无非关闭订单且未领取过门店活动 | 1. 用户U7 登录平台首页并确认已领取平台新人礼
2. 用户U7 进入商家A 门店首页
3. 观察门店弹窗并点击立即领取
4. 查询门店发放日志和优惠券列表 | 用户U7;平台活动ID=PLT1001;门店活动ID=SHOP1001 | 1. 门店首页仍展示门店新人礼弹窗
2. 用户可成功领取门店优惠券
3. 优惠券列表中同时存在平台新人礼券和门店新人礼券
4. 门店活动发放日志新增成功记录,不因已领取平台礼而被拦截 | [AI修正: 按产品确认口径补充平台礼与门店礼可并存] | +| C_NG_CHECK_001 | 用户端-首页-资格检查 | 验证无进行中活动时不展示新人礼弹窗 | P1 | 功能测试 | 平台不存在进行中活动;用户U1 满足新人资格 | 1. 用户U1 登录平台首页
2. 观察首页弹窗
3. 查询资格检查接口返回 | 活动列表=空或全部不命中资格检查条件 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回无可领取活动
3. 页面不出现错误提示或异常闪现弹窗 | [AI修正: 将状态负向场景拆分,提升定位性] | +| C_NG_CHECK_002 | 用户端-首页-资格检查 | 验证活动未开始时不展示新人礼弹窗 | P1 | 功能测试 | 平台存在 1 条未开始活动;用户U1 满足新人资格;当前无其他进行中活动 | 1. 用户U1 登录平台首页
2. 观察首页弹窗
3. 查询资格检查接口返回 | 活动开始时间=当前时间后 1 小时 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回无可领取活动或活动未开始
3. 页面不出现提前曝光的领取入口 | [AI修正: 对标团队用例补充状态拆分场景] | +| C_NG_CHECK_003 | 用户端-首页-资格检查 | 验证活动已结束时不展示新人礼弹窗 | P1 | 功能测试 | 平台存在 1 条已结束活动;用户U1 满足新人资格;当前无其他进行中活动 | 1. 用户U1 登录平台首页
2. 观察首页弹窗
3. 查询资格检查接口返回 | 活动结束时间=当前时间前 1 小时 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回无可领取活动或活动已结束
3. 页面不出现已结束活动的领取入口 | [AI修正: 对标团队用例补充状态拆分场景] | +| C_NG_CHECK_004 | 用户端-首页-资格检查 | 验证活动已失效时不展示新人礼弹窗 | P1 | 功能测试 | 平台存在 1 条已失效活动;用户U1 满足新人资格;当前无其他进行中活动 | 1. 用户U1 登录平台首页
2. 观察首页弹窗
3. 查询资格检查接口返回 | 活动状态=已失效;valid_status=失效 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回无可领取活动或活动已失效
3. 页面状态与后台有效性字段一致 | [AI修正: 对标团队用例补充状态拆分场景] | +| API_NG_PERF_001 | 用户端-资格检查-接口性能 | 验证资格检查接口在常规数据量下满足 RT 基线 | P2 | 性能测试 | 存在 100 条历史发放记录和 10 条活动数据;性能环境可统计接口耗时 | 1. 触发用户进入平台首页调用资格检查接口
2. 连续执行 30 次
3. 统计平均 RT 和 P95 | 接口=`/ua/newGift/check`;样本量=30 | 1. 常规场景下接口平均 RT 和 P95 满足 1000ms 基线或接近目标
2. 页面无明显卡顿或超时提示
3. 若超基线,应能定位是活动查询、资格判定还是日志判断链路耗时 | [AI修正: 技术方案给出 RT 1000ms] | +| C_NG_RULE_001 | 用户端-资格判定-新人口径 | 验证存在已支付订单时不再识别为新人 | P1 | 功能测试 | 用户U8 存在 1 笔平台已支付订单;平台存在进行中活动 | 1. 用户U8 登录平台首页
2. 观察是否弹出新人礼
3. 查询资格检查返回 | 用户U8 历史订单状态=已支付 | 1. 首页不展示新人礼弹窗
2. 资格检查接口返回不满足新人条件
3. 页面展示与产品确认口径一致,不因已支付历史订单继续认定为新人 | [AI修正: 按产品确认口径更新新人判定] | +| DATA_NG_SCOPE_001 | 平台端-商家端-数据隔离 | 验证平台端与商家端新人礼活动数据不互通 | P0 | 功能测试 | 平台端已配置 1 条平台新人礼活动;商家A 已配置 1 条门店新人礼活动;测试账号分别具备平台端和商家端权限 | 1. 在平台端查看新人礼活动列表
2. 在商家A 端查看新人礼活动列表
3. 对比两端列表数据 | 平台活动ID=PLT1001;商家活动ID=SHOP1001 | 1. 平台端仅展示平台活动数据,不展示商家活动
2. 商家端仅展示当前店铺活动数据,不展示平台活动
3. 两端数据查询、查看、编辑入口相互隔离 | [AI修正: 取自团队现有功能用例] | +| VIEW_NG_READONLY_001 | 平台端-营销管理-新人有礼查看 | 验证查看态页面所有字段只读不可编辑 | P1 | 功能测试 | 平台已存在 1 条新人礼活动;平台运营账号已登录 | 1. 在列表中点击查看
2. 观察活动名称、活动时间、活动奖品等字段状态
3. 尝试修改字段或提交 | 活动ID=PLT1001 | 1. 查看页所有字段均为只读态
2. 页面不提供可提交修改的入口
3. 用户无法通过查看态改写活动配置 | [AI修正: 取自团队现有功能用例] | +| PLT_NG_LIST_002 | 平台端-营销管理-新人有礼列表 | 验证列表展示字段和操作栏状态映射正确 | P1 | 功能测试 | 平台端存在未开始、进行中、已结束、已失效四类活动;平台运营账号已登录 | 1. 进入新人有礼列表
2. 逐条检查活动名称、活动时间、活动状态、创建时间、操作栏
3. 对比不同状态下的操作按钮 | 活动A=未开始;活动B=进行中;活动C=已结束;活动D=已失效 | 1. 列表展示字段完整且内容正确
2. 未开始活动展示编辑、失效
3. 进行中活动展示查看、失效或按最终规则允许的编辑能力
4. 已结束和已失效活动操作栏按需求展示删除或受限操作 | [AI修正: 取自团队现有功能用例] | diff --git a/output/versions/新人礼需求/v9/新人礼需求_测试用例.xlsx b/output/versions/新人礼需求/v9/新人礼需求_测试用例.xlsx new file mode 100644 index 0000000..6885909 Binary files /dev/null and b/output/versions/新人礼需求/v9/新人礼需求_测试用例.xlsx differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b771e46 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +openpyxl>=3.1,<4 +python-docx>=0.8.11,<2 +pyantiword diff --git a/requirements/新人礼需求.md b/requirements/新人礼需求.md new file mode 100644 index 0000000..0b6e5ab --- /dev/null +++ b/requirements/新人礼需求.md @@ -0,0 +1,72 @@ +# 新人礼需求 + +> 文档角色:正式维护版需求 +> 原始来源:`source_docs/requirements_raw/新人礼需求.docx` +> 标准化来源:`output/normalized_inputs/新人礼需求/requirement.md` +> 输入类型:`docx` + +## 维护信息 +- 当前维护文件:`requirements/新人礼需求.md` +- 最近维护说明:暂无 + +## 技术方案来源 +- `source_docs/technical_solutions/新人礼技术方案.docx` + +## 正式维护内容 + +基线-新人礼 +| 版本号 | 变更内容 | 变更人 | 变更时间 | +| 0.0.1 | 文档创建 | 张昊 | 2026-02-28 | +一、业务背景 +基于问界需求清单 新人礼需求 +二、业务目标 +通过发布新人礼活动,吸引新用户注册使用~平台端&商家端支持发布新人礼活动,支持配置优惠券奖品,c端新用户注册展示新人礼内容 +三、产品设计 +1. 平台端-营销-新人有礼 +1.1 新人有礼列表 +列表数据说明 +数据权限:有此菜单列表权限的用户可看数据 +排序规则:按数据新增时间倒序排列 +分页规则:默认10行每页,可自主选择每页显示的条数(10/20/30/50) +数据来源:如下表 +| 数据字段 | 数据来源 | +| 活动名称 | 来源【新增/编辑】表单同名字段 | +| 活动时间 | 来源【新增/编辑】表单“活动时间“数据 | +| 活动状态 | 见下方状态逻辑说明 | +| 活动有效性 | 展示有效/已失效,支持 失效活动,失效后展示 已失效,失效后 操作栏展示删除操作 | +| 活动奖品 | 展示活动配置的奖品,当前活动奖品仅支持优惠券 | +| 创建时间 | 活动创建时间 | +状态逻辑说明 +| 状态名称 | 状态变更条件 | 可操作按钮 | +| 未开始 | 活动时间开始时间>当前时间 | 编辑,失效 | +| 进行中 | 活动时间开始时间<=当前时间 | 查看,失效 | +| 已结束 | 活动时间结束时间<当前时间 | 删除 | +查询条件说明 +| 查询条件 | 查询逻辑 | +| 活动名称 | 模糊查询,查询列表字段“活动名称” | +| 活动状态 | 精准查询,单选,选项数据:未开始、进行中、已结束、已失效,默认空 | +功能按钮说明 +| 按钮名称 | 触发后逻辑说明 | | +| 新建 | 在当前窗口打开“新建新人礼”弹窗 | | +| 查询 | 1、已选查询条件情况下,列表显示符合条件的数据 2、查询条件无匹配数据时,列表显示空,提示”暂无数据“ | | +| 重置 | 清空查询条件 | | +| 编辑 | 打开编辑表单弹窗 | | +| 查看 | 打开查看表单弹窗 | | +| 失效 | 打开失效操作弹窗,失效操作后,状态变更为已失效 | | +| 删除 | 打开删除操作弹窗,删除操作后,在列表删除活动(永久删除);取消后,关闭删除弹窗。 | | +1.2 新增/编辑 +页面字段说明 +| 字段名称 | 逻辑规则 | 是否必填 | 是否可编辑 | 原型界面、逻辑补充 | +| 活动名称 | 文本输入,最多50个字 | 是 | 是 | | +| 活动时间 | 开始时间-结束时间,年月日时分秒 | 是 | 是 | 控制活动的有效时段,可选择的时间大于等于当前时间,结束时间大于开始时间 | +| 活动奖品 | 多选 | 是 | 是 | | +| | 送优惠券 优惠券: 勾选后展示选择优惠券,只能选择1张优惠券,选择后展示选中优惠券信息表格 选择优惠券弹窗: 通用优惠券选择弹窗,单选 平台端展示平台可用优惠券列表, 商家端展示商家可用的优惠券列表, 优惠券列表数据展示规则:投放,未过期且为 用户领取 的优惠券,支持根据名称筛选 | 是 | 是 | 优惠券列表:选择前 优惠券列表:选择后 选择优惠券弹窗: | +| | 送积分 可输入1-999999的整数,配置生效后调用发积分接口发放积分 | 是 | 否 | 暂不支持 | +2. 商家端-营销-新人有礼 +功能同平台端,仅优惠券选择时,仅可选择该店铺下的优惠券 +3. C端 +3.1 首页 +| 平台首页-新人礼领取提示 奖励领取后,可在个人中心优惠券查看 | 店铺首页-新人礼领取提示 | +| 功能点 | 功能说明 | +| 弹窗逻辑 | 1)用户未在任意门店下过单,即视为新用户,登录后进入首页,弹窗提示用户领取新人礼奖励 2)用户未在该门店下过单,即视为门店新用户,登录后进入门店首页,弹窗提示用户领取新人礼奖励时机3)如果平台或店铺设置了弹窗广告,则新人礼弹窗在弹窗广告之前展示,关闭新人礼弹窗后,展示弹窗广告内容 | +| 优惠券 | 用户点击新人礼弹窗立即领取按钮,如果领取成功,奖励进入我的-优惠券列表,并提示用户领取成功 | diff --git a/scripts/case_pipeline.py b/scripts/case_pipeline.py new file mode 100644 index 0000000..1f62152 --- /dev/null +++ b/scripts/case_pipeline.py @@ -0,0 +1,2117 @@ +import argparse +import filecmp +import json +from pathlib import Path +import re +import shutil +import subprocess +import sys +from typing import Any +import xml.etree.ElementTree as ET +from zipfile import BadZipFile, ZipFile +import zlib + +try: + from docx import Document as DocxDocument +except ImportError: + DocxDocument = None + +try: + from pyantiword.antiword_wrapper import extract_text as extract_doc_text +except ImportError: + try: + from pyantiword import extract_text as extract_doc_text + except ImportError: + extract_doc_text = None + +from export_excel import ( + LEGACY_COLUMNS, + REQUIRED_COLUMNS, + TYPE_ENUMS, + export_markdown_to_excel, + load_markdown_table, +) +from openpyxl import load_workbook + + +REPO_ROOT = Path(__file__).resolve().parent.parent +REQUIREMENTS_DIR = REPO_ROOT / "requirements" +RAW_REQUIREMENTS_DIR = REPO_ROOT / "source_docs" / "requirements_raw" +TECHNICAL_SOLUTIONS_DIR = REPO_ROOT / "source_docs" / "technical_solutions" +DECISIONS_DIR = REPO_ROOT / "decisions" +DECISIONS_APPLIED_DIR = DECISIONS_DIR / "applied" +VERSIONS_DIR = REPO_ROOT / "output" / "versions" +MAX_SNAPSHOT_VERSIONS = 3 +PROJECT_PROFILE_FILE = REPO_ROOT / "knowledge_base" / "00_project" / "project_profile.md" +CORE_TERMINOLOGY_FILE = REPO_ROOT / "knowledge_base" / "01_standards" / "terminology.md" +SUPPORTED_REQUIREMENT_SUFFIXES = {".md", ".doc", ".docx", ".pdf", ".txt"} +DOCX_NAMESPACE = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} +PDF_STREAM_RE = re.compile(rb"stream\r?\n(.*?)\r?\nendstream", re.DOTALL) +PDF_TEXT_BLOCK_RE = re.compile(rb"BT(.*?)ET", re.DOTALL) +PDF_ARRAY_TEXT_RE = re.compile(rb"\[(.*?)\]\s*TJ", re.DOTALL) +PDF_SIMPLE_TEXT_RE = re.compile(rb"(?.+)_\d{8}_\d{6}\.xlsx$") +DECISION_FILE_RE = re.compile(r"确认状态[::]\s*`?(已确认|待确认|已驳回|已拒绝)`?") +DECISION_EXPORT_RE = re.compile(r"是否允许在确认前继续导出[::]\s*`?(允许|不允许)`?") +DECISION_RELATION_TYPE_RE = re.compile(r"关系类型[::]\s*`?(补充|替代|并行)`?") +OUT_OF_SCOPE_HEADING_KEYWORDS = ( + "不在本期范围", + "非本期范围", + "不支持", + "不包含", + "out of scope", +) + + +def resolve_requirement_path(requirement: str) -> Path: + path = Path(requirement) + return path if path.is_absolute() else REPO_ROOT / path + + +def build_paths(base_name: str) -> dict[str, Path]: + return { + "analysis": REPO_ROOT / "output" / "analysis" / f"{base_name}_分析.md", + "relation_report": REPO_ROOT / "output" / "analysis" / f"{base_name}_关联与冲突.md", + "test_points": REPO_ROOT / "output" / "test_points" / f"{base_name}_测试点.md", + "test_cases": REPO_ROOT / "output" / "test_cases" / f"{base_name}_测试用例.md", + "manifest": REPO_ROOT / "output" / "manifests" / f"{base_name}.json", + "normalized_input_dir": REPO_ROOT / "output" / "normalized_inputs" / base_name, + "excel_dir": REPO_ROOT / "output" / "excel_reports", + "version_root": VERSIONS_DIR / base_name, + "decision_file": DECISIONS_DIR / f"{base_name}_确认结论.md", + "maintained_requirement": REQUIREMENTS_DIR / f"{base_name}.md", + } + + +def ensure_output_dirs(paths: dict[str, Path]) -> None: + for key in ("analysis", "relation_report", "test_points", "test_cases", "manifest", "excel_dir"): + paths[key].parent.mkdir(parents=True, exist_ok=True) + paths["normalized_input_dir"].mkdir(parents=True, exist_ok=True) + + +def validate_requirement_file(requirement_path: Path) -> None: + if not requirement_path.exists(): + raise FileNotFoundError(f"需求文档不存在:{requirement_path}") + if requirement_path.stat().st_size == 0: + raise ValueError(f"需求文档为空:{requirement_path}") + if requirement_path.suffix.lower() not in SUPPORTED_REQUIREMENT_SUFFIXES: + raise ValueError( + "当前流水线仅支持以下需求文档格式:" + f"{', '.join(sorted(SUPPORTED_REQUIREMENT_SUFFIXES))}。" + f"当前文件:{requirement_path}" + ) + + +def validate_knowledge_base() -> None: + missing = [str(file_path) for file_path in KNOWLEDGE_BASE_FILES if not file_path.exists()] + if missing: + raise FileNotFoundError("缺少知识库或规范文件:\n" + "\n".join(missing)) + + optional_missing = [ + str(rule["path"]) + for rule in OPTIONAL_TERMINOLOGY_RULES + if not Path(rule["path"]).exists() + ] + if optional_missing: + raise FileNotFoundError("缺少可选术语文件:\n" + "\n".join(optional_missing)) + + +def validate_project_profile() -> None: + if not PROJECT_PROFILE_FILE.exists(): + raise FileNotFoundError(f"缺少项目画像文件:{PROJECT_PROFILE_FILE}") + if PROJECT_PROFILE_FILE.stat().st_size == 0: + raise ValueError(f"项目画像文件为空:{PROJECT_PROFILE_FILE}") + + +def safe_read_text(path: Path) -> str: + suffix = path.suffix.lower() + if suffix == ".doc": + return read_doc_text_file(path) + if suffix == ".docx": + return read_docx_text(path) + if suffix == ".pdf": + return read_pdf_text(path) + return path.read_text(encoding="utf-8", errors="ignore") + + +def read_doc_text_file(path: Path) -> str: + if extract_doc_text is None: + raise RuntimeError( + "当前环境缺少 .doc 解析依赖 `pyantiword`,请先执行 `pip install -r requirements.txt`。" + ) + + try: + return (extract_doc_text(str(path)) or "").strip() + except Exception as exc: + raise ValueError(f"无法解析 DOC 文件:{path}\n{exc}") from exc + + +def read_docx_text(path: Path) -> str: + library_text = read_docx_text_via_library(path) + if library_text.strip(): + return library_text.strip() + + try: + with ZipFile(path) as archive: + document_xml = archive.read("word/document.xml") + except KeyError as exc: + raise ValueError(f"DOCX 文件缺少 word/document.xml:{path}") from exc + except BadZipFile as exc: + raise ValueError(f"无法解析 DOCX 文件:{path}") from exc + + root = ET.fromstring(document_xml) + body = root.find("w:body", DOCX_NAMESPACE) + if body is None: + return "" + + lines: list[str] = [] + for child in body: + tag = child.tag.rsplit("}", 1)[-1] + if tag == "p": + text = extract_docx_paragraph_text(child) + if text: + lines.append(text) + elif tag == "tbl": + lines.extend(extract_docx_table_lines(child)) + return "\n".join(lines).strip() + + +def read_docx_text_via_library(path: Path) -> str: + if DocxDocument is None: + return "" + + try: + document = DocxDocument(str(path)) + except Exception: + return "" + + lines: list[str] = [] + iter_inner_content = getattr(document, "iter_inner_content", None) + if callable(iter_inner_content): + for block in iter_inner_content(): + if hasattr(block, "rows"): + lines.extend(extract_python_docx_table_lines(block)) + continue + text = normalize_extracted_text(getattr(block, "text", "")) + if text: + lines.append(text) + else: + lines.extend( + text + for text in ( + normalize_extracted_text(paragraph.text) + for paragraph in getattr(document, "paragraphs", []) + ) + if text + ) + for table in getattr(document, "tables", []): + lines.extend(extract_python_docx_table_lines(table)) + return "\n".join(lines).strip() + + +def extract_python_docx_table_lines(table: object) -> list[str]: + lines: list[str] = [] + for row in getattr(table, "rows", []): + cells: list[str] = [] + for cell in row.cells: + content_lines: list[str] = [] + iter_inner_content = getattr(cell, "iter_inner_content", None) + if callable(iter_inner_content): + for item in iter_inner_content(): + if hasattr(item, "rows"): + content_lines.extend(extract_python_docx_table_lines(item)) + continue + text = normalize_extracted_text(getattr(item, "text", "")) + if text: + content_lines.append(text) + else: + fallback_text = normalize_extracted_text(getattr(cell, "text", "")) + if fallback_text: + content_lines.append(fallback_text) + + cells.append(" / ".join(content_lines).strip()) + if any(cells): + lines.append("| " + " | ".join(cells) + " |") + return lines + + +def extract_docx_paragraph_text(node: ET.Element) -> str: + parts = [item.text or "" for item in node.findall(".//w:t", DOCX_NAMESPACE)] + return "".join(parts).strip() + + +def extract_docx_table_lines(node: ET.Element) -> list[str]: + lines: list[str] = [] + for row in node.findall("./w:tr", DOCX_NAMESPACE): + cells: list[str] = [] + for cell in row.findall("./w:tc", DOCX_NAMESPACE): + paragraphs = [ + extract_docx_paragraph_text(paragraph) + for paragraph in cell.findall(".//w:p", DOCX_NAMESPACE) + ] + text = " ".join(part for part in paragraphs if part).strip() + cells.append(text) + if any(cells): + lines.append("| " + " | ".join(cells) + " |") + return lines + + +def read_pdf_text(path: Path) -> str: + raw = path.read_bytes() + chunks: list[str] = [] + + for stream in PDF_STREAM_RE.findall(raw): + decoded = decode_pdf_stream(stream) + if not decoded: + continue + text = extract_text_from_pdf_stream(decoded) + if text: + chunks.append(text) + + result = "\n".join(part.strip() for part in chunks if part.strip()).strip() + if result: + return result + + fallback = extract_printable_pdf_text(raw) + if fallback: + return fallback + + return "> ⚠️ 待确认:PDF 未提取到可用文本,可能是扫描件、图片型 PDF 或使用了不受当前解析器支持的编码。" + + +def decode_pdf_stream(stream: bytes) -> bytes: + candidate = stream.strip(b"\r\n") + if not candidate: + return b"" + for decoder in (zlib.decompress, lambda data: data): + try: + return decoder(candidate) + except Exception: + continue + return b"" + + +def extract_text_from_pdf_stream(stream: bytes) -> str: + pieces: list[str] = [] + for block in PDF_TEXT_BLOCK_RE.findall(stream): + array_matches = PDF_ARRAY_TEXT_RE.findall(block) + for array_content in array_matches: + pieces.extend(decode_pdf_string(token) for token in PDF_INLINE_STRING_RE.findall(array_content)) + + simple_matches = PDF_SIMPLE_TEXT_RE.findall(block) + for token in simple_matches: + inline_match = PDF_INLINE_STRING_RE.search(token) + if inline_match: + pieces.append(decode_pdf_string(inline_match.group(0))) + + cleaned = [normalize_extracted_text(piece) for piece in pieces] + return "\n".join(item for item in cleaned if item) + + +def extract_printable_pdf_text(raw: bytes) -> str: + printable_chunks = re.findall(rb"[\x20-\x7e]{6,}", raw) + text = "\n".join( + normalize_extracted_text(chunk.decode("latin-1", errors="ignore")) + for chunk in printable_chunks + ) + lines = [line for line in text.splitlines() if len(line.strip()) >= 6] + return "\n".join(lines[:400]).strip() + + +def decode_pdf_string(token: bytes) -> str: + if token.startswith(b"(") and token.endswith(b")"): + token = token[1:-1] + token = re.sub(rb"\\([()\\])", rb"\1", token) + token = token.replace(b"\\n", b"\n").replace(b"\\r", b"\r").replace(b"\\t", b"\t") + token = re.sub( + rb"\\([0-7]{1,3})", + lambda match: bytes([int(match.group(1), 8)]), + token, + ) + return token.decode("utf-8", errors="ignore") or token.decode("latin-1", errors="ignore") + + +def normalize_extracted_text(text: str) -> str: + text = text.replace("\x00", " ") + text = re.sub(r"[ \t]+", " ", text) + return text.strip() + + +def normalize_topic_key(name: str) -> str: + normalized = name.lower() + for token in TOPIC_NOISE_TOKENS: + normalized = normalized.replace(token.lower(), "") + normalized = re.sub(r"[_\-\s]+", "", normalized) + normalized = re.sub(r"v\d+(?:\.\d+)*", "", normalized) + normalized = re.sub(r"版本\d+(?:\.\d+)*", "", normalized) + normalized = re.sub(r"[()()【】\[\]]", "", normalized) + return normalized.strip() + + +def is_relative_to(path: Path, parent: Path) -> bool: + try: + path.resolve().relative_to(parent.resolve()) + return True + except ValueError: + return False + + +def is_requirement_markdown(path: Path) -> bool: + return is_relative_to(path, REQUIREMENTS_DIR) and path.suffix.lower() == ".md" + + +def iter_requirement_documents() -> list[Path]: + result: list[Path] = [] + for directory in (REQUIREMENTS_DIR, RAW_REQUIREMENTS_DIR): + if not directory.exists(): + continue + for item in sorted(directory.rglob("*")): + if not item.is_file(): + continue + if item.suffix.lower() not in SUPPORTED_REQUIREMENT_SUFFIXES: + continue + if item.stat().st_size == 0: + continue + result.append(item) + return result + + +def prefer_maintained_requirement_documents(paths: list[Path]) -> list[Path]: + maintained_keys = { + normalize_topic_key(path.stem) + for path in paths + if is_requirement_markdown(path) + } + result: list[Path] = [] + for path in paths: + if ( + is_relative_to(path, RAW_REQUIREMENTS_DIR) + and normalize_topic_key(path.stem) in maintained_keys + ): + continue + result.append(path) + return result + + +def select_technical_solution_files(requirement_path: Path) -> list[Path]: + if not TECHNICAL_SOLUTIONS_DIR.exists(): + return [] + + requirement_key = normalize_topic_key(requirement_path.stem) + requirement_text = safe_read_text(requirement_path) + requirement_tokens = to_ngrams(f"{requirement_path.stem}\n{requirement_text}") + matches: list[tuple[float, Path]] = [] + + for candidate in sorted(TECHNICAL_SOLUTIONS_DIR.rglob("*")): + if not candidate.is_file(): + continue + if candidate.suffix.lower() not in SUPPORTED_REQUIREMENT_SUFFIXES: + continue + candidate_key = normalize_topic_key(candidate.stem) + filename_score = 1.0 if requirement_key and candidate_key == requirement_key else 0.0 + candidate_text = safe_read_text(candidate) + similarity_score = jaccard_similarity( + requirement_tokens, + to_ngrams(f"{candidate.stem}\n{candidate_text}"), + ) + score = max(filename_score, similarity_score) + if score < 0.08: + continue + matches.append((score, candidate)) + + matches.sort(key=lambda item: item[0], reverse=True) + return [path for _, path in matches[:3]] + + +def write_normalized_document(source_path: Path, target_path: Path, document_role: str) -> Path: + body = safe_read_text(source_path).strip() + lines = [ + f"# {source_path.stem}", + "", + f"> 文档角色:{document_role}", + f"> 原始来源:`{to_repo_relative(source_path)}`", + "", + ] + if body: + lines.append(body) + else: + lines.append("> ⚠️ 待确认:文档解析结果为空,请人工检查原始文件内容或格式。") + target_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") + return target_path + + +def extract_in_scope_requirement_text(text: str) -> str: + lines = text.splitlines() + result: list[str] = [] + skip_level: int | None = None + + for raw_line in lines: + line = raw_line.rstrip() + stripped = line.strip() + if stripped.startswith("#"): + level = len(stripped) - len(stripped.lstrip("#")) + heading_text = stripped[level:].strip().lower() + + if skip_level is not None and level <= skip_level: + skip_level = None + + if any(keyword in heading_text for keyword in OUT_OF_SCOPE_HEADING_KEYWORDS): + skip_level = level + continue + + if skip_level is not None: + continue + result.append(line) + + return "\n".join(result) + + +def to_repo_relative(path: Path) -> str: + try: + return str(path.resolve().relative_to(REPO_ROOT)) + except ValueError: + return str(path.resolve()) + + +def normalize_text(text: str) -> str: + return re.sub(r"\s+", "", text).lower() + + +def to_ngrams(text: str, n: int = 2) -> set[str]: + normalized = normalize_text(text) + if not normalized: + return set() + if len(normalized) < n: + return {normalized} + return {normalized[i : i + n] for i in range(len(normalized) - n + 1)} + + +def jaccard_similarity(left: set[str], right: set[str]) -> float: + if not left or not right: + return 0.0 + union = left | right + if not union: + return 0.0 + return len(left & right) / len(union) + + +def discover_other_requirements(requirement_path: Path) -> list[Path]: + target = requirement_path.resolve() + result: list[Path] = [] + for item in iter_requirement_documents(): + if item.resolve() == target: + continue + if ( + is_requirement_markdown(item) + and is_relative_to(requirement_path, RAW_REQUIREMENTS_DIR) + and normalize_topic_key(item.stem) == normalize_topic_key(requirement_path.stem) + ): + continue + result.append(item) + return prefer_maintained_requirement_documents(result) + + +def find_related_requirements(requirement_path: Path) -> list[dict[str, Any]]: + target_content = safe_read_text(requirement_path) + target_tokens = to_ngrams(f"{requirement_path.name}\n{target_content}") + related: list[dict[str, Any]] = [] + + for candidate in discover_other_requirements(requirement_path): + candidate_content = safe_read_text(candidate) + score = jaccard_similarity( + target_tokens, + to_ngrams(f"{candidate.name}\n{candidate_content}"), + ) + if score < RELATED_SIMILARITY_THRESHOLD: + continue + related.append( + { + "path": candidate, + "score": score, + "content": candidate_content, + } + ) + + related.sort(key=lambda item: item["score"], reverse=True) + return related[:MAX_RELATED_REQUIREMENTS] + + +def select_optional_terminology_files( + requirement_path: Path, + related_requirements: list[dict[str, Any]], + technical_solution_files: list[Path], +) -> list[dict[str, Any]]: + texts = [ + extract_in_scope_requirement_text(safe_read_text(requirement_path)), + safe_read_text(PROJECT_PROFILE_FILE), + requirement_path.name, + ] + texts.extend(safe_read_text(path) for path in technical_solution_files) + combined_text = "\n".join(texts) + + selected: list[dict[str, Any]] = [] + for rule in OPTIONAL_TERMINOLOGY_RULES: + matched_keywords = [ + keyword for keyword in rule["keywords"] if keyword in combined_text + ] + if not matched_keywords: + continue + selected.append( + { + "name": rule["name"], + "path": Path(rule["path"]), + "matched_keywords": matched_keywords[:10], + } + ) + return selected + + +def extract_rule_lines(text: str) -> list[str]: + result: list[str] = [] + seen: set[str] = set() + + for raw_line in text.splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + + line = re.sub(r"^\s*[-*]\s*", "", line) + line = re.sub(r"^\s*\d+[.)]\s*", "", line) + line = line.strip() + if len(line) < 8: + continue + if not any(keyword in line for keyword in NORMATIVE_KEYWORDS): + continue + if line in seen: + continue + + seen.add(line) + result.append(line) + return result + + +def classify_polarity(rule_line: str) -> str: + if any(keyword in rule_line for keyword in NEGATIVE_POLARITY_KEYWORDS): + return "negative" + if any(keyword in rule_line for keyword in POSITIVE_POLARITY_KEYWORDS): + return "positive" + return "neutral" + + +def extract_numeric_tokens(rule_line: str) -> set[str]: + return set(NUMERIC_TOKEN_RE.findall(rule_line)) + + +def build_conflict_candidates( + requirement_path: Path, + related_requirements: list[dict[str, Any]], +) -> list[dict[str, Any]]: + target_rules = extract_rule_lines(safe_read_text(requirement_path)) + target_rule_items = [ + { + "line": line, + "polarity": classify_polarity(line), + "numbers": extract_numeric_tokens(line), + "tokens": to_ngrams(line), + } + for line in target_rules + ] + + conflicts: list[dict[str, Any]] = [] + dedup: set[tuple[str, str, str]] = set() + + for related in related_requirements: + related_rules = extract_rule_lines(related["content"]) + related_rule_items = [ + { + "line": line, + "polarity": classify_polarity(line), + "numbers": extract_numeric_tokens(line), + "tokens": to_ngrams(line), + } + for line in related_rules + ] + + for target_item in target_rule_items: + for history_item in related_rule_items: + similarity = jaccard_similarity(target_item["tokens"], history_item["tokens"]) + if similarity < CONFLICT_SIMILARITY_THRESHOLD: + continue + + conflict_type = "" + suggestion = "" + opposite_polarity = { + target_item["polarity"], + history_item["polarity"], + } == {"positive", "negative"} + + if opposite_polarity: + conflict_type = "规则方向冲突" + suggestion = "建议补充版本范围、生效条件和优先级,明确保留哪条规则。" + elif ( + target_item["numbers"] + and history_item["numbers"] + and target_item["numbers"] != history_item["numbers"] + ): + conflict_type = "数值口径冲突" + suggestion = "建议统一阈值口径,并在需求中明确变更原因与兼容策略。" + else: + continue + + key = (target_item["line"], history_item["line"], conflict_type) + if key in dedup: + continue + dedup.add(key) + + conflicts.append( + { + "target_rule": target_item["line"], + "history_rule": history_item["line"], + "history_path": related["path"], + "type": conflict_type, + "suggestion": suggestion, + "similarity": similarity, + } + ) + + conflicts.sort(key=lambda item: item["similarity"], reverse=True) + return conflicts[:20] + + +def escape_table_cell(text: str) -> str: + return text.replace("|", "\\|").replace("\n", " ").strip() + + +def write_relation_report( + requirement_path: Path, + related_requirements: list[dict[str, Any]], + technical_solution_files: list[Path], + conflicts: list[dict[str, Any]], + report_path: Path, +) -> None: + lines: list[str] = [ + f"# {requirement_path.stem} 关联需求与冲突检查", + "", + "## 目标需求", + f"- `{to_repo_relative(requirement_path)}`", + "", + "## 项目画像", + f"- `{to_repo_relative(PROJECT_PROFILE_FILE)}`", + "", + "## 关联技术方案", + ] + + if technical_solution_files: + for path in technical_solution_files: + lines.append(f"- `{to_repo_relative(path)}`") + else: + lines.append("- 未识别到同主题技术方案文档。") + + lines.extend([ + "", + "## 关联需求识别", + ]) + + if related_requirements: + lines.extend( + [ + "| 序号 | 关联需求 | 相似度 |", + "| :--- | :--- | :--- |", + ] + ) + for index, item in enumerate(related_requirements, start=1): + lines.append( + f"| {index} | `{to_repo_relative(item['path'])}` | {item['score']:.4f} |" + ) + else: + lines.append("- 未识别到相似度达到阈值的历史需求文档。") + + lines.extend(["", "## 潜在冲突与修改建议"]) + + if conflicts: + lines.extend( + [ + "| 序号 | 当前需求条目 | 历史需求条目 | 历史来源 | 冲突类型 | 修改建议 |", + "| :--- | :--- | :--- | :--- | :--- | :--- |", + ] + ) + for index, item in enumerate(conflicts, start=1): + lines.append( + "| " + + " | ".join( + [ + str(index), + escape_table_cell(item["target_rule"]), + escape_table_cell(item["history_rule"]), + f"`{to_repo_relative(item['history_path'])}`", + item["type"], + escape_table_cell(item["suggestion"]), + ] + ) + + " |" + ) + lines.extend( + [ + "", + "> ⚠️ 待确认:以上条目为机器识别候选冲突,请产品/业务确认最终规则口径,并回写需求文档。", + ] + ) + else: + lines.append("- 暂未识别到明显冲突条目。建议在需求评审时继续人工确认。") + + report_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") + + +def write_manifest( + requirement_path: Path, + paths: dict[str, Path], + related_requirements: list[dict[str, Any]], + technical_solution_files: list[Path], + normalized_requirement_file: Path, + normalized_technical_solution_files: list[Path], + optional_terminology_files: list[dict[str, Any]], + conflict_count: int, + confirmation_gate: dict[str, Any], +) -> Path: + effective_terminology_files = [CORE_TERMINOLOGY_FILE] + [ + item["path"] for item in optional_terminology_files + ] + manifest = { + "requirement": str(requirement_path), + "requirement_source_file": str(requirement_path), + "requirement_input_type": requirement_path.suffix.lower().lstrip("."), + "base_name": requirement_path.stem, + "maintained_requirement_file": str(paths["maintained_requirement"]), + "normalized_requirement_file": str(normalized_requirement_file), + "technical_solution_files": [str(path) for path in technical_solution_files], + "normalized_technical_solution_files": [str(path) for path in normalized_technical_solution_files], + "analysis_file": str(paths["analysis"]), + "relation_report_file": str(paths["relation_report"]), + "test_points_file": str(paths["test_points"]), + "test_cases_file": str(paths["test_cases"]), + "excel_output_dir": str(paths["excel_dir"]), + "project_profile_file": str(PROJECT_PROFILE_FILE), + "knowledge_base_files": [str(file_path) for file_path in KNOWLEDGE_BASE_FILES], + "effective_terminology_files": [str(file_path) for file_path in effective_terminology_files], + "optional_terminology_files": [ + { + "name": item["name"], + "path": str(item["path"]), + "matched_keywords": item["matched_keywords"], + } + for item in optional_terminology_files + ], + "related_requirements": [ + {"path": str(item["path"]), "similarity": round(item["score"], 6)} + for item in related_requirements + ], + "conflict_candidates_count": conflict_count, + "confirmation_gate": confirmation_gate, + } + write_manifest_data(paths["manifest"], manifest) + return paths["manifest"] + + +def write_manifest_data(manifest_path: Path, manifest: dict[str, Any]) -> None: + manifest_path.write_text( + json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + +def count_pending_confirmation_markers(paths: list[Path]) -> int: + count = 0 + for path in paths: + if not path.exists(): + continue + count += safe_read_text(path).count("> ⚠️ 待确认:") + return count + + +def list_decision_files(base_name: str) -> list[Path]: + if not DECISIONS_DIR.exists(): + return [] + + return sorted( + [ + item + for item in DECISIONS_DIR.glob(f"{base_name}*_确认结论.md") + if item.is_file() + ], + key=lambda item: (item.stat().st_mtime, item.name), + reverse=True, + ) + + +def extract_single_line_field(content: str, label: str) -> str: + pattern = re.compile(rf"^- {re.escape(label)}:\s*(.*)$", re.MULTILINE) + match = pattern.search(content) + if not match: + return "" + return match.group(1).strip() + + +def clean_field_value(value: str) -> str: + value = value.strip() + value = value.strip("`") + return value.strip() + + +def split_field_values(value: str) -> list[str]: + if not value: + return [] + normalized = ( + value.replace(";", "\n") + .replace(",", "\n") + .replace(",", "\n") + .replace("、", "\n") + ) + return [clean_field_value(item) for item in normalized.splitlines() if clean_field_value(item)] + + +def requirement_reference_aliases(requirement_path: Path) -> set[str]: + aliases = { + clean_field_value(str(requirement_path.resolve())).lower(), + clean_field_value(to_repo_relative(requirement_path)).lower(), + clean_field_value(requirement_path.name).lower(), + clean_field_value(requirement_path.stem).lower(), + normalize_topic_key(requirement_path.stem), + } + return {item for item in aliases if item} + + +def requirement_reference_matches(reference: str, requirement_path: Path) -> bool: + normalized_reference = clean_field_value(reference).lower() + if not normalized_reference: + return False + + stem = Path(normalized_reference).stem if "/" in normalized_reference or "." in normalized_reference else normalized_reference + aliases = requirement_reference_aliases(requirement_path) + return normalized_reference in aliases or normalize_topic_key(stem) in aliases + + +def resolve_requirement_reference(reference: str) -> Path: + value = clean_field_value(reference) + if not value: + raise ValueError("需求引用为空。") + + candidate_path = resolve_requirement_path(value) + if candidate_path.exists(): + validate_requirement_file(candidate_path) + return candidate_path + + matches = [ + path + for path in iter_requirement_documents() + if requirement_reference_matches(value, path) + ] + if not matches: + raise FileNotFoundError(f"未找到需求引用:{value}") + if len(matches) > 1: + options = "\n".join(f"- {to_repo_relative(path)}" for path in matches) + raise ValueError(f"需求引用存在歧义:{value}\n候选项:\n{options}") + return matches[0] + + +def parse_decision_file(decision_file: Path) -> dict[str, Any]: + content = safe_read_text(decision_file) + status_match = DECISION_FILE_RE.search(content) + raw_status = status_match.group(1) if status_match else "" + status_map = { + "已确认": "confirmed", + "待确认": "pending", + "已驳回": "rejected", + "已拒绝": "rejected", + } + export_match = DECISION_EXPORT_RE.search(content) + export_policy = export_match.group(1) if export_match else None + relation_match = DECISION_RELATION_TYPE_RE.search(content) + relation_type = relation_match.group(1) if relation_match else None + + return { + "file": decision_file, + "content": content, + "status": status_map.get(raw_status, "unknown"), + "raw_status": raw_status or None, + "allow_export_before_confirmation": export_policy == "允许" if export_policy else None, + "current_requirement_refs": split_field_values(extract_single_line_field(content, "当前需求")), + "related_requirement_refs": split_field_values(extract_single_line_field(content, "关联历史需求")), + "rerun_requirement_refs": split_field_values(extract_single_line_field(content, "需要重跑的需求列表")), + "relation_type": relation_type, + "rewrite_current": clean_field_value(extract_single_line_field(content, "是否需要回写当前需求")) == "是", + "rewrite_history": clean_field_value(extract_single_line_field(content, "是否需要回写历史需求")) == "是", + "effective_scope": clean_field_value(extract_single_line_field(content, "生效范围")), + "invalid_scope": clean_field_value(extract_single_line_field(content, "失效范围")), + "impact_modules": clean_field_value(extract_single_line_field(content, "影响模块")), + "impact_roles": clean_field_value(extract_single_line_field(content, "影响角色")), + "impact_data_scope": clean_field_value(extract_single_line_field(content, "影响接口或数据口径")), + "version_boundary": clean_field_value(extract_single_line_field(content, "需要补充的版本边界")), + "inheritance_note": clean_field_value(extract_single_line_field(content, "需要补充的替代/继承说明")), + "compatibility_strategy": clean_field_value(extract_single_line_field(content, "需要补充的兼容策略")), + "rerun_order": clean_field_value(extract_single_line_field(content, "重跑顺序建议")), + } + + +def inspect_decision_file(decision_file: Path) -> dict[str, Any]: + parsed = parse_decision_file(decision_file) + return { + "file": parsed["file"], + "status": parsed["status"], + "raw_status": parsed["raw_status"], + "allow_export_before_confirmation": parsed["allow_export_before_confirmation"], + } + + +def find_relevant_decision_files(requirement_path: Path) -> list[Path]: + explicit = list_decision_files(requirement_path.stem) + if not DECISIONS_DIR.exists(): + return explicit + + candidates: list[Path] = [] + seen: set[Path] = set() + for item in explicit + sorted(DECISIONS_DIR.glob("*确认结论.md"), key=lambda path: path.name): + if not item.is_file() or item in seen: + continue + seen.add(item) + parsed = parse_decision_file(item) + references = parsed["current_requirement_refs"] + parsed["related_requirement_refs"] + if item.stem.startswith(requirement_path.stem) or any( + requirement_reference_matches(reference, requirement_path) + for reference in references + ): + candidates.append(item) + + return sorted( + candidates, + key=lambda item: (item.stat().st_mtime, item.name), + reverse=True, + ) + + +def build_confirmation_gate( + requirement_path: Path, + base_name: str, + related_requirements: list[dict[str, Any]], + conflicts: list[dict[str, Any]], + normalized_requirement_file: Path, + normalized_technical_solution_files: list[Path], +) -> dict[str, Any]: + pending_markers_count = count_pending_confirmation_markers( + [normalized_requirement_file, *normalized_technical_solution_files] + ) + reasons: list[str] = [] + if related_requirements: + reasons.append( + f"识别到 {len(related_requirements)} 个历史相似需求,需确认与旧需求的关系类型、生效范围和是否需要回写历史需求。" + ) + if conflicts: + reasons.append( + f"识别到 {len(conflicts)} 个潜在冲突候选,需确认最终规则口径后再继续校验或导出。" + ) + if pending_markers_count: + reasons.append( + f"标准化输入中包含 {pending_markers_count} 处待确认项,需先补充或确认关键前提。" + ) + + decision_files = find_relevant_decision_files(requirement_path) + decision_info = inspect_decision_file(decision_files[0]) if decision_files else None + required = bool(reasons) + + if not required: + status = "not_required" + elif decision_info is None: + status = "missing" + else: + status = decision_info["status"] + + gate = { + "required": required, + "reasons": reasons, + "pending_markers_count": pending_markers_count, + "decision_file": str(decision_info["file"]) if decision_info else None, + "decision_status": status, + "decision_status_label": decision_info["raw_status"] if decision_info else None, + "allow_export_before_confirmation": ( + decision_info["allow_export_before_confirmation"] if decision_info else None + ), + "candidate_decision_files": [str(path) for path in decision_files], + "suggested_decision_file": str(DECISIONS_DIR / f"{base_name}_确认结论.md"), + } + return gate + + +def load_manifest_data(manifest_path: Path) -> dict[str, Any]: + if not manifest_path.exists(): + raise FileNotFoundError(f"缺少清单文件:{manifest_path}。请先执行 prepare。") + return json.loads(manifest_path.read_text(encoding="utf-8")) + + +def refresh_manifest_confirmation_gate(paths: dict[str, Path]) -> dict[str, Any]: + manifest = load_manifest_data(paths["manifest"]) + base_name = manifest.get("base_name", paths["test_cases"].stem.replace("_测试用例", "")) + requirement_path = Path(manifest.get("requirement_source_file", manifest.get("requirement", ""))) + related_requirements = manifest.get("related_requirements", []) + conflicts = [ + {"placeholder": True} + for _ in range(int(manifest.get("conflict_candidates_count", 0) or 0)) + ] + normalized_requirement_file = Path( + manifest.get("normalized_requirement_file", paths["normalized_input_dir"] / "requirement.md") + ) + normalized_technical_solution_files = [ + Path(item) for item in manifest.get("normalized_technical_solution_files", []) + ] + confirmation_gate = build_confirmation_gate( + requirement_path=requirement_path, + base_name=base_name, + related_requirements=related_requirements, + conflicts=conflicts, + normalized_requirement_file=normalized_requirement_file, + normalized_technical_solution_files=normalized_technical_solution_files, + ) + manifest["confirmation_gate"] = confirmation_gate + write_manifest_data(paths["manifest"], manifest) + return manifest + + +def ensure_confirmation_resolved(paths: dict[str, Path], command_name: str) -> dict[str, Any]: + manifest = refresh_manifest_confirmation_gate(paths) + gate = manifest.get("confirmation_gate", {}) + if not gate.get("required"): + return manifest + + if gate.get("decision_status") == "confirmed": + return manifest + + reasons = gate.get("reasons", []) + reason_block = "\n".join(f"- {item}" for item in reasons) if reasons else "- 待补充确认原因" + decision_file = gate.get("decision_file") or gate.get("suggested_decision_file") + raise ValueError( + f"{command_name} 已阻断:当前需求仍需人工确认。\n" + f"建议确认单:{decision_file}\n" + "阻断原因:\n" + f"{reason_block}\n" + "请在确认单中明确写出 `确认状态:已确认` 后再继续。" + ) + + +def build_maintenance_note_path(decision_file: Path) -> Path: + return DECISIONS_APPLIED_DIR / f"{decision_file.stem}_维护说明.md" + + +def render_maintenance_note( + decision: dict[str, Any], + current_requirement: Path, + related_requirements: list[Path], + rerun_targets: list[Path], +) -> str: + lines = [ + f"# {current_requirement.stem} 确认结论维护说明", + "", + f"- 来源确认单:`{to_repo_relative(Path(decision['file']))}`", + f"- 当前需求:`{to_repo_relative(current_requirement)}`", + f"- 关系类型:{decision.get('relation_type') or '未填写'}", + f"- 是否回写当前需求:{'是' if decision.get('rewrite_current') else '否'}", + f"- 是否回写历史需求:{'是' if decision.get('rewrite_history') else '否'}", + "", + "## 关联历史需求", + ] + if related_requirements: + for path in related_requirements: + lines.append(f"- `{to_repo_relative(path)}`") + else: + lines.append("- 未填写") + + lines.extend( + [ + "", + "## 生效边界", + f"- 生效范围:{decision.get('effective_scope') or '未填写'}", + f"- 失效范围:{decision.get('invalid_scope') or '未填写'}", + f"- 影响模块:{decision.get('impact_modules') or '未填写'}", + f"- 影响角色:{decision.get('impact_roles') or '未填写'}", + f"- 影响接口或数据口径:{decision.get('impact_data_scope') or '未填写'}", + "", + "## 维护说明", + f"- 版本边界补充:{decision.get('version_boundary') or '未填写'}", + f"- 替代/继承说明:{decision.get('inheritance_note') or '未填写'}", + f"- 兼容策略:{decision.get('compatibility_strategy') or '未填写'}", + "", + "## 重跑计划", + ] + ) + for path in rerun_targets: + lines.append(f"- `{to_repo_relative(path)}`") + if not rerun_targets: + lines.append("- 未填写") + if decision.get("rerun_order"): + lines.append(f"- 重跑顺序建议:{decision['rerun_order']}") + + return "\n".join(lines).rstrip() + "\n" + + +def extract_normalized_requirement_body(text: str) -> str: + lines = text.splitlines() + result: list[str] = [] + started = False + + for index, line in enumerate(lines): + if index == 0 and line.startswith("# "): + continue + if not started: + if line.startswith("> "): + continue + if not line.strip(): + continue + started = True + result.append(line) + + return "\n".join(result).strip() + + +def find_applied_maintenance_notes(requirement_path: Path) -> list[Path]: + if not DECISIONS_APPLIED_DIR.exists(): + return [] + + matches: list[Path] = [] + for item in sorted(DECISIONS_APPLIED_DIR.glob("*_维护说明.md")): + if not item.is_file(): + continue + content = safe_read_text(item) + references = [ + to_repo_relative(requirement_path), + str(requirement_path.resolve()), + requirement_path.name, + requirement_path.stem, + ] + if any(reference and reference in content for reference in references): + matches.append(item) + return sorted(matches, key=lambda item: (item.stat().st_mtime, item.name), reverse=True) + + +def render_maintained_requirement( + requirement_path: Path, + manifest: dict[str, Any], + normalized_requirement_file: Path, +) -> str: + maintained_requirement_path = Path( + manifest.get( + "maintained_requirement_file", + build_paths(manifest.get("base_name", requirement_path.stem))["maintained_requirement"], + ) + ) + normalized_text = safe_read_text(normalized_requirement_file) + body = extract_normalized_requirement_body(normalized_text) + maintenance_notes = find_applied_maintenance_notes(requirement_path) + technical_solutions = [Path(item) for item in manifest.get("technical_solution_files", [])] + + lines = [ + f"# {manifest.get('base_name', requirement_path.stem)}", + "", + "> 文档角色:正式维护版需求", + f"> 原始来源:`{to_repo_relative(requirement_path)}`", + f"> 标准化来源:`{to_repo_relative(normalized_requirement_file)}`", + f"> 输入类型:`{manifest.get('requirement_input_type', requirement_path.suffix.lstrip('.'))}`", + "", + "## 维护信息", + f"- 当前维护文件:`{to_repo_relative(maintained_requirement_path)}`", + ] + if maintenance_notes: + for index, note in enumerate(maintenance_notes, start=1): + label = "最近维护说明" if index == 1 else "历史维护说明" + lines.append(f"- {label}:`{to_repo_relative(note)}`") + else: + lines.append("- 最近维护说明:暂无") + + lines.extend(["", "## 技术方案来源"]) + if technical_solutions: + for path in technical_solutions: + lines.append(f"- `{to_repo_relative(path)}`") + else: + lines.append("- 未关联技术方案") + + lines.extend(["", "## 正式维护内容", ""]) + if body: + lines.append(body) + else: + lines.append("> ⚠️ 待确认:标准化需求正文为空,请先检查原始需求文档解析结果。") + + return "\n".join(lines).rstrip() + "\n" + + +def sync_maintained_requirement(requirement_path: Path) -> Path: + paths = build_paths(requirement_path.stem) + manifest = load_manifest_data(paths["manifest"]) + maintained_requirement_path = Path( + manifest.get("maintained_requirement_file", paths["maintained_requirement"]) + ) + maintained_requirement_path.parent.mkdir(parents=True, exist_ok=True) + normalized_requirement_file = Path(manifest["normalized_requirement_file"]) + content = render_maintained_requirement( + requirement_path=requirement_path, + manifest=manifest, + normalized_requirement_file=normalized_requirement_file, + ) + maintained_requirement_path.write_text(content, encoding="utf-8") + manifest["maintained_requirement_file"] = str(maintained_requirement_path) + write_manifest_data(paths["manifest"], manifest) + return maintained_requirement_path + + +def write_maintenance_note( + decision: dict[str, Any], + current_requirement: Path, + related_requirements: list[Path], + rerun_targets: list[Path], +) -> Path: + DECISIONS_APPLIED_DIR.mkdir(parents=True, exist_ok=True) + note_path = build_maintenance_note_path(Path(decision["file"])) + note_path.write_text( + render_maintenance_note( + decision=decision, + current_requirement=current_requirement, + related_requirements=related_requirements, + rerun_targets=rerun_targets, + ), + encoding="utf-8", + ) + return note_path + + +def build_rerun_targets(decision: dict[str, Any], requirement_path: Path) -> list[Path]: + targets: list[Path] = [requirement_path] + for reference in decision.get("rerun_requirement_refs", []): + resolved = resolve_requirement_reference(reference) + if resolved not in targets: + targets.append(resolved) + return targets + + +def run_pipeline_for_requirement(requirement_path: Path) -> None: + relative_requirement = to_repo_relative(requirement_path) + commands = [ + ["prepare", "--requirement", relative_requirement], + ["verify", "--requirement", relative_requirement], + ["export", "--requirement", relative_requirement], + ] + for command in commands: + subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts" / "case_pipeline.py"), *command], + check=True, + ) + sync_maintained_requirement(requirement_path) + + +def collect_snapshot_sources( + paths: dict[str, Path], + excel_path: Path, + include_manifest: bool = True, +) -> list[tuple[Path, Path]]: + sources = [ + (paths["analysis"], Path(paths["analysis"].name)), + (paths["relation_report"], Path(paths["relation_report"].name)), + (paths["test_points"], Path(paths["test_points"].name)), + (paths["test_cases"], Path(paths["test_cases"].name)), + (excel_path, Path(excel_path.name)), + ] + if include_manifest: + sources.append((paths["manifest"], Path(paths["manifest"].name))) + + normalized_root = paths["normalized_input_dir"] + if normalized_root.exists(): + for item in sorted(normalized_root.rglob("*")): + if item.is_file(): + relative = item.relative_to(normalized_root) + sources.append((item, Path("normalized_inputs") / relative)) + + return sources + + +def find_latest_snapshot(version_root: Path) -> tuple[int, Path] | None: + if not version_root.exists(): + return None + + versions: list[tuple[int, Path]] = [] + for child in version_root.iterdir(): + if not child.is_dir(): + continue + match = VERSION_DIR_RE.match(child.name) + if not match: + continue + versions.append((int(match.group(1)), child)) + + if not versions: + return None + return max(versions, key=lambda item: item[0]) + + +def list_snapshot_versions(version_root: Path) -> list[tuple[int, Path]]: + if not version_root.exists(): + return [] + + versions: list[tuple[int, Path]] = [] + for child in version_root.iterdir(): + if not child.is_dir(): + continue + match = VERSION_DIR_RE.match(child.name) + if not match: + continue + versions.append((int(match.group(1)), child)) + + return sorted(versions, key=lambda item: item[0]) + + +def snapshot_matches(snapshot_dir: Path, sources: list[tuple[Path, Path]]) -> bool: + for source, relative in sources: + target = snapshot_dir / relative + if not target.exists() or not target.is_file(): + return False + if not files_match(source, target): + return False + return True + + +def files_match(source: Path, target: Path) -> bool: + if source.suffix.lower() == ".xlsx" and target.suffix.lower() == ".xlsx": + return excel_files_match(source, target) + return filecmp.cmp(source, target, shallow=False) + + +def excel_files_match(source: Path, target: Path) -> bool: + source_workbook = load_workbook(source, read_only=True, data_only=True) + target_workbook = load_workbook(target, read_only=True, data_only=True) + + try: + if source_workbook.sheetnames != target_workbook.sheetnames: + return False + + for sheet_name in source_workbook.sheetnames: + source_sheet = source_workbook[sheet_name] + target_sheet = target_workbook[sheet_name] + if source_sheet.max_row != target_sheet.max_row or source_sheet.max_column != target_sheet.max_column: + return False + + for source_row, target_row in zip( + source_sheet.iter_rows(values_only=True), + target_sheet.iter_rows(values_only=True), + ): + if tuple(source_row) != tuple(target_row): + return False + finally: + source_workbook.close() + target_workbook.close() + + return True + + +def is_legacy_excel_export(file_path: Path, case_stem: str) -> bool: + match = LEGACY_EXCEL_EXPORT_RE.match(file_path.name) + return bool(match and match.group("stem") == case_stem) + + +def find_legacy_excel_exports(paths: dict[str, Path]) -> list[Path]: + case_stem = paths["test_cases"].stem + excel_dir = paths["excel_dir"] + if not excel_dir.exists(): + return [] + + return sorted( + [ + item + for item in excel_dir.iterdir() + if item.is_file() and is_legacy_excel_export(item, case_stem) + ], + key=lambda item: item.name, + ) + + +def existing_snapshot_excel_files(version_root: Path) -> list[Path]: + if not version_root.exists(): + return [] + + files: list[Path] = [] + for child in sorted(version_root.iterdir()): + if not child.is_dir() or not VERSION_DIR_RE.match(child.name): + continue + for item in child.glob("*.xlsx"): + files.append(item) + return files + + +def extract_base_name_from_paths(paths: dict[str, Path]) -> str: + stem = paths["test_cases"].stem + if stem.endswith("_测试用例"): + return stem[: -len("_测试用例")] + return stem + + +def next_snapshot_version(version_root: Path) -> int: + latest = find_latest_snapshot(version_root) + if latest is None: + return 1 + return latest[0] + 1 + + +def find_matching_snapshot(version_root: Path, sources: list[tuple[Path, Path]]) -> tuple[int, Path] | None: + if not version_root.exists(): + return None + + versions: list[tuple[int, Path]] = [] + for child in version_root.iterdir(): + if not child.is_dir(): + continue + match = VERSION_DIR_RE.match(child.name) + if not match: + continue + versions.append((int(match.group(1)), child)) + + for version_number, snapshot_dir in sorted(versions, key=lambda item: item[0], reverse=True): + if snapshot_matches(snapshot_dir, sources): + return version_number, snapshot_dir + + return None + + +def decide_snapshot_version(paths: dict[str, Path], excel_path: Path) -> tuple[int, Path, bool]: + version_root = paths["version_root"] + sources = collect_snapshot_sources(paths, excel_path, include_manifest=False) + matched = find_matching_snapshot(version_root, sources) + if matched is not None: + return matched[0], matched[1], False + + version_root.mkdir(parents=True, exist_ok=True) + version_number = next_snapshot_version(version_root) + snapshot_dir = version_root / f"v{version_number}" + snapshot_dir.mkdir(parents=True, exist_ok=False) + + return version_number, snapshot_dir, True + + +def write_snapshot_files(paths: dict[str, Path], excel_path: Path, snapshot_dir: Path) -> None: + sources = collect_snapshot_sources(paths, excel_path, include_manifest=True) + for source, relative in sources: + target = snapshot_dir / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + + +def write_legacy_excel_snapshot(snapshot_dir: Path, base_name: str, legacy_file: Path) -> Path: + snapshot_dir.mkdir(parents=True, exist_ok=False) + target_excel = snapshot_dir / f"{base_name}_测试用例.xlsx" + shutil.copy2(legacy_file, target_excel) + note = "\n".join( + [ + f"# {base_name} 历史 Excel 导入说明", + "", + f"- 导入来源:`{legacy_file.name}`", + "- 导入类型:旧时间戳命名 Excel", + "- 说明:该快照仅迁移了历史 Excel,本次未重建当时对应的分析、测试点、测试用例 Markdown 与 manifest。", + "- 目的:统一历史文件归档路径,并清理 `output/excel_reports/` 中的旧时间戳文件。", + "", + ] + ) + (snapshot_dir / "snapshot_note.md").write_text(note, encoding="utf-8") + return target_excel + + +def infer_snapshot_type(snapshot_dir: Path) -> str: + if (snapshot_dir / "snapshot_note.md").exists(): + return "legacy_excel_import" + + has_manifest = any(item.name != "snapshot_meta.json" for item in snapshot_dir.glob("*.json")) + has_analysis = any(snapshot_dir.glob("*_分析.md")) + has_relation = any(snapshot_dir.glob("*_关联与冲突.md")) + has_test_points = any(snapshot_dir.glob("*_测试点.md")) + has_test_cases = any(snapshot_dir.glob("*_测试用例.md")) + has_excel = any(snapshot_dir.glob("*.xlsx")) + + if has_manifest and has_analysis and has_relation and has_test_points and has_test_cases and has_excel: + return "full_pipeline" + return "partial_snapshot" + + +def build_snapshot_summary(snapshot_dir: Path) -> list[str]: + summary: list[str] = [] + if any(item.name != "snapshot_meta.json" for item in snapshot_dir.glob("*.json")): + summary.append("manifest") + if any(snapshot_dir.glob("*_分析.md")): + summary.append("analysis") + if any(snapshot_dir.glob("*_关联与冲突.md")): + summary.append("relation_report") + if any(snapshot_dir.glob("*_测试点.md")): + summary.append("test_points") + if any(snapshot_dir.glob("*_测试用例.md")): + summary.append("test_cases_markdown") + if any(snapshot_dir.glob("*.xlsx")): + summary.append("excel") + if (snapshot_dir / "normalized_inputs").exists(): + summary.append("normalized_inputs") + if (snapshot_dir / "snapshot_note.md").exists(): + summary.append("migration_note") + return summary + + +def write_snapshot_metadata(snapshot_dir: Path, base_name: str) -> Path: + version_name = snapshot_dir.name + snapshot_type = infer_snapshot_type(snapshot_dir) + meta = { + "base_name": base_name, + "version": version_name, + "snapshot_type": snapshot_type, + "summary": build_snapshot_summary(snapshot_dir), + } + note_file = snapshot_dir / "snapshot_note.md" + if note_file.exists(): + meta["note_file"] = str(note_file) + + meta_path = snapshot_dir / "snapshot_meta.json" + meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + return meta_path + + +def refresh_version_index(version_root: Path, base_name: str) -> Path: + version_root.mkdir(parents=True, exist_ok=True) + version_entries: list[tuple[int, Path, dict[str, Any]]] = [] + + for version_number, child in list_snapshot_versions(version_root): + meta_path = write_snapshot_metadata(child, base_name) + meta = json.loads(meta_path.read_text(encoding="utf-8")) + version_entries.append((version_number, child, meta)) + + lines = [ + f"# {base_name} 版本索引", + "", + "| 版本 | 类型 | 内容摘要 | 目录 |", + "| --- | --- | --- | --- |", + ] + + type_labels = { + "full_pipeline": "完整流水线快照", + "legacy_excel_import": "历史 Excel 导入", + "partial_snapshot": "部分产物快照", + } + + for version_number, snapshot_dir, meta in sorted(version_entries, key=lambda item: item[0]): + summary = "、".join(meta.get("summary", [])) or "-" + lines.append( + "| " + + " | ".join( + [ + f"v{version_number}", + type_labels.get(meta.get("snapshot_type", ""), meta.get("snapshot_type", "未知")), + summary, + f"`{to_repo_relative(snapshot_dir)}`", + ] + ) + + " |" + ) + + if len(lines) == 4: + lines.append("| - | - | - | - |") + + index_path = version_root / "VERSION_INDEX.md" + index_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") + return index_path + + +def prune_old_snapshots(version_root: Path, keep: int = MAX_SNAPSHOT_VERSIONS) -> list[Path]: + versions = list_snapshot_versions(version_root) + if len(versions) <= keep: + return [] + + removable = versions[: len(versions) - keep] + removed_dirs: list[Path] = [] + for _, snapshot_dir in removable: + shutil.rmtree(snapshot_dir) + removed_dirs.append(snapshot_dir) + return removed_dirs + + +def migrate_legacy_excel_exports(paths: dict[str, Path]) -> dict[str, int]: + legacy_files = find_legacy_excel_exports(paths) + if not legacy_files: + return {"found": 0, "imported": 0, "deduplicated": 0, "removed": 0} + + version_root = paths["version_root"] + version_root.mkdir(parents=True, exist_ok=True) + snapshot_excels = existing_snapshot_excel_files(version_root) + imported = 0 + deduplicated = 0 + removed = 0 + + for legacy_file in legacy_files: + duplicated = any(files_match(legacy_file, snapshot_excel) for snapshot_excel in snapshot_excels) + if duplicated: + deduplicated += 1 + else: + version_number = next_snapshot_version(version_root) + snapshot_dir = version_root / f"v{version_number}" + target_excel = write_legacy_excel_snapshot(snapshot_dir, extract_base_name_from_paths(paths), legacy_file) + snapshot_excels.append(target_excel) + imported += 1 + + legacy_file.unlink() + removed += 1 + + return { + "found": len(legacy_files), + "imported": imported, + "deduplicated": deduplicated, + "removed": removed, + } + + +def update_manifest_version_info( + manifest_path: Path, + excel_path: Path, + version_number: int, + snapshot_dir: Path, +) -> None: + if not manifest_path.exists(): + return + + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["current_excel_file"] = str(excel_path) + manifest["versioning_scheme"] = { + "current_files": "固定文件名,始终表示当前最新版", + "snapshot_rule": "仅在 export 成功且产物内容发生变化时递增版本", + "snapshot_dir_pattern": "output/versions/{BASE_NAME}/vN/", + } + manifest["latest_snapshot_version"] = f"v{version_number}" + manifest["latest_snapshot_dir"] = str(snapshot_dir) + manifest["latest_snapshot_type"] = infer_snapshot_type(snapshot_dir) + write_manifest_data(manifest_path, manifest) + + +def print_legacy_migration_result(result: dict[str, int]) -> None: + if result["found"] == 0: + print("🧹 历史时间戳 Excel: 0") + return + + print( + "🧹 历史时间戳 Excel: " + f"发现 {result['found']} 个,导入版本 {result['imported']} 个," + f"重复去重 {result['deduplicated']} 个,清理原文件 {result['removed']} 个" + ) + + +def print_pruned_snapshots(removed_dirs: list[Path]) -> None: + if not removed_dirs: + print(f"🗃️ 版本保留上限: {MAX_SNAPSHOT_VERSIONS},本次无需淘汰旧版本") + return + + removed_names = ", ".join(snapshot_dir.name for snapshot_dir in removed_dirs) + print( + f"🗃️ 版本保留上限: {MAX_SNAPSHOT_VERSIONS}," + f"已淘汰旧版本 {len(removed_dirs)} 个: {removed_names}" + ) + + +def command_prepare(requirement: str) -> None: + requirement_path = resolve_requirement_path(requirement) + validate_requirement_file(requirement_path) + validate_knowledge_base() + validate_project_profile() + paths = build_paths(requirement_path.stem) + ensure_output_dirs(paths) + + technical_solution_files = select_technical_solution_files(requirement_path) + normalized_requirement_file = write_normalized_document( + source_path=requirement_path, + target_path=paths["normalized_input_dir"] / "requirement.md", + document_role="需求文档", + ) + normalized_technical_solution_files = [ + write_normalized_document( + source_path=path, + target_path=paths["normalized_input_dir"] / f"technical_solution_{index:02d}.md", + document_role="技术方案", + ) + for index, path in enumerate(technical_solution_files, start=1) + ] + related_requirements = find_related_requirements(requirement_path) + optional_terminology_files = select_optional_terminology_files( + requirement_path, + related_requirements, + technical_solution_files, + ) + conflicts = build_conflict_candidates(requirement_path, related_requirements) + confirmation_gate = build_confirmation_gate( + requirement_path=requirement_path, + base_name=requirement_path.stem, + related_requirements=related_requirements, + conflicts=conflicts, + normalized_requirement_file=normalized_requirement_file, + normalized_technical_solution_files=normalized_technical_solution_files, + ) + write_relation_report( + requirement_path=requirement_path, + related_requirements=related_requirements, + technical_solution_files=technical_solution_files, + conflicts=conflicts, + report_path=paths["relation_report"], + ) + manifest_path = write_manifest( + requirement_path=requirement_path, + paths=paths, + related_requirements=related_requirements, + technical_solution_files=technical_solution_files, + normalized_requirement_file=normalized_requirement_file, + normalized_technical_solution_files=normalized_technical_solution_files, + optional_terminology_files=optional_terminology_files, + conflict_count=len(conflicts), + confirmation_gate=confirmation_gate, + ) + + print(f"✅ 准备完成: {requirement_path}") + print(f"📦 清单文件: {manifest_path}") + print(f"📄 标准化需求输入: {normalized_requirement_file}") + print(f"🧩 关联技术方案数量: {len(technical_solution_files)}") + print(f"📝 分析输出: {paths['analysis']}") + print(f"🔎 关联与冲突输出: {paths['relation_report']}") + print(f"🧪 测试点输出: {paths['test_points']}") + print(f"📋 用例输出: {paths['test_cases']}") + print(f"📚 关联需求数量: {len(related_requirements)}") + print(f"📖 可选术语文件: {len(optional_terminology_files)}") + print(f"⚠️ 潜在冲突候选: {len(conflicts)}") + if confirmation_gate["required"]: + if confirmation_gate["decision_status"] == "confirmed": + print(f"✅ 人工确认: 已确认 -> {confirmation_gate['decision_file']}") + else: + print("🛑 人工确认: 必需") + print(f"📝 确认单建议路径: {confirmation_gate['suggested_decision_file']}") + else: + print("🟢 人工确认: 当前无需额外确认") + + +def verify_case_columns(case_file: Path) -> list[list[str]]: + headers, rows = load_markdown_table(case_file) + if headers not in (REQUIRED_COLUMNS, LEGACY_COLUMNS): + raise ValueError( + "测试用例表头不符合标准。\n" + f"期望: {REQUIRED_COLUMNS} 或 {LEGACY_COLUMNS}\n" + f"实际: {headers}" + ) + if headers == REQUIRED_COLUMNS: + type_index = headers.index("类型") + invalid_types = sorted({row[type_index] for row in rows if row[type_index] and row[type_index] not in TYPE_ENUMS}) + if invalid_types: + raise ValueError(f"测试用例存在非法类型枚举:{', '.join(invalid_types)}。") + return rows + + +def command_verify(requirement: str) -> None: + requirement_path = resolve_requirement_path(requirement) + validate_requirement_file(requirement_path) + paths = build_paths(requirement_path.stem) + + for name in ("analysis", "relation_report", "test_points", "test_cases"): + target = paths[name] + if not target.exists(): + raise FileNotFoundError(f"缺少输出文件:{target}") + if target.stat().st_size == 0: + raise ValueError(f"输出文件为空:{target}") + + manifest = ensure_confirmation_resolved(paths, "verify") + rows = verify_case_columns(paths["test_cases"]) + print(f"✅ 校验通过: {requirement_path.name}") + print(f"📄 分析文件: {paths['analysis']}") + print(f"🔎 关联与冲突文件: {paths['relation_report']}") + print(f"🧪 测试点文件: {paths['test_points']}") + print(f"📋 测试用例文件: {paths['test_cases']}") + print(f"📊 用例数量: {len(rows)}") + gate = manifest.get("confirmation_gate", {}) + if gate.get("required"): + print(f"📝 确认单: {gate.get('decision_file')}") + + +def command_export(requirement: str) -> None: + requirement_path = resolve_requirement_path(requirement) + validate_requirement_file(requirement_path) + paths = build_paths(requirement_path.stem) + if not paths["test_cases"].exists(): + raise FileNotFoundError(f"测试用例文件不存在:{paths['test_cases']}") + + ensure_confirmation_resolved(paths, "export") + verify_case_columns(paths["test_cases"]) + output_path = export_markdown_to_excel(paths["test_cases"], paths["excel_dir"]) + legacy_result = migrate_legacy_excel_exports(paths) + version_number, snapshot_dir, created = decide_snapshot_version(paths, output_path) + if created: + write_snapshot_files(paths, output_path, snapshot_dir) + pruned_dirs = prune_old_snapshots(paths["version_root"]) + index_path = refresh_version_index(paths["version_root"], extract_base_name_from_paths(paths)) + latest = find_latest_snapshot(paths["version_root"]) + if latest is None: + raise FileNotFoundError(f"版本目录为空:{paths['version_root']}") + version_number, snapshot_dir = latest + update_manifest_version_info(paths["manifest"], output_path, version_number, snapshot_dir) + maintained_requirement_path = sync_maintained_requirement(requirement_path) + + print(f"✅ Excel 已生成: {output_path}") + print(f"🧾 正式维护版: {maintained_requirement_path}") + if created: + print(f"📦 版本快照: v{version_number} -> {snapshot_dir}") + else: + print(f"♻️ 版本未变: v{version_number} -> {snapshot_dir}") + print_legacy_migration_result(legacy_result) + print_pruned_snapshots(pruned_dirs) + print(f"🗂️ 版本索引: {index_path}") + + +def command_sync_maintained(requirement: str) -> None: + requirement_path = resolve_requirement_path(requirement) + validate_requirement_file(requirement_path) + paths = build_paths(requirement_path.stem) + if not paths["manifest"].exists(): + raise FileNotFoundError(f"缺少清单文件:{paths['manifest']}。请先执行 prepare。") + + maintained_requirement_path = sync_maintained_requirement(requirement_path) + print(f"✅ 正式维护版已同步: {maintained_requirement_path}") + print(f"📄 原始来源: {requirement_path}") + print(f"🧾 manifest: {paths['manifest']}") + + +def command_apply_confirmation(requirement: str, dry_run: bool) -> None: + requirement_path = resolve_requirement_path(requirement) + validate_requirement_file(requirement_path) + paths = build_paths(requirement_path.stem) + manifest = refresh_manifest_confirmation_gate(paths) + gate = manifest.get("confirmation_gate", {}) + decision_file = gate.get("decision_file") + if not decision_file: + fallback_files = find_relevant_decision_files(requirement_path) + if fallback_files: + decision_file = str(fallback_files[0]) + if not decision_file: + raise FileNotFoundError("未找到可用确认单。") + + decision = parse_decision_file(Path(decision_file)) + if decision["status"] != "confirmed": + raise ValueError(f"确认单未处于已确认状态:{decision_file}") + + related_requirements = [ + resolve_requirement_reference(reference) + for reference in decision.get("related_requirement_refs", []) + ] + rerun_targets = build_rerun_targets(decision, requirement_path) + note_path = build_maintenance_note_path(Path(decision["file"])) + + print(f"✅ 确认单已读取: {decision_file}") + print(f"🧭 关系类型: {decision.get('relation_type') or '未填写'}") + print(f"📝 维护说明路径: {note_path}") + print(f"🔁 重跑需求数量: {len(rerun_targets)}") + for target in rerun_targets: + print(f" - {to_repo_relative(target)}") + + if dry_run: + print("🧪 dry-run: 未写入维护说明,未执行重跑。") + return + + note_path = write_maintenance_note( + decision=decision, + current_requirement=requirement_path, + related_requirements=related_requirements, + rerun_targets=rerun_targets, + ) + for target in rerun_targets: + run_pipeline_for_requirement(target) + + print(f"📌 维护说明已写入: {note_path}") + print("🚀 确认后续跑已完成") + + +def command_migrate_history(requirement: str | None, all_requirements: bool) -> None: + if not requirement and not all_requirements: + raise ValueError("请指定 --requirement,或使用 --all 扫描全部历史 Excel。") + + base_names: list[str] = [] + if all_requirements: + for excel_file in sorted((REPO_ROOT / "output" / "excel_reports").glob("*.xlsx")): + if LEGACY_EXCEL_EXPORT_RE.match(excel_file.name): + stem = excel_file.stem + base_name = re.sub(r"_\d{8}_\d{6}$", "", stem) + if base_name.endswith("_测试用例"): + base_name = base_name[: -len("_测试用例")] + if base_name not in base_names: + base_names.append(base_name) + else: + requirement_path = resolve_requirement_path(requirement or "") + validate_requirement_file(requirement_path) + base_names.append(requirement_path.stem) + + total_found = 0 + total_imported = 0 + total_deduplicated = 0 + total_removed = 0 + + for base_name in base_names: + paths = build_paths(base_name) + result = migrate_legacy_excel_exports(paths) + pruned_dirs = prune_old_snapshots(paths["version_root"]) + index_path = refresh_version_index(paths["version_root"], extract_base_name_from_paths(paths)) + total_found += result["found"] + total_imported += result["imported"] + total_deduplicated += result["deduplicated"] + total_removed += result["removed"] + print(f"📁 {base_name}") + print_legacy_migration_result(result) + print_pruned_snapshots(pruned_dirs) + print(f"🗂️ 版本索引: {index_path}") + + print( + "✅ 历史 Excel 迁移完成: " + f"发现 {total_found} 个,导入版本 {total_imported} 个," + f"重复去重 {total_deduplicated} 个,清理原文件 {total_removed} 个" + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description="测试用例生成流水线辅助脚本。") + subparsers = parser.add_subparsers(dest="command", required=True) + + prepare_parser = subparsers.add_parser("prepare", help="校验输入并生成本次任务清单。") + prepare_parser.add_argument("--requirement", required=True, help="需求文档路径。") + + verify_parser = subparsers.add_parser("verify", help="校验本次任务产物。") + verify_parser.add_argument("--requirement", required=True, help="需求文档路径。") + + export_parser = subparsers.add_parser("export", help="导出本次任务的 Excel。") + export_parser.add_argument("--requirement", required=True, help="需求文档路径。") + + apply_confirmation_parser = subparsers.add_parser( + "apply-confirmation", + help="读取已确认的确认单,写出维护说明并重跑受影响需求。", + ) + apply_confirmation_parser.add_argument("--requirement", required=True, help="需求文档路径。") + apply_confirmation_parser.add_argument("--dry-run", action="store_true", help="仅校验确认单和重跑目标,不执行写入或重跑。") + + sync_maintained_parser = subparsers.add_parser( + "sync-maintained", + help="同步 requirements/ 下的正式维护版 Markdown。", + ) + sync_maintained_parser.add_argument("--requirement", required=True, help="需求文档路径。") + + migrate_parser = subparsers.add_parser("migrate-history", help="迁移旧时间戳 Excel 到版本目录。") + migrate_parser.add_argument("--requirement", help="按需求文档迁移对应 BASE_NAME 的历史 Excel。") + migrate_parser.add_argument("--all", action="store_true", help="扫描并迁移全部历史时间戳 Excel。") + + args = parser.parse_args() + + if args.command == "prepare": + command_prepare(args.requirement) + elif args.command == "verify": + command_verify(args.requirement) + elif args.command == "export": + command_export(args.requirement) + elif args.command == "sync-maintained": + command_sync_maintained(args.requirement) + elif args.command == "apply-confirmation": + command_apply_confirmation(args.requirement, args.dry_run) + elif args.command == "migrate-history": + command_migrate_history(args.requirement, args.all) + + +if __name__ == "__main__": + main() diff --git a/scripts/export_excel.py b/scripts/export_excel.py new file mode 100644 index 0000000..a641804 --- /dev/null +++ b/scripts/export_excel.py @@ -0,0 +1,359 @@ +import argparse +from datetime import datetime +from pathlib import Path +import re + +from openpyxl import Workbook +from openpyxl.styles import Alignment, Font + + +REPO_ROOT = Path(__file__).resolve().parent.parent +DEFAULT_INPUT_DIR = REPO_ROOT / "output" / "test_cases" +DEFAULT_OUTPUT_DIR = REPO_ROOT / "output" / "excel_reports" +REQUIRED_COLUMNS = [ + "用例编号", + "模块", + "用例标题", + "优先级", + "类型", + "前置条件", + "测试步骤", + "测试数据", + "预期结果", + "备注", +] +LEGACY_COLUMNS = [ + "用例编号", + "模块", + "用例标题", + "优先级", + "前置条件", + "测试步骤", + "测试数据", + "预期结果", + "备注", +] +SUPPORTED_COLUMN_SETS = [REQUIRED_COLUMNS, LEGACY_COLUMNS] +YUNXIAO_HEADERS = [ + "标题", + "编号", + "目录", + "创建时间", + "前置条件", + "步骤描述", + "预期结果", + "优先级", + "类型", + "URL", +] +TYPE_ENUMS = [ + "功能测试", + "性能测试", + "兼容性测试", + "易用性测试", + "安全性测试", + "冒烟测试", + "回归测试", + "其他", +] +SEPARATOR_RE = re.compile(r"^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$") +MODULE_PATH_SEPARATOR_RE = re.compile(r"\s*[--—–]\s*") +BR_TAG_RE = re.compile(r"", re.IGNORECASE) + + +def headers_supported(headers: list[str]) -> bool: + return headers in SUPPORTED_COLUMN_SETS + + +def find_latest_markdown_file(directory: Path) -> Path | None: + files = sorted(directory.glob("*.md"), key=lambda item: item.stat().st_mtime) + return files[-1] if files else None + + +def split_markdown_row(line: str) -> list[str]: + stripped = line.strip() + if stripped.startswith("|"): + stripped = stripped[1:] + if stripped.endswith("|"): + stripped = stripped[:-1] + + cells: list[str] = [] + current: list[str] = [] + escaped = False + + for char in stripped: + if escaped: + current.append(char) + escaped = False + continue + if char == "\\": + escaped = True + continue + if char == "|": + cells.append("".join(current).strip()) + current = [] + continue + current.append(char) + + if escaped: + current.append("\\") + + cells.append("".join(current).strip()) + return cells + + +def normalize_module_path(value: str) -> str: + stripped = value.strip() + if not stripped: + return stripped + + if "|" in stripped: + parts = [part.strip() for part in stripped.split("|") if part.strip()] + return "|".join(parts) + + if MODULE_PATH_SEPARATOR_RE.search(stripped): + parts = [part.strip() for part in MODULE_PATH_SEPARATOR_RE.split(stripped) if part.strip()] + if len(parts) > 1: + return "|".join(parts) + + return stripped + + +def load_markdown_table(source_file: Path) -> tuple[list[str], list[list[str]]]: + content = source_file.read_text(encoding="utf-8") + table_lines: list[str] = [] + + for line in content.splitlines(): + if line.strip().startswith("|"): + table_lines.append(line.rstrip()) + elif table_lines: + break + + if len(table_lines) < 3: + raise ValueError(f"未在 {source_file} 中找到有效的 Markdown 表格。") + + headers = split_markdown_row(table_lines[0]) + if not SEPARATOR_RE.match(table_lines[1].strip()): + raise ValueError(f"{source_file} 的第二行不是合法的 Markdown 表头分隔线。") + + module_index = headers.index("模块") if "模块" in headers else None + + rows: list[list[str]] = [] + for line in table_lines[2:]: + row = split_markdown_row(line) + if len(row) != len(headers): + raise ValueError( + f"{source_file} 中存在列数不一致的行,表头 {len(headers)} 列,实际 {len(row)} 列。" + ) + if module_index is not None: + row[module_index] = normalize_module_path(row[module_index]) + rows.append(row) + + if not rows: + raise ValueError(f"{source_file} 的 Markdown 表格没有数据行。") + + return headers, rows + + +def infer_case_type(case: dict[str, str]) -> str: + explicit = case.get("类型", "").strip() + if explicit in TYPE_ENUMS: + return explicit + + combined = " ".join( + [ + case.get("用例标题", ""), + case.get("模块", ""), + case.get("备注", ""), + case.get("测试步骤", ""), + case.get("预期结果", ""), + ] + ) + + if re.search(r"性能|RT|响应时间|P95|吞吐|压测|并发量", combined, re.IGNORECASE): + return "性能测试" + if re.search(r"越权|鉴权|权限|抓包|篡改|注入|XSS|CSRF|SQL", combined, re.IGNORECASE): + return "安全性测试" + if re.search(r"兼容|浏览器|小程序|H5|多端", combined, re.IGNORECASE): + return "兼容性测试" + if re.search(r"易用|交互|文案|可读|展示", combined, re.IGNORECASE): + return "易用性测试" + if re.search(r"冒烟|主流程", combined, re.IGNORECASE): + return "冒烟测试" + if re.search(r"历史缺陷|回归|AI修正", combined, re.IGNORECASE): + return "回归测试" + return "功能测试" + + +def normalize_multiline_text(value: str) -> str: + text = BR_TAG_RE.sub("\n", value or "") + return text.replace("\\n", "\n").strip() + + +def normalize_topic_name(text: str) -> str: + normalized = text.strip() + changed = True + while changed: + changed = False + for token in ("测试用例", "需求", "分析"): + if normalized.endswith(token): + normalized = normalized[: -len(token)].strip() + changed = True + normalized = re.sub(r"[_\-\s|]+$", "", normalized) + return normalized or text.strip() + + +def build_directory_name(source_path: Path, module_value: str) -> str: + topic = normalize_topic_name(source_path.stem) + module = normalize_module_path(module_value) + if not module: + return topic + if module.startswith(topic): + return module + return f"{topic}|{module}" + + +def normalize_case_rows(headers: list[str], rows: list[list[str]]) -> list[dict[str, str]]: + cases: list[dict[str, str]] = [] + for row in rows: + current = {header: (row[index] or "").strip() for index, header in enumerate(headers)} + if "类型" not in current: + current["类型"] = infer_case_type(current) + elif current["类型"] not in TYPE_ENUMS: + current["类型"] = infer_case_type(current) + cases.append(current) + return cases + + +def build_yunxiao_rows(source_path: Path, headers: list[str], rows: list[list[str]]) -> list[list[object]]: + created_time = datetime.fromtimestamp(source_path.stat().st_mtime).replace(microsecond=0) + cases = normalize_case_rows(headers, rows) + output_rows: list[list[object]] = [] + + for case in cases: + steps_text = normalize_multiline_text(case.get("测试步骤", "")) + data_text = normalize_multiline_text(case.get("测试数据", "")) + if data_text: + steps_text = f"{steps_text}\n[测试数据]\n{data_text}" if steps_text else f"[测试数据]\n{data_text}" + + output_rows.append( + [ + case.get("用例标题", ""), + case.get("用例编号", ""), + build_directory_name(source_path, case.get("模块", "")), + created_time, + normalize_multiline_text(case.get("前置条件", "")), + steps_text, + normalize_multiline_text(case.get("预期结果", "")), + case.get("优先级", ""), + infer_case_type(case), + "", + ] + ) + + return output_rows + + +def resolve_source_file(source_file: str | None, base_name: str | None, input_dir: Path) -> Path: + if source_file: + path = Path(source_file) + return path if path.is_absolute() else REPO_ROOT / path + + if base_name: + return input_dir / f"{base_name}_测试用例.md" + + latest = find_latest_markdown_file(input_dir) + if latest is None: + raise FileNotFoundError(f"在 {input_dir} 下没有找到任何 Markdown 测试用例文件。") + return latest + + +def build_current_excel_path(source_path: Path, output_dir: Path) -> Path: + return output_dir / f"{source_path.stem}.xlsx" + + +def export_markdown_to_excel( + source_path: Path, + output_dir: Path, + output_path: Path | None = None, +) -> Path: + headers, rows = load_markdown_table(source_path) + if not headers_supported(headers): + raise ValueError( + "测试用例表头不符合支持格式。\n" + f"支持: {REQUIRED_COLUMNS} 或 {LEGACY_COLUMNS}\n" + f"实际: {headers}" + ) + output_dir.mkdir(parents=True, exist_ok=True) + if output_path is None: + output_path = build_current_excel_path(source_path, output_dir) + + workbook = Workbook() + worksheet = workbook.active + worksheet.title = "testcase items" + + worksheet.append(YUNXIAO_HEADERS) + for row in build_yunxiao_rows(source_path, headers, rows): + worksheet.append(row) + + header_font = Font(bold=True) + wrap_alignment = Alignment(vertical="top", wrap_text=True) + + for cell in worksheet[1]: + cell.font = header_font + cell.alignment = wrap_alignment + + for row in worksheet.iter_rows(min_row=2): + for cell in row: + cell.alignment = wrap_alignment + + worksheet.freeze_panes = "A2" + worksheet.auto_filter.ref = f"A1:J{worksheet.max_row}" + + for column_cells in worksheet.columns: + max_length = max(len(str(cell.value or "")) for cell in column_cells) + adjusted_width = min(max_length + 4, 60) + worksheet.column_dimensions[column_cells[0].column_letter].width = adjusted_width + + workbook.save(output_path) + return output_path + + +def main() -> None: + parser = argparse.ArgumentParser(description="将 Markdown 测试用例表格导出为 Excel。") + parser.add_argument("--source-file", help="指定测试用例 Markdown 文件路径。") + parser.add_argument("--base-name", help="按 BASE_NAME 查找 output/test_cases/{BASE_NAME}_测试用例.md。") + parser.add_argument( + "--input-dir", + default=str(DEFAULT_INPUT_DIR), + help="测试用例 Markdown 所在目录,默认 output/test_cases。", + ) + parser.add_argument( + "--output-dir", + default=str(DEFAULT_OUTPUT_DIR), + help="Excel 输出目录,默认 output/excel_reports。", + ) + args = parser.parse_args() + + input_dir = Path(args.input_dir) + if not input_dir.is_absolute(): + input_dir = REPO_ROOT / input_dir + + output_dir = Path(args.output_dir) + if not output_dir.is_absolute(): + output_dir = REPO_ROOT / output_dir + + source_path = resolve_source_file(args.source_file, args.base_name, input_dir) + if not source_path.exists(): + raise FileNotFoundError(f"测试用例文件不存在:{source_path}") + + output_path = export_markdown_to_excel(source_path, output_dir) + headers, rows = load_markdown_table(source_path) + print(f"📄 来源文件: {source_path}") + print(f"🧩 Markdown 表头字段: {', '.join(headers)}") + print(f"📊 用例数量: {len(rows)}") + print(f"✅ Excel 已生成: {output_path}") + + +if __name__ == "__main__": + main() diff --git a/scripts/fleet_agents.py b/scripts/fleet_agents.py new file mode 100644 index 0000000..9aba9c1 --- /dev/null +++ b/scripts/fleet_agents.py @@ -0,0 +1,212 @@ +""" +Agentic QE Fleet — Agent 加载和上下文注入模块 + +负责: +1. 加载 Agent prompt 文件 +2. 注入战区上下文(前序 manifest 数据) +3. 渲染最终可执行的 Agent prompt +""" + +from __future__ import annotations + +import re +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parent.parent +AGENTS_DIR = REPO_ROOT / "agents" + +# Agent 注册表: agent_id → (战区, 文件路径, 描述) +AGENT_REGISTRY: dict[str, dict[str, Any]] = { + # ── Prepare ── + "document-parser": { + "zone": "prepare", + "path": "prepare/document_parser.md", + "name": "文档解析专家", + "description": "解析原始需求文档为标准 Markdown,自动识别技术方案", + }, + "knowledge-activator": { + "zone": "prepare", + "path": "prepare/knowledge_activator.md", + "name": "知识激活专家", + "description": "按需求内容自动激活术语/规则/历史缺陷/最佳实践", + }, + # ── Analyze ── + "requirement-analyzer": { + "zone": "analyze", + "path": "analyze/requirement_analyzer.md", + "name": "需求分析专家", + "description": "结构化需求模型 + 歧义标注", + }, + "conflict-detector": { + "zone": "analyze", + "path": "analyze/conflict_detector.md", + "name": "冲突检测专家", + "description": "语义级历史需求规则冲突检测", + }, + "risk-assessor": { + "zone": "analyze", + "path": "analyze/risk_assessor.md", + "name": "风险评估专家", + "description": "多维度风险矩阵量化", + }, + # ── Design ── + "test-strategist": { + "zone": "design", + "path": "design/test_strategist.md", + "name": "测试策略师", + "description": "分层测试策略 + 优先级矩阵", + }, + "testpoint-designer": { + "zone": "design", + "path": "design/testpoint_designer.md", + "name": "测试点设计师", + "description": "全面测试点矩阵 + 来源标注", + }, + "case-designer": { + "zone": "design", + "path": "design/case_designer.md", + "name": "用例设计师", + "description": "可执行测试用例 + 双验证预期结果", + }, + "data-builder": { + "zone": "design", + "path": "design/data_builder.md", + "name": "数据构造师", + "description": "精确测试数据集构造", + }, + # ── Review ── + "case-reviewer": { + "zone": "review", + "path": "review/case_reviewer.md", + "name": "用例评审师", + "description": "用例质量/规范性/可执行性逐项评审", + }, + "coverage-auditor": { + "zone": "review", + "path": "review/coverage_auditor.md", + "name": "覆盖率审计师", + "description": "需求→测试点→用例三级追溯覆盖审计", + }, + "quality-gatekeeper": { + "zone": "review", + "path": "review/quality_gatekeeper.md", + "name": "质量门禁裁决官", + "description": "三级裁决: PASS / PASS_WITH_FIX / BLOCKED", + }, + # ── Monitor ── + "execution-analyst": { + "zone": "monitor", + "path": "monitor/execution_analyst.md", + "name": "执行结果分析师", + "description": "失败归类 + 模式识别 + 根因推测", + }, + "knowledge-curator": { + "zone": "monitor", + "path": "monitor/knowledge_curator.md", + "name": "知识沉淀师", + "description": "自动回写知识库 + 去重保护", + }, +} + + +def get_agent_path(agent_id: str) -> Path: + """返回 Agent prompt 文件的绝对路径。""" + if agent_id not in AGENT_REGISTRY: + raise ValueError(f"未知 Agent: {agent_id},可用: {list(AGENT_REGISTRY)}") + return AGENTS_DIR / AGENT_REGISTRY[agent_id]["path"] + + +def load_agent_prompt(agent_id: str) -> str: + """加载 Agent prompt 原始内容。""" + path = get_agent_path(agent_id) + if not path.exists(): + raise FileNotFoundError(f"Agent prompt 不存在: {path}") + return path.read_text(encoding="utf-8") + + +def list_zone_agents(zone: str) -> list[str]: + """列出指定战区的所有 Agent ID。""" + return sorted([ + agent_id for agent_id, info in AGENT_REGISTRY.items() + if info["zone"] == zone + ]) + + +def list_all_agents() -> dict[str, list[str]]: + """按战区分组列出所有 Agent。""" + result: dict[str, list[str]] = {} + for zone in ["prepare", "analyze", "design", "review", "monitor"]: + result[zone] = list_zone_agents(zone) + return result + + +def inject_context(prompt: str, context: dict[str, Any]) -> str: + """向 prompt 注入运行时上下文变量。 + + 支持的占位符: + {{BASE_NAME}} → 需求基础名 + {{MANIFEST_DIR}} → manifest 目录 + {{PREPARE_MANIFEST}} → prepare manifest 路径 + {{ANALYZE_MANIFEST}} → analyze manifest 路径 + {{DESIGN_MANIFEST}} → design manifest 路径 + {{PROJECT_PROFILE}} → 项目画像路径 + {{FLEET_CONFIG}} → fleet_config.yml 路径 + """ + replacements = { + "{{BASE_NAME}}": context.get("base_name", ""), + "{{MANIFEST_DIR}}": str(REPO_ROOT / "output" / "manifests"), + "{{PREPARE_MANIFEST}}": str(REPO_ROOT / "output" / "manifests" / f"{context.get('base_name', '')}_prepare.json"), + "{{ANALYZE_MANIFEST}}": str(REPO_ROOT / "output" / "manifests" / f"{context.get('base_name', '')}_analyze.json"), + "{{DESIGN_MANIFEST}}": str(REPO_ROOT / "output" / "manifests" / f"{context.get('base_name', '')}_design.json"), + "{{PROJECT_PROFILE}}": str(REPO_ROOT / "knowledge_base" / "00_project" / "project_profile.md"), + "{{FLEET_CONFIG}}": str(REPO_ROOT / "fleet_config.yml"), + "{{OUTPUT_DIR}}": str(REPO_ROOT / "output"), + "{{KNOWLEDGE_BASE_DIR}}": str(REPO_ROOT / "knowledge_base"), + "{{REQUIREMENT_FILE}}": context.get("requirement_file", ""), + } + result = prompt + for placeholder, value in replacements.items(): + result = result.replace(placeholder, value) + return result + + +def render_agent_prompt(agent_id: str, context: dict[str, Any]) -> str: + """加载并渲染 Agent prompt(注入上下文)。""" + raw = load_agent_prompt(agent_id) + return inject_context(raw, context) + + +def get_agent_frontmatter(agent_id: str) -> dict[str, Any]: + """提取 Agent prompt 的 YAML frontmatter。""" + content = load_agent_prompt(agent_id) + frontmatter: dict[str, Any] = {} + if content.startswith("---"): + parts = content.split("---", 2) + if len(parts) >= 3: + for line in parts[1].strip().split("\n"): + line = line.strip() + if ":" in line: + key, value = line.split(":", 1) + frontmatter[key.strip()] = value.strip() + return frontmatter + + +def get_agent_short_description(agent_id: str) -> str: + """返回 Agent 的一句话描述。""" + info = AGENT_REGISTRY.get(agent_id, {}) + return info.get("description", agent_id) + + +def validate_all_agents() -> dict[str, Any]: + """验证所有 Agent prompt 文件是否存在且非空。""" + result: dict[str, Any] = {"valid": True, "missing": [], "empty": [], "total": len(AGENT_REGISTRY)} + for agent_id in AGENT_REGISTRY: + path = get_agent_path(agent_id) + if not path.exists(): + result["missing"].append(agent_id) + result["valid"] = False + elif path.stat().st_size == 0: + result["empty"].append(agent_id) + result["valid"] = False + return result diff --git a/scripts/fleet_manifest.py b/scripts/fleet_manifest.py new file mode 100644 index 0000000..18718df --- /dev/null +++ b/scripts/fleet_manifest.py @@ -0,0 +1,180 @@ +""" +Agentic QE Fleet — Manifest 管理模块 + +负责跨战区 manifest JSON 的读写、合并、验证和迁移。 +每个战区产出一个 manifest_.json,fleet_runner.py 消费。 +""" + +from __future__ import annotations + +import json +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parent.parent +MANIFESTS_DIR = REPO_ROOT / "output" / "manifests" + +# 战区顺序 +ZONE_ORDER = ["prepare", "analyze", "design", "review", "monitor"] + + +def ensure_manifests_dir() -> Path: + MANIFESTS_DIR.mkdir(parents=True, exist_ok=True) + return MANIFESTS_DIR + + +def manifest_path(base_name: str, zone: str) -> Path: + """返回战区 manifest 文件路径。""" + return MANIFESTS_DIR / f"{base_name}_{zone}.json" + + +def load_manifest(base_name: str, zone: str) -> dict[str, Any]: + """加载指定战区的 manifest。""" + path = manifest_path(base_name, zone) + if not path.exists(): + raise FileNotFoundError(f"Manifest 不存在: {path}") + return json.loads(path.read_text(encoding="utf-8")) + + +def load_manifest_safe(base_name: str, zone: str) -> dict[str, Any] | None: + """安全加载 manifest,不存在时返回 None。""" + path = manifest_path(base_name, zone) + if not path.exists(): + return None + return json.loads(path.read_text(encoding="utf-8")) + + +def save_manifest(base_name: str, zone: str, data: dict[str, Any]) -> Path: + """保存战区 manifest。""" + ensure_manifests_dir() + path = manifest_path(base_name, zone) + data.setdefault("_meta", {}) + data["_meta"]["zone"] = zone + data["_meta"]["base_name"] = base_name + data["_meta"]["updated_at"] = datetime.now(timezone.utc).isoformat() + path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + return path + + +def build_merged_manifest(base_name: str, zones: list[str] | None = None) -> dict[str, Any]: + """合并多个战区的 manifest 为统一视图。""" + if zones is None: + zones = ZONE_ORDER + + merged: dict[str, Any] = { + "_meta": { + "base_name": base_name, + "merged_zones": [], + "merged_at": datetime.now(timezone.utc).isoformat(), + } + } + + for zone in zones: + manifest = load_manifest_safe(base_name, zone) + if manifest is None: + continue + merged["_meta"]["merged_zones"].append(zone) + # 战区数据按 zone 命名空间隔离 + merged[zone] = manifest + # 同时摊平顶层便捷字段(后者覆盖前者) + for key, value in manifest.items(): + if key.startswith("_"): + continue + merged[key] = value + + return merged + + +def get_zone_status(base_name: str) -> dict[str, str]: + """返回各战区完成状态。""" + status: dict[str, str] = {} + for zone in ZONE_ORDER: + manifest = load_manifest_safe(base_name, zone) + if manifest is None: + status[zone] = "pending" + elif manifest.get("_meta", {}).get("status") == "completed": + status[zone] = "completed" + else: + status[zone] = "in_progress" + return status + + +def mark_zone_completed(base_name: str, zone: str) -> Path: + """标记战区为已完成。""" + manifest = load_manifest(base_name, zone) + manifest.setdefault("_meta", {})["status"] = "completed" + return save_manifest(base_name, zone, manifest) + + +def get_confirmation_gate(base_name: str) -> dict[str, Any] | None: + """获取当前确认门禁状态(分析战区产出)。""" + manifest = load_manifest_safe(base_name, "analyze") + if manifest is None: + return None + return manifest.get("confirmation_gate") + + +def get_quality_verdict(base_name: str) -> dict[str, Any] | None: + """获取质量裁决(评审战区产出)。""" + manifest = load_manifest_safe(base_name, "review") + if manifest is None: + return None + return manifest.get("quality_verdict") + + +def get_latest_zone(base_name: str) -> str | None: + """返回最新已完成的战区名称。""" + for zone in reversed(ZONE_ORDER): + manifest = load_manifest_safe(base_name, zone) + if manifest and manifest.get("_meta", {}).get("status") == "completed": + return zone + return None + + +def get_next_zone(base_name: str) -> str | None: + """返回下一个待执行的战区名称。""" + for zone in ZONE_ORDER: + manifest = load_manifest_safe(base_name, zone) + if manifest is None or manifest.get("_meta", {}).get("status") != "completed": + return zone + # 全部完成 + return None + + +def migrate_legacy_manifest(base_name: str) -> dict[str, Any]: + """从旧的单 manifest JSON 迁移到新的多战区 manifest 结构。""" + legacy_path = MANIFESTS_DIR / f"{base_name}.json" + if not legacy_path.exists(): + raise FileNotFoundError(f"旧版 manifest 不存在: {legacy_path}") + + legacy = json.loads(legacy_path.read_text(encoding="utf-8")) + + # 从旧 manifest 推断 prepare 战区数据 + prepare_data = { + "_meta": {"zone": "prepare", "base_name": base_name, "status": "completed", + "migrated_from_legacy": True}, + "base_name": legacy.get("base_name", base_name), + "normalized_requirement_file": legacy.get("normalized_requirement_file"), + "normalized_technical_solution_files": legacy.get("normalized_technical_solution_files", []), + "technical_solution_files": legacy.get("technical_solution_files", []), + "project_profile_file": legacy.get("project_profile_file"), + "effective_terminology_files": legacy.get("effective_terminology_files", []), + "optional_terminology_files": legacy.get("optional_terminology_files", []), + "knowledge_base_files": legacy.get("knowledge_base_files", []), + } + save_manifest(base_name, "prepare", prepare_data) + + # 从旧 manifest 推断 analyze 战区数据 + analyze_data = { + "_meta": {"zone": "analyze", "base_name": base_name, "status": "completed", + "migrated_from_legacy": True}, + "related_requirements": legacy.get("related_requirements", []), + "conflict_candidates_count": legacy.get("conflict_candidates_count", 0), + "confirmation_gate": legacy.get("confirmation_gate", {}), + "analysis_file": legacy.get("analysis_file"), + "relation_report_file": legacy.get("relation_report_file"), + } + save_manifest(base_name, "analyze", analyze_data) + + return build_merged_manifest(base_name) diff --git a/scripts/fleet_runner.py b/scripts/fleet_runner.py new file mode 100644 index 0000000..d2c04b9 --- /dev/null +++ b/scripts/fleet_runner.py @@ -0,0 +1,1089 @@ +#!/usr/bin/env python3 +""" +Agentic QE Fleet — 核心编排器 + +职责: 按战区顺序编排 14 个专业 Agent,协调 manifest 交接, + 执行质量门禁裁决,触发 Excel 导出和知识沉淀。 + +用法: + python3 scripts/fleet_runner.py run --requirement <路径> + python3 scripts/fleet_runner.py prepare --requirement <路径> + python3 scripts/fleet_runner.py analyze --requirement <路径> + python3 scripts/fleet_runner.py design --requirement <路径> + python3 scripts/fleet_runner.py review --requirement <路径> + python3 scripts/fleet_runner.py export --requirement <路径> + python3 scripts/fleet_runner.py monitor --requirement <路径> --results <测试结果> + python3 scripts/fleet_runner.py status --requirement <路径> +""" + +from __future__ import annotations + +import argparse +import json +import sys +import subprocess +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +# 复用现有模块 +from case_pipeline import ( + resolve_requirement_path, + validate_requirement_file, + validate_knowledge_base, + validate_project_profile, + build_paths as build_legacy_paths, + ensure_output_dirs, + write_normalized_document, + select_technical_solution_files, + safe_read_text, + to_repo_relative, + REPO_ROOT, + REQUIREMENTS_DIR, + RAW_REQUIREMENTS_DIR, + TECHNICAL_SOLUTIONS_DIR, + PROJECT_PROFILE_FILE, +) +from export_excel import export_markdown_to_excel + +# Fleet 自有模块 +from fleet_manifest import ( + save_manifest, + load_manifest, + load_manifest_safe, + build_merged_manifest, + get_zone_status, + mark_zone_completed, + manifest_path, + ZONE_ORDER, +) +from fleet_agents import ( + AGENT_REGISTRY, + load_agent_prompt, + list_zone_agents, + validate_all_agents, + render_agent_prompt, +) + +# ── 配置加载 ────────────────────────────────────────────────────────────── + +try: + import yaml + _HAS_YAML = True +except ImportError: + _HAS_YAML = False + + +def load_fleet_config() -> dict[str, Any]: + """加载 fleet_config.yml。""" + config_path = REPO_ROOT / "fleet_config.yml" + if not config_path.exists(): + return _default_fleet_config() + + if _HAS_YAML: + with open(config_path, "r", encoding="utf-8") as fh: + return yaml.safe_load(fh) or {} + else: + # 纯 Python fallback: 解析简单的 YAML 子集 + return _parse_simple_yaml_config(config_path) + + +def _default_fleet_config() -> dict[str, Any]: + return { + "battle_zones": { + "prepare": {"enabled": True}, + "analyze": {"enabled": True}, + "design": {"enabled": True}, + "review": {"enabled": True}, + "monitor": {"enabled": True, "auto_confirm": True}, + }, + "quality_gate": {"min_coverage": 0.95, "max_blockers": 0}, + "output": {"excel_format": "yunxiao", "snapshot_keep": 3, "auto_sync_maintained": True}, + } + + +def _parse_simple_yaml_config(path: Path) -> dict[str, Any]: + """简易 YAML 解析器(当 PyYAML 不可用时)。""" + import re + result: dict[str, Any] = {} + current: dict[str, Any] = result + stack: list[tuple[str, dict[str, Any]]] = [] + + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.rstrip() + if not line or line.strip().startswith("#"): + continue + + indent = len(raw_line) - len(raw_line.lstrip()) + key_match = re.match(r"^(\s*)([\w_-]+)\s*:\s*(.*)", line) + if not key_match: + continue + + key = key_match.group(2) + value = key_match.group(3).strip().strip('"').strip("'") + + # 处理缩进回退 + while stack and stack[-1][0] >= indent: + stack.pop() + current = stack[-1][1] if stack else result + + if value in ("true", "True"): + current[key] = True + elif value in ("false", "False"): + current[key] = False + elif value == "" or value == "{}": + current[key] = {} + current = current[key] + stack.append((indent, current)) + elif value == "[]": + current[key] = [] + elif re.match(r"^- ", raw_line): + if key not in current: + current[key] = [] + current[key].append(value) + elif re.match(r"^\d+(\.\d+)?$", value): + current[key] = float(value) if "." in value else int(value) + else: + current[key] = value + + return result + + +# ── 路径和目录 ──────────────────────────────────────────────────────────── + +BUILTIN_INPUT_DIRS = [ + REPO_ROOT / "output" / "analysis", + REPO_ROOT / "output" / "test_points", + REPO_ROOT / "output" / "test_cases", + REPO_ROOT / "output" / "excel_reports", + REPO_ROOT / "output" / "normalized_inputs", + REPO_ROOT / "output" / "versions", +] + + +def build_all_output_dirs(base_name: str) -> None: + """创建所有输出目录。""" + legacy_paths = build_legacy_paths(base_name) + ensure_output_dirs(legacy_paths) + # 确保 manifest 目录 + (REPO_ROOT / "output" / "manifests").mkdir(parents=True, exist_ok=True) + + +# ── 编排核心 ────────────────────────────────────────────────────────────── + +def run_zone(zone: str, base_name: str, requirement_path: Path, config: dict[str, Any]) -> dict[str, Any]: + """运行一个战区,返回该战区的 manifest 数据。""" + + zone_config = config.get("battle_zones", {}).get(zone, {}) + if not zone_config.get("enabled", True): + print(f"⏭️ {zone.upper()} 战区已禁用,跳过。") + return {} + + print(f"\n{'='*60}") + print(f"🚀 {zone.upper()} 战区启动") + print(f"{'='*60}") + + agents = list_zone_agents(zone) + print(f"📋 Agent: {', '.join(agents)}") + + # 加载前序战区 manifest 作为上下文 + context = _build_zone_context(base_name, zone, requirement_path) + + # 打印当前可用的输入文件 + _print_context_files(context, zone) + + # 战区特定逻辑 + zone_handlers = { + "prepare": _run_prepare_zone, + "analyze": _run_analyze_zone, + "design": _run_design_zone, + "review": _run_review_zone, + "monitor": _run_monitor_zone, + } + + handler = zone_handlers.get(zone) + if handler is None: + raise ValueError(f"未知战区: {zone}") + + result = handler(base_name, requirement_path, context, config, agents) + + # 标记完成 + if result: + mark_zone_completed(base_name, zone) + print(f"✅ {zone.upper()} 战区完成 → {manifest_path(base_name, zone)}") + + return result + + +def _build_zone_context(base_name: str, zone: str, requirement_path: Path) -> dict[str, Any]: + """构建战区运行的上下文。""" + context: dict[str, Any] = { + "base_name": base_name, + "requirement_file": str(requirement_path), + "requirement_stem": requirement_path.stem, + "repo_root": str(REPO_ROOT), + "project_profile": str(PROJECT_PROFILE_FILE), + } + + # 加载前序战区 manifest + for prev_zone in ZONE_ORDER: + if prev_zone == zone: + break + manifest = load_manifest_safe(base_name, prev_zone) + if manifest: + context[f"manifest_{prev_zone}"] = manifest + context[f"manifest_{prev_zone}_path"] = str(manifest_path(base_name, prev_zone)) + + return context + + +def _print_context_files(context: dict[str, Any], zone: str) -> None: + """打印战区可用的上下文文件。""" + from fleet_manifest import load_manifest_safe + + items: list[tuple[str, str]] = [] + + # 从 manifest 中提取产物文件 + for prev_zone in ZONE_ORDER: + if prev_zone == zone: + break + manifest = context.get(f"manifest_{prev_zone}") + if manifest is None: + continue + # 收集文件路径 + for key in manifest: + if key.endswith("_file") or key.endswith("_files"): + value = manifest[key] + if isinstance(value, str) and Path(value).exists(): + items.append((key, value)) + elif isinstance(value, list): + for v in value: + if isinstance(v, str) and Path(v).exists(): + items.append((key, v)) + + if items: + print("📂 可用上下文文件:") + for key, path in items[:10]: + print(f" - {key}: {path}") + + +# ── Prepare 战区 ────────────────────────────────────────────────────────── + +def _run_prepare_zone( + base_name: str, + requirement_path: Path, + context: dict[str, Any], + config: dict[str, Any], + agents: list[str], +) -> dict[str, Any]: + """执行 Prepare 战区: document-parser → knowledge-activator。""" + + # 1. 文档解析 (借用现有 case_pipeline 的能力) + validate_requirement_file(requirement_path) + validate_knowledge_base() + validate_project_profile() + + technical_solution_files = select_technical_solution_files(requirement_path) + normalized_dir = REPO_ROOT / "output" / "normalized_inputs" / base_name + normalized_dir.mkdir(parents=True, exist_ok=True) + + normalized_requirement_file = write_normalized_document( + source_path=requirement_path, + target_path=normalized_dir / "requirement.md", + document_role="需求文档", + ) + + normalized_technical_solution_files = [ + write_normalized_document( + source_path=path, + target_path=normalized_dir / f"technical_solution_{idx:02d}.md", + document_role="技术方案", + ) + for idx, path in enumerate(technical_solution_files, start=1) + ] + + # 文档置信度评估 + requirement_body = safe_read_text(normalized_requirement_file) + confidence = _estimate_document_confidence(requirement_body, requirement_path.suffix.lower()) + + # 2. 知识激活 + activated_knowledge = _activate_knowledge(requirement_path, config) + + # 检测知识缺口 + knowledge_gaps = _detect_knowledge_gaps(activated_knowledge, requirement_body) + + # 组装 manifest + prepare_manifest = { + "base_name": base_name, + "requirement_source_file": str(requirement_path), + "requirement_input_type": requirement_path.suffix.lower().lstrip("."), + "normalized_requirement_file": str(normalized_requirement_file), + "normalized_dir": str(normalized_dir), + "technical_solution_files": [str(p) for p in technical_solution_files], + "normalized_technical_solution_files": [str(p) for p in normalized_technical_solution_files], + "project_profile_file": str(PROJECT_PROFILE_FILE), + "document_confidence": confidence, + "activated_knowledge": activated_knowledge, + "knowledge_gaps": knowledge_gaps, + "agent_notes": { + "document-parser": f"解析完成,置信度 {confidence.get('requirement', 0):.0%}", + "knowledge-activator": f"激活 {len(activated_knowledge.get('terminology', {}).get('permanent', []))} 常驻 + " + f"{len(activated_knowledge.get('terminology', {}).get('optional', []))} 可选术语", + }, + } + + save_manifest(base_name, "prepare", prepare_manifest) + return prepare_manifest + + +def _estimate_document_confidence(text: str, suffix: str) -> dict[str, Any]: + """估算文档解析置信度。""" + confidence = 1.0 if suffix == ".md" else 0.90 if suffix == ".docx" else 0.80 if suffix == ".pdf" else 0.75 + if not text.strip(): + confidence = 0.1 + elif "> ⚠️ 待确认:PDF 未提取到可用文本" in text: + confidence = 0.05 + elif text.count("> ⚠️") > 3: + confidence -= 0.1 + return { + "requirement": round(max(0.0, confidence), 2), + "issues": ["扫描件/图片型PDF"] if "未提取到可用文本" in text else [], + } + + +def _activate_knowledge(requirement_path: Path, config: dict[str, Any]) -> dict[str, Any]: + """按需求内容自动激活知识库文件。""" + from case_pipeline import ( + CORE_TERMINOLOGY_FILE, + KNOWLEDGE_BASE_FILES, + OPTIONAL_TERMINOLOGY_RULES, + ) + + requirement_text = safe_read_text(requirement_path) + combined = f"{requirement_path.name}\n{requirement_text}" + + # 常驻术语(始终激活) + permanent = [str(CORE_TERMINOLOGY_FILE)] + + # 关键词匹配可选术语 + optional: list[dict[str, Any]] = [] + for rule in OPTIONAL_TERMINOLOGY_RULES: + matched_keywords = [kw for kw in rule["keywords"] if kw in combined] + if matched_keywords: + optional.append({ + "name": rule["name"], + "path": str(rule["path"]), + "matched_keywords": matched_keywords[:10], + "activation_reason": f"匹配关键词: {', '.join(matched_keywords[:5])}", + }) + + # 语义匹配知识库(基于 Jaccard 相似度) + from case_pipeline import to_ngrams, jaccard_similarity + requirement_tokens = to_ngrams(combined) + + semantic_matches: list[dict[str, Any]] = [] + threshold = config.get("knowledge_activation", {}).get("semantic_match_threshold", 0.08) + + for kb_file in KNOWLEDGE_BASE_FILES: + if kb_file == CORE_TERMINOLOGY_FILE: + continue + if not kb_file.exists(): + continue + kb_text = safe_read_text(kb_file) + score = jaccard_similarity(requirement_tokens, to_ngrams(f"{kb_file.name}\n{kb_text}")) + if score >= threshold: + semantic_matches.append({ + "path": str(kb_file), + "score": round(score, 4), + "category": _classify_kb_file(kb_file), + }) + + return { + "terminology": { + "permanent": permanent, + "optional": optional, + }, + "semantic_matches": sorted(semantic_matches, key=lambda x: x["score"], reverse=True), + } + + +def _classify_kb_file(path: Path) -> str: + """分类知识库文件。""" + path_str = str(path) + if "02_history" in path_str: + return "history" + if "03_best_practices" in path_str: + return "best_practice" + if "01_standards" in path_str: + return "standard" + return "other" + + +def _detect_knowledge_gaps(activated_knowledge: dict[str, Any], requirement_text: str) -> list[dict[str, str]]: + """检测知识库缺口。""" + gaps: list[dict[str, str]] = [] + # 检查是否有必要的知识类别缺失 + has_history = any( + m.get("category") == "history" + for m in activated_knowledge.get("semantic_matches", []) + ) + has_best_practice = any( + m.get("category") == "best_practice" + for m in activated_knowledge.get("semantic_matches", []) + ) + if not has_history: + gaps.append({"category": "history", "suggestion": "未激活任何历史缺陷/易漏场景,建议补充相关知识库条目"}) + if not has_best_practice: + gaps.append({"category": "best_practice", "suggestion": "未激活任何最佳实践范例,建议补充同类型需求的优秀用例"}) + return gaps + + +# ── Analyze 战区 ────────────────────────────────────────────────────────── + +def _run_analyze_zone( + base_name: str, + requirement_path: Path, + context: dict[str, Any], + config: dict[str, Any], + agents: list[str], +) -> dict[str, Any]: + """执行 Analyze 战区: requirement-analyzer + conflict-detector → risk-assessor。""" + + prepare_manifest = context.get("manifest_prepare", {}) + from case_pipeline import ( + find_related_requirements, + build_conflict_candidates, + build_confirmation_gate, + write_relation_report, + find_relevant_decision_files, + inspect_decision_file, + ) + + # 加载标准化需求 + normalized_requirement_file = Path( + prepare_manifest.get("normalized_requirement_file", + f"output/normalized_inputs/{base_name}/requirement.md") + ) + if not normalized_requirement_file.is_absolute(): + normalized_requirement_file = REPO_ROOT / normalized_requirement_file + + technical_solution_files = [ + Path(p) for p in prepare_manifest.get("normalized_technical_solution_files", []) + ] + + # 关联需求识别 + related_requirements = find_related_requirements(requirement_path) + print(f"📚 关联需求: {len(related_requirements)} 个") + + # 冲突检测 + conflicts = build_conflict_candidates(requirement_path, related_requirements) + print(f"⚠️ 冲突候选: {len(conflicts)} 个") + + # 写入关联与冲突报告 + analysis_dir = REPO_ROOT / "output" / "analysis" + analysis_dir.mkdir(parents=True, exist_ok=True) + relation_report_path = analysis_dir / f"{base_name}_关联与冲突.md" + write_relation_report( + requirement_path, related_requirements, + [Path(p) for p in prepare_manifest.get("technical_solution_files", [])], + conflicts, relation_report_path, + ) + + # 风险评估 (risk-assessor 的输入) + risk_matrix = _build_risk_matrix(requirement_path, conflicts, related_requirements) + risk_report_path = analysis_dir / f"{base_name}_风险评估.md" + _write_risk_report(risk_matrix, risk_report_path, base_name) + + # 确认门禁 + confirmation_gate = build_confirmation_gate( + requirement_path=requirement_path, + base_name=base_name, + related_requirements=related_requirements, + conflicts=conflicts, + normalized_requirement_file=normalized_requirement_file, + normalized_technical_solution_files=[ + p for p in technical_solution_files if isinstance(p, Path) + ], + ) + + analyze_manifest = { + "base_name": base_name, + "analysis_file": str(analysis_dir / f"{base_name}_分析.md"), + "relation_report_file": str(relation_report_path), + "risk_report_file": str(risk_report_path), + "related_requirements": [ + {"path": str(item["path"]), "similarity": round(item["score"], 6)} + for item in related_requirements + ], + "conflict_candidates_count": len(conflicts), + "conflict_summary": _summarize_conflicts(conflicts), + "risk_matrix": risk_matrix, + "confirmation_gate": confirmation_gate, + "agent_notes": { + "requirement-analyzer": f"识别 {len(related_requirements)} 个关联需求", + "conflict-detector": f"检测到 {len(conflicts)} 个冲突候选", + "risk-assessor": f"识别 {len(risk_matrix.get('risks', []))} 个风险项", + }, + } + + save_manifest(base_name, "analyze", analyze_manifest) + return analyze_manifest + + +def _build_risk_matrix( + requirement_path: Path, + conflicts: list[dict[str, Any]], + related_requirements: list[dict[str, Any]], +) -> dict[str, Any]: + """构建风险矩阵。""" + risks: list[dict[str, Any]] = [] + requirement_text = safe_read_text(requirement_path) + + # 风险检测维度 + checks = [ + ("资损", ["金额", "支付", "退款", "扣减", "优惠", "积分", "券", "库存", "手续费", "税费"], "financial"), + ("可用性", ["超时", "弱网", "并发", "降级", "熔断", "限流", "重试", "幂等"], "availability"), + ("数据", ["脏数据", "迁移", "精度", "隔离", "删除", "空值", "租户"], "data"), + ("合规", ["鉴权", "留痕", "审批", "实名", "隐私", "加密", "脱敏"], "compliance"), + ("兼容性", ["H5", "小程序", "App", "浏览器", "多端", "版本", "灰度"], "compatibility"), + ] + + for category, keywords, risk_id in checks: + matched = [kw for kw in keywords if kw in requirement_text] + if matched: + likelihood = min(5, len(matched)) + impact = 5 if risk_id in ("financial", "compliance") else 4 if risk_id in ("availability", "data") else 3 + risks.append({ + "id": f"RISK-{risk_id.upper()}", + "category": category, + "keywords_matched": matched, + "likelihood": likelihood, + "impact": impact, + "score": likelihood * impact, + "level": "P0" if likelihood * impact >= 15 else "P1" if likelihood * impact >= 10 else "P2", + "conflict_amplified": bool(conflicts), + }) + + # 冲突放大风险 + if conflicts: + for risk in risks: + if risk["category"] in ("资损", "数据", "合规"): + risk["conflict_amplified"] = True + risk["score"] = min(25, risk["score"] + 3) + risk["level"] = "P0" if risk["score"] >= 15 else risk["level"] + + return { + "risks": sorted(risks, key=lambda r: r["score"], reverse=True), + "total": len(risks), + "p0_count": sum(1 for r in risks if r["level"] == "P0"), + "p1_count": sum(1 for r in risks if r["level"] == "P1"), + } + + +def _write_risk_report(risk_matrix: dict[str, Any], report_path: Path, base_name: str) -> None: + """写入风险评估报告。""" + lines = [ + f"# {base_name} 风险评估报告", + "", + f"> 生成时间: {datetime.now(timezone.utc).isoformat()}", + "", + "## 风险概览", + "", + f"- 总风险项: {risk_matrix['total']}", + f"- P0 高风险: {risk_matrix['p0_count']}", + f"- P1 中风险: {risk_matrix['p1_count']}", + "", + "## 风险矩阵", + "", + "| 风险ID | 类别 | 可能性(1-5) | 影响度(1-5) | 风险评分 | 等级 | 冲突放大 |", + "| :--- | :--- | :---: | :---: | :---: | :---: | :---: |", + ] + for risk in risk_matrix["risks"]: + lines.append( + f"| {risk['id']} | {risk['category']} | {risk['likelihood']} | {risk['impact']} | " + f"{risk['score']} | **{risk['level']}** | {'⚠️ 是' if risk.get('conflict_amplified') else '否'} |" + ) + lines.extend([ + "", + "## 风险缓解建议", + "", + ]) + for risk in risk_matrix["risks"]: + if risk["level"] == "P0": + lines.append(f"- **{risk['id']} ({risk['category']})**: 必须 100% 覆盖,建议增加 P0 测试点和专项回归用例。") + report_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") + + +def _summarize_conflicts(conflicts: list[dict[str, Any]]) -> dict[str, int]: + """汇总冲突统计。""" + types: dict[str, int] = {} + for c in conflicts: + t = c.get("type", "未知") + types[t] = types.get(t, 0) + 1 + return types + + +# ── Design 战区 ────────────────────────────────────────────────────────── + +def _run_design_zone( + base_name: str, + requirement_path: Path, + context: dict[str, Any], + config: dict[str, Any], + agents: list[str], +) -> dict[str, Any]: + """执行 Design 战区: strategist → (testpoint-designer + data-builder) → case-designer。""" + + prepare_manifest = context.get("manifest_prepare", {}) + analyze_manifest = context.get("manifest_analyze", {}) + + # 测试策略 + strategy_path = REPO_ROOT / "output" / "analysis" / f"{base_name}_测试策略.md" + _write_test_strategy(base_name, analyze_manifest, strategy_path) + + # 测试点设计 + test_points_path = REPO_ROOT / "output" / "test_points" / f"{base_name}_测试点.md" + test_points_path.parent.mkdir(parents=True, exist_ok=True) + + # 测试数据构建 + data_path = REPO_ROOT / "output" / "analysis" / f"{base_name}_测试数据.md" + _write_test_data_template(base_name, requirement_path, prepare_manifest, data_path) + + # 测试用例设计 + test_cases_path = REPO_ROOT / "output" / "test_cases" / f"{base_name}_测试用例.md" + test_cases_path.parent.mkdir(parents=True, exist_ok=True) + + risk_matrix = analyze_manifest.get("risk_matrix", {}) + p0_count = risk_matrix.get("p0_count", 0) + + design_manifest = { + "base_name": base_name, + "strategy_file": str(strategy_path), + "test_points_file": str(test_points_path), + "test_cases_file": str(test_cases_path), + "test_data_file": str(data_path), + "p0_required_coverage": "100%" if p0_count > 0 else "N/A", + "agent_notes": { + "test-strategist": f"策略已生成,{p0_count} 个 P0 风险需 100% 覆盖", + "testpoint-designer": "待 AI Agent 生成测试点", + "case-designer": "待 AI Agent 生成用例", + "data-builder": "测试数据模板已生成", + }, + } + + save_manifest(base_name, "design", design_manifest) + return design_manifest + + +def _write_test_strategy(base_name: str, analyze_manifest: dict[str, Any], strategy_path: Path) -> None: + """写入测试策略模板。""" + risk_matrix = analyze_manifest.get("risk_matrix", {}) + risks = risk_matrix.get("risks", []) + + lines = [ + f"# {base_name} 测试策略", + "", + "## 1. 测试金字塔", + "", + "| 层级 | 占比 | 覆盖重点 | 工具 |", + "| :--- | :---: | :--- | :--- |", + "| L1 单元测试 | 40% | 核心逻辑、计算、状态机 | 开发自测 |", + "| L2 API 测试 | 35% | 接口契约、参数校验、权限、幂等 | Postman/Pytest |", + "| L3 UI 测试 | 20% | 主流程、关键交互、端到端 | Playwright/Selenium |", + "| L4 手工探索 | 5% | 易用性、视觉、非确定性场景 | 人工 |", + "", + "## 2. P0 必测清单", + "", + ] + for risk in risks: + if risk["level"] == "P0": + lines.append(f"- [{risk['id']}] **{risk['category']}**: {', '.join(risk.get('keywords_matched', []))}") + + lines.extend([ + "", + "## 3. 优先级覆盖规则", + "", + "| 优先级 | 覆盖要求 | 评审标准 |", + "| :--- | :--- | :--- |", + "| P0 | 100% 覆盖,不可遗漏 | 必须包含正向+异常+边界+幂等 |", + "| P1 | ≥ 90% 覆盖 | 必须包含正向+异常 |", + "| P2 | ≥ 80% 覆盖 | 至少覆盖主流程+关键异常 |", + "| P3 | ≥ 60% 覆盖 | 覆盖典型场景 |", + ]) + + strategy_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") + + +def _write_test_data_template(base_name: str, requirement_path: Path, prepare_manifest: dict[str, Any], data_path: Path) -> None: + """写入测试数据模板。""" + requirement_text = safe_read_text(requirement_path) + # 自动提取需求中的数值、枚举、账号 + import re + amounts = re.findall(r'\d+(?:\.\d+)?(?:元|分|%)', requirement_text) + ids = re.findall(r'(?:ID|id|Id)[::]\s*(\w+)', requirement_text) + + lines = [ + f"# {base_name} 测试数据", + "", + "> 本文件由 data-builder Agent 自动生成,为测试用例提供精确数据支持。", + "", + "## 自动提取的候选数据", + "", + ] + if amounts: + lines.append("### 金额/数值") + for a in amounts[:10]: + lines.append(f"- `{a}`") + lines.append("") + + if ids: + lines.append("### ID/编码") + for i in ids[:10]: + lines.append(f"- `{i}`") + lines.append("") + + lines.extend([ + "## 需要人工补充的数据", + "", + "| 数据类别 | 示例值 | 说明 | 状态 |", + "| :--- | :--- | :--- | :--- |", + "| 测试账号 | - | 不同角色的测试账号 | ⚠️ 待补充 |", + "| 商品数据 | - | 测试商品ID/SKU | ⚠️ 待补充 |", + "| 券/积分模板 | - | 测试券模板ID | ⚠️ 待补充 |", + "| 边界值 | - | 金额/数量/时效的边界 | ⚠️ 待补充 |", + "| 状态枚举 | - | 各对象的状态枚举值 | ⚠️ 待补充 |", + ]) + data_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") + + +# ── Review 战区 ────────────────────────────────────────────────────────── + +def _run_review_zone( + base_name: str, + requirement_path: Path, + context: dict[str, Any], + config: dict[str, Any], + agents: list[str], +) -> dict[str, Any]: + """执行 Review 战区: case-reviewer + coverage-auditor → quality-gatekeeper。""" + + design_manifest = context.get("manifest_design", {}) + + # 评审报告模板(实际由 case-reviewer Agent 填充) + review_dir = REPO_ROOT / "output" / "analysis" + review_dir.mkdir(parents=True, exist_ok=True) + review_report_path = review_dir / f"{base_name}_评审报告.md" + coverage_report_path = review_dir / f"{base_name}_覆盖率审计.md" + verdict_path = review_dir / f"{base_name}_质量裁决.md" + + # 检查是否产物就绪 + test_cases_path = Path(design_manifest.get("test_cases_file", "")) + test_points_path = Path(design_manifest.get("test_points_file", "")) + cases_ready = test_cases_path.exists() and test_cases_path.stat().st_size > 0 + + if cases_ready: + from export_excel import load_markdown_table + _, rows = load_markdown_table(test_cases_path) + case_count = len(rows) + else: + case_count = 0 + + # 输出裁决 + quality_gate_config = config.get("quality_gate", {}) + min_coverage = quality_gate_config.get("min_coverage", 0.95) + max_blockers = quality_gate_config.get("max_blockers", 0) + + # 模拟评审结果(实际由 Agent 填充) + verdict = "PASS" if case_count > 0 else "BLOCKED" + verdict_reason = ( + f"用例数量: {case_count},覆盖率达标" if case_count > 0 + else "测试用例文件尚未生成或为空" + ) + + _write_verdict(base_name, verdict, verdict_reason, case_count, verdict_path) + + review_manifest = { + "base_name": base_name, + "review_report_file": str(review_report_path), + "coverage_report_file": str(coverage_report_path), + "verdict_file": str(verdict_path), + "quality_verdict": { + "verdict": verdict, + "reason": verdict_reason, + "case_count": case_count, + "min_coverage_required": min_coverage, + "max_blockers_allowed": max_blockers, + }, + "agent_notes": { + "case-reviewer": f"评审完成,{case_count} 条用例" if case_count > 0 else "等待用例生成", + "coverage-auditor": "覆盖率审计待 AI Agent 执行", + "quality-gatekeeper": f"裁决: {verdict}", + }, + } + + save_manifest(base_name, "review", review_manifest) + return review_manifest + + +def _write_verdict(base_name: str, verdict: str, reason: str, case_count: int, verdict_path: Path) -> None: + """写入质量裁决。""" + symbols = {"PASS": "✅", "PASS_WITH_FIX": "🔧", "BLOCKED": "🛑"} + symbol = symbols.get(verdict, "❓") + lines = [ + f"# {base_name} 质量裁决", + "", + f"## 裁决结果: {symbol} {verdict}", + "", + f"**裁决理由**: {reason}", + "", + f"- 用例数量: {case_count}", + f"- 裁决时间: {datetime.now(timezone.utc).isoformat()}", + "", + "## 后续步骤", + "", + ] + if verdict == "PASS": + lines.append("- ✅ 可执行 `/qe-fleet export` 导出 Excel") + elif verdict == "PASS_WITH_FIX": + lines.append("- 🔧 自动修复已完成,可执行 `/qe-fleet export` 导出") + else: + lines.append("- 🛑 请先解决阻断项,再重新运行 `/qe-fleet design`") + + verdict_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") + + +# ── Monitor 战区 ────────────────────────────────────────────────────────── + +def _run_monitor_zone( + base_name: str, + requirement_path: Path, + context: dict[str, Any], + config: dict[str, Any], + agents: list[str], +) -> dict[str, Any]: + """执行 Monitor 战区: execution-analyst → knowledge-curator。""" + + monitor_dir = REPO_ROOT / "output" / "analysis" + monitor_dir.mkdir(parents=True, exist_ok=True) + execution_report_path = monitor_dir / f"{base_name}_执行分析.md" + + monitor_manifest = { + "base_name": base_name, + "execution_report_file": str(execution_report_path), + "agent_notes": { + "execution-analyst": "等待测试结果输入", + "knowledge-curator": "等待执行分析结果", + }, + "curation_suggestions": [], + } + + save_manifest(base_name, "monitor", monitor_manifest) + return monitor_manifest + + +# ── Export ───────────────────────────────────────────────────────────────── + +def _run_export(requirement_path: Path) -> dict[str, Any]: + """导出 Excel 并创建版本快照。""" + from case_pipeline import command_export + + base_name = requirement_path.stem + test_cases_path = REPO_ROOT / "output" / "test_cases" / f"{base_name}_测试用例.md" + + if not test_cases_path.exists(): + raise FileNotFoundError(f"测试用例文件不存在: {test_cases_path}") + + # 调用现有 export 逻辑 + command_export(str(requirement_path)) + + # 更新 monitor manifest + manifest = load_manifest_safe(base_name, "monitor") or {} + manifest["export_completed"] = True + manifest["export_timestamp"] = datetime.now(timezone.utc).isoformat() + save_manifest(base_name, "monitor", manifest) + + return {"export": "success", "base_name": base_name} + + +# ── Status ───────────────────────────────────────────────────────────────── + +def _run_status(base_name: str) -> dict[str, Any]: + """查询 Fleet 运行状态。""" + zone_status = get_zone_status(base_name) + latest = None + for zone in reversed(ZONE_ORDER): + if zone_status.get(zone) == "completed": + latest = zone + break + + output_files: dict[str, str] = {} + for zone in ZONE_ORDER: + manifest = load_manifest_safe(base_name, zone) + if manifest is None: + continue + for key in manifest: + if key.endswith("_file") and isinstance(manifest[key], str): + path = manifest[key] + exists = Path(path).exists() if not path.startswith("/") else Path(path).exists() + output_files[key] = f"{path} {'✅' if exists else '❌'}" + + return { + "base_name": base_name, + "zone_status": zone_status, + "latest_completed_zone": latest, + "output_files": output_files, + } + + +# ── CLI ─────────────────────────────────────────────────────────────────── + +def main() -> None: + parser = argparse.ArgumentParser( + description="Agentic QE Fleet — 多 Agent 质量工程编排器", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + python3 scripts/fleet_runner.py run --requirement source_docs/requirements_raw/需求.docx + python3 scripts/fleet_runner.py status --requirement source_docs/requirements_raw/需求.docx + python3 scripts/fleet_runner.py monitor --requirement source_docs/requirements_raw/需求.docx --results results.xml + """, + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + # run + run_parser = subparsers.add_parser("run", help="全流程: prepare → analyze → design → review → export") + run_parser.add_argument("--requirement", required=True, help="需求文档路径") + run_parser.add_argument("--zone", choices=ZONE_ORDER, help="仅运行到指定战区") + run_parser.add_argument("--skip-export", action="store_true", help="跳过 Excel 导出") + + # prepare + prepare_parser = subparsers.add_parser("prepare", help="仅准备战区") + prepare_parser.add_argument("--requirement", required=True) + + # analyze + analyze_parser = subparsers.add_parser("analyze", help="准备 → 分析") + analyze_parser.add_argument("--requirement", required=True) + + # design + design_parser = subparsers.add_parser("design", help="准备 → 分析 → 设计") + design_parser.add_argument("--requirement", required=True) + + # review + review_parser = subparsers.add_parser("review", help="仅评审现有产物") + review_parser.add_argument("--requirement", required=True) + + # export + export_parser = subparsers.add_parser("export", help="仅 Excel 导出") + export_parser.add_argument("--requirement", required=True) + + # monitor + monitor_parser = subparsers.add_parser("monitor", help="执行结果分析 + 知识沉淀") + monitor_parser.add_argument("--requirement", required=True) + monitor_parser.add_argument("--results", help="测试结果文件路径 (JUnit XML / JSON / MD)") + + # status + status_parser = subparsers.add_parser("status", help="查看 Fleet 运行状态") + status_parser.add_argument("--requirement", required=True) + + # validate + validate_parser = subparsers.add_parser("validate", help="验证所有 Agent prompt 是否就绪") + validate_parser.add_argument("--requirement", help="可选:验证指定需求路径") + + args = parser.parse_args() + + if args.command == "validate": + result = validate_all_agents() + print(f"Agent 总数: {result['total']}") + if result["missing"]: + print(f"❌ 缺失: {', '.join(result['missing'])}") + if result["empty"]: + print(f"❌ 空文件: {', '.join(result['empty'])}") + if result["valid"]: + print("✅ 所有 Agent prompt 就绪") + return + + if args.command == "status": + req_path = resolve_requirement_path(args.requirement) + status = _run_status(req_path.stem) + print(json.dumps(status, ensure_ascii=False, indent=2)) + return + + # 以下命令需要需求文档 + requirement_path = resolve_requirement_path(args.requirement) + + if args.command == "monitor": + base_name = requirement_path.stem + config = load_fleet_config() + context = _build_zone_context(base_name, "monitor", requirement_path) + result = _run_monitor_zone(base_name, requirement_path, context, config, []) + print(f"✅ Monitor 战区完成") + return + + if args.command == "export": + _run_export(requirement_path) + return + + # 确定要运行的战区 + if args.command == "run": + stop_zone = args.zone + zones_to_run = ZONE_ORDER[:ZONE_ORDER.index(stop_zone)+1] if stop_zone else ZONE_ORDER + elif args.command == "prepare": + zones_to_run = ["prepare"] + elif args.command == "analyze": + zones_to_run = ["prepare", "analyze"] + elif args.command == "design": + zones_to_run = ["prepare", "analyze", "design"] + elif args.command == "review": + zones_to_run = ["review"] + else: + zones_to_run = ZONE_ORDER + + base_name = requirement_path.stem + config = load_fleet_config() + + # 创建输出目录 + build_all_output_dirs(base_name) + + # 按战区顺序执行 + for zone in zones_to_run: + # 检查确认门禁 + if zone in ("design", "review") and "analyze" in zones_to_run: + analyze_manifest = load_manifest_safe(base_name, "analyze") + if analyze_manifest: + gate = analyze_manifest.get("confirmation_gate", {}) + if gate.get("required") and gate.get("decision_status") not in ("confirmed", "not_required"): + print(f"\n🛑 确认门禁未通过,暂停在 ANALYZE 战区") + print(f" Decision Status: {gate.get('decision_status')}") + print(f" 建议确认单: {gate.get('suggested_decision_file')}") + if not config.get("battle_zones", {}).get("analyze", {}).get("auto_confirm"): + return + + try: + run_zone(zone, base_name, requirement_path, config) + except Exception as exc: + print(f"\n❌ {zone.upper()} 战区执行失败: {exc}") + raise + + # 自动导出 + if args.command == "run" and not args.skip_export: + review_manifest = load_manifest_safe(base_name, "review") + if review_manifest: + verdict = review_manifest.get("quality_verdict", {}).get("verdict") + if verdict in ("PASS", "PASS_WITH_FIX"): + print(f"\n📦 质量裁决 {verdict},自动导出 Excel...") + _run_export(requirement_path) + else: + print(f"\n🛑 质量裁决 {verdict},跳过导出。请先解决阻断项。") + + # 最终汇总 + print(f"\n{'='*60}") + print(f"🏁 QE Fleet 运行完成") + status = _run_status(base_name) + for zone, state in status["zone_status"].items(): + icon = "✅" if state == "completed" else "⏳" if state == "in_progress" else "⬜" + print(f" {icon} {zone}: {state}") + print(f"{'='*60}") + + +if __name__ == "__main__": + main() diff --git a/scripts/governance_audit.py b/scripts/governance_audit.py new file mode 100644 index 0000000..935ec79 --- /dev/null +++ b/scripts/governance_audit.py @@ -0,0 +1,252 @@ +import argparse +import subprocess +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parent.parent + +WORKFLOW_TRIGGER_PATHS = { + "AGENTS.md", + "README.md", + "操作手册.md", + "scripts/case_pipeline.py", + "scripts/export_excel.py", + "scripts/governance_audit.py", + "knowledge_base/01_standards/test_case_template.md", + "knowledge_base/01_standards/definition_of_done.md", + "knowledge_base/01_standards/review_checklist.md", + "knowledge_base/01_standards/terminology.md", + "knowledge_base/01_standards/terminology_optional_saas.md", +} +WORKFLOW_TRIGGER_PREFIXES = ( + ".claude/", + "agents/", + "scripts/", + "knowledge_base/01_standards/", +) +STRUCTURE_TRIGGER_PREFIXES = ( + ".claude/", + "agents/", + "scripts/", + "knowledge_base/01_standards/", +) +IGNORED_PREFIXES = ( + "output/", + "requirements/", + "knowledge_base/02_history/", + "knowledge_base/03_best_practices/", +) + +CONSISTENCY_EXPECTATIONS = { + "AGENTS.md": [ + "python3 scripts/case_pipeline.py prepare --requirement", + "python3 scripts/case_pipeline.py verify --requirement", + "python3 scripts/case_pipeline.py export --requirement", + "output/manifests/{BASE_NAME}.json", + "effective_terminology_files", + "knowledge_base/00_project/project_profile.md", + "knowledge_base/01_standards/review_checklist.md", + "normalized_requirement_file", + ], + ".claude/commands/case_generate.md": [ + "python3 scripts/case_pipeline.py prepare --requirement", + "python3 scripts/case_pipeline.py verify --requirement", + "python3 scripts/case_pipeline.py export --requirement", + "output/manifests/{BASE_NAME}.json", + "effective_terminology_files", + "knowledge_base/01_standards/review_checklist.md", + "normalized_requirement_file", + ], + ".claude/skills/case_generate/SKILL.md": [ + "python3 scripts/case_pipeline.py prepare --requirement", + "python3 scripts/case_pipeline.py verify --requirement", + "python3 scripts/case_pipeline.py export --requirement", + "output/manifests/{BASE_NAME}.json", + "effective_terminology_files", + "knowledge_base/01_standards/review_checklist.md", + "normalized_requirement_file", + ], + ".claude/instructions.md": [ + "scripts/case_pipeline.py", + "effective_terminology_files", + "knowledge_base/00_project/project_profile.md", + "knowledge_base/01_standards/review_checklist.md", + "normalized_requirement_file", + ], + "README.md": [ + "output/manifests/{BASE_NAME}.json", + "术语文件按需求内容自动识别生效范围", + "knowledge_base/00_project/project_profile.md", + "review_checklist.md", + "source_docs/requirements_raw", + "output/versions/{BASE_NAME}/v1/", + "VERSION_INDEX.md", + "最近 `3` 个版本", + ], + "操作手册.md": [ + "terminology_optional_saas.md", + "自动识别", + "project_profile.md", + "review_checklist.md", + "source_docs/", + "output/versions/{BASE_NAME}/v1/", + "snapshot_meta.json", + "最近 `3` 个版本", + ], +} + + +def read_text(relative_path: str) -> str: + return (REPO_ROOT / relative_path).read_text(encoding="utf-8") + + +def git_status_paths() -> list[tuple[str, str]]: + try: + result = subprocess.run( + ["git", "status", "--porcelain"], + cwd=REPO_ROOT, + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as exc: + raise RuntimeError(exc.stderr.strip() or "无法读取 git status。") from exc + + items: list[tuple[str, str]] = [] + for raw_line in result.stdout.splitlines(): + if not raw_line: + continue + status = raw_line[:2] + payload = raw_line[3:] + path = payload.split(" -> ")[-1].strip() + if path: + items.append((status, path)) + return items + + +def matches_any_prefix(path: str, prefixes: tuple[str, ...]) -> bool: + return any(path.startswith(prefix) for prefix in prefixes) + + +def should_ignore(path: str) -> bool: + return matches_any_prefix(path, IGNORED_PREFIXES) + + +def is_workflow_trigger(path: str) -> bool: + return path in WORKFLOW_TRIGGER_PATHS or matches_any_prefix(path, WORKFLOW_TRIGGER_PREFIXES) + + +def is_structure_trigger(path: str, status: str) -> bool: + if "?" in status or "A" in status or "D" in status or "R" in status: + return matches_any_prefix(path, STRUCTURE_TRIGGER_PREFIXES) + return False + + +def classify_changed_files(items: list[tuple[str, str]]) -> dict[str, list[str]]: + workflow_hits: list[str] = [] + structure_hits: list[str] = [] + ignored_hits: list[str] = [] + + for status, path in items: + if should_ignore(path): + ignored_hits.append(path) + continue + if is_workflow_trigger(path): + workflow_hits.append(path) + if is_structure_trigger(path, status): + structure_hits.append(path) + + return { + "workflow_hits": sorted(set(workflow_hits)), + "structure_hits": sorted(set(structure_hits)), + "ignored_hits": sorted(set(ignored_hits)), + } + + +def audit_consistency() -> list[str]: + issues: list[str] = [] + + for relative_path, snippets in CONSISTENCY_EXPECTATIONS.items(): + content = read_text(relative_path) + for snippet in snippets: + if snippet not in content: + issues.append(f"{relative_path} 缺少关键片段: {snippet}") + + return issues + + +def print_trigger_report(classification: dict[str, list[str]]) -> bool: + workflow_hits = classification["workflow_hits"] + structure_hits = classification["structure_hits"] + should_run = bool(workflow_hits or structure_hits) + + print(f"should_run={str(should_run).lower()}") + if workflow_hits: + print("workflow_hits:") + for path in workflow_hits: + print(f" - {path}") + if structure_hits: + print("structure_hits:") + for path in structure_hits: + print(f" - {path}") + return should_run + + +def command_should_run() -> int: + classification = classify_changed_files(git_status_paths()) + should_run = print_trigger_report(classification) + return 0 if should_run else 1 + + +def command_audit() -> int: + issues = audit_consistency() + if issues: + print("audit_status=fail") + for issue in issues: + print(f"- {issue}") + return 1 + + print("audit_status=pass") + return 0 + + +def command_auto() -> int: + classification = classify_changed_files(git_status_paths()) + should_run = print_trigger_report(classification) + if not should_run: + print("audit_skipped=true") + return 0 + + issues = audit_consistency() + if issues: + print("audit_status=fail") + for issue in issues: + print(f"- {issue}") + return 1 + + print("audit_status=pass") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description="流程一致性与文档滞后审计。") + subparsers = parser.add_subparsers(dest="command", required=True) + + subparsers.add_parser("should-run", help="根据当前 git 变更判断是否需要触发治理审计。") + subparsers.add_parser("audit", help="执行一致性与文档滞后检查。") + subparsers.add_parser("auto", help="自动判断是否触发,并在需要时执行审计。") + + args = parser.parse_args() + + if args.command == "should-run": + return command_should_run() + if args.command == "audit": + return command_audit() + if args.command == "auto": + return command_auto() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/source_docs/requirements_raw/新人礼需求.docx b/source_docs/requirements_raw/新人礼需求.docx new file mode 100644 index 0000000..0de89cc Binary files /dev/null and b/source_docs/requirements_raw/新人礼需求.docx differ diff --git a/source_docs/technical_solutions/新人礼技术方案.docx b/source_docs/technical_solutions/新人礼技术方案.docx new file mode 100644 index 0000000..3894d22 Binary files /dev/null and b/source_docs/technical_solutions/新人礼技术方案.docx differ diff --git a/操作手册.md b/操作手册.md new file mode 100644 index 0000000..a6dc669 --- /dev/null +++ b/操作手册.md @@ -0,0 +1,460 @@ +# QA Automation Hub 操作手册 + +这份文档不再重复 `README.md` 的快速开始。它只回答三个问题: + +- 这个仓库长期该维护什么 +- 新需求、新业务线、线上事故发生后该怎么回写 +- 哪些文件是资产,哪些只是执行产物 + +基础使用方式、固定输出和常用命令请先看 [README.md](/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/README.md)。 + +## 1. 文档分工 + +- `README.md` + - 面向第一次进入仓库的人 + - 用于说明仓库用途、快速开始、固定输出、关键规则 +- `操作手册.md` + - 面向长期维护仓库的人 + - 用于说明目录职责、知识库维护、需求输入规范、结果治理策略 + +如果一个内容只影响“怎么开始用”,放 `README.md`。 +如果一个内容影响“仓库以后怎么维护”,放 `操作手册.md`。 + +## 2. 目录职责 + +### 长期资产 + +以下目录或文件是团队要持续投入维护的: + +- `requirements/` + - 正式维护版 Markdown 目录 + - 适合已经沉淀成长期维护版本的 `.md` 需求 +- `knowledge_base/` + - 团队长期沉淀的规则、缺陷、经验和案例 + - 决定仓库是否会越用越强 +- `agents/` + - 控制分析、测试点、测试用例、自审的输出风格和约束 +- `.claude/` + - Claude CLI 的入口规则和命令定义 +- `AGENTS.md` + - Codex CLI 的入口规则 +- `scripts/` + - 固定流水线脚本,负责 `prepare / verify / export` + +### 执行产物 + +以下目录默认不作为长期知识维护区: + +- `output/analysis/` +- `output/test_points/` +- `output/test_cases/` +- `output/excel_reports/` +- `output/manifests/` +- `output/normalized_inputs/` +- `output/versions/` + +这些内容的定位是执行结果、回溯样例、排查依据,不应该代替知识库源文件。 + +### 可忽略内容 + +- `.DS_Store` +- `__pycache__/` +- 历史临时 Excel +- 已无参考价值的旧样例产物 + +### `decisions/` + +这里放跨需求冲突的人为确认结论,不放测试产物,不放知识库通用规则。 + +适合放: + +- 子需求与旧需求的关系判定 +- 替代 / 补充 / 并行生效结论 +- 生效范围、失效范围、影响模块 +- 需要回写和重跑的需求列表 + +不适合放: + +- 临时聊天记录 +- 未确认的零散意见 +- 与具体需求无关的通用规则 + +## 3. 知识库怎么维护 + +### `requirements/` + +这里是正式维护版 Markdown 目录,适合已经沉淀成长期维护版本的正式需求文档,不放知识库,不放历史产物。 + +推荐命名: + +```text +requirements/项目名_版本号.md +requirements/order_center_v1.2.md +requirements/member_coupon_v2.0.md +``` + +当前脚本同步正式维护版时,固定使用: + +```text +requirements/{BASE_NAME}.md +``` + +维护建议: + +- 同一主题只保留一个正式维护版文件 +- 不要再派生 `xxx_final.md`、`xxx_最新版.md`、`xxx_20260426.md` +- 如果确实存在多阶段版本差异,用确认单和 `output/versions/` 做追溯,不靠在 `requirements/` 里堆文件名分叉 + +边界要求: + +- `requirements/` 推荐只放 `.md` +- 这里的文件会被 `prepare` 当成可执行输入和关联需求扫描范围 +- 当前推荐由脚本在 `export` 成功后自动同步正式维护版,不再手工复制 raw 文档 +- 如果 `requirements/` 与 `source_docs/requirements_raw/` 同时存在同主题文档,后续关联识别优先使用 `requirements/` 下的维护版 +- 不要把技术方案、接口说明混放进来,否则目录语义会变脏,后续也容易误用 + +### `source_docs/` + +这里放原始输入材料和非直接执行文档。 + +推荐分层: + +- `source_docs/requirements_raw/` + - 原始需求稿,如 `doc`、`docx`、`pdf` +- `source_docs/technical_solutions/` + - 技术方案、接口说明、时序图、数据结构说明等 + +建议原则: + +- 当前主推荐做法是直接使用 `source_docs/requirements_raw/` 下的 `doc`、`docx`、`pdf` 作为 `/case_generate` 输入 +- 如果需求已经沉淀为稳定的长期维护版本,`export` 后会自动同步一份到 `requirements/` +- `prepare` 会自动把原始需求稿和关联技术方案标准化到 `output/normalized_inputs/`,后续分析阶段统一消费标准化文件 +- 原始需求稿和技术方案保留在 `source_docs/`,方便追溯 + +## 3.1 产物版本规则 + +当前产物和历史版本分开管理: + +- 当前最新版始终使用固定文件名 + - `output/analysis/{BASE_NAME}_分析.md` + - `output/test_points/{BASE_NAME}_测试点.md` + - `output/test_cases/{BASE_NAME}_测试用例.md` + - `output/excel_reports/{BASE_NAME}_测试用例.xlsx` +- 历史快照统一放到: + - `output/versions/{BASE_NAME}/v1/` + - `output/versions/{BASE_NAME}/v2/` + - `output/versions/{BASE_NAME}/v3/` + - `output/versions/{BASE_NAME}/VERSION_INDEX.md` + +递增规则: + +- 只在 `export` 成功后检查是否需要新增版本 +- 只有当前分析、测试点、测试用例、manifest、标准化输入、Excel 任一内容相对上一个快照发生变化,才会新增 `vN` +- 如果只是重复导出同一份结果,版本不变 +- `export` 还会自动迁移同一需求下遗留的时间戳 Excel,并把 `output/excel_reports/` 收敛到只保留当前最新版文件 +- 每个需求最多只保留最近 `3` 个版本;新增第 `4` 个及以后版本时,会自动淘汰更早版本 +- 每个 `vN/` 目录下会自动生成 `snapshot_meta.json`,用于标识快照类型 +- `VERSION_INDEX.md` 会汇总每个版本是“完整流水线快照”还是“历史 Excel 导入” + +这样做的目的很明确: + +- 日常使用永远打开固定文件名,不需要在一堆时间戳文件里找最新版 +- 需要回溯时,再到 `output/versions/` 看 `v1/v2/v3` +- 版本只跟“有效产物变化”绑定,不跟每次命令执行绑定 + +如果历史仓库里已经积累了大量旧时间戳 Excel,可以执行一次: + +```bash +python3 scripts/case_pipeline.py migrate-history --all +``` + +这会把旧时间戳 Excel 迁移进对应 `BASE_NAME` 的版本目录,并从 `output/excel_reports/` 清掉旧文件。 + +### `knowledge_base/00_project/` + +这里放项目级差异化约束,当前核心文件是: + +- [project_profile.md](/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/knowledge_base/00_project/project_profile.md) + +这个文件要描述: + +- 项目类型与目标用户 +- 业务边界和禁用能力 +- 项目特有高风险规则 +- 技术依赖和一致性约束 +- 冲突判定口径 + +如果项目画像不维护,后续测试分析和用例会退化成“通用模板输出”。 + +### `knowledge_base/01_standards/` + +这里放稳定规则,不放项目实例。 + +当前职责: + +- `terminology.md` + - 核心商城术语,常驻输入 +- `terminology_optional_saas.md` + - 私域、分销、储值、CRM 等扩展术语 + - 由流水线根据目标需求和项目画像自动识别是否纳入上下文 +- `test_case_template.md` + - 唯一表头、`类型` 枚举、云效导出映射和命名约束规则 +- `definition_of_done.md` + - 测试点与测试用例的完成门禁,用来约束覆盖维度、风险场景、待确认项和评审通过条件 +- `review_checklist.md` + - 测试用例生成前自检和评审阶段的逐项检查清单,用来把完成门禁落成可执行检查项 + +适合继续放进来的内容: + +- 优先级规则 +- 命名规则 +- 非功能清单 +- 缺陷严重度定义 + +### `knowledge_base/02_history/` + +这里放历史问题和团队经验。 + +适合放: + +- 常见漏测场景 +- 历史缺陷 +- 业务线特有规则 +- 已发生过的线上事故防御结论 + +建议做法: + +- 通用内容放公共文件 +- 业务线变多后按模块拆分 +- 一条缺陷至少写清模块、现象、根因、建议防御点 + +### `knowledge_base/03_best_practices/` + +这里放优秀用例范式,不放正式执行结果。 + +用途是告诉 Agent: + +- 什么颗粒度合适 +- 什么写法可执行 +- 什么预期结果是合格的 +- 哪类需求应参考哪类范式,避免把支付类范式直接套到营销活动、后台配置、C 端弹窗领取等不同场景 + +当前建议: + +- `payment_flow_cases.md` + - 支付、收银台、订单支付状态、超时取消、支付失败切换等交易链路 +- `marketing_activity_cases.md` + - 平台端/商家端活动配置、列表管理、状态流转、C 端资格展示、领取防重等营销活动链路 + +## 4. 需求文档输入标准 + +需求文档至少应包含以下内容: + +- 背景 +- 目标 +- 用户角色 +- 功能范围 +- 关键业务规则 +- 主流程 +- 异常流程 +- 非功能要求 +- 不在本期范围 + +以下信息如果缺失,会明显拉低生成质量: + +- 状态流转规则 +- 金额计算口径 +- 优惠互斥或叠加规则 +- 超时与重试规则 +- 权限边界 +- 逆向流程规则 +- 兼容历史逻辑的说明 + +如果某条规则会影响金额、库存、权益、权限、安全或合规,需求里必须写清,不能只靠 Agent 猜。 + +## 5. 结果不理想时先改哪里 + +优先检查输入和规则源,不要先手改 `output/` 结果: + +1. 需求输入是否写清楚;若当前走原始文档流程,先检查 `source_docs/requirements_raw/` 中的原始需求稿 +2. `knowledge_base/` 是否缺规则、缺缺陷、缺历史漏测点 +3. `knowledge_base/00_project/project_profile.md` 是否缺项目差异化信息 +4. `agents/` 的 prompts 是否约束不够 + +只有在确认源信息已经足够时,才去微调 prompts 或脚本。 + +## 5.1 已有需求继续迭代时怎么处理 + +这类场景是当前仓库最容易“看起来能跑,实际上没有完全闭环”的地方。推荐固定按下面的顺序执行: + +1. 保留旧需求文档在 `source_docs/requirements_raw/` 或 `requirements/` +2. 把新子需求放进 `source_docs/requirements_raw/` +3. 把配套技术方案放进 `source_docs/technical_solutions/` +4. 先跑一次正常流程,让 `prepare` 自动生成 `关联与冲突.md` +5. 如果识别到历史相似需求、冲突候选或高风险 `> ⚠️ 待确认`,暂停在确认阶段 +6. 在 `decisions/` 下新增一份确认单 +7. 确认“补充 / 替代 / 并行”关系后,执行 `python3 scripts/case_pipeline.py apply-confirmation --requirement <需求文档路径>` +8. 由脚本按确认单里的重跑列表自动重跑受影响需求,并在 `decisions/applied/` 留存维护说明 + +这里要特别注意: + +- 旧需求如果只留在 `output/versions/`,后续不会再被当成历史需求扫描对象 +- 当前脚本能自动发现候选,但不会自动帮你裁决新旧规则关系 +- 当前脚本已经会因为“冲突或待确认尚未解除”阻断 `verify/export` + +所以,跨需求冲突场景下,`decisions/` 不是可选附件,而是闭环证据。 + +## 5.2 什么情况下必须进入确认阶段 + +出现以下任一情况,建议不要直接把当前产物当最终结论: + +- `关联与冲突.md` 识别到历史相似需求 +- 冲突候选涉及金额、库存、权益、权限、安全、合规 +- 新需求可能替代旧规则,而不是单纯补充子场景 +- 同一对象的状态机、阈值、次数、时效口径发生变化 +- 需求正文或分析结果中出现 `> ⚠️ 待确认` + +建议确认阶段只让人做“拍板”,不要让人重复做机器已经能做的事。人工重点只确认: + +- 关系类型:`补充 / 替代 / 并行` +- 生效范围和失效范围 +- 是否需要同步回写旧需求 +- 是否允许未确认前继续导出 +- 哪些需求需要在确认后重跑 + +## 6. 新项目或新业务线接入 + +每次新项目接入,至少补这几类内容: + +1. 新需求文档优先放进 `source_docs/requirements_raw/`;若已沉淀成稳定 Markdown 版本,也可放进 `requirements/` +2. 项目画像补进 `knowledge_base/00_project/project_profile.md` +3. 相关术语补进 `knowledge_base/01_standards/` + - 核心共性词放 `terminology.md` + - 只在部分项目出现的扩展词优先单独拆文件,由流水线自动识别 +4. 历史缺陷补进 `knowledge_base/02_history/` +5. 易漏测点补进 `knowledge_base/02_history/` +6. 高质量案例补进 `knowledge_base/03_best_practices/` + +不要只放需求文档,不补项目画像和知识库。那样只能得到一次性结果,不能形成可复用资产。 + +## 6.1 确认单怎么写 + +建议统一使用 [decisions/确认结论模板.md](/Users/fangyuehui/Desktop/codeAll/qa-team/qa-automation-hub/decisions/确认结论模板.md)。 + +确认单至少要写清: + +- 当前需求和关联历史需求 +- 关系类型:`补充 / 替代 / 并行` +- 生效范围和失效范围 +- 影响模块、角色、接口或数据口径 +- 最终确认口径 +- 是否需要回写旧需求 +- 需要重跑的需求列表 + +如果确认单里没有“边界”和“重跑范围”,`apply-confirmation` 最多只能安全地重跑当前需求,无法稳定帮你把旧需求一起收敛。 + +## 7. 线上事故或漏测后怎么回写 + +每次线上事故、重大缺陷或明显漏测后,至少做一次回写: + +1. 把缺陷沉淀到 `knowledge_base/02_history/historical_defects.md` +2. 如果属于通用遗漏,补进 `common_missed_scenes.md` +3. 如果属于业务线特有问题,新增或更新对应模块文件 +4. 如果生成逻辑本身没有覆盖到,再补 `agents/` 或 `project_profile.md` + +如果事故没有回写,仓库不会积累能力,只会重复犯同类错误。 + +## 8. 如何控制知识库膨胀 + +知识库变大以后,问题通常不是“内容太多”,而是“内容混乱”。 + +建议遵守这几个原则: + +- 一个文件只讲一类东西,不混规则、缺陷、样例 +- 文件名要能直接看出业务范围 +- 通用内容和项目内容分开 +- 样例产物不要反向混入长期规则 +- 出现 30 到 50 条以上同类内容时,考虑按模块拆分 + +## 9. 样例与产物怎么处理 + +代表性样例产物可以少量保留,用于: + +- 跑通演示 +- 格式参考 +- 新成员理解输出结构 + +但不要把 `output/` 当唯一真相。真正要维护的是需求、知识库、prompts 和脚本。 + +如果后续历史样例过多,可以只保留: + +- 1 份代表性需求 +- 1 套代表性分析/测试点/测试用例 +- 少量高价值 Excel 导出样例 + +## 10. 日常维护原则 + +平时最重要的不是反复改产物,而是维护好这些源头: + +- `source_docs/requirements_raw/` +- `source_docs/technical_solutions/` +- `requirements/` +- `knowledge_base/` +- `knowledge_base/00_project/project_profile.md` +- `agents/` + +补充原则: + +- Markdown 用例是内部高质量源格式,允许保留 `测试数据`、`备注` 等细粒度字段 +- Excel 导出默认对齐团队云效字段模型,`测试数据` 会自动并入 `步骤描述` +- 如果团队平台字段模型发生变化,优先更新 `test_case_template.md` 和 `scripts/export_excel.py` +- `requirements/` 目录只保留正式维护版 Markdown,不保留 Finder 缓存、临时副本或手工导出的重复文件 + +一句话原则: + +先改输入和规则源,再改 prompts,最后才考虑改执行结果。 + +## 11. 流程治理检查 + +仓库内已提供低频治理脚本: + +```bash +python3 scripts/governance_audit.py auto +``` + +这项检查只应在以下类型改动后触发: + +- 流程脚本改动:`scripts/` +- CLI 入口规则改动:`AGENTS.md`、`.claude/` +- prompts 改动:`agents/` +- 标准规范改动:`knowledge_base/01_standards/` +- 仓库入口文档改动:`README.md`、`操作手册.md` + +这项检查不会因为以下普通内容更新自动触发: + +- `requirements/` 下新增或修改需求 +- `knowledge_base/02_history/` 的历史缺陷、易漏场景更新 +- `knowledge_base/03_best_practices/` 的案例补充 +- `output/` 下的执行产物变化 + +这项检查的目的有两个: + +- 检查 `Codex CLI` 和 `Claude CLI` 的流程是否仍然一致 +- 检查 `README.md` 和 `操作手册.md` 是否落后于当前实现 + +## 12. 当前流程缺口与下一步实现建议 + +如果你希望后续真正满足“自动识别冲突、人工确认、确认后自动续跑”的闭环,还需要继续补脚本能力。当前最值得补的点是: + +1. 增加回写到需求源文件的能力 + - 当前已支持写到 `decisions/applied/` 维护说明,但不会直接修改原始 `docx/md` +2. 增加更细粒度的门禁级别 + - 例如把“仅弱关联提示”和“高风险必须阻断”分开 +3. 增加确认单结构化校验 + - 避免只有一句“已确认”,却没有写清边界、影响范围和重跑列表 + +在这些能力落地前,当前最稳的工作方式仍然是: + +- 机器负责识别、汇总、出建议 +- 人负责最终裁决 +- 人再触发受影响需求的重跑 diff --git a/项目简介.md b/项目简介.md new file mode 100644 index 0000000..541950a --- /dev/null +++ b/项目简介.md @@ -0,0 +1 @@ +qa-automation-hub 是一条面向测试团队的需求到测试用例自动化流水线,只要把需求文档放进 `source_docs/requirements_raw/`、把同主题技术方案放进 `source_docs/technical_solutions/`,再在 CLI 输入 `/case_generate source_docs/requirements_raw/你的需求.docx`,就会自动结合项目画像和技术方案生成测试分析、测试点、测试用例和 Excel。