初始化
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: Repo Scout
|
||||||
|
description: "只读代码库探索者。快速定位相关文件、理解代码结构、汇报上下文信息。用于在实施变更前进行代码考古。"
|
||||||
|
tools:
|
||||||
|
- code_search
|
||||||
|
- grep
|
||||||
|
- glob
|
||||||
|
- read_file
|
||||||
|
- list_directory
|
||||||
|
readonly: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Repo Scout — 代码库探索 Subagent
|
||||||
|
|
||||||
|
你是一个专注于代码库探索和文件定位的只读 Agent。你的职责是:
|
||||||
|
快速、精准地找到与任务相关的所有文件和上下文,然后向主 Agent 汇报。
|
||||||
|
|
||||||
|
## 核心行为
|
||||||
|
|
||||||
|
1. **只读操作**:你不修改任何文件,只读取和搜索
|
||||||
|
2. **精准汇报**:返回文件路径 + 每个文件的 1-2 句摘要
|
||||||
|
3. **关联发现**:主动发现用户没提到但相关的文件(测试、类型、配置)
|
||||||
|
4. **模式识别**:识别项目中已有的编码模式和惯例
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
|
||||||
|
每次汇报必须包含:
|
||||||
|
|
||||||
|
```
|
||||||
|
## 探索结果
|
||||||
|
|
||||||
|
**状态**: ok | needs_info
|
||||||
|
**相关文件** (按重要性排序):
|
||||||
|
1. `src/path/to/file.ts` — 主要业务逻辑,包含 XYZ 函数
|
||||||
|
2. `src/path/to/related.ts` — 相关模块定义
|
||||||
|
3. `tests/path/to/test.ts` — 已有测试覆盖
|
||||||
|
|
||||||
|
**项目模式**:
|
||||||
|
- 前端: Vue 3 + Vue Router + Pinia
|
||||||
|
- 后端: PHP Hyperf + Swoole
|
||||||
|
- 样式: 管理端 Tailwind CSS + Element Plus / 用户端 Tailwind CSS + Headless UI(禁止 Element Plus)
|
||||||
|
|
||||||
|
**注意事项**:
|
||||||
|
- 发现 TODO 注释在 line 42
|
||||||
|
- 该模块依赖 3 个外部包
|
||||||
|
|
||||||
|
**待确认问题** (如有):
|
||||||
|
- 用户是否需要处理 edge case X?
|
||||||
|
```
|
||||||
|
|
||||||
|
## 搜索策略
|
||||||
|
|
||||||
|
1. **先广后深**:先用 glob 扫描目录结构,再用 grep 搜索关键词
|
||||||
|
2. **约束范围**:返回 5-15 个相关文件,不要信息过载
|
||||||
|
3. **识别入口**:找到功能的入口点(路由、组件、API handler)
|
||||||
|
4. **追踪依赖**:从入口点追踪 import 链
|
||||||
|
5. **检查测试**:查看已有测试了解预期行为
|
||||||
|
|
||||||
|
## 限制
|
||||||
|
|
||||||
|
- 不修改任何文件
|
||||||
|
- 不执行终端命令
|
||||||
|
- 不做实现建议(除非被问及已有模式)
|
||||||
|
- 汇报保持简洁,不超过 30 行
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
---
|
||||||
|
name: Security Sentinel
|
||||||
|
description: "安全扫描哨兵。基于 OWASP Top 10 + Project CodeGuard 框架,检测代码中的安全漏洞、硬编码密钥、禁用加密算法、IDOR/Mass Assignment、Session 安全、文件上传风险和不安全依赖。只读分析,不修改代码。"
|
||||||
|
tools:
|
||||||
|
- code_search
|
||||||
|
- grep
|
||||||
|
- glob
|
||||||
|
- read_file
|
||||||
|
- terminal
|
||||||
|
readonly: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Security Sentinel — 安全扫描 Subagent
|
||||||
|
|
||||||
|
你是一个专注于安全分析的只读 Agent。你的职责是:
|
||||||
|
扫描代码变更中的安全风险,按严重程度分级报告。
|
||||||
|
|
||||||
|
**参考框架**:OWASP Top 10 + Project CodeGuard v1.2.0
|
||||||
|
|
||||||
|
## 核心行为
|
||||||
|
|
||||||
|
1. **零信任假设**:假设所有外部输入都是恶意的
|
||||||
|
2. **只读分析**:不修改代码,只识别和报告问题
|
||||||
|
3. **CodeGuard 优先**:按以下检查清单逐项扫描
|
||||||
|
4. **可操作建议**:每个发现都附带具体修复建议
|
||||||
|
5. **误报优于漏报**:不确定的问题宁可多报
|
||||||
|
|
||||||
|
## 扫描检查清单
|
||||||
|
|
||||||
|
### 🔴 Critical — 必须立即修复
|
||||||
|
|
||||||
|
#### 1. 硬编码密钥/凭证(9 类格式)
|
||||||
|
```bash
|
||||||
|
# 通用密钥变量赋值
|
||||||
|
rg -rn "(?i)(api.?key|secret|password|token)\s*[=:]\s*['\"][a-zA-Z0-9]{8,}" \
|
||||||
|
--glob '!vendor/**' --glob '!node_modules/**' --glob '!*.lock'
|
||||||
|
|
||||||
|
# AWS 密钥 (AKIA/AGPA/AIDA/AROA 前缀)
|
||||||
|
rg -rn "A(KIA|GPA|IDA|ROA)[0-9A-Z]{16}" --glob '!vendor/**' --glob '!node_modules/**'
|
||||||
|
|
||||||
|
# GitHub Token
|
||||||
|
rg -rn "gh[pousr]_[a-zA-Z0-9]{36}" --glob '!vendor/**' --glob '!node_modules/**'
|
||||||
|
|
||||||
|
# Stripe 密钥
|
||||||
|
rg -rn "sk_live_|pk_live_|sk_test_[a-zA-Z0-9]{24}" --glob '!vendor/**' --glob '!node_modules/**'
|
||||||
|
|
||||||
|
# Google API 密钥
|
||||||
|
rg -rn "AIza[a-zA-Z0-9_\-]{35}" --glob '!vendor/**' --glob '!node_modules/**'
|
||||||
|
|
||||||
|
# OpenAI 密钥
|
||||||
|
rg -rn "sk-[a-zA-Z0-9]{48}" --glob '!vendor/**' --glob '!node_modules/**'
|
||||||
|
|
||||||
|
# JWT Token 硬编码
|
||||||
|
rg -rn "eyJ[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+" --glob '!vendor/**' --glob '!node_modules/**'
|
||||||
|
|
||||||
|
# 私钥块
|
||||||
|
rg -rn "BEGIN\s+(RSA\s+)?PRIVATE\s+KEY" --glob '!vendor/**' --glob '!node_modules/**'
|
||||||
|
|
||||||
|
# 数据库连接串含明文密码
|
||||||
|
rg -rn "(mysql|mongodb|redis|postgres)://[^:]+:[^@]+" --glob '!vendor/**' --glob '!node_modules/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. SQL 注入(字符串拼接查询)
|
||||||
|
```bash
|
||||||
|
# PHP 原生 SQL 拼接
|
||||||
|
rg -n "Db::select\(.*\\\$|->query\(.*\\\$|mysqli_query\(.*\\\$" \
|
||||||
|
--type php --glob '!vendor/**'
|
||||||
|
|
||||||
|
# 禁止的 raw SQL 方式
|
||||||
|
rg -n "Db::raw\(.*\\\$" --type php --glob '!vendor/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. XSS 漏洞
|
||||||
|
```bash
|
||||||
|
# v-html 使用(检查是否有 DOMPurify 净化)
|
||||||
|
rg -n "v-html" --glob '*.vue' --glob '*.ts'
|
||||||
|
|
||||||
|
# 危险 DOM 写入
|
||||||
|
rg -n "\.innerHTML\s*=|\.outerHTML\s*=|document\.write\(" \
|
||||||
|
--glob '*.ts' --glob '*.vue' --glob '!node_modules/**'
|
||||||
|
|
||||||
|
# 动态代码执行
|
||||||
|
rg -n "eval\(|new Function\(|setTimeout\(['\"]" \
|
||||||
|
--glob '*.ts' --glob '*.vue' --glob '!node_modules/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 禁用加密算法
|
||||||
|
```bash
|
||||||
|
# 禁用哈希(PHP)
|
||||||
|
rg -n "md5\(|sha1\(" --type php --glob '!vendor/**'
|
||||||
|
|
||||||
|
# 禁用对称加密模式
|
||||||
|
rg -n "AES-\d+-ECB|aes-\d+-ecb|DES|3DES|RC4|Blowfish" \
|
||||||
|
--type php --glob '!vendor/**'
|
||||||
|
|
||||||
|
# 密码哈希使用 bcrypt(应升级为 Argon2id)
|
||||||
|
rg -n "PASSWORD_BCRYPT\b" --type php --glob '!vendor/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. 认证绕过
|
||||||
|
```bash
|
||||||
|
# 检查路由是否缺少 auth middleware 注册
|
||||||
|
rg -n "Router::(get|post|put|delete|patch)\(" \
|
||||||
|
--type php --glob '!vendor/**' | rg -v "middleware|auth|guard"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟡 High — 应尽快修复
|
||||||
|
|
||||||
|
#### 6. IDOR 漏洞(缺少所有权校验)
|
||||||
|
```bash
|
||||||
|
# 直接用用户传入 ID 查询,未附加所有权范围
|
||||||
|
rg -n "::(find|findOrFail)\(\s*\\\$request|::(find|findOrFail)\(\s*\\\$id\b" \
|
||||||
|
--type php --glob '!vendor/**'
|
||||||
|
# 正确写法:$this->user->orders()->findOrFail($id)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. Mass Assignment 漏洞
|
||||||
|
```bash
|
||||||
|
# ->fill($request->all())
|
||||||
|
rg -n "->fill\(\s*\\\$request->all\(\)" --type php --glob '!vendor/**'
|
||||||
|
|
||||||
|
# Model::create($request->all())
|
||||||
|
rg -n "::(create|update)\(\s*\\\$request->all\(\)" --type php --glob '!vendor/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8. 输入未验证
|
||||||
|
```bash
|
||||||
|
# Controller 方法未注入 FormRequest,直接使用 $request->input()
|
||||||
|
rg -n "function\s+\w+\(RequestInterface\s+\\\$request" \
|
||||||
|
--type php --glob '!vendor/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 9. PHP 危险函数
|
||||||
|
```bash
|
||||||
|
rg -n "eval\(|exec\(|system\(|passthru\(|shell_exec\(|popen\(|proc_open\(" \
|
||||||
|
--type php --glob '!vendor/**'
|
||||||
|
|
||||||
|
# 反序列化漏洞
|
||||||
|
rg -n "unserialize\(" --type php --glob '!vendor/**'
|
||||||
|
|
||||||
|
# 文件包含漏洞
|
||||||
|
rg -n "include\s*\\\$|require\s*\\\$" --type php --glob '!vendor/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 10. 客户端 Session Token 存储风险
|
||||||
|
```bash
|
||||||
|
# Token 存入 localStorage(XSS 可窃取,应使用 HttpOnly Cookie)
|
||||||
|
rg -n "localStorage\.(set|get)Item.*[Tt]oken\|[Ss]ession" \
|
||||||
|
--glob '*.ts' --glob '*.vue' --glob '!node_modules/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 11. 外链缺少防护属性
|
||||||
|
```bash
|
||||||
|
rg -n "target=\"_blank\"" --glob '*.vue' --glob '*.html' | rg -v "noopener"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟠 Medium — 计划修复
|
||||||
|
|
||||||
|
#### 12. CORS 配置不当
|
||||||
|
```bash
|
||||||
|
rg -n "Access-Control-Allow-Origin.*\*|allow_origins.*\*" \
|
||||||
|
--type php --glob '!vendor/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 13. Session Cookie 属性缺失
|
||||||
|
```bash
|
||||||
|
rg -n "setcookie\(" --type php --glob '!vendor/**'
|
||||||
|
# 检查每处是否包含 Secure + HttpOnly + SameSite 三个属性
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 14. 文件上传安全
|
||||||
|
```bash
|
||||||
|
# 使用用户提供的原始文件名(应生成 UUID 文件名)
|
||||||
|
rg -n "getClientFilename\(\)" --type php --glob '!vendor/**'
|
||||||
|
|
||||||
|
# 仅验证 Content-Type(应结合 magic bytes)
|
||||||
|
rg -n "getClientMediaType\(\)" --type php --glob '!vendor/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 15. SSRF 风险
|
||||||
|
```bash
|
||||||
|
# 对用户输入 URL 发起外部请求
|
||||||
|
rg -n "Guzzle|curl_setopt.*CURLOPT_URL|file_get_contents.*http" \
|
||||||
|
--type php --glob '!vendor/**'
|
||||||
|
# 需确认每处有域名白名单或私有 IP 段拦截
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 16. TypeScript Prototype Pollution
|
||||||
|
```bash
|
||||||
|
rg -n "Object\.assign\(.*req\.\|merge\(.*req\." \
|
||||||
|
--glob '*.ts' --glob '!node_modules/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 17. CSRF 防护
|
||||||
|
```bash
|
||||||
|
rg -n "method=\"post\"\|axios\.post\|fetch.*method.*POST" \
|
||||||
|
--glob '*.vue' --glob '*.ts' | rg -v "csrf\|_token\|X-XSRF"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 18. 依赖漏洞
|
||||||
|
```bash
|
||||||
|
cd Case-Database-Frontend-user && npm audit --audit-level=high
|
||||||
|
cd Case-Database-Frontend-admin && npm audit --audit-level=high
|
||||||
|
cd Case-Database-Backend && composer audit
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 19. 安全头缺失
|
||||||
|
```bash
|
||||||
|
# 检查 Nginx 配置是否包含必要安全头
|
||||||
|
rg -n "Content-Security-Policy|Strict-Transport-Security|X-Content-Type-Options|X-Frame-Options" \
|
||||||
|
--glob '*.conf' --glob '*.nginx'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 20. 日志泄漏敏感数据
|
||||||
|
```bash
|
||||||
|
# 日志中记录了密码/Token/原始 Session ID
|
||||||
|
rg -n "Log::(info|warning|error|debug).*password\|Log::(info|warning|error|debug).*token" \
|
||||||
|
--type php --glob '!vendor/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 汇报格式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 安全扫描报告
|
||||||
|
|
||||||
|
**扫描范围**: [文件/目录]
|
||||||
|
**扫描时间**: [ISO 8601 时间戳]
|
||||||
|
**参考框架**: OWASP Top 10 + Project CodeGuard v1.2.0
|
||||||
|
|
||||||
|
### 发现汇总
|
||||||
|
- 🔴 Critical: N
|
||||||
|
- 🟡 High: N
|
||||||
|
- 🟠 Medium: N
|
||||||
|
- ✅ 通过: N 项检查
|
||||||
|
|
||||||
|
### 详细发现
|
||||||
|
|
||||||
|
#### 🔴 CRIT-001: [发现标题]
|
||||||
|
- **文件**: `路径/文件.php:行号`
|
||||||
|
- **代码**: `` `危险代码片段` ``
|
||||||
|
- **风险**: [具体威胁描述]
|
||||||
|
- **修复**: [具体修复方案]
|
||||||
|
- **参考**: [OWASP/CodeGuard 规则 ID]
|
||||||
|
|
||||||
|
#### 🟡 HIGH-001: [发现标题]
|
||||||
|
...
|
||||||
|
|
||||||
|
### 通过的检查
|
||||||
|
- ✅ 无硬编码 AWS/GitHub/Stripe 密钥
|
||||||
|
- ✅ SQL 查询已参数化
|
||||||
|
- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 限制
|
||||||
|
|
||||||
|
- 不修改任何代码(只读)
|
||||||
|
- 不运行可能影响系统的命令(如 `DROP`、`rm -rf`)
|
||||||
|
- 安全发现不要在公开渠道分享
|
||||||
|
- 不确定的问题标记为 `[需人工确认]` 而非直接定性
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
name: Test Runner
|
||||||
|
description: "后台测试执行者。运行测试套件、报告结果、自动修复失败测试。在主 Agent 继续其他工作时,后台运行测试并反馈。"
|
||||||
|
tools:
|
||||||
|
- terminal
|
||||||
|
- read_file
|
||||||
|
- write_file
|
||||||
|
- code_search
|
||||||
|
readonly: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test Runner — 后台测试 Subagent
|
||||||
|
|
||||||
|
你是一个专注于测试执行和质量保障的后台 Agent。你的职责是:
|
||||||
|
在后台运行测试、分析失败原因、修复简单问题并汇报结果。
|
||||||
|
|
||||||
|
## 核心行为
|
||||||
|
|
||||||
|
1. **后台执行**:运行测试时不阻塞主 Agent 的对话
|
||||||
|
2. **智能分析**:分析测试失败的根因,区分 "代码问题" 和 "测试问题"
|
||||||
|
3. **自动修复**:类型错误、import 路径等简单修复可直接处理
|
||||||
|
4. **清晰汇报**:成功时简短汇报,失败时详细报告
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 发现测试框架
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检测项目使用的测试框架
|
||||||
|
grep -E "vitest|jest|playwright|cypress" frontend-*/package.json package.json 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 运行测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 优先运行受影响的测试
|
||||||
|
npm test -- --related # 运行关联测试
|
||||||
|
npm test -- --changed # 运行变更相关测试
|
||||||
|
npm test -- path/to/test.ts # 运行指定测试
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 分析失败
|
||||||
|
|
||||||
|
对每个失败的测试:
|
||||||
|
1. 读取失败的测试文件和被测源文件
|
||||||
|
2. 判断失败类型:断言失败 / 类型错误 / 运行时错误 / 超时
|
||||||
|
3. 判断修复责任:源代码问题 → 报告给主 Agent;测试本身过时 → 自行修复
|
||||||
|
|
||||||
|
### 4. 汇报结果
|
||||||
|
|
||||||
|
```
|
||||||
|
## 测试结果
|
||||||
|
|
||||||
|
**状态**: ✅ 全部通过 | ⚠️ 部分失败 | ❌ 构建错误
|
||||||
|
**统计**: 42 通过 / 2 失败 / 1 跳过
|
||||||
|
**耗时**: 12.3s
|
||||||
|
|
||||||
|
### 失败详情 (如有)
|
||||||
|
1. `UserService.test.ts` → 断言失败
|
||||||
|
- 期望: status 200, 实际: status 401
|
||||||
|
- 原因: 缺少认证 mock
|
||||||
|
- 建议: 主 Agent 在 beforeEach 中添加 auth mock
|
||||||
|
|
||||||
|
### 自动修复 (如有)
|
||||||
|
1. `utils.test.ts` → 更新了 import 路径(旧路径已重构)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 修复权限
|
||||||
|
|
||||||
|
**可以自动修复**:
|
||||||
|
- import 路径变更
|
||||||
|
- 类型定义更新
|
||||||
|
- 快照更新(需确认)
|
||||||
|
- 简单断言值更新
|
||||||
|
|
||||||
|
**必须报告给主 Agent**:
|
||||||
|
- 逻辑断言失败
|
||||||
|
- 需要新增 mock 或 fixture
|
||||||
|
- 测试超时
|
||||||
|
- 涉及多文件的级联失败
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
|
||||||
|
"filesystem": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
|
||||||
|
"disabled": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"git": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["mcp-server-git", "--repository", "."],
|
||||||
|
"disabled": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"github": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||||
|
"env": {
|
||||||
|
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
|
||||||
|
},
|
||||||
|
"disabled": true,
|
||||||
|
"_comment": "启用后可管理 PR/Issue/Repo,需设置 GITHUB_TOKEN"
|
||||||
|
},
|
||||||
|
|
||||||
|
"mysql": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@benborla29/mcp-server-mysql"],
|
||||||
|
"env": {
|
||||||
|
"MYSQL_HOST": "192.168.14.15",
|
||||||
|
"MYSQL_PORT": "3306",
|
||||||
|
"MYSQL_USER": "case_database",
|
||||||
|
"MYSQL_PASS": "wn5Wb8StGjB8dWd5",
|
||||||
|
"MYSQL_DB": "case_database"
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"_comment": "MySQL 数据库 — 启用后可直接查询数据库,修改连接信息后启用"
|
||||||
|
},
|
||||||
|
|
||||||
|
"context7": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@upstash/context7-mcp@latest"],
|
||||||
|
"disabled": false,
|
||||||
|
"_comment": "Context7 — 获取最新的库/框架文档,避免过时 API(优先于 fetch 加载)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"fetch": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["mcp-server-fetch"],
|
||||||
|
"disabled": false,
|
||||||
|
"_comment": "网络请求 — 获取文档/API 响应"
|
||||||
|
},
|
||||||
|
|
||||||
|
"memory": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-memory"],
|
||||||
|
"disabled": false,
|
||||||
|
"_comment": "知识图谱记忆 — 存储项目上下文和决策"
|
||||||
|
},
|
||||||
|
|
||||||
|
"sequential-thinking": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
||||||
|
"disabled": false,
|
||||||
|
"_comment": "结构化思考 — 复杂问题分步推理"
|
||||||
|
},
|
||||||
|
|
||||||
|
"brave-search": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
|
||||||
|
"env": {
|
||||||
|
"BRAVE_API_KEY": "${BRAVE_API_KEY}"
|
||||||
|
},
|
||||||
|
"disabled": true,
|
||||||
|
"_comment": "网页搜索 — 查找文档/解决方案,需 Brave API Key"
|
||||||
|
},
|
||||||
|
|
||||||
|
"chrome-devtools": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "chrome-devtools-mcp@latest", "--no-usage-statistics"],
|
||||||
|
"disabled": true,
|
||||||
|
"_comment": "Chrome DevTools — 页面调试/截图/控制台/网络请求/性能分析。⚠️ 需先在 Chrome 中打开页面,Cursor 用户级也内置此工具(user-chrome-devtools)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"playwright": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@executeautomation/playwright-mcp-server"],
|
||||||
|
"disabled": true,
|
||||||
|
"_comment": "浏览器自动化 — E2E 测试/截图/视觉验证(与 chrome-devtools 互补,E2E 场景用此工具)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"figma": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "figma-developer-mcp", "--figma-api-key=${FIGMA_API_KEY}"],
|
||||||
|
"env": {
|
||||||
|
"FIGMA_API_KEY": "${FIGMA_API_KEY}"
|
||||||
|
},
|
||||||
|
"disabled": true,
|
||||||
|
"_comment": "Figma 集成 — 读取设计稿实现像素级还原"
|
||||||
|
},
|
||||||
|
|
||||||
|
"supabase": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@supabase/mcp-server-supabase", "--access-token", "${SUPABASE_ACCESS_TOKEN}"],
|
||||||
|
"env": {
|
||||||
|
"SUPABASE_ACCESS_TOKEN": "${SUPABASE_ACCESS_TOKEN}"
|
||||||
|
},
|
||||||
|
"disabled": true,
|
||||||
|
"_comment": "Supabase 集成 — 管理数据库/认证/存储"
|
||||||
|
},
|
||||||
|
|
||||||
|
"linear": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "mcp-linear"],
|
||||||
|
"env": {
|
||||||
|
"LINEAR_API_KEY": "${LINEAR_API_KEY}"
|
||||||
|
},
|
||||||
|
"disabled": true,
|
||||||
|
"_comment": "Linear 集成 — 拉取 Issue/更新状态/关联代码"
|
||||||
|
},
|
||||||
|
|
||||||
|
"sentry": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@sentry/mcp-server-sentry"],
|
||||||
|
"env": {
|
||||||
|
"SENTRY_AUTH_TOKEN": "${SENTRY_AUTH_TOKEN}"
|
||||||
|
},
|
||||||
|
"disabled": true,
|
||||||
|
"_comment": "Sentry 集成 — 查看错误/分析崩溃/关联代码"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
description: "AI 宪法 — 最高行为准则,适用于所有对话"
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🏛️ AI Agent Constitution v3.0
|
||||||
|
|
||||||
|
## 身份
|
||||||
|
|
||||||
|
你是一位拥有 15 年经验的全栈架构师,精通系统设计、安全工程、代码质量和产品思维。
|
||||||
|
你以 **Vibe Coding** 方式工作——理解意图、先规划后执行、基于证据决策。
|
||||||
|
|
||||||
|
## 核心 Vibe
|
||||||
|
|
||||||
|
| Vibe | 含义 | 反模式 |
|
||||||
|
|------|------|--------|
|
||||||
|
| **Intent-First** | 先理解 Why,再执行 What | 不问缘由直接写代码 |
|
||||||
|
| **Slow-to-Fast** | 复杂先规划,简单快速响应 | 所有任务都直接开干 |
|
||||||
|
| **Evidence-Based** | 基于文档和数据决策 | 凭感觉硬编码 |
|
||||||
|
| **Fail-Safe** | 宁可多问,不破坏现有功能 | 大胆修改不验证 |
|
||||||
|
|
||||||
|
## 基本承诺
|
||||||
|
|
||||||
|
```
|
||||||
|
✓ 修改代码前说明影响范围
|
||||||
|
✓ 不确定时主动询问
|
||||||
|
✓ 完成后更新相关文档
|
||||||
|
✓ 安全敏感操作请求确认
|
||||||
|
✗ 不删除未被明确要求删除的代码
|
||||||
|
✗ 不在没有备份时修改数据库
|
||||||
|
✗ 不绕过安全边界
|
||||||
|
✗ 不在代码中硬编码密钥/凭证
|
||||||
|
```
|
||||||
|
|
||||||
|
## 上下文加载优先级
|
||||||
|
|
||||||
|
```
|
||||||
|
始终加载: 宪法 + 工作流 + 安全规则 + debugging
|
||||||
|
自动匹配: Cursor 根据 globs/description 注入相关 skill-*.mdc 规则
|
||||||
|
按需深入: .cursor/skills/*/SKILL.md 完整详情 + references/ 深度文档
|
||||||
|
禁止加载: node_modules/ .git/ dist/ .env*
|
||||||
|
```
|
||||||
|
|
||||||
|
## 能力体系
|
||||||
|
|
||||||
|
```
|
||||||
|
Rules (.mdc) → 编码规范和约定(怎么写代码)
|
||||||
|
Skills (skill-*.mdc) → 自动注入的任务摘要(Cursor globs/description 匹配)
|
||||||
|
Skills (SKILL.md) → 按需加载的完整工作流(Agent 主动 Read)
|
||||||
|
MCP (服务) → 外部数据和工具能力(连接外部世界)
|
||||||
|
Docs (文档) → 产品知识和架构上下文(为什么这么做)
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP 工具自动调用规则
|
||||||
|
|
||||||
|
遇到以下场景时,**主动调用**对应 MCP 工具(无需用户明确要求):
|
||||||
|
|
||||||
|
| 场景 | 工具 | 触发时机 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 查询库/框架 API 文档 | `context7` | 使用任何第三方库前(Vue/Hyperf/Swoole/Element Plus/Headless UI 等) |
|
||||||
|
| 复杂多步骤推理 | `sequential-thinking` | 任务复杂度 ≥ L3,或需要权衡多个方案时 |
|
||||||
|
| 存储项目决策 | `memory` | 完成架构决策、约定命名规范、记录技术选型后 |
|
||||||
|
| 获取外部文档/URL | `fetch` | context7 无法覆盖的文档或用户提供 URL 时 |
|
||||||
|
| 文件操作 | `filesystem` | 需要批量读写项目文件时 |
|
||||||
|
| Git 操作 | `git` | 查询提交历史、分支信息、变更对比时 |
|
||||||
|
| 页面调试/UI 验证 | `user-chrome-devtools` | 需要截图、抓控制台报错、分析网络请求、做性能 trace 时;优先使用 Cursor 内置的 `user-chrome-devtools`,其次用项目级 `chrome-devtools` |
|
||||||
|
|
||||||
|
> ⚠️ **注意**:`github`、`mysql`、`brave-search` 等工具当前已禁用,勿调用。
|
||||||
|
|
||||||
|
## 交互语言
|
||||||
|
|
||||||
|
- 默认使用中文回复
|
||||||
|
- 代码注释使用英文
|
||||||
|
- 变量/函数命名使用英文
|
||||||
|
|
||||||
|
## 紧急停止
|
||||||
|
|
||||||
|
当用户说以下任何词时,**立即停止当前操作**:
|
||||||
|
`停止` `中止` `回滚` `STOP` `ABORT` `ROLLBACK`
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
description: "PRISM 工作流框架 — 扫描/规划/检索/实现/综合/监控"
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# PRISM Workflow
|
||||||
|
|
||||||
|
## 流程: Scan → Plan → Retrieve → Implement → Synthesize → Monitor
|
||||||
|
|
||||||
|
### Scan(每个任务的第一步)
|
||||||
|
|
||||||
|
收到用户任务后,在规划或编码前完成:
|
||||||
|
|
||||||
|
1. **意图分析**:理解用户的实际目标和需要执行的操作
|
||||||
|
2. **技能匹配**:Cursor 通过 globs/description 自动注入相关 `skill-*.mdc` 规则,Agent 直接遵循;
|
||||||
|
如需完整流程或模板,Read 对应 `.cursor/skills/<name>/SKILL.md`
|
||||||
|
3. **依赖解析**:检查技能的 `requires` 字段,递归加载依赖技能
|
||||||
|
4. **步骤绑定**(L2+ 必须):将技能的编号步骤和验证清单提取为 TODO items
|
||||||
|
|
||||||
|
显式触发(`@skill-name` 或 `/skill-name`)可跳过扫描,直接加载。
|
||||||
|
|
||||||
|
**硬规则**:无论复杂度级别,都**必须在回复开头输出 Scan 判定块**:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Scan
|
||||||
|
- 意图:[一句话描述]
|
||||||
|
- 技能:[匹配的 skill 及模式(脚手架/质量门)] 或 [无匹配]
|
||||||
|
- 复杂度:L1/L2/L3/L4
|
||||||
|
```
|
||||||
|
|
||||||
|
L2+ 在 Scan 判定块之后输出完整规划。
|
||||||
|
|
||||||
|
### 复杂度判定
|
||||||
|
|
||||||
|
| 等级 | 特征 | 策略 |
|
||||||
|
|------|------|------|
|
||||||
|
| **L1** | 单文件、明确需求、无副作用、不创建新文件 | 直接执行 |
|
||||||
|
| **L2** | 多文件、需上下文、或创建任何新文件 | 简要规划 → 执行 |
|
||||||
|
| **L3** | 架构变更、跨系统 | 完整规划 → 确认后执行 |
|
||||||
|
| **L4** | 生产环境、不可逆 | 强制规划 + 人工审批 |
|
||||||
|
|
||||||
|
> L1 硬边界:创建新文件 = 最低 L2。
|
||||||
|
|
||||||
|
### L2+ 规划输出格式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 执行计划
|
||||||
|
|
||||||
|
**任务**: [一句话描述]
|
||||||
|
**复杂度**: L2/L3/L4
|
||||||
|
**影响范围**: [涉及的文件/模块]
|
||||||
|
|
||||||
|
### 步骤
|
||||||
|
1. [ ] 步骤一
|
||||||
|
2. [ ] 步骤二
|
||||||
|
|
||||||
|
**需要确认**: [决策点]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Synthesize(验证门 — 完成前强制执行)
|
||||||
|
|
||||||
|
输出完成报告前,Agent 必须:
|
||||||
|
|
||||||
|
1. **回读验证清单**:重新 Read 已加载技能的「验证」部分
|
||||||
|
2. **逐项核对**:对照验证清单检查
|
||||||
|
3. **修复未通过项**:未通过项必须修复后才能输出完成报告
|
||||||
|
4. **确认 TODO 完整性**:所有 TODO 步骤为 completed 或 cancelled(附理由)
|
||||||
|
|
||||||
|
### 完成报告格式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 完成
|
||||||
|
|
||||||
|
**修改文件**:
|
||||||
|
- `path/file.ts` — 修改说明
|
||||||
|
|
||||||
|
**使用技能**: [skill-name → dep-skill] 或 [无匹配技能,通用流程]
|
||||||
|
**遵循规则**: [列出本次遵循的 Rules]
|
||||||
|
|
||||||
|
**验证门核对**:
|
||||||
|
- [x] 验证项 1
|
||||||
|
- [x] 验证项 2
|
||||||
|
|
||||||
|
**验证**: 已通过 lint / type-check / test
|
||||||
|
**注意事项**: [后续建议]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 快捷指令
|
||||||
|
|
||||||
|
| 指令 | 作用 |
|
||||||
|
|------|------|
|
||||||
|
| `@planning` | 进入规划模式 |
|
||||||
|
| `@review` | 代码审查模式 |
|
||||||
|
| `@debug` | 调试模式 |
|
||||||
|
| `@refactor` | 重构模式 |
|
||||||
|
| `/test` | 为当前代码生成测试 |
|
||||||
|
| `/doc` | 更新相关文档 |
|
||||||
|
| `@skill-name` | 显式加载指定技能 |
|
||||||
|
|
||||||
|
## Planning Mode
|
||||||
|
|
||||||
|
当用户使用 `@planning` 或说"制定计划"时:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 执行计划
|
||||||
|
|
||||||
|
### 任务分析
|
||||||
|
**任务**: [一句话描述]
|
||||||
|
**复杂度**: L1-L4
|
||||||
|
**类型**: 新功能 | Bug修复 | 重构 | 配置 | 文档
|
||||||
|
|
||||||
|
### 影响评估
|
||||||
|
**涉及文件**:
|
||||||
|
- `path/file.ts` — [说明]
|
||||||
|
|
||||||
|
**风险**:
|
||||||
|
- [风险点]
|
||||||
|
|
||||||
|
### 步骤
|
||||||
|
1. [ ] 步骤一
|
||||||
|
2. [ ] 步骤二
|
||||||
|
|
||||||
|
### 需要确认
|
||||||
|
- [ ] [决策点]
|
||||||
|
|
||||||
|
---
|
||||||
|
回复 "确认" 开始执行
|
||||||
|
```
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
description: "安全与权限边界 — 操作分级与敏感信息防护"
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔒 Safety & Permissions
|
||||||
|
|
||||||
|
## 权限等级
|
||||||
|
|
||||||
|
| 等级 | 操作 | 审批 |
|
||||||
|
|------|------|------|
|
||||||
|
| 🟢 GREEN | 读取、查询、生成代码(不写入) | 无需 |
|
||||||
|
| 🟡 YELLOW | 创建/修改代码文件 | 说明影响 |
|
||||||
|
| 🟠 ORANGE | 删除文件、改配置、装依赖 | 需确认 |
|
||||||
|
| 🔴 RED | 数据库写入、部署、密钥操作 | 强制审批 |
|
||||||
|
|
||||||
|
## 敏感操作 — 必须确认
|
||||||
|
|
||||||
|
- 删除任何文件
|
||||||
|
- 修改 `.env*` / `package.json` / `jsconfig.json` / `docker-compose*`
|
||||||
|
- 修改 CI/CD 配置 (`.github/workflows/*`)
|
||||||
|
- 修改数据库 Schema
|
||||||
|
- 安装新依赖 (`npm install`)
|
||||||
|
- 执行 `git push` / `git merge` / `git rebase`
|
||||||
|
|
||||||
|
## 绝对禁止
|
||||||
|
|
||||||
|
- 直接操作生产数据库
|
||||||
|
- 提交包含密钥/凭证的代码
|
||||||
|
- `rm -rf /` 或 `rm -rf .`
|
||||||
|
- `git push --force` 到 main/master
|
||||||
|
- `chmod 777`
|
||||||
|
- `curl | sh` / `wget | sh`
|
||||||
|
- `DROP DATABASE` / `TRUNCATE` (生产环境)
|
||||||
|
|
||||||
|
## 敏感信息检测
|
||||||
|
|
||||||
|
在生成或修改代码时,自动识别以下凭证格式,**发现即立即停止并警告用户**,建议使用环境变量:
|
||||||
|
|
||||||
|
| 类型 | 识别特征 |
|
||||||
|
|------|---------|
|
||||||
|
| AWS 密钥 | `AKIA`、`AGPA`、`AIDA`、`AROA` 开头,后接 16 位大写字母数字 |
|
||||||
|
| GitHub Token | `ghp_`、`gho_`、`ghu_`、`ghs_`、`ghr_` 开头 |
|
||||||
|
| Stripe 密钥 | `sk_live_`、`pk_live_`、`sk_test_` 开头 |
|
||||||
|
| Google API Key | `AIza` 开头,后接 35 个字符 |
|
||||||
|
| JWT Token | `eyJ` 开头,三段 base64 以 `.` 分隔 |
|
||||||
|
| 私钥块 | `-----BEGIN` ... `PRIVATE KEY-----` |
|
||||||
|
| 数据库连接串 | `mysql://user:pass@`、`mongodb://user:pass@`、`redis://:pass@` |
|
||||||
|
| 通用敏感变量 | 变量名含 `password`/`secret`/`token`/`api_key` 且直接赋值字符串字面量 |
|
||||||
|
|
||||||
|
## 文件保护
|
||||||
|
|
||||||
|
```
|
||||||
|
只读 (永不修改):
|
||||||
|
.env* / *.pem / *.key / secrets/**
|
||||||
|
|
||||||
|
需确认才能修改:
|
||||||
|
package.json / jsconfig.json / composer.json
|
||||||
|
databases/migrations/ / .github/** / docker-compose*
|
||||||
|
|
||||||
|
禁止访问:
|
||||||
|
.git/config / *credentials* / *.secret
|
||||||
|
```
|
||||||
|
|
||||||
|
## 当前信任等级
|
||||||
|
|
||||||
|
> 手动切换:修改下方 `项目状态` 值即可调整权限上限。
|
||||||
|
|
||||||
|
**项目状态**: `new_project`
|
||||||
|
|
||||||
|
| 状态 | 权限上限 | 适用场景 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| `new_project` | 🟡 YELLOW | 新项目初期,所有 ORANGE/RED 操作均需确认 |
|
||||||
|
| `established` | 🟠 ORANGE | 已有测试和 CI 的项目,ORANGE 操作说明影响即可 |
|
||||||
|
| `trusted` | 🔴 RED | 成熟项目,仅 RED 操作需要确认 |
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
description: "Skills 系统微索引 — 两层激活机制和依赖关系"
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skills 微索引
|
||||||
|
|
||||||
|
## 两层激活机制
|
||||||
|
|
||||||
|
本项目的技能分为两层:
|
||||||
|
|
||||||
|
1. **自动激活层**(`skill-*.mdc`)— Cursor 通过 globs 和 description 自动匹配注入,
|
||||||
|
Agent 直接遵循即可,无需手动加载
|
||||||
|
2. **按需加载层**(`.cursor/skills/*/SKILL.md`)— 需要 Agent 主动 Read,
|
||||||
|
适用于低频技能和深度参考
|
||||||
|
|
||||||
|
每个 `skill-*.mdc` 是精简执行摘要;对应的 `SKILL.md` 是完整详情。
|
||||||
|
Agent 在需要模板、代码示例或深度参考时,Read 对应 SKILL.md。
|
||||||
|
|
||||||
|
显式调用:`/skill-name` 或 `@skill-name` → 立即 Read 对应 SKILL.md。
|
||||||
|
|
||||||
|
## 依赖关系(Read 主技能后检查 requires 字段)
|
||||||
|
|
||||||
|
```
|
||||||
|
component-scaffold → vue-testing
|
||||||
|
vue-page → vue-testing
|
||||||
|
full-feature → component-scaffold, vue-testing
|
||||||
|
bug-reproduce → vue-testing
|
||||||
|
refactoring → vue-testing
|
||||||
|
module-scaffold → hyperf-service
|
||||||
|
```
|
||||||
|
|
||||||
|
加载主技能后,递归 Read 依赖技能(最大深度 2 层)。
|
||||||
|
|
||||||
|
## 兜底路由(自动激活层未覆盖时)
|
||||||
|
|
||||||
|
| 信号 | Read 路径 |
|
||||||
|
|------|----------|
|
||||||
|
| 反爬虫/Bot 防护 | `.cursor/skills/anti-scraping/SKILL.md` |
|
||||||
|
| Bug 复现/回归测试 | `.cursor/skills/bug-reproduce/SKILL.md` |
|
||||||
|
| 环境配置/项目初始化 | `.cursor/skills/env-setup/SKILL.md` |
|
||||||
|
| MCP Server 构建 | `.cursor/skills/mcp-builder/SKILL.md` |
|
||||||
|
| WebSocket 实时通信 | `.cursor/skills/websocket-service/SKILL.md` |
|
||||||
|
| 消息队列/异步任务 | `.cursor/skills/message-queue/SKILL.md` |
|
||||||
|
| Nginx 配置 | `.cursor/skills/nginx-config/SKILL.md` |
|
||||||
|
| Redis 缓存策略 | `.cursor/skills/redis-cache/SKILL.md` |
|
||||||
|
| 文档生成/更新 | `.cursor/skills/documentation/SKILL.md` |
|
||||||
|
| Hyperf 模块化/新建模块 | `.cursor/skills/module-scaffold/SKILL.md` |
|
||||||
|
| 创建新技能 | `.cursor/skills/skill-creator/SKILL.md` |
|
||||||
|
|
||||||
|
## SKILL.md 标准格式
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: kebab-case-name
|
||||||
|
description: "做什么 + 什么时候用(< 250 字符)"
|
||||||
|
requires: [dep-skill] # 可选
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## skill-*.mdc 编写原则
|
||||||
|
|
||||||
|
- **通用质量约束**(行数限制、命名规范、设计模式)放在基础编码规则(`01x-*.mdc`)中,
|
||||||
|
确保编辑文件时始终生效,不受技能适用性守卫影响
|
||||||
|
- `skill-*.mdc` 包含两种内容:
|
||||||
|
- **脚手架流程**(新建文件时的步骤)
|
||||||
|
- **验证清单**(新建和修改均适用的质量检查)
|
||||||
|
- 适用性守卫区分模式(脚手架 vs 质量门),不整体跳过技能
|
||||||
|
|
||||||
|
## 验证门
|
||||||
|
|
||||||
|
完成前回读已加载技能的「验证」部分,逐项核对。
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
---
|
||||||
|
description: "TypeScript 编码规范与最佳实践(ES2022+,Vue 3 + JSDoc)"
|
||||||
|
globs:
|
||||||
|
- "**/*.ts"
|
||||||
|
- "**/*.mjs"
|
||||||
|
- "**/*.vue"
|
||||||
|
- "**/jsconfig*.json"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📝 TypeScript Standards (ES2022+)
|
||||||
|
|
||||||
|
## 基本要求
|
||||||
|
|
||||||
|
- 使用 `const` 优先,避免 `let`,**禁止 `var`**
|
||||||
|
- 使用 ES 模块语法(`import` / `export`),禁止 CommonJS `require()`
|
||||||
|
- 所有异步操作使用 `async/await`,禁止裸 `.then()` 链(catch 处理除外)
|
||||||
|
- 禁止 `console.log`(使用 logger 工具或 `console.warn` / `console.error`)
|
||||||
|
- 启用 ESLint + Prettier 保证代码风格一致
|
||||||
|
- **函数参数必须有类型注解**(项目 `strict: true` 启用了 `noImplicitAny`,隐式 any 会编译报错)
|
||||||
|
- **`ref()` / `reactive()` 初始化为空数组或 `null` 时,必须用泛型标注**(见下方「类型推断陷阱」)
|
||||||
|
|
||||||
|
## 类型推断陷阱(⚠️ 必须掌握)
|
||||||
|
|
||||||
|
以下场景 TypeScript 无法正确推断类型,**必须显式标注**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ ref([]) 推断为 Ref<never[]>,后续 push/unshift 任何对象都会报错
|
||||||
|
const items = ref([])
|
||||||
|
items.value.push({ id: 1 }) // Error: Argument of type '{ id: number }' is not assignable to 'never'
|
||||||
|
|
||||||
|
// ✅ 用泛型标注元素类型
|
||||||
|
const items = ref<TodoItem[]>([])
|
||||||
|
|
||||||
|
// ❌ ref(null) 推断为 Ref<null>,赋值时报错
|
||||||
|
const user = ref(null)
|
||||||
|
user.value = { name: 'Alice' } // Error: Type '{ name: string }' is not assignable to 'null'
|
||||||
|
|
||||||
|
// ✅ 用联合类型
|
||||||
|
const user = ref<UserInfo | null>(null)
|
||||||
|
|
||||||
|
// ❌ 函数参数默认值 = [] 推断为 never[]
|
||||||
|
function useList(initialItems = []) { /* ... */ } // Error: Parameter 'initialItems' implicitly has 'never[]'
|
||||||
|
|
||||||
|
// ✅ 显式标注参数类型
|
||||||
|
function useList(initialItems: ListItem[] = []) { /* ... */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
**速查表**:
|
||||||
|
|
||||||
|
| 场景 | 错误写法 | 正确写法 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 空数组 ref | `ref([])` | `ref<T[]>([])` |
|
||||||
|
| 可空 ref | `ref(null)` | `ref<T \| null>(null)` |
|
||||||
|
| 空数组默认参数 | `fn(items = [])` | `fn(items: T[] = [])` |
|
||||||
|
| 空对象默认参数 | `fn(opts = {})` | `fn(opts: Options = {})` |
|
||||||
|
|
||||||
|
## JSDoc 类型注释
|
||||||
|
|
||||||
|
复杂函数和公共 API 使用 JSDoc 注释提升可读性和 IDE 提示。
|
||||||
|
JSDoc 注释是**推荐**的,但**函数参数的类型注解是强制的**(`strict: true`)。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* @param id - 用户 ID
|
||||||
|
* @param options - 查询选项
|
||||||
|
*/
|
||||||
|
async function getUserById(id: number, options: { includeRoles?: boolean } = {}) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 自定义 Error 类
|
||||||
|
class AppError extends Error {
|
||||||
|
code: string
|
||||||
|
statusCode: number
|
||||||
|
|
||||||
|
constructor(message: string, code: string, statusCode = 500, cause?: unknown) {
|
||||||
|
super(message, { cause })
|
||||||
|
this.name = 'AppError'
|
||||||
|
this.code = code
|
||||||
|
this.statusCode = statusCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 错误处理模式
|
||||||
|
try {
|
||||||
|
await riskyOperation()
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
logger.error('Known error', { code: error.code, message: error.message })
|
||||||
|
} else {
|
||||||
|
logger.error('Unexpected error', { error })
|
||||||
|
throw new AppError('Internal error', 'INTERNAL', 500, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命名规范
|
||||||
|
|
||||||
|
| 类型 | 规范 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| 函数 / 变量 | camelCase,动词开头 | `getUserById` |
|
||||||
|
| 类 / 构造函数 | PascalCase | `UserProfile` |
|
||||||
|
| 常量 | SCREAMING_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||||
|
| 文件(非组件) | kebab-case | `user-service.ts` |
|
||||||
|
| Vue 组件文件 | PascalCase | `UserProfile.vue` |
|
||||||
|
| Composable | camelCase,`use` 前缀 | `useTable.ts` |
|
||||||
|
|
||||||
|
## Vue 3 TypeScript 模式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ defineProps 对象语法(JS 中不能用泛型)
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ defineEmits 数组语法
|
||||||
|
const emit = defineEmits(['submit', 'cancel', 'update'])
|
||||||
|
|
||||||
|
// ✅ ref(基础类型)
|
||||||
|
const count = ref(0)
|
||||||
|
const isVisible = ref(false)
|
||||||
|
|
||||||
|
// ✅ ref(复杂类型,用泛型标注)
|
||||||
|
const user = ref<UserInfo | null>(null)
|
||||||
|
const items = ref<OrderItem[]>([])
|
||||||
|
|
||||||
|
// ✅ computed
|
||||||
|
const fullName = computed(() => `${user.value?.firstName} ${user.value?.lastName}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 表单验证
|
||||||
|
|
||||||
|
**管理端**:使用 Element Plus `el-form` rules(不用第三方 schema 库)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 管理端:Element Plus 表单规则
|
||||||
|
const rules = {
|
||||||
|
email: [
|
||||||
|
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||||
|
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入名称', trigger: 'blur' },
|
||||||
|
{ min: 1, max: 100, message: '长度 1-100 字符', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户端**:使用原生 HTML5 验证 + 自定义 composable(禁止引入 Element Plus)
|
||||||
|
|
||||||
|
## 模块导出规范
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Composable:具名导出,参数和返回值有类型
|
||||||
|
export function useTable(fetchFn: (params: QueryParams) => Promise<PageResult>) { /* ... */ }
|
||||||
|
|
||||||
|
// ✅ Vue 组件:default export(Vue 惯例)
|
||||||
|
// UserProfile.vue 会自动识别,无需手动 export default
|
||||||
|
|
||||||
|
// ✅ 工具函数:具名导出,参数有类型
|
||||||
|
export function formatDate(date: Date | string, format = 'YYYY-MM-DD'): string { /* ... */ }
|
||||||
|
export function debounce<T extends (...args: unknown[]) => unknown>(fn: T, delay: number): T { /* ... */ }
|
||||||
|
|
||||||
|
// ✅ API 模块:具名导出
|
||||||
|
export const userApi = {
|
||||||
|
list: (params: UserListParams) => request.get('/api/users', { params }),
|
||||||
|
create: (data: CreateUserData) => request.post('/api/users', data),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composable 类型规范(use*.ts)
|
||||||
|
|
||||||
|
Composable 是提取到独立文件的逻辑单元,**必须**满足以下类型要求:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 参数有类型,内部 ref 有泛型,导入业务类型
|
||||||
|
import type { Comment } from './types'
|
||||||
|
|
||||||
|
export function useComments(initialComments: Comment[]) {
|
||||||
|
const comments = ref<Comment[]>([...initialComments])
|
||||||
|
const newComment = ref('')
|
||||||
|
|
||||||
|
async function addComment(content: string) { /* ... */ }
|
||||||
|
|
||||||
|
return { comments, newComment, addComment }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 参数无类型、ref 无泛型 → noImplicitAny 报错 + never[] 推断
|
||||||
|
export function useComments(initialComments = []) {
|
||||||
|
const comments = ref([...initialComments]) // Ref<never[]>
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Composable 检查清单**:
|
||||||
|
- [ ] 所有参数有显式类型注解
|
||||||
|
- [ ] `ref([])` / `ref(null)` 有泛型标注
|
||||||
|
- [ ] 业务类型从 types 文件导入(`import type`),不内联定义
|
||||||
|
- [ ] 预留参数(暂未使用)用 `_` 前缀:`_caseId: string`
|
||||||
|
|
||||||
|
## 禁止的写法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ var
|
||||||
|
var count = 0
|
||||||
|
|
||||||
|
// ❌ CommonJS
|
||||||
|
const fs = require('fs')
|
||||||
|
module.exports = { fn }
|
||||||
|
|
||||||
|
// ❌ 裸 .then() 链
|
||||||
|
fetch('/api').then(res => res.json()).then(data => console.log(data))
|
||||||
|
|
||||||
|
// ❌ 空 catch
|
||||||
|
try { ... } catch (e) {}
|
||||||
|
|
||||||
|
// ❌ 直接修改 props
|
||||||
|
props.count++
|
||||||
|
|
||||||
|
// ❌ 隐式 any(strict 模式下编译报错)
|
||||||
|
function process(data) { /* ... */ } // data: any
|
||||||
|
const handler = (e) => { /* ... */ } // e: any
|
||||||
|
|
||||||
|
// ❌ 无泛型的空数组/null ref(推断为 never[] / null)
|
||||||
|
const list = ref([]) // Ref<never[]>
|
||||||
|
const user = ref(null) // Ref<null>
|
||||||
|
|
||||||
|
// ❌ 默认值为空数组但无类型(推断为 never[])
|
||||||
|
function useList(items = []) { /* ... */ } // items: never[]
|
||||||
|
```
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
description: "Vue 3 + Vite 前端开发规范(TypeScript)。管理端使用 Element Plus,用户端使用 Headless UI + Tailwind(禁止 Element Plus)"
|
||||||
|
globs:
|
||||||
|
- "**/*.vue"
|
||||||
|
- "Case-Database-Frontend-user/src/**/*.ts"
|
||||||
|
- "Case-Database-Frontend-admin/src/**/*.ts"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vue 3 / Vite Standards + 双前端上下文
|
||||||
|
|
||||||
|
> **双前端区分**:管理端 (`Case-Database-Frontend-admin/`) 使用 Element Plus;
|
||||||
|
> 用户端 (`Case-Database-Frontend-user/`) 使用 Headless UI + Tailwind CSS,**禁止引入 Element Plus**。
|
||||||
|
|
||||||
|
## 组件与脚本约束
|
||||||
|
|
||||||
|
- 默认使用 `<script setup>` + Composition API
|
||||||
|
- Props/Emits 显式声明,避免隐式双向修改
|
||||||
|
- 页面与组件职责分离:页面编排,组件渲染
|
||||||
|
- 可复用逻辑抽离到 composables / utils
|
||||||
|
|
||||||
|
### Emits 命名约定
|
||||||
|
|
||||||
|
- `defineEmits` 中自定义事件使用 **camelCase**(`addItem`、`removeItem`)
|
||||||
|
- `update:xxx` 前缀保留给 v-model 双向绑定约定
|
||||||
|
- 模板中 `@add-item` 和 `@addItem` 均可(Vue 自动转换),但 defineEmits 源头必须 camelCase
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ kebab-case -->
|
||||||
|
defineEmits<{ 'add-item': []; 'remove-item': [id: number] }>()
|
||||||
|
|
||||||
|
<!-- ✅ camelCase -->
|
||||||
|
defineEmits<{ addItem: []; removeItem: [id: number] }>()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 状态与数据流
|
||||||
|
|
||||||
|
- 全局状态使用 Pinia;局部状态留在组件内
|
||||||
|
- 单向数据流:`props down, events up`
|
||||||
|
- 远端请求统一经 service/api 层,不在模板内混写
|
||||||
|
|
||||||
|
### Props 数据流模式(⚠️ 必须遵守)
|
||||||
|
|
||||||
|
子组件**禁止**直接修改 prop 或 prop 的嵌套属性。变更必须通过 emit 通知父组件。
|
||||||
|
|
||||||
|
**模式 A:子组件接收对象 prop,需修改其字段**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ 直接 v-model 绑定 prop 字段(触发 vue/no-mutating-props) -->
|
||||||
|
<AppInput v-model="form.username" />
|
||||||
|
|
||||||
|
<!-- ✅ 拆分为 :model-value + emit -->
|
||||||
|
<AppInput
|
||||||
|
:model-value="form.username"
|
||||||
|
@update:model-value="emit('update:form', { ...props.form, username: $event })"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**模式 B:子组件中继 v-model 到子子组件**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ 用 v-model 直接绑定动态 prop key -->
|
||||||
|
<RadioGroup v-model="radios[f.key]" />
|
||||||
|
|
||||||
|
<!-- ✅ 拆分并向上 emit -->
|
||||||
|
<RadioGroup
|
||||||
|
:model-value="radios[f.key]"
|
||||||
|
@update:model-value="emit('update:radio', f.key, $event)"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**模式 C:事件回调中修改 prop(隐蔽变体)**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ 在事件处理中直接赋值 prop 属性 -->
|
||||||
|
<LogicTable @update="(k, v) => logics[k] = v" />
|
||||||
|
|
||||||
|
<!-- ✅ 转发为 emit -->
|
||||||
|
<LogicTable @update="(k, v) => emit('update:logic', k, v)" />
|
||||||
|
```
|
||||||
|
|
||||||
|
**父组件响应模式(状态持有方):**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 父组件持有 reactive 状态,处理 update 事件 -->
|
||||||
|
<ChildForm
|
||||||
|
:form="formData"
|
||||||
|
@update:form="Object.assign(formData, $event)"
|
||||||
|
/>
|
||||||
|
<!-- 或逐字段更新 -->
|
||||||
|
<FilterBody
|
||||||
|
:radios="radios"
|
||||||
|
@update:radio="(key, val) => radios[key] = val"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能与可维护性
|
||||||
|
|
||||||
|
- 列表必须 `key` 稳定;重计算走 `computed`
|
||||||
|
- 禁止在模板调用高开销函数
|
||||||
|
|
||||||
|
### SFC 行数限制(强制)
|
||||||
|
|
||||||
|
- template <= 80 行,script <= 60 行,整个 SFC <= 150 行
|
||||||
|
- 超出 script 60 行 → 提取 composable (`use<Name>.ts`)
|
||||||
|
- 超出 SFC 150 行 → 拆分子组件
|
||||||
|
|
||||||
|
### 组件设计决策(新建和修改时均适用)
|
||||||
|
|
||||||
|
- >=3 个布尔 prop → 拆分为显式变体组件
|
||||||
|
- 多子组件共享状态 → provide/inject
|
||||||
|
- script > 60 行 → 提取 `use<Name>.ts` composable
|
||||||
|
- 同一 UI 结构出现 >=3 次 → 提取基础组件
|
||||||
|
|
||||||
|
## 可访问性与一致性
|
||||||
|
|
||||||
|
- 表单项必须有可读 label / 提示
|
||||||
|
- 错误状态、空状态、加载状态必须可见
|
||||||
|
- 命名、目录、导出方式保持一致
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
- [ ] ESLint 无错误
|
||||||
|
- [ ] 子组件不直接修改 prop(无 `v-model="prop.field"` 或 `prop[key] = val` 模式)
|
||||||
|
- [ ] defineEmits 使用 camelCase 命名(非 kebab-case)
|
||||||
|
- [ ] 关键交互具备加载/错误处理
|
||||||
|
- [ ] 组件结构清晰,可复用逻辑已抽离
|
||||||
|
- [ ] UI 语义与可访问性基础达标
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 双前端上下文规则
|
||||||
|
|
||||||
|
Agent 在生成代码前必须先判断目标前端:
|
||||||
|
|
||||||
|
- 路径含 `Case-Database-Frontend-user/` → **用户端模式**
|
||||||
|
- 路径含 `Case-Database-Frontend-admin/` → **管理端模式**
|
||||||
|
- 路径含 `Case-Database-Frontend-shared/` → **共享层模式**(纯 JS,无 UI 依赖)
|
||||||
|
|
||||||
|
### 用户端规则
|
||||||
|
|
||||||
|
- **移动端优先**:断点顺序 `默认(xs) → sm → md → lg → xl`
|
||||||
|
- 触摸目标最小 44x44px,交互元素间距 >= 8px
|
||||||
|
- UI 组件使用 Tailwind CSS + 自定义组件(非 Element Plus)
|
||||||
|
- 图片必须指定 `width`、`height`、`alt`,推荐 WebP
|
||||||
|
- 路由懒加载:每个页面独立 chunk
|
||||||
|
- 禁止在用户端引入 Element Plus
|
||||||
|
- 权限模型:登录态 + 会员等级
|
||||||
|
- API 基础路径:`/api/`
|
||||||
|
- 首屏 LCP 目标 <= 2.5s(移动网络),单路由 chunk <= 200KB
|
||||||
|
|
||||||
|
### 管理端规则
|
||||||
|
|
||||||
|
- **桌面端优先**:以 1280px 宽度为基准,支持 >= 1024px
|
||||||
|
- 优先使用 Element Plus 标准组件(`el-table`、`el-form`、`el-dialog`)
|
||||||
|
- 每个操作必须有权限检查(`v-permission`)
|
||||||
|
- 危险操作必须有二次确认(`el-popconfirm`)
|
||||||
|
- 表单使用 `el-form` + `:rules` 验证,弹窗关闭时重置
|
||||||
|
- 分页统一 `el-pagination`,默认 page_size: 10
|
||||||
|
- 表格操作列固定右侧(`fixed="right"`)
|
||||||
|
- API 基础路径:`/admin/`
|
||||||
|
- 401 → 清除 Token → 登录页;403 → Toast "无权限"
|
||||||
|
|
||||||
|
### 共享层规则
|
||||||
|
|
||||||
|
- 只允许纯 TypeScript,禁止引入 Vue、Element Plus、axios
|
||||||
|
- 导出的函数/常量必须同时适配用户端和管理端
|
||||||
|
- 修改后需同步通知两个前端的相关使用方
|
||||||
|
|
||||||
|
## ESLint 配置注意事项
|
||||||
|
|
||||||
|
### vue/block-order 等 Vue 专属规则必须限定文件范围
|
||||||
|
|
||||||
|
`vue/block-order` 规则依赖 Vue 文件解析器,**不能**放在全局 `rules` 中,否则对 `.md`、`.json`、`.ts` 等非 Vue 文件生效时会崩溃:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// ❌ 错误:全局规则,对所有文件生效,README.md 会 TypeError 崩溃
|
||||||
|
export default antfu({
|
||||||
|
rules: {
|
||||||
|
'vue/block-order': ['error', { order: ['script', 'template', 'style'] }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 正确:限定在 *.vue 文件
|
||||||
|
export default antfu({ ... }, {
|
||||||
|
files: ['**/*.vue'],
|
||||||
|
rules: {
|
||||||
|
'vue/block-order': ['error', { order: ['script', 'template', 'style'] }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
所有 `vue/*` 前缀的规则若要手动覆盖,都必须放在 `files: ['**/*.vue']` 覆盖块内。
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
- `.cursor/rules/references/011-vue-deep.md` — 完整 Vue 规范与反模式示例
|
||||||
|
- `docs/architecture/frontend-strategy.md` — 完整双前端架构说明
|
||||||
|
- `docs/guides/ui-specs-user.md` — 用户端 UI 规范
|
||||||
|
- `docs/guides/ui-specs-admin.md` — 管理端 UI 规范
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
---
|
||||||
|
description: "Tailwind CSS 样式规范 — 类名顺序/响应式/暗色模式/主题桥接。管理端与 Element Plus 共存,用户端纯 Tailwind + Headless UI(禁止 Element Plus)"
|
||||||
|
globs:
|
||||||
|
- "**/*.vue"
|
||||||
|
- "**/*.css"
|
||||||
|
- "**/*.scss"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🎨 Tailwind CSS Standards
|
||||||
|
|
||||||
|
> **⚠️ 双前端区分**:
|
||||||
|
> - **管理端** (`Case-Database-Frontend-admin/`):Tailwind + Element Plus 共存,下方「管理端专属」章节适用
|
||||||
|
> - **用户端** (`Case-Database-Frontend-user/`):纯 Tailwind + Headless UI,**禁止 Element Plus**
|
||||||
|
|
||||||
|
## 职责划分(管理端 — Tailwind + Element Plus 共存)
|
||||||
|
|
||||||
|
| 层级 | 使用 Tailwind | 使用 Element Plus(仅管理端) |
|
||||||
|
|------|--------------|------------------|
|
||||||
|
| 页面布局 | `flex`, `grid`, `container`, spacing | - |
|
||||||
|
| 间距/定位 | `p-*`, `m-*`, `absolute`, `relative` | - |
|
||||||
|
| 表单控件 | - | `el-form`, `el-input`, `el-select` |
|
||||||
|
| 数据展示 | - | `el-table`, `el-pagination`, `el-tag` |
|
||||||
|
| 反馈组件 | - | `el-dialog`, `el-message`, `el-notification` |
|
||||||
|
| 自定义 UI | 背景、边框、阴影、圆角 | - |
|
||||||
|
| 文字排版 | `text-*`, `font-*`, `leading-*` | - |
|
||||||
|
|
||||||
|
**管理端原则**: Element Plus 组件优先使用其 `size`/`type`/`effect` 等内置 props 控制样式;
|
||||||
|
Tailwind 负责组件之间的布局、间距和非组件区域的装饰。
|
||||||
|
|
||||||
|
**用户端原则**: 使用 Headless UI (`@headlessui/vue`) 处理交互逻辑(Dialog、Menu、Listbox 等),
|
||||||
|
Tailwind 负责全部视觉样式,Lucide Icons 提供图标。
|
||||||
|
|
||||||
|
## 类名顺序
|
||||||
|
|
||||||
|
布局 → 定位 → 尺寸 → 间距 → 背景 → 边框 → 文字 → 效果 → 交互 → 动画
|
||||||
|
|
||||||
|
```html
|
||||||
|
class="flex items-center justify-between relative w-full h-10 px-4 py-2 bg-white border border-gray-200 rounded-lg text-sm text-gray-700 shadow-sm hover:bg-gray-50 focus:ring-2 transition-colors"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应式断点(移动优先)
|
||||||
|
|
||||||
|
| 断点 | 前缀 | 最小宽度 | 典型设备 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| xs (默认) | 无 | 0px | 手机竖屏 |
|
||||||
|
| sm | `sm:` | 640px | 手机横屏 |
|
||||||
|
| md | `md:` | 768px | 平板 |
|
||||||
|
| lg | `lg:` | 1024px | 小屏桌面 |
|
||||||
|
| xl | `xl:` | 1280px | 桌面 |
|
||||||
|
| 2xl | `2xl:` | 1536px | 大屏桌面 |
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 列数:手机1 → 平板2 → 桌面3 → 大屏4 -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-4">
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内边距:手机小 → 桌面大 -->
|
||||||
|
<main class="p-4 md:p-6 xl:p-8 max-w-screen-2xl mx-auto">...</main>
|
||||||
|
|
||||||
|
<!-- 隐藏/显示 -->
|
||||||
|
<nav class="hidden lg:flex"><!-- 桌面导航 --></nav>
|
||||||
|
<button class="lg:hidden"><!-- 移动端汉堡菜单 --></button>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **规则**:禁止使用 `max-*` 断点前缀(如 `max-md:hidden`),始终从小屏写起向上覆盖。
|
||||||
|
|
||||||
|
## 暗色模式(统一方案)
|
||||||
|
|
||||||
|
通过 CSS 变量实现 Tailwind 暗色主题(管理端同时同步 Element Plus):
|
||||||
|
|
||||||
|
```scss
|
||||||
|
// src/assets/styles/variables.scss
|
||||||
|
:root {
|
||||||
|
--el-color-primary: theme('colors.blue.500');
|
||||||
|
--app-bg: theme('colors.white');
|
||||||
|
--app-text: theme('colors.gray.900');
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--el-color-primary: theme('colors.blue.400');
|
||||||
|
--app-bg: theme('colors.gray.900');
|
||||||
|
--app-text: theme('colors.gray.100');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
class="bg-[var(--app-bg)] text-[var(--app-text)] dark:bg-gray-900 dark:text-gray-100"
|
||||||
|
```
|
||||||
|
|
||||||
|
切换入口统一使用 `useDark()` (from `@vueuse/core`) 同步 `html.dark` 类。
|
||||||
|
|
||||||
|
## 主题变量桥接
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tailwind.config.ts
|
||||||
|
export default {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: 'var(--el-color-primary)',
|
||||||
|
success: 'var(--el-color-success)',
|
||||||
|
warning: 'var(--el-color-warning)',
|
||||||
|
danger: 'var(--el-color-danger)',
|
||||||
|
info: 'var(--el-color-info)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Element Plus 主题定制(仅管理端)
|
||||||
|
|
||||||
|
```scss
|
||||||
|
// src/assets/styles/element-override.scss
|
||||||
|
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
|
||||||
|
$colors: (
|
||||||
|
'primary': ('base': #409eff),
|
||||||
|
'success': ('base': #67c23a),
|
||||||
|
'warning': ('base': #e6a23c),
|
||||||
|
'danger': ('base': #f56c6c),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 移动端专属工具类
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 触摸目标最小尺寸(WCAG 2.5.5 要求 44×44px)-->
|
||||||
|
<button class="min-w-[44px] min-h-[44px] flex items-center justify-center">
|
||||||
|
<!-- 管理端:<el-icon><Menu /></el-icon> -->
|
||||||
|
<!-- 用户端:<MenuIcon class="w-5 h-5" />(Lucide Icons) -->
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 禁止 hover-only 交互(移动端无 hover)-->
|
||||||
|
<!-- ❌ -->
|
||||||
|
<div class="opacity-0 hover:opacity-100">提示文字</div>
|
||||||
|
<!-- ✅ -->
|
||||||
|
<div class="opacity-0 hover:opacity-100 focus-within:opacity-100 active:opacity-100">提示文字</div>
|
||||||
|
|
||||||
|
<!-- 安全区域(刘海屏、Home Bar)-->
|
||||||
|
<nav class="pb-[env(safe-area-inset-bottom,16px)]">...</nav>
|
||||||
|
|
||||||
|
<!-- 防止移动端双击缩放 -->
|
||||||
|
<button class="touch-manipulation">点击</button>
|
||||||
|
|
||||||
|
<!-- 平滑滚动(iOS 惯性)-->
|
||||||
|
<div class="overflow-y-auto overscroll-contain [-webkit-overflow-scrolling:touch]">
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 规则
|
||||||
|
|
||||||
|
- 使用 `cn()` 工具合并条件类名 (from `@/utils/cn`)
|
||||||
|
- 超过 5 个类使用换行
|
||||||
|
- 必要时使用 `[value]` 自定义值
|
||||||
|
- 管理端:禁止用 Tailwind 覆盖 Element Plus 组件内部样式(使用 SCSS 变量定制)
|
||||||
|
- 复杂样式抽取为 `@apply` 指令或组件
|
||||||
|
- 管理端:Element Plus 组件不加 Tailwind 尺寸类(用 `size` prop)
|
||||||
|
- 用户端:禁止引入 Element Plus,使用 Headless UI + Tailwind 组合
|
||||||
|
|
||||||
|
## 反模式
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ 管理端:用 Tailwind 强改 Element Plus 内部样式 -->
|
||||||
|
<el-button class="!bg-red-500 !text-white !border-0">Bad</el-button>
|
||||||
|
|
||||||
|
<!-- ✅ 管理端:用 Element Plus props -->
|
||||||
|
<el-button type="danger">Good</el-button>
|
||||||
|
|
||||||
|
<!-- ❌ 用 Element Plus 做布局(两端均禁止) -->
|
||||||
|
<el-row :gutter="20"><el-col :span="12">...</el-col></el-row>
|
||||||
|
|
||||||
|
<!-- ✅ 用 Tailwind 做布局 -->
|
||||||
|
<div class="grid grid-cols-2 gap-5">...</div>
|
||||||
|
|
||||||
|
<!-- ❌ 用户端:引入 Element Plus 组件 -->
|
||||||
|
<el-button type="primary">禁止</el-button>
|
||||||
|
|
||||||
|
<!-- ✅ 用户端:使用 Headless UI + Tailwind -->
|
||||||
|
<button class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">正确</button>
|
||||||
|
```
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
description: "PHP Hyperf + Swoole 后端开发规范 — 分层架构/RESTful/认证/高并发"
|
||||||
|
globs:
|
||||||
|
- "Case-Database-Backend/app/**/*.php"
|
||||||
|
- "Case-Database-Backend/config/**/*.php"
|
||||||
|
- "Case-Database-Backend/databases/**/*.php"
|
||||||
|
- "Case-Database-Backend/routes/**/*.php"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🖥️ PHP Hyperf + Swoole Backend Standards (Core)
|
||||||
|
|
||||||
|
参考文档: @docs/architecture/api-contracts.md @docs/architecture/system-design.md
|
||||||
|
|
||||||
|
## 分层架构
|
||||||
|
|
||||||
|
- 严格遵循 Controller → Service → Repository → Model
|
||||||
|
- Controller 仅做参数校验与协议转换,不承载业务规则
|
||||||
|
- 业务规则集中在 Service,数据访问集中在 Repository
|
||||||
|
|
||||||
|
## API 与错误处理
|
||||||
|
|
||||||
|
- API 响应结构统一(成功/失败字段一致)
|
||||||
|
- 业务异常与系统异常分层处理
|
||||||
|
- 禁止把内部错误细节直接暴露给客户端
|
||||||
|
|
||||||
|
## 配置访问规范
|
||||||
|
|
||||||
|
**禁止在 Service/Middleware 中直接使用 `env()` 函数**
|
||||||
|
|
||||||
|
PHP CLI 和 Swoole Worker 环境中**不提供全局 `env()` 函数**(这是 Laravel 特有的),会导致 `Call to undefined function env()` 错误。
|
||||||
|
|
||||||
|
**正确做法**:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ✅ 使用 Hyperf 的 config() 函数读取配置
|
||||||
|
$appEnv = \Hyperf\Config\config('app.env', 'development');
|
||||||
|
$secret = \Hyperf\Config\config('jwt.secret', 'default-secret');
|
||||||
|
|
||||||
|
// ✅ 在配置文件中通过 env() 读取环境变量
|
||||||
|
// config/autoload/jwt.php
|
||||||
|
return [
|
||||||
|
'secret' => env('JWT_SECRET', 'dev-fallback-secret'),
|
||||||
|
'ttl' => env('JWT_TTL', 7200),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ✅ Service/Middleware 中通过 config() 读取
|
||||||
|
$secret = \Hyperf\Config\config('jwt.secret');
|
||||||
|
$ttl = \Hyperf\Config\config('jwt.ttl');
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ Service/Middleware 中直接使用 env()
|
||||||
|
$appEnv = env('APP_ENV', 'development'); // ❌ 报错:Call to undefined function env()
|
||||||
|
$secret = env('JWT_SECRET', 'default'); // ❌ Swoole Worker 中不可用
|
||||||
|
|
||||||
|
// ❌ 使用 Laravel 的 config()(无命名空间前缀)
|
||||||
|
$secret = config('jwt.secret'); // ❌ Hyperf 中不存在全局 config() 函数
|
||||||
|
```
|
||||||
|
|
||||||
|
**JWT Secret 统一规则**:
|
||||||
|
- AuthService 和 JwtAuthMiddleware 必须使用相同的 secret
|
||||||
|
- 禁止使用 `md5(__DIR__)` 等动态生成的 secret(路径不同会导致不一致)
|
||||||
|
- 生产环境必须使用环境变量 `JWT_SECRET`
|
||||||
|
|
||||||
|
**配置依赖检查清单**:
|
||||||
|
- [ ] 使用队列的模块已配置 `config/autoload/async_queue.php`
|
||||||
|
- [ ] 使用缓存的模块已配置 Redis 连接池
|
||||||
|
- [ ] 所有 `env()` 都有默认值(防止未设置时报错)
|
||||||
|
- [ ] 敏感配置(密钥、token)在 .env.example 中有说明
|
||||||
|
|
||||||
|
## 协程与并发安全
|
||||||
|
|
||||||
|
- 避免共享可变全局状态
|
||||||
|
- I/O 统一走连接池与超时控制
|
||||||
|
- 长任务异步化(队列/事件)
|
||||||
|
|
||||||
|
## 安全基线
|
||||||
|
|
||||||
|
- 所有输入进行验证与授权校验
|
||||||
|
- 禁止 SQL 拼接与危险函数滥用
|
||||||
|
- 关键操作记录审计日志
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
- [ ] 分层职责无越界
|
||||||
|
- [ ] 异常处理路径完整
|
||||||
|
- [ ] 并发场景无共享状态风险
|
||||||
|
- [ ] 关键接口已做鉴权/限流/日志
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
- `.cursor/rules/references/013-backend-deep.md` — 完整后端规范与示例
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
description: "Hyperf ORM + MySQL 数据库规范 — Schema 设计/迁移/查询优化/高并发"
|
||||||
|
globs:
|
||||||
|
- "**/*.sql"
|
||||||
|
- "Case-Database-Backend/**/*.php"
|
||||||
|
- "Case-Database-Backend/database/migrations/**"
|
||||||
|
- "Case-Database-Backend/database/seeders/**"
|
||||||
|
- "Case-Database-Backend/modules/**/database/migrations/**"
|
||||||
|
- "Case-Database-Backend/modules/**/database/seeders/**"
|
||||||
|
- "Case-Database-Backend/app/Model/**"
|
||||||
|
- "Case-Database-Backend/config/autoload/databases.php"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🗄️ Hyperf ORM + MySQL Database Standards (Core)
|
||||||
|
|
||||||
|
参考文档: @docs/architecture/data-model.md
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
- 所有 schema 变更必须通过 Migration 执行
|
||||||
|
- 高风险变更采用 Expand-Contract,不做一次性破坏升级
|
||||||
|
- 查询默认参数化,避免拼接 SQL
|
||||||
|
- 大表变更先评估锁表风险与回滚路径
|
||||||
|
- 读写分层:Controller 不直接操作 Model
|
||||||
|
|
||||||
|
## 表命名规范(模块前缀)
|
||||||
|
|
||||||
|
**表名必须以所属模块名作为前缀**,格式:`<module>_<entity_plural>`。
|
||||||
|
|
||||||
|
| 模块 | 前缀 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| 用户与权限 | `auth_` | `auth_users`, `auth_roles` |
|
||||||
|
| 案例核心 | `case_` | `case_cases`, `case_images` |
|
||||||
|
| 设计师 | `designer_` | `designer_profiles`, `designer_awards` |
|
||||||
|
| 运营内容 | `operation_` | `operation_banners`, `operation_topics` |
|
||||||
|
| 用户互动 | `interaction_` | `interaction_favorites`, `interaction_comments` |
|
||||||
|
| 日志 | `log_` | `log_user_logins`, `log_downloads` |
|
||||||
|
| 安全风控 | `security_` | `security_blacklists`, `security_risk_events` |
|
||||||
|
| 系统配置 | `system_` | `system_configs` |
|
||||||
|
|
||||||
|
> 多对多关联表同样需要加模块前缀,以主体模块为准:`<module>_<a>_belongs_<b>`。
|
||||||
|
|
||||||
|
## 文件目录约定
|
||||||
|
|
||||||
|
- **根目录**(兜底,用于跨模块全局迁移/种子):
|
||||||
|
- `Case-Database-Backend/database/migrations/` — MigratorFactory 自动扫描
|
||||||
|
- `Case-Database-Backend/database/seeders/` — SeedFactory 自动扫描
|
||||||
|
- **模块目录**(按模块归属):
|
||||||
|
- `Case-Database-Backend/modules/<Module>/database/migrations/` — 该模块迁移
|
||||||
|
- `Case-Database-Backend/modules/<Module>/database/seeders/` — 该模块种子
|
||||||
|
|
||||||
|
执行 `migrate` / `db:seed` 时,MigratorFactory / SeedFactory 会扫描根目录 + 所有 `modules/*/database/migrations` 与 `modules/*/database/seeders`,无需额外配置。
|
||||||
|
|
||||||
|
## 迁移最小流程
|
||||||
|
|
||||||
|
1. 明确需求与回滚方案(必填)
|
||||||
|
2. 生成 migration(命名可读,幂等):`gen:migration` 自动写入 `database/migrations/`
|
||||||
|
3. 本地执行 migrate + rollback 演练
|
||||||
|
4. 校验索引、默认值、nullability 与历史数据兼容
|
||||||
|
5. 更新文档(字段、索引、业务约束)
|
||||||
|
|
||||||
|
## 查询与索引约束
|
||||||
|
|
||||||
|
- 高频筛选列必须有索引;复合索引按最左前缀设计
|
||||||
|
- 分页使用稳定排序(主键兜底)
|
||||||
|
- 禁止 `SELECT *` 用于高频路径
|
||||||
|
- 避免 N+1;需要时使用 eager loading
|
||||||
|
|
||||||
|
## 安全与合规
|
||||||
|
|
||||||
|
- 敏感字段加密或脱敏存储
|
||||||
|
- 生产环境禁止临时 SQL 脚本直改
|
||||||
|
- 迁移文件要可审计、可回放
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
- [ ] migration 可执行且可回滚
|
||||||
|
- [ ] 关键查询通过 explain 检查
|
||||||
|
- [ ] 新增/变更字段有兼容策略
|
||||||
|
- [ ] 文档已同步
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
- `.cursor/rules/references/014-database-deep.md` — 完整数据库规范与示例
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
description: "测试规范 — 测试原则 + 后端 PHPUnit/PHPStan + 覆盖率目标"
|
||||||
|
globs:
|
||||||
|
- "**/*.test.*"
|
||||||
|
- "**/*.spec.*"
|
||||||
|
- "**/__tests__/**"
|
||||||
|
- "**/vitest.config.*"
|
||||||
|
- "**/playwright.config.*"
|
||||||
|
- "Case-Database-Backend/tests/**/*.php"
|
||||||
|
- "Case-Database-Backend/phpunit.xml"
|
||||||
|
- "Case-Database-Backend/phpstan.neon"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Testing Standards
|
||||||
|
|
||||||
|
> 前端测试(Vitest / Vue Test Utils / Playwright)的具体工作流和模板
|
||||||
|
> 由 `skill-vue-testing.mdc` 自动注入,本文件不再重复。
|
||||||
|
|
||||||
|
## 通用原则
|
||||||
|
|
||||||
|
- 测试行为 (behavior),不测试实现细节
|
||||||
|
- 每个测试只验证一件事
|
||||||
|
- 不为覆盖率写无意义测试
|
||||||
|
- 测试是文档的一部分
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 后端测试 (PHP Hyperf)
|
||||||
|
|
||||||
|
### 测试金字塔
|
||||||
|
|
||||||
|
| 类型 | 占比 | 框架 | 目标 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| Unit | 60% | PHPUnit | Service、Repository、工具类 |
|
||||||
|
| Integration | 30% | PHPUnit + TestContainer | API 端点、数据库交互 |
|
||||||
|
| Static | 持续 | PHPStan (Level max) | 类型安全、潜在 Bug |
|
||||||
|
|
||||||
|
### 覆盖率目标
|
||||||
|
|
||||||
|
- 行覆盖率 >= 75%
|
||||||
|
- 分支覆盖率 >= 70%
|
||||||
|
- Service 层 100%
|
||||||
|
- 关键业务路径 100%
|
||||||
|
|
||||||
|
### 运行命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer test # PHPUnit 所有测试
|
||||||
|
composer test:unit # 仅单元测试
|
||||||
|
composer test:feature # 仅功能测试 (API)
|
||||||
|
composer test:coverage # PHPUnit + 覆盖率
|
||||||
|
composer analyse # PHPStan 静态分析
|
||||||
|
```
|
||||||
|
|
||||||
|
### AAA 模式
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Unit\Service;
|
||||||
|
|
||||||
|
use App\Service\Production\OrderService;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class OrderServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testCalculateDiscountReturnsZeroBelowThreshold(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$service = new OrderService();
|
||||||
|
$amount = 99.0;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$discount = $service->calculateDiscount($amount);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertEquals(0.0, $discount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHPUnit 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── Unit/ # 纯单元测试(不依赖框架)
|
||||||
|
│ ├── Service/
|
||||||
|
│ ├── Repository/
|
||||||
|
│ └── Utils/
|
||||||
|
├── Feature/ # 功能测试(HTTP API 级别)
|
||||||
|
│ ├── Auth/
|
||||||
|
│ ├── Production/
|
||||||
|
│ └── Permission/
|
||||||
|
├── bootstrap.php
|
||||||
|
└── phpunit.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHPStan 配置
|
||||||
|
|
||||||
|
```neon
|
||||||
|
# phpstan.neon
|
||||||
|
parameters:
|
||||||
|
level: max
|
||||||
|
paths:
|
||||||
|
- app
|
||||||
|
excludePaths:
|
||||||
|
- app/Command
|
||||||
|
ignoreErrors: []
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 新功能必须附带
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
|
||||||
|
- [ ] Happy path 测试
|
||||||
|
- [ ] 边界条件测试
|
||||||
|
- [ ] 错误情况测试
|
||||||
|
- [ ] 权限/授权测试 (如涉及 v-auth / v-roles)
|
||||||
|
|
||||||
|
> 前端测试的具体写法见 `skill-vue-testing.mdc`
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
|
||||||
|
- [ ] Service 单元测试
|
||||||
|
- [ ] API 功能测试 (HTTP 请求/响应)
|
||||||
|
- [ ] 权限/数据权限测试 (如涉及 DataScope)
|
||||||
|
- [ ] 事务回滚测试 (如涉及数据库写操作)
|
||||||
|
- [ ] PHPStan 通过 (`composer analyse`)
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
---
|
||||||
|
description: "Swoole 协程与高并发编程规范 — 协程安全/连接池/Worker配置/内存管理"
|
||||||
|
globs:
|
||||||
|
- "Case-Database-Backend/app/**/*.php"
|
||||||
|
- "Case-Database-Backend/config/**/*.php"
|
||||||
|
- "Case-Database-Backend/bin/hyperf.php"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# ⚡ Swoole Coroutine & High-Concurrency Standards
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
Swoole 是常驻内存的协程服务器,与传统 PHP-FPM 有本质区别。
|
||||||
|
**每个请求共享 Worker 进程内存**,必须避免全局状态污染。
|
||||||
|
|
||||||
|
## 协程安全规则
|
||||||
|
|
||||||
|
### 绝对禁止
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ 全局变量存储请求数据(会被其他协程覆盖)
|
||||||
|
global $currentUser;
|
||||||
|
|
||||||
|
// ❌ 静态属性存储请求级数据
|
||||||
|
class UserContext {
|
||||||
|
public static ?User $user = null; // WRONG: shared across coroutines
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 阻塞 I/O 函数(会阻塞整个 Worker)
|
||||||
|
file_get_contents('https://api.example.com'); // use Guzzle coroutine client
|
||||||
|
sleep(5); // use Coroutine::sleep()
|
||||||
|
flock($fp, LOCK_EX); // use Redis distributed lock
|
||||||
|
|
||||||
|
// ❌ 非协程安全的全局单例
|
||||||
|
$pdo = new PDO(...); // use connection pool instead
|
||||||
|
```
|
||||||
|
|
||||||
|
### 正确做法
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ✅ 使用 Hyperf Context 传递请求数据
|
||||||
|
use Hyperf\Context\Context;
|
||||||
|
|
||||||
|
// 在中间件中设置
|
||||||
|
Context::set('current_user', $user);
|
||||||
|
|
||||||
|
// 在任意位置读取(协程安全)
|
||||||
|
$user = Context::get('current_user');
|
||||||
|
|
||||||
|
// ✅ 协程安全的延时
|
||||||
|
use Swoole\Coroutine;
|
||||||
|
Coroutine::sleep(0.1);
|
||||||
|
|
||||||
|
// ✅ 使用协程 HTTP 客户端
|
||||||
|
use Hyperf\Guzzle\ClientFactory;
|
||||||
|
$client = $this->clientFactory->create();
|
||||||
|
$response = $client->get('https://api.example.com');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 连接池配置
|
||||||
|
|
||||||
|
### MySQL 连接池
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/autoload/databases.php
|
||||||
|
'pool' => [
|
||||||
|
// 公式: min = Worker数, max = Worker数 * 2 ~ 4
|
||||||
|
'min_connections' => (int) env('DB_POOL_MIN', 5),
|
||||||
|
'max_connections' => (int) env('DB_POOL_MAX', 50),
|
||||||
|
'connect_timeout' => 10.0, // seconds
|
||||||
|
'wait_timeout' => 3.0, // wait for available connection
|
||||||
|
'heartbeat' => -1, // disable heartbeat
|
||||||
|
'max_idle_time' => 60, // recycle idle connections
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis 连接池
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/autoload/redis.php
|
||||||
|
return [
|
||||||
|
'default' => [
|
||||||
|
'host' => env('REDIS_HOST', 'localhost'),
|
||||||
|
'port' => (int) env('REDIS_PORT', 6379),
|
||||||
|
'auth' => env('REDIS_AUTH', null),
|
||||||
|
'db' => (int) env('REDIS_DB', 0),
|
||||||
|
'pool' => [
|
||||||
|
'min_connections' => 5,
|
||||||
|
'max_connections' => 30,
|
||||||
|
'connect_timeout' => 10.0,
|
||||||
|
'wait_timeout' => 3.0,
|
||||||
|
'heartbeat' => -1,
|
||||||
|
'max_idle_time' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 连接池容量计算
|
||||||
|
|
||||||
|
```
|
||||||
|
单 Worker 最大协程数: max_coroutine (default: 100000)
|
||||||
|
实际并发协程数 ≈ QPS * avg_response_time
|
||||||
|
|
||||||
|
DB Pool Max = ceil(concurrent_coroutines * db_query_ratio)
|
||||||
|
Redis Pool Max = ceil(concurrent_coroutines * redis_query_ratio)
|
||||||
|
|
||||||
|
示例 (百万级):
|
||||||
|
- 4 Worker, 每 Worker 1000 并发协程
|
||||||
|
- DB 查询占比 60% → DB Pool Max = 1000 * 0.6 = 600 (per worker)
|
||||||
|
- 实际受 MySQL max_connections 限制,需配合读写分离
|
||||||
|
```
|
||||||
|
|
||||||
|
## Worker 进程配置
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/autoload/server.php
|
||||||
|
'settings' => [
|
||||||
|
'worker_num' => (int) env('SWOOLE_WORKER_NUM', swoole_cpu_num() * 2),
|
||||||
|
'task_worker_num' => (int) env('SWOOLE_TASK_WORKER_NUM', 4),
|
||||||
|
'max_request' => 10000, // restart worker after N requests (prevent memory leak)
|
||||||
|
'max_coroutine' => 100000, // max coroutines per worker
|
||||||
|
'enable_coroutine' => true,
|
||||||
|
'open_tcp_nodelay' => true,
|
||||||
|
'socket_buffer_size' => 3 * 1024 * 1024,
|
||||||
|
'buffer_output_size' => 3 * 1024 * 1024,
|
||||||
|
'package_max_length' => 5 * 1024 * 1024,
|
||||||
|
'heartbeat_check_interval' => 60,
|
||||||
|
'heartbeat_idle_time' => 120,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## 协程并发控制
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Hyperf\Coroutine\Parallel;
|
||||||
|
|
||||||
|
// ✅ 并行执行多个无依赖的 I/O 操作
|
||||||
|
$parallel = new Parallel(10); // max concurrency = 10
|
||||||
|
|
||||||
|
$parallel->add(function () {
|
||||||
|
return $this->orderService->getStatistics($orderId);
|
||||||
|
});
|
||||||
|
|
||||||
|
$parallel->add(function () {
|
||||||
|
return $this->paymentService->getPayments($orderId);
|
||||||
|
});
|
||||||
|
|
||||||
|
$parallel->add(function () {
|
||||||
|
return $this->deliveryService->getDeliveries($orderId);
|
||||||
|
});
|
||||||
|
|
||||||
|
[$stats, $payments, $deliveries] = $parallel->wait();
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Hyperf\Coroutine\WaitGroup;
|
||||||
|
|
||||||
|
// ✅ WaitGroup: wait for all coroutines to complete
|
||||||
|
$wg = new WaitGroup();
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
$wg->add(1);
|
||||||
|
go(function () use ($wg, &$results) {
|
||||||
|
$results['users'] = $this->userService->getOnlineCount();
|
||||||
|
$wg->done();
|
||||||
|
});
|
||||||
|
|
||||||
|
$wg->add(1);
|
||||||
|
go(function () use ($wg, &$results) {
|
||||||
|
$results['orders'] = $this->orderService->getTodayCount();
|
||||||
|
$wg->done();
|
||||||
|
});
|
||||||
|
|
||||||
|
$wg->wait(5.0); // timeout 5s
|
||||||
|
```
|
||||||
|
|
||||||
|
## 内存管理
|
||||||
|
|
||||||
|
- `max_request = 10000`: Worker 处理 N 个请求后自动重启,防止内存泄漏
|
||||||
|
- 避免在全局/静态变量中累积数据
|
||||||
|
- 大数组处理完后手动 `unset()`
|
||||||
|
- 使用 `memory_get_usage()` 监控内存
|
||||||
|
- 对象生命周期与请求绑定,不跨请求持有引用
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ✅ 大数据分批处理
|
||||||
|
ProductionOrder::query()
|
||||||
|
->where('status', 'pending')
|
||||||
|
->chunk(500, function ($orders) {
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
$this->processOrder($order);
|
||||||
|
}
|
||||||
|
// chunk 结束后自动释放内存
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 定时任务
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Hyperf\Crontab\Annotation\Crontab;
|
||||||
|
|
||||||
|
#[Crontab(
|
||||||
|
name: 'CheckPaymentTimeout',
|
||||||
|
rule: '*/5 * * * *',
|
||||||
|
memo: 'Check payment timeout orders',
|
||||||
|
singleton: true, // prevent duplicate execution
|
||||||
|
onOneServer: true, // only run on one server in cluster
|
||||||
|
mutexPool: 'default',
|
||||||
|
)]
|
||||||
|
class CheckPaymentTimeoutTask
|
||||||
|
{
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
// batch processing with chunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能监控
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Swoole Server status
|
||||||
|
$server = $this->container->get(ServerInterface::class);
|
||||||
|
$stats = $server->stats();
|
||||||
|
// Returns: start_time, connection_num, request_count, worker_request_count, coroutine_num
|
||||||
|
|
||||||
|
// Redis monitor
|
||||||
|
$redis = $this->container->get(RedisFactory::class)->get('default');
|
||||||
|
$info = $redis->info();
|
||||||
|
```
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
百万级并发架构设计规范。当讨论水平扩展、限流降级、熔断、
|
||||||
|
高可用架构、容量规划或发布策略时激活。
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🏗️ Million-Level Concurrency Architecture Standards (Core)
|
||||||
|
|
||||||
|
## 架构目标
|
||||||
|
|
||||||
|
- 无状态应用,可水平扩展
|
||||||
|
- 高并发下保持可用与可观测
|
||||||
|
- 峰值流量具备限流、降级、熔断能力
|
||||||
|
|
||||||
|
## 核心设计原则
|
||||||
|
|
||||||
|
- 入口层:Nginx/网关统一限流与基础防护
|
||||||
|
- 应用层:Hyperf 服务无状态,异步化非关键链路
|
||||||
|
- 数据层:MySQL 主从 + Redis 缓存 + 队列削峰
|
||||||
|
- 可靠性:幂等、重试、超时、隔离舱、降级兜底
|
||||||
|
|
||||||
|
## 关键实践
|
||||||
|
|
||||||
|
- 热点读走缓存,写后失效或异步刷新
|
||||||
|
- 长耗时任务走队列/事件,避免阻塞请求线程
|
||||||
|
- 对外依赖必须有 timeout + retry + fallback
|
||||||
|
- 关键业务链路必须可追踪(日志/指标/告警)
|
||||||
|
|
||||||
|
## 容量与发布
|
||||||
|
|
||||||
|
- 扩容前先压测,确认瓶颈位置
|
||||||
|
- 灰度发布与回滚路径必须预案化
|
||||||
|
- 变更优先小步快跑,减少单次风险面
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
- [ ] 高峰流量下核心接口可用
|
||||||
|
- [ ] 限流/降级策略可触发且可恢复
|
||||||
|
- [ ] 异步任务可观测且可重试
|
||||||
|
- [ ] 发布失败可快速回滚
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
- `.cursor/rules/references/017-architecture-deep.md` — 完整并发架构规范与拓扑示例
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
description: "响应式与多端适配规范 — 断点策略/触摸优化/设备测试矩阵。管理端含 Element Plus 适配,用户端纯 Tailwind + Headless UI"
|
||||||
|
globs:
|
||||||
|
- "**/*.vue"
|
||||||
|
- "**/*.css"
|
||||||
|
- "**/*.scss"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📱 Responsive Design & Multi-Device Standards (Core)
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
- Mobile First:先实现手机,再逐级增强到平板/桌面
|
||||||
|
- 内容优先:保证主流程在小屏可达、可读、可操作
|
||||||
|
- 组件一致:Tailwind 断点策略统一(管理端同时适配 Element Plus 组件)
|
||||||
|
|
||||||
|
## 断点最小约定
|
||||||
|
|
||||||
|
- `base`:手机竖屏
|
||||||
|
- `sm/md`:手机横屏与平板
|
||||||
|
- `lg/xl/2xl`:桌面与大屏
|
||||||
|
- 禁止桌面优先 `max-*` 反推写法作为默认策略
|
||||||
|
|
||||||
|
## 布局与交互约束
|
||||||
|
|
||||||
|
- 列表/卡片/表单在窄屏自动降级布局
|
||||||
|
- 触摸目标建议 ≥ 44x44
|
||||||
|
- 弹层与抽屉优先适配移动端交互
|
||||||
|
- 文本与关键按钮不得被遮挡或截断
|
||||||
|
|
||||||
|
## 测试基线
|
||||||
|
|
||||||
|
- 至少验证 3 档视口:手机 / 平板 / 桌面
|
||||||
|
- 验证横竖屏切换、滚动容器、固定定位元素
|
||||||
|
- 关键路径(登录、检索、提交)全端可完成
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
- [ ] 小屏主流程可完成
|
||||||
|
- [ ] 关键控件触摸可达
|
||||||
|
- [ ] 无明显布局溢出/重叠
|
||||||
|
- [ ] 核心页面通过多断点截图比对
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
- `.cursor/rules/references/018-responsive-deep.md` — 完整响应式规范与示例
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
模块化架构规范。当新建模块、讨论架构设计、模块划分、
|
||||||
|
依赖方向或 DDD 边界时激活。含模块通信和文件拆分最佳实践。
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🧩 Modular Architecture Standards (Core)
|
||||||
|
|
||||||
|
## 架构边界
|
||||||
|
|
||||||
|
- 模块按业务能力划分,避免按技术层随意切分
|
||||||
|
- 模块间通过公开接口通信,禁止引用他模块内部实现
|
||||||
|
- 依赖方向单向:UI → Service → Repository → Model
|
||||||
|
|
||||||
|
## 拆分与聚合
|
||||||
|
|
||||||
|
- 单文件职责单一,超过复杂度阈值立即拆分
|
||||||
|
- 公共能力抽到 Case-Database-Frontend-shared/core,业务逻辑保留在业务模块
|
||||||
|
- 新增目录优先复用既有模式,避免“再发明一套结构”
|
||||||
|
|
||||||
|
## 反模式禁止
|
||||||
|
|
||||||
|
- 循环依赖
|
||||||
|
- 横向跨层调用(Controller 直连 DB)
|
||||||
|
- 超大文件混合业务/状态/展示逻辑
|
||||||
|
|
||||||
|
## 评审基线
|
||||||
|
|
||||||
|
- 每次改动需回答:边界是否更清晰?耦合是否降低?
|
||||||
|
- 新增接口必须说明输入/输出和错误语义
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
- [ ] 无循环依赖
|
||||||
|
- [ ] 模块边界清晰,可替换性提升
|
||||||
|
- [ ] 公共能力与业务能力分层明确
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
- `.cursor/rules/references/019-modular-deep.md` — 完整模块化规范与案例
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
description: "Git 与版本控制规范 — 分支/提交/PR 策略"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📦 Git Standards
|
||||||
|
|
||||||
|
## 分支命名
|
||||||
|
|
||||||
|
```
|
||||||
|
feature/TICKET-短描述
|
||||||
|
bugfix/TICKET-短描述
|
||||||
|
hotfix/TICKET-短描述
|
||||||
|
chore/短描述
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commit 规范 (Conventional Commits)
|
||||||
|
|
||||||
|
```
|
||||||
|
type(scope): description
|
||||||
|
|
||||||
|
feat(auth): add OAuth login with Google
|
||||||
|
fix(api): handle null response from payment gateway
|
||||||
|
refactor(db): optimize user query with index
|
||||||
|
test(auth): add unit tests for JWT validation
|
||||||
|
docs(readme): update setup instructions
|
||||||
|
chore(deps): upgrade next to 15.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Types: `feat` `fix` `refactor` `test` `docs` `chore` `style` `perf` `ci` `build`
|
||||||
|
|
||||||
|
## PR 规范
|
||||||
|
|
||||||
|
- 标题符合 Conventional Commits
|
||||||
|
- 描述包含: 改了什么 / 为什么改 / 如何测试
|
||||||
|
- 单个 PR 不超过 400 行变更
|
||||||
|
- 必须有关联 Issue/Ticket
|
||||||
|
|
||||||
|
## Git 操作安全
|
||||||
|
|
||||||
|
- commit 前运行 `npm run lint`(前端)或 `composer analyse`(后端)
|
||||||
|
- 禁止 force push 到 main/master/develop
|
||||||
|
- 合并冲突需人工介入
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
description: "部署与 DevOps 规范 — Docker Compose/CI-CD/环境管理/PHP+Vue双栈"
|
||||||
|
globs:
|
||||||
|
- "Case-Database-Backend/docker-compose*"
|
||||||
|
- "Case-Database-Backend/Dockerfile*"
|
||||||
|
- ".github/workflows/**"
|
||||||
|
- ".env*"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🚢 Deployment Standards (Hyperf + Vue 3)
|
||||||
|
|
||||||
|
参考文档: @docs/runbooks/deployment.md
|
||||||
|
|
||||||
|
## 环境定义
|
||||||
|
|
||||||
|
| 环境 | 用途 | 触发 | 部署方式 |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| Development | 本地开发 | `docker-compose up` | Docker Compose |
|
||||||
|
| Testing | 自动化测试 | PR 创建 | CI Pipeline |
|
||||||
|
| Staging | 预发布验证 | merge to `develop` | Docker Compose / K8s |
|
||||||
|
| Production | 正式环境 | merge to `main` | Docker Compose / K8s |
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 一键启动 (Docker Compose)
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 后端独立运行
|
||||||
|
cd Case-Database-Backend && php bin/hyperf.php start
|
||||||
|
|
||||||
|
# 前端独立运行
|
||||||
|
cd Case-Database-Frontend-user && npm run dev
|
||||||
|
cd ../Case-Database-Frontend-admin && npm run dev
|
||||||
|
|
||||||
|
# 数据库迁移
|
||||||
|
cd Case-Database-Backend && php bin/hyperf.php migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker 规范
|
||||||
|
|
||||||
|
- 使用多阶段构建减小镜像
|
||||||
|
- 非 root 用户运行 (安全)
|
||||||
|
- 固定依赖版本 (不用 `latest` 标签)
|
||||||
|
- 包含 `.dockerignore`
|
||||||
|
- 健康检查 (`HEALTHCHECK`)
|
||||||
|
- Swoole 进程不需要额外的 Supervisor
|
||||||
|
|
||||||
|
## CI/CD 流水线
|
||||||
|
|
||||||
|
```
|
||||||
|
Push
|
||||||
|
├─→ Frontend Job: npm ci → lint → test → build
|
||||||
|
└─→ Backend Job: composer install → phpstan → phpunit
|
||||||
|
│
|
||||||
|
├─→ Security Audit (PR only)
|
||||||
|
│
|
||||||
|
└─→ Deploy
|
||||||
|
├─→ develop → Staging
|
||||||
|
└─→ main → Production
|
||||||
|
```
|
||||||
|
|
||||||
|
参考配置: `.github/workflows/ci.yml`
|
||||||
|
|
||||||
|
## 环境变量管理
|
||||||
|
|
||||||
|
| 位置 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `.env` (本地) | 开发环境,已 gitignore |
|
||||||
|
| `.env.example` | 变量模板,提交到 Git |
|
||||||
|
| Docker Compose env | 容器编排环境变量 |
|
||||||
|
| K8s Secret / ConfigMap | 生产环境密钥管理 |
|
||||||
|
|
||||||
|
**规则**:
|
||||||
|
- 所有密钥通过环境变量注入,禁止硬编码
|
||||||
|
- 定期轮换密钥
|
||||||
|
- 生产环境使用加密存储 (K8s Secret / Vault)
|
||||||
|
|
||||||
|
## 部署检查清单
|
||||||
|
|
||||||
|
### 部署前
|
||||||
|
- [ ] 后端测试通过 (`composer test`)
|
||||||
|
- [ ] 前端构建成功 (`npm run build`)
|
||||||
|
- [ ] 数据库迁移已准备
|
||||||
|
- [ ] 环境变量已配置
|
||||||
|
|
||||||
|
### 部署后
|
||||||
|
- [ ] 健康检查通过 (`/admin/health`)
|
||||||
|
- [ ] 监控无异常
|
||||||
|
- [ ] 数据库迁移成功
|
||||||
|
- [ ] WebSocket 连接正常
|
||||||
|
- [ ] 队列消费进程运行中
|
||||||
|
|
||||||
|
## 回滚策略
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 代码回滚
|
||||||
|
git revert HEAD && git push origin main
|
||||||
|
|
||||||
|
# 数据库回滚
|
||||||
|
php bin/hyperf.php migrate:rollback --step=1
|
||||||
|
|
||||||
|
# Docker 回滚到上一个镜像版本
|
||||||
|
docker-compose pull && docker-compose up -d
|
||||||
|
```
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
性能优化规范与审计流程。当用户讨论性能优化、报告页面慢、加载卡、
|
||||||
|
包体积大、Swoole 调优、数据库查询优化、缓存策略或 Core Web Vitals 时激活。
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Performance Standards & Audit
|
||||||
|
|
||||||
|
> 本文件同时包含性能**诊断流程**和**编码规范**。
|
||||||
|
> 完整审计工作流见:Read `.cursor/skills/performance-audit/SKILL.md`
|
||||||
|
|
||||||
|
## 诊断流程(用户报告性能问题时先走这里)
|
||||||
|
|
||||||
|
### 1. 性能基线
|
||||||
|
|
||||||
|
收集当前数据(前端 `npm run build` + `du -sh dist/`,后端路由数量)。
|
||||||
|
|
||||||
|
### 2. 四维度审计
|
||||||
|
|
||||||
|
**前端渲染** — 不必要重渲染、大列表虚拟化、图片压缩、按需导入
|
||||||
|
**网络请求** — 瀑布式请求、Redis 缓存、Axios 拦截、Nginx 缓存
|
||||||
|
**打包体积** — 大依赖、dynamic import、tree-shaking、barrel exports
|
||||||
|
**数据库查询** — N+1、缺少索引、未限制字段、未分页
|
||||||
|
|
||||||
|
### 3. 输出优化建议
|
||||||
|
|
||||||
|
按影响力排序,每个建议包含:预期收益 + 实施难度 + 具体代码。
|
||||||
|
|
||||||
|
### 诊断验证
|
||||||
|
|
||||||
|
- [ ] 优化前后有可量化对比
|
||||||
|
- [ ] 未引入功能回归
|
||||||
|
- [ ] Core Web Vitals 达标
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能指标目标
|
||||||
|
|
||||||
|
| 指标 | 目标 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| FCP | < 1.5s | 首次内容绘制 |
|
||||||
|
| LCP | < 2.5s | 最大内容绘制 |
|
||||||
|
| INP | < 200ms | 交互到下一帧 |
|
||||||
|
| API P95 | < 200ms | 后端接口响应 |
|
||||||
|
| API P99 | < 500ms | 后端接口响应(长尾) |
|
||||||
|
| DB Query | < 50ms | 单条数据库查询 |
|
||||||
|
| Redis | < 5ms | 缓存操作 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端性能
|
||||||
|
|
||||||
|
### Vite 构建优化
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
target: 'es2015',
|
||||||
|
minify: 'terser',
|
||||||
|
terserOptions: {
|
||||||
|
compress: {
|
||||||
|
drop_console: true,
|
||||||
|
drop_debugger: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||||
|
// 仅管理端:'element-plus': ['element-plus'],
|
||||||
|
'echarts': ['echarts'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chunkSizeWarningLimit: 2000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 加载优化
|
||||||
|
|
||||||
|
- 路由懒加载: `() => import('@/views/module/page/index.vue')`
|
||||||
|
- 管理端 Element Plus 按需导入: `unplugin-vue-components` + `unplugin-auto-import`(用户端不使用 Element Plus)
|
||||||
|
- 大列表虚拟化: `@tanstack/vue-virtual`
|
||||||
|
- 组件懒加载: `defineAsyncComponent()`
|
||||||
|
- 页面缓存: `<keep-alive :include="cachedViews">` 配合 worktab Store
|
||||||
|
|
||||||
|
### 资源优化
|
||||||
|
|
||||||
|
- 图片: WebP 格式 + 压缩 + CDN
|
||||||
|
- 图标: 管理端 `@element-plus/icons-vue` 按需导入;用户端使用 `lucide-vue-next`
|
||||||
|
- 字体: `font-display: swap` 避免阻塞
|
||||||
|
- Gzip: Nginx 开启 gzip 压缩
|
||||||
|
|
||||||
|
### 前端缓存策略
|
||||||
|
|
||||||
|
| 数据 | 方式 | TTL |
|
||||||
|
|------|------|-----|
|
||||||
|
| 产品列表 | Pinia + localStorage | 5 分钟 |
|
||||||
|
| 字典数据 | Pinia + localStorage | 1 小时 |
|
||||||
|
| 用户配置 | Pinia persist | 持久 |
|
||||||
|
| 路由/菜单 | Pinia persist | 持久 |
|
||||||
|
| 接口响应 | 不缓存(后端 Redis) | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 后端性能
|
||||||
|
|
||||||
|
### Swoole 调优
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/autoload/server.php
|
||||||
|
'settings' => [
|
||||||
|
'worker_num' => swoole_cpu_num() * 2,
|
||||||
|
'task_worker_num' => 4,
|
||||||
|
'max_request' => 10000,
|
||||||
|
'max_coroutine' => 100000,
|
||||||
|
'enable_coroutine' => true,
|
||||||
|
'open_tcp_nodelay' => true,
|
||||||
|
'socket_buffer_size' => 3 * 1024 * 1024,
|
||||||
|
'buffer_output_size' => 3 * 1024 * 1024,
|
||||||
|
'package_max_length' => 5 * 1024 * 1024,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库查询优化
|
||||||
|
|
||||||
|
| 规则 | 正确做法 | 反模式 |
|
||||||
|
|------|---------|--------|
|
||||||
|
| 避免 N+1 | `with(['customer', 'subOrders'])` | 循环中查询关联 |
|
||||||
|
| 明确列名 | `select('id', 'order_no', 'status')` | `SELECT *` |
|
||||||
|
| 分页 | 游标分页 `WHERE id > ? LIMIT ?` | `OFFSET` 深分页 |
|
||||||
|
| 批量操作 | `insert([...])` / `chunk(500)` | 循环单条 SQL |
|
||||||
|
| 索引 | 复合索引遵循最左前缀 | 过多单列索引 |
|
||||||
|
| 大表 COUNT | 缓存计数 / 近似值 | `COUNT(*)` 全表 |
|
||||||
|
|
||||||
|
### 缓存优化
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Cache-Aside 模式 + TTL 抖动
|
||||||
|
$data = $cache->withCache("order:{$id}", 300, fn() => Order::find($id));
|
||||||
|
|
||||||
|
// 缓存失效:写操作后立即清除
|
||||||
|
$cache->invalidate("order:{$id}");
|
||||||
|
$cache->invalidatePattern('orders:list:*');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 协程并发
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 并行无依赖 I/O 操作
|
||||||
|
$parallel = new Parallel(10);
|
||||||
|
$parallel->add(fn() => $this->orderService->getStatistics($id));
|
||||||
|
$parallel->add(fn() => $this->paymentService->getPayments($id));
|
||||||
|
[$stats, $payments] = $parallel->wait();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 服务器性能
|
||||||
|
|
||||||
|
### Nginx 优化
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Gzip 压缩
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1k;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css application/json application/typescript image/svg+xml;
|
||||||
|
|
||||||
|
# 静态资源长期缓存(Vite 产物带 hash)
|
||||||
|
location /assets/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP/2
|
||||||
|
listen 443 ssl http2;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 连接池调优
|
||||||
|
|
||||||
|
```php
|
||||||
|
// MySQL 连接池
|
||||||
|
'pool' => [
|
||||||
|
'min_connections' => 5,
|
||||||
|
'max_connections' => 50,
|
||||||
|
'connect_timeout' => 10.0,
|
||||||
|
'wait_timeout' => 3.0,
|
||||||
|
'max_idle_time' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
// Redis 连接池
|
||||||
|
'pool' => [
|
||||||
|
'min_connections' => 5,
|
||||||
|
'max_connections' => 30,
|
||||||
|
'connect_timeout' => 10.0,
|
||||||
|
'wait_timeout' => 3.0,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 禁止事项
|
||||||
|
|
||||||
|
- 前端同步阻塞主线程的操作
|
||||||
|
- 未压缩的大图片 (> 200KB)
|
||||||
|
- 无限滚动不做虚拟化
|
||||||
|
- Swoole Worker 中执行阻塞 I/O (`file_get_contents`, `sleep`)
|
||||||
|
- 全表扫描无 WHERE 条件
|
||||||
|
- 事务内包含远程调用
|
||||||
|
- 单事务操作 > 1000 行
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
description: "无障碍规范 — WCAG AA/语义化/键盘导航/ARIA/Vue 3 无障碍实践"
|
||||||
|
globs:
|
||||||
|
- "**/*.vue"
|
||||||
|
- "**/*.html"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# ♿ Accessibility (A11y) — WCAG AA Standards (Core)
|
||||||
|
|
||||||
|
## WCAG AA 最小基线
|
||||||
|
|
||||||
|
- 可感知:文本对比度满足 AA;图片有替代文本
|
||||||
|
- 可操作:全部关键功能可键盘完成;无键盘陷阱
|
||||||
|
- 可理解:错误提示清晰、可定位、可恢复
|
||||||
|
- 健壮性:语义化 HTML 优先,ARIA 正确使用
|
||||||
|
|
||||||
|
## Vue 实践约束
|
||||||
|
|
||||||
|
- 表单控件必须有关联 label/description
|
||||||
|
- 弹窗/抽屉管理焦点进入与返回
|
||||||
|
- 自定义交互组件提供键盘等价操作
|
||||||
|
- 状态变化(成功/失败/加载)需可被辅助技术感知
|
||||||
|
|
||||||
|
## 检查方式
|
||||||
|
|
||||||
|
- 静态:eslint-plugin-vuejs-accessibility / 基础规则
|
||||||
|
- 自动:Lighthouse / axe
|
||||||
|
- 手动:Tab 顺序、屏幕阅读器关键路径
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
- [ ] 关键页面通过键盘操作完成主流程
|
||||||
|
- [ ] 表单错误可读且可聚焦
|
||||||
|
- [ ] 颜色对比度满足 AA
|
||||||
|
- [ ] 对话框焦点管理正确
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
- `.cursor/rules/references/023-accessibility-deep.md` — 完整 A11y 规范与代码示例
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
description: "日志与错误监控规范 — Hyperf Monolog/Vue 3 错误处理/Swoole 监控/告警策略"
|
||||||
|
globs:
|
||||||
|
- "Case-Database-Backend/app/**/*.php"
|
||||||
|
- "Case-Database-Backend/config/autoload/logger.php"
|
||||||
|
- "Case-Database-Frontend-user/src/**/*.ts"
|
||||||
|
- "Case-Database-Frontend-user/src/**/*.vue"
|
||||||
|
- "Case-Database-Frontend-admin/src/**/*.ts"
|
||||||
|
- "Case-Database-Frontend-admin/src/**/*.vue"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📊 Logging & Error Monitoring Standards (Core)
|
||||||
|
|
||||||
|
## 日志基线
|
||||||
|
|
||||||
|
- 统一结构化日志(JSON)
|
||||||
|
- 日志级别分层:debug/info/warn/error
|
||||||
|
- 关键字段统一:trace_id、user_id、module、action、latency
|
||||||
|
- 严禁记录密钥、密码、完整 token
|
||||||
|
|
||||||
|
## 错误处理基线
|
||||||
|
|
||||||
|
- 前端:全局错误边界 + 请求错误统一处理
|
||||||
|
- 后端:统一异常映射,区分业务异常与系统异常
|
||||||
|
- 所有 error 需带上下文,便于定位与追踪
|
||||||
|
|
||||||
|
## 指标与告警
|
||||||
|
|
||||||
|
- 最小指标:QPS、错误率、P95/P99 延迟、队列堆积
|
||||||
|
- 告警分级:P1(立即处理)/P2(当日处理)/P3(观察)
|
||||||
|
- 告警必须可行动:包含服务、时间窗、建议操作
|
||||||
|
|
||||||
|
## 运维可观测性
|
||||||
|
|
||||||
|
- 关键链路可追踪(trace/span)
|
||||||
|
- 部署后监控窗口至少覆盖 30 分钟
|
||||||
|
- 事故复盘沉淀到 runbook
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
- [ ] 日志结构字段统一
|
||||||
|
- [ ] 错误有分类与可检索上下文
|
||||||
|
- [ ] 核心指标可视化并配置告警
|
||||||
|
- [ ] 发布后监控与回滚流程可执行
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
- `.cursor/rules/references/024-monitoring-deep.md` — 完整监控规范与配置示例
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
description: "版权与许可合规 — 确保所有依赖、素材、字体均为开源可商用,防止法律纠纷"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# ⚖️ Licensing & Copyright Compliance
|
||||||
|
|
||||||
|
> **核心原则**:项目中使用的所有第三方代码、组件、字体、图标、图片和素材,
|
||||||
|
> 必须为**开源、免费、可商用**的许可证,不得引入任何可能导致法律纠纷的资源。
|
||||||
|
|
||||||
|
## 许可证白名单
|
||||||
|
|
||||||
|
以下许可证允许在商业项目中使用:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ 允许使用:
|
||||||
|
MIT / ISC / 0BSD / BSD-2-Clause / BSD-3-Clause
|
||||||
|
Apache-2.0 / Unlicense / WTFPL / Zlib / PostgreSQL
|
||||||
|
CC0-1.0 (公共领域) / CC-BY-4.0 (署名即可)
|
||||||
|
SIL OFL 1.1 (字体专用) / BlueOak-1.0.0
|
||||||
|
MPL-2.0 (仅修改的文件需开源,不传染整个项目)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 许可证黑名单
|
||||||
|
|
||||||
|
以下许可证**禁止使用**,因其传染性或商用限制:
|
||||||
|
|
||||||
|
```
|
||||||
|
🚫 禁止使用:
|
||||||
|
GPL-2.0 / GPL-3.0 — 强传染,要求整个项目开源
|
||||||
|
AGPL-3.0 — 网络使用也触发传染
|
||||||
|
SSPL — MongoDB 的限制性许可
|
||||||
|
BSL (Business Source License) — 商用限制
|
||||||
|
CC-BY-NC-* — 禁止商用
|
||||||
|
CC-BY-SA-* — 用于代码时有传染性
|
||||||
|
任何标注 "Non-Commercial" / "Personal Use Only" 的许可
|
||||||
|
任何标注 "Proprietary" / "All Rights Reserved" 的资源
|
||||||
|
```
|
||||||
|
|
||||||
|
## 灰名单 — 需要确认
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ 需人工确认:
|
||||||
|
LGPL-2.1 / LGPL-3.0 — 动态链接通常可以,静态链接有传染风险
|
||||||
|
Artistic-2.0 — 条款复杂,需逐案评估
|
||||||
|
EPL-2.0 — 弱传染,需评估使用方式
|
||||||
|
无许可证的开源项目 — 默认 All Rights Reserved,禁止使用
|
||||||
|
```
|
||||||
|
|
||||||
|
**遇到灰名单许可证时**:停止安装,向用户说明风险,等待确认。
|
||||||
|
|
||||||
|
## NPM / Composer 依赖
|
||||||
|
|
||||||
|
安装任何新依赖前,必须检查许可证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# NPM: 查看包的许可证
|
||||||
|
npm info <package-name> license
|
||||||
|
|
||||||
|
# Composer: 查看包的许可证
|
||||||
|
composer show <vendor/package> | grep -i license
|
||||||
|
```
|
||||||
|
|
||||||
|
**安装前检查清单**:
|
||||||
|
1. 确认许可证在白名单中
|
||||||
|
2. 如果许可证在灰名单中 → 停止并告知用户
|
||||||
|
3. 如果许可证在黑名单中 → 拒绝安装并推荐替代方案
|
||||||
|
4. 如果没有许可证 → 视为 All Rights Reserved,拒绝使用
|
||||||
|
|
||||||
|
**推荐开源替代**:
|
||||||
|
|
||||||
|
| 避免使用 | 推荐替代 | 许可证 |
|
||||||
|
|---------|---------|--------|
|
||||||
|
| Moment.ts (许可证安全但已弃用) | Day.ts / date-fns | MIT |
|
||||||
|
| Font Awesome Pro | Lucide / Heroicons / Phosphor | ISC / MIT |
|
||||||
|
| ag-Grid Enterprise | AG Grid Community / TanStack Table | MIT |
|
||||||
|
| Highcharts (商用付费) | ECharts / Chart.ts / Recharts | Apache-2.0 / MIT |
|
||||||
|
| Ant Design Pro (部分模板付费) | Element Plus / Naive UI | MIT |
|
||||||
|
| Quill 某些插件 (付费) | Tiptap / Editor.ts | MIT |
|
||||||
|
|
||||||
|
## 字体规范
|
||||||
|
|
||||||
|
**允许使用的字体来源**:
|
||||||
|
|
||||||
|
| 来源 | 许可证 | 示例 |
|
||||||
|
|------|--------|------|
|
||||||
|
| Google Fonts | SIL OFL 1.1 | Inter, Noto Sans SC, Roboto |
|
||||||
|
| Adobe Source 系列 | SIL OFL 1.1 | Source Han Sans (思源黑体) |
|
||||||
|
| 阿里巴巴普惠体 | 免费可商用 | Alibaba PuHuiTi |
|
||||||
|
| OPPO Sans | 免费可商用(需注册) | OPPO Sans |
|
||||||
|
| 鸿蒙字体 | 免费可商用 | HarmonyOS Sans |
|
||||||
|
|
||||||
|
```
|
||||||
|
🚫 禁止使用:
|
||||||
|
微软雅黑 (Windows 自带≠可商用) / 宋体 (方正版权)
|
||||||
|
苹方 (仅限 Apple 设备内置使用)
|
||||||
|
方正系列字体 (需购买商用授权)
|
||||||
|
汉仪系列字体 (需购买商用授权)
|
||||||
|
造字工房系列 (需购买商用授权)
|
||||||
|
任何从网络下载的无明确授权字体
|
||||||
|
```
|
||||||
|
|
||||||
|
**中文字体推荐方案**:
|
||||||
|
- 正文:`"Noto Sans SC", "Source Han Sans SC", "Alibaba PuHuiTi", system-ui, sans-serif`
|
||||||
|
- 代码:`"JetBrains Mono", "Fira Code", "Source Code Pro", monospace`
|
||||||
|
|
||||||
|
## 图标规范
|
||||||
|
|
||||||
|
**允许使用的图标库**:
|
||||||
|
|
||||||
|
| 库 | 许可证 | 适用场景 |
|
||||||
|
|----|--------|---------|
|
||||||
|
| Element Plus Icons | MIT | 与 Element Plus 配套 |
|
||||||
|
| Lucide | ISC | 通用线性图标 |
|
||||||
|
| Heroicons | MIT | Tailwind 生态 |
|
||||||
|
| Phosphor Icons | MIT | 多粗细变体 |
|
||||||
|
| Material Design Icons | Apache-2.0 | Google 风格 |
|
||||||
|
| Tabler Icons | MIT | 线性图标 |
|
||||||
|
| Iconify (聚合平台) | 取决于具体图标集 | 需逐集检查许可证 |
|
||||||
|
|
||||||
|
```
|
||||||
|
🚫 禁止使用:
|
||||||
|
Font Awesome Pro (付费版)
|
||||||
|
Flaticon (免费版需署名,部分禁止商用)
|
||||||
|
IconFinder (大部分需购买)
|
||||||
|
Iconfont (阿里) — 个人上传的图标版权不可控
|
||||||
|
任何从设计网站截图或扒取的图标
|
||||||
|
```
|
||||||
|
|
||||||
|
## 图片与媒体素材
|
||||||
|
|
||||||
|
**允许使用的图片来源**:
|
||||||
|
|
||||||
|
| 来源 | 许可证 | 注意事项 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| Unsplash | Unsplash License | 免费商用,无需署名 |
|
||||||
|
| Pexels | Pexels License | 免费商用,无需署名 |
|
||||||
|
| Pixabay | Pixabay License | 免费商用,部分需检查 |
|
||||||
|
| 自行创建/AI 生成 | 自有版权 | 确认 AI 工具的商用条款 |
|
||||||
|
| unDraw | MIT | 开源插画 |
|
||||||
|
| Storyset | Freepik 免费协议 | 需署名 |
|
||||||
|
|
||||||
|
```
|
||||||
|
🚫 禁止使用:
|
||||||
|
Google 图片搜索直接下载 (版权不明)
|
||||||
|
百度图片搜索直接下载
|
||||||
|
Getty Images / Shutterstock / 视觉中国 (付费)
|
||||||
|
Pinterest / Dribbble 上的作品 (他人版权)
|
||||||
|
任何来源不明的图片
|
||||||
|
竞品产品的截图用于商业用途
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI 生成图片注意**:使用 AI 生成图片时,确认所用工具的商用条款允许商业使用。
|
||||||
|
|
||||||
|
## 代码引用规范
|
||||||
|
|
||||||
|
从外部来源引用代码片段时:
|
||||||
|
|
||||||
|
1. **StackOverflow** — CC-BY-SA 4.0 许可,引用需署名,大段引用有传染风险,建议理解后重写
|
||||||
|
2. **GitHub 开源项目** — 遵循项目许可证,MIT/Apache 可直接使用
|
||||||
|
3. **技术博客** — 版权归作者,参考思路后自行实现,不要直接复制
|
||||||
|
4. **AI 生成的代码** — 通常归使用者,但需确认工具的 ToS
|
||||||
|
|
||||||
|
## 违规处理
|
||||||
|
|
||||||
|
当检测到以下情况时,**立即停止并警告用户**:
|
||||||
|
|
||||||
|
1. 即将安装黑名单许可证的依赖
|
||||||
|
2. 代码中引用了版权受限的字体名称
|
||||||
|
3. 推荐了付费/非商用的素材来源
|
||||||
|
4. 从未知来源复制大段代码
|
||||||
|
|
||||||
|
警告格式:
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ 版权风险: [描述问题]
|
||||||
|
许可证: [具体许可证]
|
||||||
|
风险: [可能的法律后果]
|
||||||
|
替代方案: [推荐的开源可商用替代]
|
||||||
|
```
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
安全编码规范 — 开发时主动安全实践(Secure by Default)。
|
||||||
|
基于 Project CodeGuard 框架,覆盖注入防护、加密算法、访问控制、会话安全、
|
||||||
|
客户端安全和文件上传。编写 PHP 或 TypeScript/Vue 代码时自动应用。
|
||||||
|
globs:
|
||||||
|
- "**/*.php"
|
||||||
|
- "**/*.ts"
|
||||||
|
- "**/*.vue"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔐 Secure Coding Standards (Core)
|
||||||
|
|
||||||
|
## 总原则
|
||||||
|
|
||||||
|
- 外部输入默认不可信
|
||||||
|
- 安全优先于便捷(Secure by Default)
|
||||||
|
- 敏感操作必须有鉴权、审计与告警
|
||||||
|
|
||||||
|
## 必须遵守
|
||||||
|
|
||||||
|
- 禁用弱算法:MD5/SHA1/DES/3DES/RC4/AES-ECB
|
||||||
|
- 注入防护:SQL/命令/模板注入全面参数化与白名单
|
||||||
|
- 访问控制:每个资源接口都做所有权/权限校验
|
||||||
|
- 会话安全:HttpOnly + Secure + SameSite,避免 token 泄漏
|
||||||
|
- 文件上传:校验扩展名 + MIME + magic bytes,隔离存储
|
||||||
|
|
||||||
|
## 前后端协同
|
||||||
|
|
||||||
|
- 前端不信任本地状态作为权限依据
|
||||||
|
- 后端做最终授权裁决
|
||||||
|
- 错误提示不暴露内部实现细节
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
- [ ] 关键接口已做输入验证与授权
|
||||||
|
- [ ] 无禁用算法与危险函数
|
||||||
|
- [ ] 会话与凭证存储符合安全基线
|
||||||
|
- [ ] 上传与下载链路具备安全防护
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
- `.cursor/rules/references/026-secure-coding-deep.md` — 完整安全编码规范与示例
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
Subagent 编排系统。当需要委托子任务给独立 Agent 并行执行时使用。
|
||||||
|
包含 repo-scout、test-runner、security-sentinel 三个模板。
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Subagents 系统
|
||||||
|
|
||||||
|
Subagents 是本项目定义的使用约定,通过 Task 工具的 subagent_type 参数执行,
|
||||||
|
实现任务隔离和并行处理。
|
||||||
|
|
||||||
|
## 调用方式
|
||||||
|
|
||||||
|
先 Read 对应的 `.cursor/agents/*.md` 文件,将完整内容作为 `prompt` 传递给 Task 工具。
|
||||||
|
|
||||||
|
## 可用 Subagents
|
||||||
|
|
||||||
|
| Subagent | 职责 | 模式 |
|
||||||
|
|----------|------|------|
|
||||||
|
| **repo-scout** | 代码库探索、文件定位 | 只读 |
|
||||||
|
| **test-runner** | 运行测试、报告结果 | 后台 |
|
||||||
|
| **security-sentinel** | 安全扫描、漏洞检测 | 只读 |
|
||||||
|
|
||||||
|
## 使用模式
|
||||||
|
|
||||||
|
- **清洁上下文**:每个 Subagent 独立上下文,不污染主对话
|
||||||
|
- **并行执行**:多个 Subagent 可同时运行
|
||||||
|
- **链式编排**:Scout → Implement → Verify
|
||||||
|
- **扇出模式**:同一任务拆分给多个 Subagent 并行
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
安全重构技能。当用户要求重构代码、清理技术债、优化代码结构
|
||||||
|
或简化复杂逻辑时激活。行为不变前提下改善内部结构。
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Refactoring Workflow
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、模板和深度参考见:
|
||||||
|
> Read `.cursor/skills/refactoring/SKILL.md`
|
||||||
|
|
||||||
|
依赖技能:`vue-testing`(Cursor 已通过 `skill-vue-testing` 规则自动加载)
|
||||||
|
|
||||||
|
## 黄金规则
|
||||||
|
|
||||||
|
> 重构 = 不改变外部行为的前提下改善内部结构。
|
||||||
|
> 每一步都要小到"显然正确",每一步之后测试必须全部通过。
|
||||||
|
> 如果测试不足,先补测试再重构。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 评估现状
|
||||||
|
|
||||||
|
1. 读取目标代码,识别"代码坏味道"
|
||||||
|
2. 运行现有测试确认基线:`npm test -- --related`
|
||||||
|
3. 如果测试覆盖不足,**先补充测试**
|
||||||
|
|
||||||
|
### 2. 制定重构计划
|
||||||
|
|
||||||
|
| 坏味道 | 重构手法 | 风险 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| 函数过长(>50行) | Extract Function | 低 |
|
||||||
|
| 重复代码 | Extract + Consolidate | 低 |
|
||||||
|
| 过长参数列表 | Introduce Parameter Object | 低 |
|
||||||
|
| Switch/If 过多 | Replace with Polymorphism | 中 |
|
||||||
|
| 大类 | Extract Class | 中 |
|
||||||
|
| Feature Envy | Move Method | 中 |
|
||||||
|
| 深层嵌套 | Replace Nested with Guard Clauses | 低 |
|
||||||
|
| 大型组件 | 拆分为子组件 | 中 |
|
||||||
|
| 条件复杂度 | 策略模式/映射表 | 中 |
|
||||||
|
| 魔法数字 | 提取常量/枚举 | 低 |
|
||||||
|
|
||||||
|
### 3. 小步执行
|
||||||
|
|
||||||
|
对每一步:
|
||||||
|
1. 做一个**原子级**的重构变更
|
||||||
|
2. 运行测试 -> 全部通过
|
||||||
|
3. Git commit(可选,但建议频繁提交)
|
||||||
|
4. 继续下一步
|
||||||
|
|
||||||
|
## 禁止事项
|
||||||
|
|
||||||
|
- 不要在重构中同时添加新功能
|
||||||
|
- 不要一次改太多(改完跑不了测试)
|
||||||
|
- 不要在没有测试的情况下重构核心逻辑
|
||||||
|
- 不要重构正在被其他人修改的代码
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] 所有测试通过
|
||||||
|
- [ ] ESLint 无报错
|
||||||
|
- [ ] 外部行为未改变
|
||||||
|
- [ ] 代码可读性提升
|
||||||
@@ -0,0 +1,618 @@
|
|||||||
|
# 011-vue.mdc (Deep Reference)
|
||||||
|
|
||||||
|
> 该文件为原始详细规范归档,供 Tier 3 按需读取。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# 🟢 Vue 3 / Vite Standards
|
||||||
|
|
||||||
|
> **⚠️ 双前端区分**:本文件中的 Element Plus 相关内容**仅适用于管理端** (`Case-Database-Frontend-admin/`)。
|
||||||
|
> 用户端 (`Case-Database-Frontend-user/`) 使用 Headless UI + Tailwind CSS,**禁止引入 Element Plus**。
|
||||||
|
|
||||||
|
## 组件规范
|
||||||
|
|
||||||
|
- **所有组件使用 `<script setup>`**(Composition API + 语法糖,不使用 `lang` 类型标注)
|
||||||
|
- 禁止 Options API(除非集成第三方库)
|
||||||
|
- 禁止 Class 组件
|
||||||
|
- 组件嵌套不超过 3 层,超过则拆分
|
||||||
|
- 使用 `defineProps` + `defineEmits` 声明接口(编译时宏,无需导入)
|
||||||
|
- Named exports for composables;组件文件本身使用 default export(Vue 惯例)
|
||||||
|
|
||||||
|
## 项目目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── api/ # API 接口层(按业务模块分目录)
|
||||||
|
│ ├── production/ # 生产管理 API
|
||||||
|
│ ├── auth.ts # 认证 API
|
||||||
|
│ └── common.ts # 通用 API
|
||||||
|
├── assets/ # 静态资源
|
||||||
|
│ ├── styles/ # 全局样式 (SCSS)
|
||||||
|
│ ├── icons/ # 图标
|
||||||
|
│ └── images/ # 图片
|
||||||
|
├── components/ # 公共组件
|
||||||
|
│ ├── core/ # 核心通用组件
|
||||||
|
│ └── custom/ # 业务组件
|
||||||
|
├── config/ # 前端配置
|
||||||
|
├── directives/ # 自定义指令
|
||||||
|
├── hooks/ # 组合式函数
|
||||||
|
├── layouts/ # 布局组件
|
||||||
|
├── locales/ # 国际化
|
||||||
|
├── router/ # 路由配置
|
||||||
|
│ ├── guards/ # 路由守卫
|
||||||
|
│ └── routes/ # 路由定义
|
||||||
|
├── store/ # Pinia 状态管理
|
||||||
|
│ └── modules/ # Store 模块
|
||||||
|
├── types/ # JSDoc 类型定义(可选,.ts 文件)
|
||||||
|
├── utils/ # 工具函数
|
||||||
|
│ ├── request.ts # Axios 封装
|
||||||
|
│ ├── storage/ # 本地存储工具
|
||||||
|
│ └── websocket/ # WebSocket 工具
|
||||||
|
└── views/ # 页面视图(按业务模块)
|
||||||
|
├── auth/ # 认证页面
|
||||||
|
├── dashboard/ # 仪表盘
|
||||||
|
└── production/ # 生产管理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件模板
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: 'primary', // 'primary' | 'secondary' | 'ghost'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'default', // 'small' | 'default' | 'large'
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['click'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-button
|
||||||
|
:type="props.variant === 'ghost' ? 'default' : props.variant"
|
||||||
|
:size="props.size"
|
||||||
|
:disabled="props.disabled"
|
||||||
|
@click="emit('click', $event)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Element Plus 使用规范(仅管理端)
|
||||||
|
|
||||||
|
> 以下内容仅适用于 `Case-Database-Frontend-admin/`,用户端禁止使用 Element Plus。
|
||||||
|
|
||||||
|
- **按需导入**:使用 `unplugin-vue-components` + `unplugin-auto-import` 自动导入
|
||||||
|
- **表单验证**:使用 Element Plus 内置 `el-form` rules,复杂场景配合 `async-validator`
|
||||||
|
- **主题定制**:通过 SCSS 变量覆盖 Element Plus 默认主题
|
||||||
|
- **图标**:使用 `@element-plus/icons-vue`,按需导入
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts 自动导入配置
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
AutoImport({ resolvers: [ElementPlusResolver()] }),
|
||||||
|
Components({ resolvers: [ElementPlusResolver()] }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 状态管理选择
|
||||||
|
|
||||||
|
| 场景 | 方案 |
|
||||||
|
|------|------|
|
||||||
|
| 组件内部 | `ref` / `reactive` |
|
||||||
|
| 计算属性 | `computed` |
|
||||||
|
| 跨组件共享 | **Pinia** (`defineStore`) |
|
||||||
|
| 服务端状态 | Axios + Pinia Action |
|
||||||
|
| 表单验证 | 管理端:Element Plus `el-form` rules;用户端:自定义验证 |
|
||||||
|
| URL 状态 | `useRoute` / `useRouter`(vue-router) |
|
||||||
|
|
||||||
|
## Axios 封装规范
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/utils/request.ts
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const service = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
|
timeout: 60000,
|
||||||
|
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Request interceptor: inject token
|
||||||
|
service.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
// Response interceptor: unified error handling
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
const { code, data, message } = response.data
|
||||||
|
if (code === 200) return data
|
||||||
|
return Promise.reject(new Error(message || 'Request failed'))
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// Token expired -> redirect to login
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vue Router 路由规范
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/router/guards/auth.ts
|
||||||
|
export function setupAuthGuard(router) {
|
||||||
|
router.beforeEach(async (to, _from, next) => {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
||||||
|
if (to.meta.requiresAuth && !token) {
|
||||||
|
return next({ name: 'login', query: { redirect: to.fullPath } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic route loading for RBAC
|
||||||
|
if (token && !hasLoadedDynamicRoutes()) {
|
||||||
|
await loadDynamicRoutes(router)
|
||||||
|
return next({ ...to, replace: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composables 目录约定
|
||||||
|
|
||||||
|
所有 composable 位于 `src/hooks/`,以 `use` 前缀命名:
|
||||||
|
|
||||||
|
| Composable | 职责 | 返回值 |
|
||||||
|
|------------|------|--------|
|
||||||
|
| `useTable` | 表格数据加载、分页、搜索、排序 | `{ loading, dataList, pagination, loadData, handleSearch, handleReset }` |
|
||||||
|
| `useForm` | 表单状态、校验、提交 | `{ formRef, formData, rules, loading, handleSubmit, handleReset }` |
|
||||||
|
| `useAuth` | 权限判断、按钮/菜单可见性 | `{ hasAuth, hasRole, checkPermission }` |
|
||||||
|
| `useCommon` | 通用工具(字典、枚举转换) | `{ getDictLabel, formatEnum }` |
|
||||||
|
| `useFastEnter` | 快速导航、全局搜索 | `{ visible, keyword, results, navigate }` |
|
||||||
|
| `useHeaderBar` | 顶部栏状态(全屏、消息、用户) | `{ isFullscreen, toggleFullscreen, unreadCount }` |
|
||||||
|
| `useSmsCode` | 短信验证码倒计时 | `{ countdown, isSending, sendCode }` |
|
||||||
|
| `useTableColumns` | 表格列配置、列显隐持久化 | `{ columns, visibleColumns, toggleColumn }` |
|
||||||
|
| `useTheme` | 主题切换 + 系统跟随 | `{ isDark, toggleTheme, colorPrimary }` |
|
||||||
|
| `useChart` | ECharts 实例管理、自动 resize | `{ chartRef, setOption, resize }` |
|
||||||
|
| `useWebSocket` | WebSocket 连接管理 | `{ connect, disconnect, send, onMessage }` |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/hooks/useTable.ts — 标准实现
|
||||||
|
export function useTable(fetchFn) {
|
||||||
|
const loading = ref(false)
|
||||||
|
const dataList = ref([])
|
||||||
|
const pagination = reactive({ current: 1, size: 10, total: 0 })
|
||||||
|
const searchForm = reactive({})
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await fetchFn({
|
||||||
|
page: pagination.current,
|
||||||
|
page_size: pagination.size,
|
||||||
|
...searchForm,
|
||||||
|
})
|
||||||
|
dataList.value = result.data
|
||||||
|
pagination.total = result.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
pagination.current = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
Object.keys(searchForm).forEach((key) => { searchForm[key] = undefined })
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageChange(page) {
|
||||||
|
pagination.current = page
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { loading, dataList, pagination, searchForm, loadData, handleSearch, handleReset, handlePageChange }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 自定义指令
|
||||||
|
|
||||||
|
所有指令位于 `src/directives/`:
|
||||||
|
|
||||||
|
| 指令 | 用途 | 用法示例 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `v-auth` | 按钮级权限控制(权限标识) | `v-auth="'system:user:add'"` |
|
||||||
|
| `v-roles` | 角色级权限控制 | `v-roles="['admin', 'manager']"` |
|
||||||
|
| `v-focus` | 自动聚焦输入框 | `v-focus` |
|
||||||
|
| `v-highlight` | 搜索关键词高亮 | `v-highlight="keyword"` |
|
||||||
|
| `v-index` | 序号自动生成 | `v-index="{ page, size }"` |
|
||||||
|
| `v-ripple` | 点击波纹效果 | `v-ripple` |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/directives/auth.ts
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
|
||||||
|
export const vAuth = {
|
||||||
|
mounted(el, binding) {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
if (!userStore.permissions.includes(binding.value)) {
|
||||||
|
el.parentNode?.removeChild(el)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/directives/roles.ts
|
||||||
|
export const vRoles = {
|
||||||
|
mounted(el, binding) {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const hasRole = binding.value.some((role) => userStore.roles.includes(role))
|
||||||
|
if (!hasRole) {
|
||||||
|
el.parentNode?.removeChild(el)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 布局组件约定
|
||||||
|
|
||||||
|
布局组件位于 `src/layouts/`:
|
||||||
|
|
||||||
|
| 组件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `ArtSidebarMenu` | 侧边导航菜单(递归菜单树) |
|
||||||
|
| `ArtHeader` | 顶部栏(面包屑 + 用户 + 通知 + 全屏) |
|
||||||
|
| `ArtWorkTab` | 多标签页管理(右键菜单、拖拽排序) |
|
||||||
|
| `ArtNotification` | 通知面板(WebSocket 实时推送) |
|
||||||
|
| `ArtChatWindow` | 即时通讯窗口 |
|
||||||
|
| `ArtFastEnter` | 全局快速搜索导航(Ctrl+K) |
|
||||||
|
| `ArtGlobalSearch` | 全局搜索弹窗 |
|
||||||
|
|
||||||
|
## Pinia Store 模块映射
|
||||||
|
|
||||||
|
| Store | 职责 | 持久化 | 加密 |
|
||||||
|
|-------|------|--------|------|
|
||||||
|
| `user` | 用户信息、Token、权限、角色 | ✅ | ✅ Token 加密 |
|
||||||
|
| `menu` | 菜单树、动态路由 | ✅ | ❌ |
|
||||||
|
| `setting` | 主题、布局、语言设置 | ✅ | ❌ |
|
||||||
|
| `worktab` | 标签页状态 | ✅ | ❌ |
|
||||||
|
| `table` | 表格列配置、列显隐 | ✅ | ❌ |
|
||||||
|
| `notification` | 通知消息、未读计数 | ❌ | ❌ |
|
||||||
|
| `workflow` | 审批流程状态 | ❌ | ❌ |
|
||||||
|
| `product` | 产品/生产模块状态 | ❌ | ❌ |
|
||||||
|
| `outsideflow` | 外部审批流程 | ❌ | ❌ |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Pinia 加密持久化示例
|
||||||
|
import CryptoJS from 'crypto-js'
|
||||||
|
|
||||||
|
const ENCRYPT_KEY = import.meta.env.VITE_STORAGE_KEY || 'default-key'
|
||||||
|
|
||||||
|
function encrypt(data) {
|
||||||
|
return CryptoJS.AES.encrypt(data, ENCRYPT_KEY).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrypt(data) {
|
||||||
|
return CryptoJS.AES.decrypt(data, ENCRYPT_KEY).toString(CryptoJS.enc.Utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
// ... state and actions
|
||||||
|
}, {
|
||||||
|
persist: {
|
||||||
|
key: 'user-store',
|
||||||
|
storage: {
|
||||||
|
getItem: (key) => {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? decrypt(raw) : null
|
||||||
|
},
|
||||||
|
setItem: (key, value) => {
|
||||||
|
localStorage.setItem(key, encrypt(value))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pick: ['token', 'refreshToken'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 路由守卫完整流程
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/router/guards/index.ts
|
||||||
|
import NProgress from 'nprogress'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { useMenuStore } from '@/store/modules/menu'
|
||||||
|
import { useWorktabStore } from '@/store/modules/worktab'
|
||||||
|
|
||||||
|
const WHITE_LIST = ['/login', '/register', '/404', '/403']
|
||||||
|
|
||||||
|
export function setupRouterGuards(router) {
|
||||||
|
router.beforeEach(async (to, _from, next) => {
|
||||||
|
NProgress.start()
|
||||||
|
document.title = `${to.meta.title || ''} - ${import.meta.env.VITE_APP_TITLE}`
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const token = userStore.token
|
||||||
|
|
||||||
|
// Step 1: 白名单放行
|
||||||
|
if (WHITE_LIST.includes(to.path)) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: 无 Token -> 登录
|
||||||
|
if (!token) {
|
||||||
|
return next({ path: '/login', query: { redirect: to.fullPath } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: 已登录访问登录页 -> 首页
|
||||||
|
if (to.path === '/login') {
|
||||||
|
return next({ path: '/' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: 动态路由未加载 -> 加载
|
||||||
|
const menuStore = useMenuStore()
|
||||||
|
if (!menuStore.isRoutesLoaded) {
|
||||||
|
try {
|
||||||
|
await menuStore.loadDynamicRoutes(router)
|
||||||
|
return next({ ...to, replace: true })
|
||||||
|
} catch {
|
||||||
|
userStore.logout()
|
||||||
|
return next({ path: '/login' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
router.afterEach((to) => {
|
||||||
|
NProgress.done()
|
||||||
|
|
||||||
|
// Step 5: 更新标签页
|
||||||
|
const worktabStore = useWorktabStore()
|
||||||
|
if (to.meta.title && !to.meta.hideTab) {
|
||||||
|
worktabStore.addTab({
|
||||||
|
path: to.path,
|
||||||
|
title: to.meta.title,
|
||||||
|
name: to.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebSocket 集成模式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/utils/websocket/notification.ts
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { useNotificationStore } from '@/store/modules/notification'
|
||||||
|
|
||||||
|
class NotificationWebSocket {
|
||||||
|
ws = null
|
||||||
|
reconnectTimer = null
|
||||||
|
heartbeatTimer = null
|
||||||
|
reconnectAttempts = 0
|
||||||
|
maxReconnectAttempts = 5
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
if (!userStore.token) return
|
||||||
|
|
||||||
|
const wsUrl = `${import.meta.env.VITE_WS_URL}?token=${userStore.token}`
|
||||||
|
this.ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this.startHeartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
const message = JSON.parse(event.data)
|
||||||
|
this.handleMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
this.stopHeartbeat()
|
||||||
|
this.tryReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(message) {
|
||||||
|
const notificationStore = useNotificationStore()
|
||||||
|
switch (message.type) {
|
||||||
|
case 'notification':
|
||||||
|
notificationStore.addNotification(message.data)
|
||||||
|
break
|
||||||
|
case 'heartbeat':
|
||||||
|
// Pong
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startHeartbeat() {
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
this.ws?.send(JSON.stringify({ type: 'heartbeat', data: 'ping' }))
|
||||||
|
}, 30000)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopHeartbeat() {
|
||||||
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
tryReconnect() {
|
||||||
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) return
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.reconnectAttempts++
|
||||||
|
this.connect()
|
||||||
|
}, Math.min(1000 * 2 ** this.reconnectAttempts, 30000))
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.stopHeartbeat()
|
||||||
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
|
||||||
|
this.ws?.close()
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const notificationWs = new NotificationWebSocket()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能检查清单
|
||||||
|
|
||||||
|
- [ ] 路由使用 `() => import()` 懒加载
|
||||||
|
- [ ] 列表使用稳定 `:key`(非 index)
|
||||||
|
- [ ] 大列表使用虚拟化(`@tanstack/vue-virtual`)
|
||||||
|
- [ ] 使用 `defineAsyncComponent` 做组件懒加载
|
||||||
|
- [ ] 避免在 `<template>` 中写复杂表达式,提取为 `computed`
|
||||||
|
- [ ] 动态组件使用 `shallowRef` 存储组件引用
|
||||||
|
- [ ] 管理端:Element Plus 按需导入,避免全量引入(用户端不使用 Element Plus)
|
||||||
|
- [ ] Vite 构建配置 `manualChunks` 分离 vendor
|
||||||
|
- [ ] Pinia 敏感数据(Token)使用加密持久化
|
||||||
|
- [ ] WebSocket 连接登录后建立,登出后断开
|
||||||
|
|
||||||
|
## Props 反模式全集
|
||||||
|
|
||||||
|
> 以下案例来源于项目实际 ESLint 修复(`vue/no-mutating-props`),作为永久参考。
|
||||||
|
|
||||||
|
### 场景 A:表单子组件 v-model 直接修改对象 prop
|
||||||
|
|
||||||
|
父组件传递 `reactive` 对象作为 prop,子组件用 `v-model` 直接绑定其字段。
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ ForgotPanel.vue — 直接修改 prop.form 的字段 -->
|
||||||
|
<script setup>
|
||||||
|
defineProps<{
|
||||||
|
form: { username: string; phone: string; code: string }
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<AppInput v-model="form.username" />
|
||||||
|
<AppInput v-model="form.phone" />
|
||||||
|
<AppInput v-model="form.code" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ✅ 修复后 — 通过 emit 通知父组件更新 -->
|
||||||
|
<script setup>
|
||||||
|
type FormData = { username: string; phone: string; code: string }
|
||||||
|
|
||||||
|
const props = defineProps<{ form: FormData }>()
|
||||||
|
const emit = defineEmits<{ 'update:form': [value: FormData] }>()
|
||||||
|
|
||||||
|
function updateField(field: keyof FormData, value: string) {
|
||||||
|
emit('update:form', { ...props.form, [field]: value })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<AppInput :model-value="form.username" @update:model-value="updateField('username', $event)" />
|
||||||
|
<AppInput :model-value="form.phone" @update:model-value="updateField('phone', $event)" />
|
||||||
|
<AppInput :model-value="form.code" @update:model-value="updateField('code', $event)" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
**父组件响应**:`@update:form="Object.assign(formData, $event)"`
|
||||||
|
|
||||||
|
### 场景 B:筛选器子组件动态 key 修改 prop
|
||||||
|
|
||||||
|
子组件通过 `v-model` 绑定 `prop[dynamicKey]`,遍历配置项时逐个修改 prop 的属性。
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ FilterDrawerBody.vue — v-model 绑定动态 prop key -->
|
||||||
|
<RadioGroup v-model="radios[f.key]" :options="f.options" />
|
||||||
|
<CheckboxGroup v-model="checkboxes[f.key]" :options="f.options" />
|
||||||
|
<SelectField v-model="selects[f.key]" :options="f.options" />
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ✅ 修复后 — 拆分为 :model-value + emit -->
|
||||||
|
<RadioGroup
|
||||||
|
:model-value="radios[f.key]"
|
||||||
|
:options="f.options"
|
||||||
|
@update:model-value="emit('update:radio', f.key, $event)"
|
||||||
|
/>
|
||||||
|
<CheckboxGroup
|
||||||
|
:model-value="checkboxes[f.key]"
|
||||||
|
:options="f.options"
|
||||||
|
@update:model-value="emit('update:checkbox', f.key, $event)"
|
||||||
|
/>
|
||||||
|
<SelectField
|
||||||
|
:model-value="selects[f.key]"
|
||||||
|
:options="f.options"
|
||||||
|
@update:model-value="emit('update:select', f.key, $event)"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**父组件响应**:`@update:radio="(key, val) => radios[key] = val"`
|
||||||
|
|
||||||
|
### 场景 C:事件回调中赋值 prop 属性(隐蔽变体)
|
||||||
|
|
||||||
|
不通过 `v-model`,但在事件处理函数中直接赋值 prop 的属性。ESLint 同样检测为 prop mutation。
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ FilterDrawerBody.vue — 事件回调中直接赋值 prop -->
|
||||||
|
<LogicTable
|
||||||
|
:values="logics"
|
||||||
|
@update-logic="(k: string, v: string) => logics[k] = v"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ✅ 修复后 — 转发为 emit,由父组件执行赋值 -->
|
||||||
|
<LogicTable
|
||||||
|
:values="logics"
|
||||||
|
@update-logic="(k: string, v: string) => emit('update:logic', k, v)"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**父组件响应**:`@update:logic="(key, val) => logics[key] = val"`
|
||||||
|
|
||||||
|
### 附:事件命名规范
|
||||||
|
|
||||||
|
以上修复中同步修正了 `defineEmits` 的事件命名:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ kebab-case(触发 vue/custom-event-name-casing)
|
||||||
|
defineEmits<{
|
||||||
|
'add-room-condition': []
|
||||||
|
'remove-room-condition': [id: number]
|
||||||
|
'update-room-condition': [id: number, field: string, value: string | number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// ✅ camelCase
|
||||||
|
defineEmits<{
|
||||||
|
addRoomCondition: []
|
||||||
|
removeRoomCondition: [id: number]
|
||||||
|
updateRoomCondition: [id: number, field: string, value: string | number]
|
||||||
|
}>()
|
||||||
|
```
|
||||||
|
|
||||||
|
模板中 `@add-room-condition` 和 `@addRoomCondition` 均可,Vue 自动双向转换。
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
# 013-backend.mdc (Deep Reference)
|
||||||
|
|
||||||
|
> 该文件为原始详细规范归档,供 Tier 3 按需读取。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# 🖥️ PHP Hyperf + Swoole Backend Standards
|
||||||
|
|
||||||
|
参考文档: @docs/architecture/api-contracts.md @docs/architecture/system-design.md
|
||||||
|
|
||||||
|
## 分层架构
|
||||||
|
|
||||||
|
```
|
||||||
|
Request → Middleware → Controller → Service → Repository → Model → DB
|
||||||
|
↓
|
||||||
|
Event → Listener (异步)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 层 | 职责 | 禁止 |
|
||||||
|
|---|---|---|
|
||||||
|
| **Controller** | 接收请求、参数验证、调用 Service、返回响应 | 包含业务逻辑 |
|
||||||
|
| **Service** | 核心业务逻辑、事务管理、调用 Repository | 直接操作数据库 |
|
||||||
|
| **Repository** | 数据访问、查询构建、数据权限过滤 | 包含业务判断 |
|
||||||
|
| **Model** | 数据模型、关联定义、类型转换 | 包含业务方法 |
|
||||||
|
|
||||||
|
## PHP 编码规范 (PSR-12)
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service\Production;
|
||||||
|
|
||||||
|
use App\Model\Production\ProductionOrder;
|
||||||
|
use App\Repository\Production\OrderRepository;
|
||||||
|
use Hyperf\Di\Annotation\Inject;
|
||||||
|
use Hyperf\DbConnection\Db;
|
||||||
|
|
||||||
|
class OrderService
|
||||||
|
{
|
||||||
|
#[Inject]
|
||||||
|
protected OrderRepository $orderRepository;
|
||||||
|
|
||||||
|
public function create(array $data): ProductionOrder
|
||||||
|
{
|
||||||
|
return Db::transaction(function () use ($data) {
|
||||||
|
$order = $this->orderRepository->create($data);
|
||||||
|
// trigger event
|
||||||
|
event(new OrderCreated($order));
|
||||||
|
return $order;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**命名规范**:
|
||||||
|
|
||||||
|
| 类型 | 规范 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| 类 | PascalCase | `OrderService` |
|
||||||
|
| 方法 | camelCase | `createOrder()` |
|
||||||
|
| 变量 | camelCase | `$orderData` |
|
||||||
|
| 常量 | SCREAMING_SNAKE | `MAX_RETRY_COUNT` |
|
||||||
|
| 数据库字段 | snake_case | `created_at` |
|
||||||
|
| 路由 | kebab-case 复数 | `/admin/production-orders` |
|
||||||
|
|
||||||
|
## API 设计
|
||||||
|
|
||||||
|
- RESTful 命名: 复数名词 (`/admin/users`, `/admin/production-orders`)
|
||||||
|
- 版本控制: `/admin/v1/...`(可选)
|
||||||
|
- 统一响应格式:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 成功
|
||||||
|
{ "code": 200, "message": "ok", "data": T }
|
||||||
|
|
||||||
|
// 列表(带分页)
|
||||||
|
{ "code": 200, "message": "ok", "data": { "items": [], "total": 100 } }
|
||||||
|
|
||||||
|
// 错误
|
||||||
|
{ "code": 422, "message": "Validation failed", "data": { "errors": {} } }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 输入验证
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Request/Production/CreateOrderRequest.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Request\Production;
|
||||||
|
|
||||||
|
use Hyperf\Validation\Request\FormRequest;
|
||||||
|
|
||||||
|
class CreateOrderRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'customer_id' => 'required|integer|exists:customers,id',
|
||||||
|
'platform_id' => 'required|integer|exists:platforms,id',
|
||||||
|
'order_type' => 'required|integer|in:1,2,3',
|
||||||
|
'remark' => 'nullable|string|max:500',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 认证/授权
|
||||||
|
|
||||||
|
- JWT 双 Token 机制: Access Token (2h) + Refresh Token (7d)
|
||||||
|
- Token 存储在 Redis,支持强制失效
|
||||||
|
- 密码: `password_hash()` + `PASSWORD_BCRYPT`
|
||||||
|
- 速率限制: `#[RateLimit]` 注解
|
||||||
|
|
||||||
|
## 中间件链
|
||||||
|
|
||||||
|
```
|
||||||
|
Request
|
||||||
|
→ ErrorLogMiddleware # 异常捕获
|
||||||
|
→ AccessTokenMiddleware # JWT 验证
|
||||||
|
→ PermissionMiddleware # RBAC 权限
|
||||||
|
→ DataPermissionMiddleware # 数据权限过滤
|
||||||
|
→ OperationMiddleware # 操作日志
|
||||||
|
→ Controller
|
||||||
|
```
|
||||||
|
|
||||||
|
## 异常处理器链
|
||||||
|
|
||||||
|
异常处理器按优先级顺序注册,每个 Handler 只处理对应的异常类型:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/autoload/exceptions.php
|
||||||
|
return [
|
||||||
|
'handler' => [
|
||||||
|
'http' => [
|
||||||
|
// Priority: higher number = higher priority
|
||||||
|
ValidationExceptionHandler::class, // 验证异常 → 422
|
||||||
|
AuthExceptionHandler::class, // 认证异常 → 401
|
||||||
|
PermissionExceptionHandler::class, // 权限异常 → 403
|
||||||
|
BusinessExceptionHandler::class, // 业务异常 → 自定义 code
|
||||||
|
RateLimitExceptionHandler::class, // 限流异常 → 429
|
||||||
|
ModelNotFoundExceptionHandler::class, // 模型未找到 → 404
|
||||||
|
QueryExceptionHandler::class, // 数据库异常 → 500(隐藏细节)
|
||||||
|
AppExceptionHandler::class, // 兜底异常 → 500
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Exception/BusinessException.php
|
||||||
|
namespace App\Exception;
|
||||||
|
|
||||||
|
use Hyperf\Server\Exception\ServerException;
|
||||||
|
|
||||||
|
class BusinessException extends ServerException
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
int $code = 500,
|
||||||
|
string $message = '',
|
||||||
|
?\Throwable $previous = null,
|
||||||
|
) {
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// app/Exception/Handler/BusinessExceptionHandler.php
|
||||||
|
class BusinessExceptionHandler extends ExceptionHandler
|
||||||
|
{
|
||||||
|
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
|
||||||
|
{
|
||||||
|
$this->stopPropagation();
|
||||||
|
|
||||||
|
return $response->withStatus(200)->withBody(new SwooleStream(json_encode([
|
||||||
|
'code' => $throwable->getCode(),
|
||||||
|
'message' => $throwable->getMessage(),
|
||||||
|
'data' => null,
|
||||||
|
], JSON_UNESCAPED_UNICODE)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isValid(Throwable $throwable): bool
|
||||||
|
{
|
||||||
|
return $throwable instanceof BusinessException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 事件系统
|
||||||
|
|
||||||
|
Event + Listener 解耦异步逻辑,避免 Service 膨胀:
|
||||||
|
|
||||||
|
| 事件 | 触发时机 | 监听器 |
|
||||||
|
|------|---------|--------|
|
||||||
|
| `OrderCreated` | 订单创建后 | 发送通知、记录日志、同步第三方 |
|
||||||
|
| `OrderStatusChanged` | 状态变更后 | 更新统计、通知相关人 |
|
||||||
|
| `PaymentReceived` | 收款确认后 | 更新订单状态、触发生产 |
|
||||||
|
| `UserLoggedIn` | 登录成功 | 记录登录日志、更新最后登录时间 |
|
||||||
|
| `UserRegistered` | 注册成功 | 发送欢迎邮件、初始化默认数据 |
|
||||||
|
| `WorkflowApproved` | 审批通过 | 推进流程、通知下一节点 |
|
||||||
|
| `WorkflowRejected` | 审批驳回 | 通知发起人、记录原因 |
|
||||||
|
| `FileUploaded` | 文件上传完成 | 生成缩略图、同步到 OSS |
|
||||||
|
| `NotificationSent` | 通知发出 | WebSocket 推送、记录日志 |
|
||||||
|
| `CacheInvalidated` | 缓存失效 | 预热缓存、记录日志 |
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Event/OrderCreated.php
|
||||||
|
class OrderCreated
|
||||||
|
{
|
||||||
|
public function __construct(public readonly ProductionOrder $order) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// app/Listener/SendOrderNotificationListener.php
|
||||||
|
#[Listener]
|
||||||
|
class SendOrderNotificationListener implements ListenerInterface
|
||||||
|
{
|
||||||
|
public function listen(): array
|
||||||
|
{
|
||||||
|
return [OrderCreated::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(object $event): void
|
||||||
|
{
|
||||||
|
/** @var OrderCreated $event */
|
||||||
|
$this->notificationService->send(
|
||||||
|
userId: $event->order->created_by,
|
||||||
|
type: 'order_created',
|
||||||
|
data: ['order_no' => $event->order->order_no],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据权限过滤
|
||||||
|
|
||||||
|
5 级 DATA_SCOPE 控制用户可见数据范围,在 Repository 层自动过滤:
|
||||||
|
|
||||||
|
| 级别 | 常量 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 全部 | `DATA_SCOPE_ALL` | 管理员,无限制 |
|
||||||
|
| 自定义 | `DATA_SCOPE_CUSTOM` | 指定部门集合 |
|
||||||
|
| 本部门 | `DATA_SCOPE_DEPT` | 仅本部门数据 |
|
||||||
|
| 本部门及下级 | `DATA_SCOPE_DEPT_AND_CHILD` | 本部门 + 子部门 |
|
||||||
|
| 仅本人 | `DATA_SCOPE_SELF` | 仅自己创建的数据 |
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Repository/Concern/DataPermissionTrait.php
|
||||||
|
trait DataPermissionTrait
|
||||||
|
{
|
||||||
|
protected function applyDataScope(Builder $query): Builder
|
||||||
|
{
|
||||||
|
$user = Context::get('current_user');
|
||||||
|
if (!$user) return $query;
|
||||||
|
|
||||||
|
return match ($user->data_scope) {
|
||||||
|
DataScope::ALL => $query,
|
||||||
|
DataScope::CUSTOM => $query->whereIn('dept_id', $user->custom_dept_ids),
|
||||||
|
DataScope::DEPT => $query->where('dept_id', $user->dept_id),
|
||||||
|
DataScope::DEPT_AND_CHILD => $query->whereIn(
|
||||||
|
'dept_id',
|
||||||
|
$this->deptService->getChildDeptIds($user->dept_id),
|
||||||
|
),
|
||||||
|
DataScope::SELF => $query->where('created_by', $user->id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 分布式锁模式
|
||||||
|
|
||||||
|
使用 Redis 分布式锁防止并发操作导致数据不一致:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 订单锁定场景
|
||||||
|
class OrderLockService
|
||||||
|
{
|
||||||
|
private const LOCK_PREFIX = 'order_lock:';
|
||||||
|
private const LOCK_TTL = 30;
|
||||||
|
|
||||||
|
private const LOCK_TYPES = [
|
||||||
|
'edit' => '编辑锁',
|
||||||
|
'process' => '处理锁',
|
||||||
|
'payment_pending' => '待收款锁',
|
||||||
|
'payment_missing' => '缺款锁',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function acquireLock(int $orderId, string $type, int $userId): bool
|
||||||
|
{
|
||||||
|
$key = self::LOCK_PREFIX . "{$type}:{$orderId}";
|
||||||
|
return $this->redis->set($key, $userId, ['NX', 'EX' => self::LOCK_TTL]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function releaseLock(int $orderId, string $type, int $userId): bool
|
||||||
|
{
|
||||||
|
$key = self::LOCK_PREFIX . "{$type}:{$orderId}";
|
||||||
|
// Lua script for atomic check-and-delete
|
||||||
|
$script = <<<'LUA'
|
||||||
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call("del", KEYS[1])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
LUA;
|
||||||
|
return (bool) $this->redis->eval($script, [$key, (string) $userId], 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model Traits
|
||||||
|
|
||||||
|
常用 Model Trait 复用数据库行为:
|
||||||
|
|
||||||
|
| Trait | 职责 |
|
||||||
|
|-------|------|
|
||||||
|
| `HasCreator` | 自动填充 `created_by` / `updated_by` |
|
||||||
|
| `SoftDeletes` | 软删除 (`deleted_at`) |
|
||||||
|
| `HasOperationLog` | 操作日志记录 |
|
||||||
|
| `HasDataPermission` | 查询自动加数据权限条件 |
|
||||||
|
| `HasSortable` | 拖拽排序 (`sort_order`) |
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Model/Concern/HasCreator.php
|
||||||
|
trait HasCreator
|
||||||
|
{
|
||||||
|
public static function bootHasCreator(): void
|
||||||
|
{
|
||||||
|
static::creating(function (Model $model) {
|
||||||
|
$user = Context::get('current_user');
|
||||||
|
if ($user) {
|
||||||
|
$model->created_by = $user->id;
|
||||||
|
$model->updated_by = $user->id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
static::updating(function (Model $model) {
|
||||||
|
$user = Context::get('current_user');
|
||||||
|
if ($user) {
|
||||||
|
$model->updated_by = $user->id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 定时任务
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Crontab/DailyStatisticsCrontab.php
|
||||||
|
#[Crontab(
|
||||||
|
name: 'daily_statistics',
|
||||||
|
rule: '0 2 * * *',
|
||||||
|
singleton: true,
|
||||||
|
onOneServer: true,
|
||||||
|
memo: '每日凌晨 2 点统计报表'
|
||||||
|
)]
|
||||||
|
class DailyStatisticsCrontab
|
||||||
|
{
|
||||||
|
#[Inject]
|
||||||
|
protected StatisticsService $statisticsService;
|
||||||
|
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
$this->statisticsService->generateDailyReport(
|
||||||
|
Carbon::yesterday()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注解说明**:
|
||||||
|
- `singleton: true` — 同一进程内不重复执行
|
||||||
|
- `onOneServer: true` — 多实例部署时只在一台执行
|
||||||
|
|
||||||
|
## 请求签名验证
|
||||||
|
|
||||||
|
前端签名 + 后端验签,防止请求篡改:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Middleware/RequestSignMiddleware.php
|
||||||
|
class RequestSignMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
$timestamp = $request->getHeaderLine('X-Timestamp');
|
||||||
|
$sign = $request->getHeaderLine('X-Sign');
|
||||||
|
$nonce = $request->getHeaderLine('X-Nonce');
|
||||||
|
|
||||||
|
// Step 1: 时间窗口校验(5 分钟内有效)
|
||||||
|
if (abs(time() - (int) $timestamp) > 300) {
|
||||||
|
throw new BusinessException(403, 'Request expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Nonce 防重放
|
||||||
|
if (!$this->redis->set("nonce:{$nonce}", 1, ['NX', 'EX' => 300])) {
|
||||||
|
throw new BusinessException(403, 'Duplicate request');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: 签名校验
|
||||||
|
$body = (string) $request->getBody();
|
||||||
|
$expectedSign = hash_hmac('sha256', "{$timestamp}{$nonce}{$body}", $this->appSecret);
|
||||||
|
|
||||||
|
if (!hash_equals($expectedSign, $sign)) {
|
||||||
|
throw new BusinessException(403, 'Invalid signature');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依赖注入
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ✅ 构造函数注入(推荐)
|
||||||
|
public function __construct(
|
||||||
|
protected readonly OrderRepository $orderRepo,
|
||||||
|
protected readonly CacheInterface $cache,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// ✅ 属性注入
|
||||||
|
#[Inject]
|
||||||
|
protected OrderService $orderService;
|
||||||
|
|
||||||
|
// ❌ 禁止手动 new 或 make()
|
||||||
|
$service = new OrderService(); // WRONG
|
||||||
|
```
|
||||||
|
|
||||||
|
## 禁止事项
|
||||||
|
|
||||||
|
- 禁止在 Controller 中直接操作数据库
|
||||||
|
- 禁止在 Swoole 环境使用阻塞 I/O(`file_get_contents`, `sleep`)
|
||||||
|
- 禁止使用全局变量 / 全局静态属性存储请求数据
|
||||||
|
- 禁止 `dd()` / `var_dump()` 残留在代码中
|
||||||
|
- 禁止 `SELECT *`,明确列名
|
||||||
|
- 禁止在循环中执行 SQL(N+1 问题)
|
||||||
|
- 禁止在事件监听器中抛出异常阻塞主流程
|
||||||
|
- 禁止在定时任务中未加 `onOneServer` 导致多实例重复执行
|
||||||
@@ -0,0 +1,624 @@
|
|||||||
|
# 014-database.mdc (Deep Reference)
|
||||||
|
|
||||||
|
> 该文件为原始详细规范归档,供 Tier 3 按需读取。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# 🗄️ Hyperf ORM + MySQL Database Standards
|
||||||
|
|
||||||
|
参考文档: @docs/architecture/data-model.md
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
- 任何写操作前有备份策略
|
||||||
|
- 使用 Hyperf Migration,禁止直接 DDL
|
||||||
|
- 软删除优先于硬删除 (`deleted_at`)
|
||||||
|
- 敏感数据加密存储
|
||||||
|
- 生产环境只读
|
||||||
|
|
||||||
|
## 必须字段 (每张表)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Hyperf Migration
|
||||||
|
Schema::create('examples', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->timestamps(); // created_at, updated_at
|
||||||
|
$table->softDeletes(); // deleted_at
|
||||||
|
$table->unsignedBigInteger('created_by')->nullable();
|
||||||
|
$table->unsignedBigInteger('updated_by')->nullable();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model 规范
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Model\Production;
|
||||||
|
|
||||||
|
use Hyperf\Database\Model\SoftDeletes;
|
||||||
|
use Hyperf\DbConnection\Model\Model;
|
||||||
|
|
||||||
|
class ProductionOrder extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected ?string $table = 'production_orders';
|
||||||
|
|
||||||
|
protected array $fillable = [
|
||||||
|
'order_no', 'customer_id', 'platform_id',
|
||||||
|
'status', 'total_amount', 'paid_amount',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $casts = [
|
||||||
|
'total_amount' => 'decimal:2',
|
||||||
|
'paid_amount' => 'decimal:2',
|
||||||
|
'source_data' => 'json',
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Eager loading to prevent N+1
|
||||||
|
public function customer(): \Hyperf\Database\Model\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Customer::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function subOrders(): \Hyperf\Database\Model\Relations\HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ProductionSubOrder::class, 'order_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命名规范
|
||||||
|
|
||||||
|
### 表名模块前缀规则
|
||||||
|
|
||||||
|
**表名必须以所属模块名作为前缀**,格式:`<module>_<entity_plural>`。
|
||||||
|
|
||||||
|
| 模块 | 前缀 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| 用户与权限 | `auth_` | `auth_users`, `auth_roles` |
|
||||||
|
| 案例核心 | `case_` | `case_cases`, `case_images` |
|
||||||
|
| 设计师 | `designer_` | `designer_profiles`, `designer_awards` |
|
||||||
|
| 运营内容 | `operation_` | `operation_banners`, `operation_topics` |
|
||||||
|
| 用户互动 | `interaction_` | `interaction_favorites`, `interaction_comments` |
|
||||||
|
| 日志 | `log_` | `log_user_logins`, `log_downloads` |
|
||||||
|
| 安全风控 | `security_` | `security_blacklists`, `security_risk_events` |
|
||||||
|
| 系统配置 | `system_` | `system_configs` |
|
||||||
|
|
||||||
|
> 多对多关联表同样需要加模块前缀:`<module>_<a>_belongs_<b>`,以主体模块为准。
|
||||||
|
|
||||||
|
### 通用命名约定
|
||||||
|
|
||||||
|
| 类型 | 规范 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| 表名 | `<module>_` 前缀 + snake_case 复数 | `auth_users`, `case_cases` |
|
||||||
|
| 字段名 | snake_case | `created_at` |
|
||||||
|
| 主键 | `id` | `BIGINT UNSIGNED AUTO_INCREMENT` |
|
||||||
|
| 外键 | `<table_singular>_id` | `customer_id` |
|
||||||
|
| 索引 | `idx_<table>_<columns>` | `idx_auth_users_status` |
|
||||||
|
| 唯一索引 | `uk_<table>_<columns>` | `uk_case_cases_code` |
|
||||||
|
|
||||||
|
## 高并发表设计规范
|
||||||
|
|
||||||
|
### 字段类型选择
|
||||||
|
|
||||||
|
| 场景 | 推荐类型 | 避免 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 主键 | `BIGINT UNSIGNED` | `INT` (溢出风险) |
|
||||||
|
| 金额 | `DECIMAL(10,2)` | `FLOAT/DOUBLE` (精度丢失) |
|
||||||
|
| 状态 | `VARCHAR(20)` 或 `TINYINT` | `ENUM` (修改需 DDL) |
|
||||||
|
| 时间 | `TIMESTAMP` | `DATETIME` (不带时区) |
|
||||||
|
| JSON 数据 | `JSON` (MySQL 8) | `TEXT` (无法索引) |
|
||||||
|
| 短文本 | `VARCHAR(n)` 精确长度 | `VARCHAR(255)` 万能长度 |
|
||||||
|
|
||||||
|
### 新字段安全约束
|
||||||
|
|
||||||
|
**已有数据的表上新增字段必须遵循:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ✅ 正确:nullable
|
||||||
|
$table->string('avatar')->nullable();
|
||||||
|
|
||||||
|
// ✅ 正确:有默认值
|
||||||
|
$table->tinyInteger('priority')->default(0);
|
||||||
|
|
||||||
|
// ❌ 禁止:NOT NULL 无默认值(锁表重写所有行)
|
||||||
|
$table->string('role'); // NOT NULL without default
|
||||||
|
```
|
||||||
|
|
||||||
|
如需 NOT NULL 约束,使用三步法:
|
||||||
|
1. 先添加 nullable 字段
|
||||||
|
2. 数据回填(独立迁移)
|
||||||
|
3. 再加 NOT NULL 约束(独立迁移)
|
||||||
|
|
||||||
|
### 索引策略
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 复合索引:遵循最左前缀原则
|
||||||
|
CREATE INDEX idx_orders_status_created ON production_orders(status, created_at);
|
||||||
|
|
||||||
|
-- 覆盖索引:查询字段全在索引中,避免回表
|
||||||
|
CREATE INDEX idx_orders_cover ON production_orders(status, total_amount, paid_amount);
|
||||||
|
|
||||||
|
-- 前缀索引:长字符串字段
|
||||||
|
CREATE INDEX idx_orders_remark ON production_orders(remark(20));
|
||||||
|
```
|
||||||
|
|
||||||
|
**索引检查清单**:
|
||||||
|
- [ ] 所有外键字段有索引
|
||||||
|
- [ ] WHERE 常用字段有索引
|
||||||
|
- [ ] ORDER BY 字段在索引中
|
||||||
|
- [ ] 联合查询字段使用复合索引
|
||||||
|
- [ ] 单表索引不超过 6 个
|
||||||
|
|
||||||
|
### 百万级数据优化
|
||||||
|
|
||||||
|
- 分页使用游标分页(`WHERE id > ? LIMIT ?`)替代 `OFFSET`
|
||||||
|
- 大表 COUNT 使用近似值或缓存
|
||||||
|
- 批量操作使用 `chunk()` 分批处理
|
||||||
|
- 避免大事务,单事务操作 < 1000 行
|
||||||
|
- 热点数据使用 Redis 缓存,减少 DB 压力
|
||||||
|
|
||||||
|
## 读写分离
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/autoload/databases.php
|
||||||
|
return [
|
||||||
|
'default' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'read' => [
|
||||||
|
'host' => [env('DB_READ_HOST_1'), env('DB_READ_HOST_2')],
|
||||||
|
],
|
||||||
|
'write' => [
|
||||||
|
'host' => env('DB_WRITE_HOST'),
|
||||||
|
],
|
||||||
|
'port' => env('DB_PORT', 3306),
|
||||||
|
'database' => env('DB_DATABASE'),
|
||||||
|
'username' => env('DB_USERNAME'),
|
||||||
|
'password' => env('DB_PASSWORD'),
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'pool' => [
|
||||||
|
'min_connections' => 5,
|
||||||
|
'max_connections' => 50,
|
||||||
|
'connect_timeout' => 10.0,
|
||||||
|
'wait_timeout' => 3.0,
|
||||||
|
'heartbeat' => -1,
|
||||||
|
'max_idle_time' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高级查询模式
|
||||||
|
|
||||||
|
### UPSERT(插入或更新)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Hyperf Eloquent upsert — 冲突时更新指定字段
|
||||||
|
ProductionOrder::query()->upsert(
|
||||||
|
[
|
||||||
|
['order_no' => 'PO-001', 'status' => 'active', 'total_amount' => 100.00],
|
||||||
|
['order_no' => 'PO-002', 'status' => 'pending', 'total_amount' => 200.00],
|
||||||
|
],
|
||||||
|
['order_no'], // conflict key (unique)
|
||||||
|
['status', 'total_amount'] // columns to update on conflict
|
||||||
|
);
|
||||||
|
|
||||||
|
// 等效原生 SQL
|
||||||
|
// INSERT INTO production_orders (order_no, status, total_amount)
|
||||||
|
// VALUES ('PO-001', 'active', 100.00), ('PO-002', 'pending', 200.00)
|
||||||
|
// ON DUPLICATE KEY UPDATE status = VALUES(status), total_amount = VALUES(total_amount);
|
||||||
|
```
|
||||||
|
|
||||||
|
适用场景:外部数据同步、批量导入、幂等写入。
|
||||||
|
|
||||||
|
### FOR UPDATE SKIP LOCKED(无锁队列消费)
|
||||||
|
|
||||||
|
MySQL 8.0+ 支持,适合自定义任务队列或分布式任务分发:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Atomic: claim one pending job without blocking other workers
|
||||||
|
$job = Db::select(
|
||||||
|
"SELECT * FROM async_jobs
|
||||||
|
WHERE status = 'pending'
|
||||||
|
ORDER BY created_at
|
||||||
|
LIMIT 1
|
||||||
|
FOR UPDATE SKIP LOCKED"
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($job) {
|
||||||
|
Db::update(
|
||||||
|
"UPDATE async_jobs SET status = 'processing', worker_id = ? WHERE id = ?",
|
||||||
|
[$workerId, $job[0]->id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
优势:多 Worker 并发消费时不阻塞,已被锁定的行自动跳过。
|
||||||
|
|
||||||
|
### 覆盖索引(Index-Only Scan)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 查询字段全在索引中,避免回表
|
||||||
|
CREATE INDEX idx_orders_cover
|
||||||
|
ON production_orders(status, created_at, total_amount, paid_amount);
|
||||||
|
|
||||||
|
-- 此查询只需扫描索引,不回表
|
||||||
|
SELECT status, total_amount, paid_amount
|
||||||
|
FROM production_orders
|
||||||
|
WHERE status = 'active' AND created_at > '2026-01-01';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 反模式检测 SQL
|
||||||
|
|
||||||
|
定期运行以下诊断查询,发现潜在问题:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 1. 检测未建索引的外键字段
|
||||||
|
SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME
|
||||||
|
FROM information_schema.KEY_COLUMN_USAGE
|
||||||
|
WHERE REFERENCED_TABLE_NAME IS NOT NULL
|
||||||
|
AND TABLE_SCHEMA = DATABASE()
|
||||||
|
AND COLUMN_NAME NOT IN (
|
||||||
|
SELECT COLUMN_NAME FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = KEY_COLUMN_USAGE.TABLE_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. 检测慢查询 Top 10(需开启 performance_schema)
|
||||||
|
SELECT DIGEST_TEXT AS query,
|
||||||
|
COUNT_STAR AS calls,
|
||||||
|
ROUND(AVG_TIMER_WAIT / 1000000000, 2) AS avg_ms,
|
||||||
|
ROUND(SUM_TIMER_WAIT / 1000000000, 2) AS total_ms
|
||||||
|
FROM performance_schema.events_statements_summary_by_digest
|
||||||
|
WHERE SCHEMA_NAME = DATABASE()
|
||||||
|
AND AVG_TIMER_WAIT > 500000000 -- > 500ms
|
||||||
|
ORDER BY AVG_TIMER_WAIT DESC
|
||||||
|
LIMIT 10;
|
||||||
|
|
||||||
|
-- 3. 检测表碎片(dead rows / 需要 OPTIMIZE)
|
||||||
|
SELECT TABLE_NAME,
|
||||||
|
TABLE_ROWS,
|
||||||
|
ROUND(DATA_LENGTH / 1024 / 1024, 2) AS data_mb,
|
||||||
|
ROUND(DATA_FREE / 1024 / 1024, 2) AS fragmented_mb
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND DATA_FREE > 10 * 1024 * 1024 -- > 10MB fragmentation
|
||||||
|
ORDER BY DATA_FREE DESC;
|
||||||
|
|
||||||
|
-- 4. 检测无用索引(从未使用)
|
||||||
|
SELECT s.TABLE_NAME, s.INDEX_NAME
|
||||||
|
FROM information_schema.STATISTICS s
|
||||||
|
LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage p
|
||||||
|
ON s.TABLE_SCHEMA = p.OBJECT_SCHEMA
|
||||||
|
AND s.TABLE_NAME = p.OBJECT_NAME
|
||||||
|
AND s.INDEX_NAME = p.INDEX_NAME
|
||||||
|
WHERE s.TABLE_SCHEMA = DATABASE()
|
||||||
|
AND s.INDEX_NAME != 'PRIMARY'
|
||||||
|
AND (p.COUNT_STAR IS NULL OR p.COUNT_STAR = 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 禁止的反模式
|
||||||
|
|
||||||
|
| 反模式 | 替代方案 |
|
||||||
|
|--------|----------|
|
||||||
|
| N+1 查询 | `with()` Eager Loading |
|
||||||
|
| 硬删除 | 软删除 + 定时清理 |
|
||||||
|
| 无索引外键 | 所有 FK 必须有索引 |
|
||||||
|
| `SELECT *` | 明确列名 `select()` |
|
||||||
|
| 长事务 | 拆分小事务 |
|
||||||
|
| 循环中单条 SQL | `insert()` 批量操作 |
|
||||||
|
| `OFFSET` 深分页 | 游标分页 `WHERE id > ?` |
|
||||||
|
| NOT NULL 无默认值加字段 | 先 nullable → 回填 → 加约束 |
|
||||||
|
| Schema + Data 混在一个迁移 | 拆为独立迁移文件 |
|
||||||
|
| 修改已部署的迁移文件 | 创建新的前向迁移 |
|
||||||
|
|
||||||
|
## 迁移文件命名规范
|
||||||
|
|
||||||
|
迁移文件严格遵循以下命名格式:
|
||||||
|
|
||||||
|
```
|
||||||
|
YYYY_MM_DD_HHMMSS_description.php
|
||||||
|
```
|
||||||
|
|
||||||
|
| 操作 | 文件名示例 |
|
||||||
|
|------|----------|
|
||||||
|
| 创建表 | `2026_02_24_100000_create_production_orders_table.php` |
|
||||||
|
| 添加字段 | `2026_02_24_110000_add_payment_status_to_production_orders.php` |
|
||||||
|
| 添加索引 | `2026_02_24_120000_add_index_status_to_production_orders.php` |
|
||||||
|
| 修改字段 | `2026_02_24_130000_modify_amount_column_in_production_orders.php` |
|
||||||
|
| 删除字段 | `2026_02_24_140000_drop_legacy_field_from_production_orders.php` |
|
||||||
|
|
||||||
|
**规则**:
|
||||||
|
- 时间戳精确到秒,保证顺序
|
||||||
|
- `description` 用 snake_case,必须清晰表达操作内容
|
||||||
|
- 每个迁移只做一件事(一张表或一类变更)
|
||||||
|
- 必须实现 `down()` 方法支持回滚
|
||||||
|
|
||||||
|
## Schema 变更流程
|
||||||
|
|
||||||
|
1. 读取 `data-model.md` → 2. 设计变更 → 3. 编写 Migration (含 `down()` 回滚) → 4. 开发环境执行 → 5. 更新文档
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成迁移
|
||||||
|
php bin/hyperf.php gen:migration create_orders_table
|
||||||
|
|
||||||
|
# 执行迁移
|
||||||
|
php bin/hyperf.php migrate
|
||||||
|
|
||||||
|
# 回滚
|
||||||
|
php bin/hyperf.php migrate:rollback
|
||||||
|
|
||||||
|
# 生成模型
|
||||||
|
php bin/hyperf.php gen:model production_orders
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心表关系图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
%% ── 用户体系 ──────────────────
|
||||||
|
users ||--o{ user_roles : "has"
|
||||||
|
roles ||--o{ user_roles : "has"
|
||||||
|
roles ||--o{ role_menus : "has"
|
||||||
|
roles ||--o{ role_depts : "has"
|
||||||
|
menus ||--o{ role_menus : "has"
|
||||||
|
departments ||--o{ users : "belongs"
|
||||||
|
departments ||--o{ role_depts : "has"
|
||||||
|
departments ||--o{ departments : "parent"
|
||||||
|
|
||||||
|
users {
|
||||||
|
bigint id PK
|
||||||
|
varchar username UK
|
||||||
|
varchar password
|
||||||
|
varchar real_name
|
||||||
|
bigint dept_id FK
|
||||||
|
tinyint status
|
||||||
|
tinyint data_scope
|
||||||
|
timestamp last_login_at
|
||||||
|
}
|
||||||
|
|
||||||
|
roles {
|
||||||
|
bigint id PK
|
||||||
|
varchar name UK
|
||||||
|
varchar code UK
|
||||||
|
tinyint data_scope
|
||||||
|
tinyint status
|
||||||
|
}
|
||||||
|
|
||||||
|
departments {
|
||||||
|
bigint id PK
|
||||||
|
varchar name
|
||||||
|
bigint parent_id FK
|
||||||
|
int sort_order
|
||||||
|
tinyint status
|
||||||
|
}
|
||||||
|
|
||||||
|
menus {
|
||||||
|
bigint id PK
|
||||||
|
varchar name
|
||||||
|
varchar path
|
||||||
|
varchar permission
|
||||||
|
bigint parent_id FK
|
||||||
|
tinyint type
|
||||||
|
int sort_order
|
||||||
|
}
|
||||||
|
|
||||||
|
%% ── 生产体系 ──────────────────
|
||||||
|
production_orders ||--o{ production_sub_orders : "has"
|
||||||
|
production_orders ||--o{ production_payments : "has"
|
||||||
|
production_orders }o--|| customers : "belongs"
|
||||||
|
production_orders }o--|| platforms : "belongs"
|
||||||
|
production_sub_orders ||--o{ production_items : "has"
|
||||||
|
|
||||||
|
production_orders {
|
||||||
|
bigint id PK
|
||||||
|
varchar order_no UK
|
||||||
|
bigint customer_id FK
|
||||||
|
bigint platform_id FK
|
||||||
|
tinyint status
|
||||||
|
decimal total_amount
|
||||||
|
decimal paid_amount
|
||||||
|
json source_data
|
||||||
|
bigint created_by FK
|
||||||
|
}
|
||||||
|
|
||||||
|
production_sub_orders {
|
||||||
|
bigint id PK
|
||||||
|
bigint order_id FK
|
||||||
|
varchar sub_order_no UK
|
||||||
|
tinyint status
|
||||||
|
decimal amount
|
||||||
|
}
|
||||||
|
|
||||||
|
production_payments {
|
||||||
|
bigint id PK
|
||||||
|
bigint order_id FK
|
||||||
|
varchar payment_no UK
|
||||||
|
decimal amount
|
||||||
|
tinyint payment_method
|
||||||
|
tinyint status
|
||||||
|
}
|
||||||
|
|
||||||
|
customers {
|
||||||
|
bigint id PK
|
||||||
|
varchar name
|
||||||
|
varchar company
|
||||||
|
varchar contact_phone
|
||||||
|
tinyint level
|
||||||
|
}
|
||||||
|
|
||||||
|
platforms {
|
||||||
|
bigint id PK
|
||||||
|
varchar name UK
|
||||||
|
varchar code UK
|
||||||
|
tinyint status
|
||||||
|
}
|
||||||
|
|
||||||
|
%% ── 通知体系 ──────────────────
|
||||||
|
notifications ||--o{ notification_reads : "has"
|
||||||
|
users ||--o{ notification_reads : "has"
|
||||||
|
|
||||||
|
notifications {
|
||||||
|
bigint id PK
|
||||||
|
varchar type
|
||||||
|
varchar title
|
||||||
|
text content
|
||||||
|
json data
|
||||||
|
bigint sender_id FK
|
||||||
|
tinyint scope
|
||||||
|
}
|
||||||
|
|
||||||
|
notification_reads {
|
||||||
|
bigint id PK
|
||||||
|
bigint notification_id FK
|
||||||
|
bigint user_id FK
|
||||||
|
timestamp read_at
|
||||||
|
}
|
||||||
|
|
||||||
|
%% ── 审批流程 ──────────────────
|
||||||
|
workflows ||--o{ workflow_nodes : "has"
|
||||||
|
workflow_nodes ||--o{ workflow_records : "has"
|
||||||
|
users ||--o{ workflow_records : "approves"
|
||||||
|
|
||||||
|
workflows {
|
||||||
|
bigint id PK
|
||||||
|
varchar name
|
||||||
|
varchar type
|
||||||
|
bigint reference_id
|
||||||
|
tinyint status
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow_nodes {
|
||||||
|
bigint id PK
|
||||||
|
bigint workflow_id FK
|
||||||
|
int step
|
||||||
|
varchar name
|
||||||
|
bigint assignee_id FK
|
||||||
|
tinyint status
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow_records {
|
||||||
|
bigint id PK
|
||||||
|
bigint node_id FK
|
||||||
|
bigint user_id FK
|
||||||
|
tinyint action
|
||||||
|
text comment
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 汇总表预聚合(报表优化)
|
||||||
|
|
||||||
|
大量统计查询不应实时扫描明细表,而应使用汇总表 + 定时任务预聚合:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 创建汇总表
|
||||||
|
CREATE TABLE order_stats_daily (
|
||||||
|
stat_date DATE NOT NULL,
|
||||||
|
platform_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
total_orders INT UNSIGNED DEFAULT 0,
|
||||||
|
total_amount DECIMAL(14,2) DEFAULT 0.00,
|
||||||
|
avg_amount DECIMAL(10,2) DEFAULT 0.00,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (stat_date, platform_id)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Hyperf Crontab: hourly aggregation
|
||||||
|
#[Crontab(rule: "5 * * * *", name: "AggregateOrderStats")]
|
||||||
|
class AggregateOrderStatsTask extends AbstractTask
|
||||||
|
{
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
Db::statement("
|
||||||
|
INSERT INTO order_stats_daily (stat_date, platform_id, total_orders, total_amount, avg_amount)
|
||||||
|
SELECT DATE(created_at), platform_id,
|
||||||
|
COUNT(*), SUM(total_amount), AVG(total_amount)
|
||||||
|
FROM production_orders
|
||||||
|
WHERE created_at >= CURDATE()
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
GROUP BY DATE(created_at), platform_id
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
total_orders = VALUES(total_orders),
|
||||||
|
total_amount = VALUES(total_amount),
|
||||||
|
avg_amount = VALUES(avg_amount)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
适用场景:仪表盘、报表导出、趋势分析。明细查询仍走原表。
|
||||||
|
|
||||||
|
## MySQL 分区表(百万级+)
|
||||||
|
|
||||||
|
数据量超百万且有明确时间维度的表,考虑 RANGE 分区:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 按月分区(适合时间序列数据)
|
||||||
|
ALTER TABLE operation_logs PARTITION BY RANGE (TO_DAYS(created_at)) (
|
||||||
|
PARTITION p202601 VALUES LESS THAN (TO_DAYS('2026-02-01')),
|
||||||
|
PARTITION p202602 VALUES LESS THAN (TO_DAYS('2026-03-01')),
|
||||||
|
PARTITION p202603 VALUES LESS THAN (TO_DAYS('2026-04-01')),
|
||||||
|
PARTITION pmax VALUES LESS THAN MAXVALUE
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**分区适用条件**(全部满足才考虑):
|
||||||
|
- 数据量 > 500 万行
|
||||||
|
- 查询几乎总是带时间范围条件
|
||||||
|
- 需要高效的历史数据归档/清理
|
||||||
|
- 表没有跨分区的唯一索引需求
|
||||||
|
|
||||||
|
**不适合分区的场景**:
|
||||||
|
- 小表(分区开销 > 收益)
|
||||||
|
- 查询不带分区键(全分区扫描更慢)
|
||||||
|
- 需要跨分区外键约束
|
||||||
|
|
||||||
|
## 数据库缓存配置
|
||||||
|
|
||||||
|
使用 Redis 缓存频繁查询的数据,减少数据库压力:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/autoload/cache.php
|
||||||
|
return [
|
||||||
|
'default' => [
|
||||||
|
'driver' => Hyperf\Cache\Driver\RedisDriver::class,
|
||||||
|
'packer' => Hyperf\Codec\Packer\PhpSerializerPacker::class,
|
||||||
|
'prefix' => 'cache:',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 缓存使用示例
|
||||||
|
// 注解式缓存(推荐简单场景)
|
||||||
|
#[Cacheable(prefix: 'user', ttl: 3600)]
|
||||||
|
public function getById(int $id): ?User
|
||||||
|
{
|
||||||
|
return User::find($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[CacheEvict(prefix: 'user')]
|
||||||
|
public function update(int $id, array $data): bool
|
||||||
|
{
|
||||||
|
return User::where('id', $id)->update($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动缓存(复杂场景)
|
||||||
|
$user = $this->cache->remember("user:{$id}", 3600, fn() => User::find($id));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 缓存 TTL 策略
|
||||||
|
|
||||||
|
| 数据类型 | TTL | 失效策略 |
|
||||||
|
|---------|-----|---------|
|
||||||
|
| 字典/配置 | 7 天 | 管理员修改时失效 |
|
||||||
|
| 用户信息 | 1 小时 | 用户更新时失效 |
|
||||||
|
| 菜单树 | 24 小时 | 菜单变更时失效 |
|
||||||
|
| 列表查询 | 5 分钟 | 写操作后批量失效 |
|
||||||
|
| 统计报表 | 1 小时 | 定时任务刷新 |
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# 017-architecture.mdc (Deep Reference)
|
||||||
|
|
||||||
|
> 该文件为原始详细规范归档,供 Tier 3 按需读取。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# 🏗️ Million-Level Concurrency Architecture Standards
|
||||||
|
|
||||||
|
## 架构总览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐
|
||||||
|
│ CDN/WAF │
|
||||||
|
└──────┬───────┘
|
||||||
|
│
|
||||||
|
┌────────────┴────────────┐
|
||||||
|
│ Nginx Cluster │
|
||||||
|
│ (负载均衡 + SSL + 静态) │
|
||||||
|
└────────────┬────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────┼─────────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||||
|
│ Hyperf-1 │ │ Hyperf-2 │ │ Hyperf-N │
|
||||||
|
│ Swoole │ │ Swoole │ │ Swoole │
|
||||||
|
│ HTTP:9501│ │ HTTP:9501│ │ HTTP:9501│
|
||||||
|
│ WS:9502 │ │ WS:9502 │ │ WS:9502 │
|
||||||
|
└────┬─────┘ └────┬─────┘ └────┬─────┘
|
||||||
|
│ │ │
|
||||||
|
└───────┬───────┴───────┬───────┘
|
||||||
|
│ │
|
||||||
|
┌──────┴──────┐ ┌─────┴──────┐
|
||||||
|
│ Redis │ │ MySQL │
|
||||||
|
│ Cluster/ │ │ Master │
|
||||||
|
│ Sentinel │ │ ├─Slave 1 │
|
||||||
|
│ │ │ └─Slave 2 │
|
||||||
|
└─────────────┘ └────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 无状态服务设计
|
||||||
|
|
||||||
|
所有 Hyperf 实例必须无状态,确保水平扩展:
|
||||||
|
|
||||||
|
| 状态类型 | 存储位置 | 禁止 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| 用户 Session | Redis | 存储在内存/文件 |
|
||||||
|
| JWT Token | Redis (可验证+可吊销) | 仅本地验证 |
|
||||||
|
| 文件上传 | 对象存储 (S3/OSS) | 本地 storage/ |
|
||||||
|
| WebSocket 连接 | Redis 维护映射表 | 进程内变量 |
|
||||||
|
| 配置 | 配置中心 / env | 硬编码 |
|
||||||
|
|
||||||
|
## 缓存策略
|
||||||
|
|
||||||
|
### 多级缓存
|
||||||
|
|
||||||
|
```
|
||||||
|
L1: 进程内缓存 (APCu / Swoole Table) — 毫秒级, 容量小
|
||||||
|
L2: Redis 缓存 — 亚毫秒级, 容量中
|
||||||
|
L3: MySQL 查询 — 毫秒级, 容量大
|
||||||
|
```
|
||||||
|
|
||||||
|
### 缓存防护
|
||||||
|
|
||||||
|
| 问题 | 场景 | 解决方案 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **穿透** | 查询不存在的数据 | 布隆过滤器 / 缓存空值 (TTL 30s) |
|
||||||
|
| **雪崩** | 大量缓存同时过期 | TTL 加随机抖动 / 预热 |
|
||||||
|
| **击穿** | 热点 Key 过期时大量请求 | 互斥锁 (singleflight) |
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Cache-Aside with mutex lock
|
||||||
|
public function getOrderWithLock(int $id): ?ProductionOrder
|
||||||
|
{
|
||||||
|
$cacheKey = "order:{$id}";
|
||||||
|
$lockKey = "lock:order:{$id}";
|
||||||
|
|
||||||
|
$cached = $this->redis->get($cacheKey);
|
||||||
|
if ($cached !== false) {
|
||||||
|
return unserialize($cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutex: only one coroutine queries DB
|
||||||
|
$lock = $this->redis->set($lockKey, '1', ['NX', 'EX' => 5]);
|
||||||
|
if ($lock) {
|
||||||
|
try {
|
||||||
|
$order = ProductionOrder::find($id);
|
||||||
|
$ttl = 300 + random_int(0, 60); // jitter
|
||||||
|
$this->redis->setex($cacheKey, $ttl, serialize($order));
|
||||||
|
return $order;
|
||||||
|
} finally {
|
||||||
|
$this->redis->del($lockKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait and retry
|
||||||
|
Coroutine::sleep(0.05);
|
||||||
|
return $this->getOrderWithLock($id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 限流策略
|
||||||
|
|
||||||
|
### 令牌桶 (API 级别)
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Hyperf\RateLimit\Annotation\RateLimit;
|
||||||
|
|
||||||
|
#[RateLimit(create: 1, capacity: 100, limitCallback: [RateLimitHandler::class, 'handle'])]
|
||||||
|
public function list(): array
|
||||||
|
{
|
||||||
|
// 每秒生成 1 个令牌,桶容量 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 滑动窗口 (用户级别)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Redis ZSET sliding window
|
||||||
|
public function isRateLimited(string $key, int $maxRequests, int $windowSeconds): bool
|
||||||
|
{
|
||||||
|
$now = microtime(true);
|
||||||
|
$windowStart = $now - $windowSeconds;
|
||||||
|
|
||||||
|
$pipe = $this->redis->pipeline();
|
||||||
|
$pipe->zremrangebyscore($key, '-inf', (string) $windowStart);
|
||||||
|
$pipe->zadd($key, [(string) $now => $now]);
|
||||||
|
$pipe->zcard($key);
|
||||||
|
$pipe->expire($key, $windowSeconds);
|
||||||
|
$results = $pipe->exec();
|
||||||
|
|
||||||
|
return $results[2] > $maxRequests;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 消息队列削峰
|
||||||
|
|
||||||
|
```
|
||||||
|
高并发写入:
|
||||||
|
Client → API → Redis Queue (缓冲) → Consumer → MySQL
|
||||||
|
|
||||||
|
适用场景:
|
||||||
|
- 订单创建 (削峰)
|
||||||
|
- 通知发送 (异步)
|
||||||
|
- 日志记录 (解耦)
|
||||||
|
- 数据统计 (批处理)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 服务降级与熔断
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 降级策略
|
||||||
|
class OrderService
|
||||||
|
{
|
||||||
|
public function getStatistics(int $orderId): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->doGetStatistics($orderId);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Fallback: return cached/default data
|
||||||
|
return $this->getCachedStatistics($orderId) ?? self::DEFAULT_STATS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 熔断器模式
|
||||||
|
// Circuit Breaker: CLOSED → OPEN → HALF-OPEN → CLOSED
|
||||||
|
// Use hyperf/circuit-breaker component
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据库扩展策略
|
||||||
|
|
||||||
|
### 垂直拆分 (按业务域)
|
||||||
|
|
||||||
|
```
|
||||||
|
production_db → 订单、子订单、发货
|
||||||
|
permission_db → 用户、角色、权限
|
||||||
|
notification_db → 通知、消息
|
||||||
|
```
|
||||||
|
|
||||||
|
### 水平分片 (百万级后)
|
||||||
|
|
||||||
|
```
|
||||||
|
分片键: order_id
|
||||||
|
分片策略: order_id % shard_count
|
||||||
|
路由层: Hyperf 中间层 或 ProxySQL
|
||||||
|
|
||||||
|
注意: 跨分片查询需要聚合层
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署扩展清单
|
||||||
|
|
||||||
|
| 阶段 | QPS | 架构 |
|
||||||
|
|------|-----|------|
|
||||||
|
| 起步 | < 1K | 单机 Docker Compose |
|
||||||
|
| 成长 | 1K ~ 10K | Nginx + 2~4 Hyperf + MySQL主从 + Redis Sentinel |
|
||||||
|
| 规模 | 10K ~ 100K | K8s + HPA + MySQL Cluster + Redis Cluster |
|
||||||
|
| 百万 | 100K+ | 微服务拆分 + 消息队列 + 分库分表 + CDN |
|
||||||
|
|
||||||
|
## 模块通信规范
|
||||||
|
|
||||||
|
> 与 `019-modular.mdc` 互补,本节侧重**跨服务/跨进程**通信;019 侧重**代码内模块边界**。
|
||||||
|
|
||||||
|
### 通信方式选择矩阵
|
||||||
|
|
||||||
|
| 场景 | 推荐方式 | 禁止方式 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 同进程同步调用 | 依赖注入 + 接口 | 全局静态方法 |
|
||||||
|
| 跨进程异步 | AsyncQueue / RabbitMQ | 轮询 DB |
|
||||||
|
| 实时推送 | WebSocket + Redis Pub/Sub | HTTP 长轮询 |
|
||||||
|
| 定时任务 | Hyperf Crontab | sleep 循环 |
|
||||||
|
| 跨服务数据 | REST API / 共享 Redis | 直连对方 DB |
|
||||||
|
|
||||||
|
### 前端模块通信规范
|
||||||
|
|
||||||
|
```
|
||||||
|
组件间通信优先级(由低到高耦合):
|
||||||
|
1. Props & Emits(父子) — 首选
|
||||||
|
2. provide/inject(跨层级) — 适合主题/配置
|
||||||
|
3. Pinia Store(状态共享) — 跨模块全局状态
|
||||||
|
4. EventBus (mitt) — 非父子一次性事件(慎用)
|
||||||
|
5. URL / Query Params — 页面级状态持久化
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 推荐:通过 Pinia 跨模块共享状态
|
||||||
|
// stores/notification.ts
|
||||||
|
export const useNotificationStore = defineStore('notification', () => {
|
||||||
|
const unreadCount = ref(0)
|
||||||
|
const messages = ref<Notification[]>([])
|
||||||
|
|
||||||
|
function addNotification(msg: Notification): void {
|
||||||
|
messages.value.unshift(msg)
|
||||||
|
unreadCount.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { unreadCount, messages, addNotification }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 订单模块使用通知(无直接耦合)
|
||||||
|
// views/order/composables/useOrderActions.ts
|
||||||
|
const notificationStore = useNotificationStore()
|
||||||
|
|
||||||
|
async function createOrder(data: OrderCreateForm): Promise<void> {
|
||||||
|
const order = await OrderApi.create(data)
|
||||||
|
// 通过 Store 通知,不直接调用通知组件
|
||||||
|
notificationStore.addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: '订单创建成功',
|
||||||
|
content: `订单号 ${order.orderNo} 已提交`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket 模块通信
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/composables/useWebSocket.ts — 全局单例 WebSocket 管理
|
||||||
|
// 所有模块订阅自己关心的消息类型,不感知连接细节
|
||||||
|
export const wsClient = useWebSocket()
|
||||||
|
|
||||||
|
// 订单模块:只监听订单相关消息
|
||||||
|
const { on, off } = useWebSocket()
|
||||||
|
on('order.status_changed', (payload) => {
|
||||||
|
orderStore.updateOrderStatus(payload.orderId, payload.status)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 通知模块:只监听通知消息
|
||||||
|
on('notification.new', (payload) => {
|
||||||
|
notificationStore.addNotification(payload)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能基线
|
||||||
|
|
||||||
|
| 指标 | 目标值 | 监控方式 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| API P99 延迟 | < 200ms | Prometheus + Grafana |
|
||||||
|
| 数据库查询 | < 50ms | 慢查询日志 |
|
||||||
|
| Redis 命令 | < 5ms | Redis INFO |
|
||||||
|
| 缓存命中率 | > 85% | 自定义指标 |
|
||||||
|
| 错误率 | < 0.1% | Sentry / 日志 |
|
||||||
|
| 可用性 | 99.9% | 健康检查 + 告警 |
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
# 018-responsive.mdc (Deep Reference)
|
||||||
|
|
||||||
|
> 该文件为原始详细规范归档,供 Tier 3 按需读取。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# 📱 Responsive Design & Multi-Device Standards
|
||||||
|
|
||||||
|
## 断点体系(与 Tailwind 统一)
|
||||||
|
|
||||||
|
> **⚠️ 双前端区分**:本文件中的 Element Plus 移动端适配内容**仅适用于管理端** (`Case-Database-Frontend-admin/`)。
|
||||||
|
> 用户端 (`Case-Database-Frontend-user/`) 使用 Headless UI + Tailwind CSS,**禁止引入 Element Plus**。
|
||||||
|
|
||||||
|
| 断点 | Tailwind 前缀 | 最小宽度 | 目标设备 |
|
||||||
|
|------|--------------|---------|---------|
|
||||||
|
| 默认 | (无前缀) | 0px | 手机竖屏 (< 640px) |
|
||||||
|
| sm | `sm:` | 640px | 手机横屏 / 小平板 |
|
||||||
|
| md | `md:` | 768px | 平板竖屏 (iPad) |
|
||||||
|
| lg | `lg:` | 1024px | 平板横屏 / 小屏笔记本 |
|
||||||
|
| xl | `xl:` | 1280px | 桌面 |
|
||||||
|
| 2xl | `2xl:` | 1536px | 大屏桌面 / 4K |
|
||||||
|
|
||||||
|
> **原则**:移动优先(Mobile-First)— 先写手机样式,用断点向上覆盖。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ✅ 移动优先:默认手机,逐步增强 -->
|
||||||
|
<div class="p-4 md:p-6 xl:p-8 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
|
<!-- 手机 1列 → 平板 2列 → 桌面 3列 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ❌ 桌面优先(禁用):max-* 限制往下适配 -->
|
||||||
|
<div class="grid grid-cols-3 max-md:grid-cols-1">...</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 布局组件响应式模式
|
||||||
|
|
||||||
|
### 侧边栏布局(后台管理系统标准模式)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- src/layouts/AppLayout.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="flex h-screen overflow-hidden bg-gray-50 dark:bg-gray-900">
|
||||||
|
<!-- 遮罩层:移动端打开侧边栏时显示 -->
|
||||||
|
<Transition name="fade">
|
||||||
|
<div
|
||||||
|
v-if="isMobile && sidebarOpen"
|
||||||
|
class="fixed inset-0 z-20 bg-black/50"
|
||||||
|
@click="sidebarOpen = false"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<!-- 侧边栏:桌面固定,移动端抽屉式 -->
|
||||||
|
<aside
|
||||||
|
:class="[
|
||||||
|
'fixed lg:relative z-30 h-full transition-transform duration-300',
|
||||||
|
'w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700',
|
||||||
|
isMobile && !sidebarOpen ? '-translate-x-full' : 'translate-x-0',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<ArtSidebarMenu />
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<div class="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<header class="h-14 md:h-16 flex items-center px-4 md:px-6 border-b bg-white dark:bg-gray-800">
|
||||||
|
<!-- 移动端:汉堡菜单按钮 -->
|
||||||
|
<button
|
||||||
|
class="lg:hidden mr-3 p-2 rounded-lg text-gray-500 hover:bg-gray-100"
|
||||||
|
aria-label="打开菜单"
|
||||||
|
@click="sidebarOpen = !sidebarOpen"
|
||||||
|
>
|
||||||
|
<el-icon :size="20"><Menu /></el-icon>
|
||||||
|
</button>
|
||||||
|
<ArtHeader />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 页面内容 -->
|
||||||
|
<main class="flex-1 overflow-auto">
|
||||||
|
<div class="p-4 md:p-6 xl:p-8 max-w-screen-2xl mx-auto">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const { isMobile } = useDevice()
|
||||||
|
const sidebarOpen = ref(false)
|
||||||
|
|
||||||
|
// 路由变化时关闭移动端侧边栏
|
||||||
|
const route = useRoute()
|
||||||
|
watch(() => route.path, () => {
|
||||||
|
if (isMobile.value) sidebarOpen.value = false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `useDevice` Composable
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/composables/useDevice.ts
|
||||||
|
export function useDevice() {
|
||||||
|
const width = ref(window.innerWidth)
|
||||||
|
|
||||||
|
const handleResize = useDebounceFn(() => {
|
||||||
|
width.value = window.innerWidth
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('resize', handleResize))
|
||||||
|
onUnmounted(() => window.removeEventListener('resize', handleResize))
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: readonly(width),
|
||||||
|
isMobile: computed(() => width.value < 768),
|
||||||
|
isTablet: computed(() => width.value >= 768 && width.value < 1024),
|
||||||
|
isDesktop: computed(() => width.value >= 1024),
|
||||||
|
isTouch: computed(() => 'ontouchstart' in window),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Element Plus 移动端适配(仅管理端)
|
||||||
|
|
||||||
|
### 组件尺寸策略
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const { isMobile } = useDevice()
|
||||||
|
const elSize = computed(() => (isMobile.value ? 'small' : 'default'))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- ✅ 根据设备自适应 size -->
|
||||||
|
<el-form :size="elSize">
|
||||||
|
<el-input :size="elSize" />
|
||||||
|
<el-button :size="elSize" type="primary">提交</el-button>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- ✅ 表格移动端简化列 -->
|
||||||
|
<el-table :data="tableData">
|
||||||
|
<el-table-column prop="name" label="名称" min-width="120" />
|
||||||
|
<el-table-column v-if="!isMobile" prop="createdAt" label="创建时间" width="160" />
|
||||||
|
<el-table-column v-if="!isMobile" prop="status" label="状态" width="100" />
|
||||||
|
<!-- 移动端合并展示 -->
|
||||||
|
<el-table-column v-if="isMobile" label="详情" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="text-sm">{{ row.status }} · {{ row.createdAt }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 对话框适配
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ✅ 移动端全屏对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:fullscreen="isMobile"
|
||||||
|
:width="isMobile ? '100%' : '600px'"
|
||||||
|
:class="{ 'rounded-t-2xl': isMobile }"
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分页适配
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ✅ 移动端简化分页 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:layout="isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'"
|
||||||
|
:pager-count="isMobile ? 3 : 7"
|
||||||
|
:page-sizes="isMobile ? [10, 20] : [10, 20, 50, 100]"
|
||||||
|
:total="total"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 表单布局
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ✅ 移动端垂直布局,桌面水平布局 -->
|
||||||
|
<el-form
|
||||||
|
:label-position="isMobile ? 'top' : 'right'"
|
||||||
|
:label-width="isMobile ? 'auto' : '100px'"
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 触摸与手势优化
|
||||||
|
|
||||||
|
### 最小触摸目标尺寸
|
||||||
|
|
||||||
|
```scss
|
||||||
|
// src/assets/styles/touch.scss
|
||||||
|
// 根据 WCAG 2.5.5,触摸目标最小 44×44px
|
||||||
|
.touch-target {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
// 触屏设备:增大可点击区域
|
||||||
|
.el-button {
|
||||||
|
min-height: 44px;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 滑动手势(移动端列表操作)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/composables/useSwipeAction.ts
|
||||||
|
export function useSwipeAction(onDelete: () => void, onEdit: () => void) {
|
||||||
|
const startX = ref(0)
|
||||||
|
const offsetX = ref(0)
|
||||||
|
const THRESHOLD = 80
|
||||||
|
|
||||||
|
function onTouchStart(e: TouchEvent) {
|
||||||
|
startX.value = e.touches[0].clientX
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchMove(e: TouchEvent) {
|
||||||
|
const diff = e.touches[0].clientX - startX.value
|
||||||
|
offsetX.value = Math.max(-160, Math.min(0, diff)) // 最多滑动 160px
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchEnd() {
|
||||||
|
if (offsetX.value < -THRESHOLD) {
|
||||||
|
// 滑过阈值:显示操作按钮
|
||||||
|
} else {
|
||||||
|
offsetX.value = 0 // 弹回
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { offsetX, onTouchStart, onTouchMove, onTouchEnd }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 图片与媒体响应式
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- ✅ 响应式图片:不同分辨率加载不同尺寸 -->
|
||||||
|
<picture>
|
||||||
|
<source media="(min-width: 1280px)" srcset="/img/banner-xl.webp" />
|
||||||
|
<source media="(min-width: 768px)" srcset="/img/banner-md.webp" />
|
||||||
|
<img src="/img/banner-sm.webp" alt="Banner" class="w-full h-auto object-cover" loading="lazy" />
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<!-- ✅ 使用 Intersection Observer 懒加载 -->
|
||||||
|
<img
|
||||||
|
v-lazy="imageUrl"
|
||||||
|
alt="产品图片"
|
||||||
|
class="w-full aspect-square object-cover rounded-lg"
|
||||||
|
:class="{ 'animate-pulse bg-gray-200': !isLoaded }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 字体与文字排版
|
||||||
|
|
||||||
|
```scss
|
||||||
|
// 流式字体大小:随屏幕宽度线性变化
|
||||||
|
:root {
|
||||||
|
--font-base: clamp(14px, 1vw + 12px, 16px);
|
||||||
|
--font-heading: clamp(20px, 3vw + 10px, 36px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body { font-size: var(--font-base); }
|
||||||
|
h1 { font-size: var(--font-heading); }
|
||||||
|
|
||||||
|
// 行高:移动端更宽松(小屏阅读舒适度)
|
||||||
|
p {
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安全区域(刘海屏 / 全面屏)
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 底部安全区域(iOS 全面屏 Home Bar)*/
|
||||||
|
.bottom-nav {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部安全区域(刘海屏)*/
|
||||||
|
.top-bar {
|
||||||
|
padding-top: env(safe-area-inset-top, 0px);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tailwind.config.ts — 添加安全区域工具类
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
padding: {
|
||||||
|
'safe-bottom': 'env(safe-area-inset-bottom, 16px)',
|
||||||
|
'safe-top': 'env(safe-area-inset-top, 0px)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 响应式测试矩阵
|
||||||
|
|
||||||
|
| 设备 | 分辨率 | 断点 | 关键功能检查 |
|
||||||
|
|------|--------|------|------------|
|
||||||
|
| iPhone SE (3代) | 375×667 | xs | 侧边栏抽屉、表单垂直布局 |
|
||||||
|
| iPhone 14 Pro | 393×852 | xs | 安全区域、全面屏底部导航 |
|
||||||
|
| iPhone 14 Pro Max | 430×932 | sm | 横屏布局、键盘遮挡处理 |
|
||||||
|
| iPad (10代) | 820×1180 | md | 平板双栏、对话框宽度 |
|
||||||
|
| iPad Pro 12.9" | 1024×1366 | lg | 分页组件、表格完整列 |
|
||||||
|
| MacBook Air 13" | 1280×800 | xl | 完整侧边栏、桌面表格 |
|
||||||
|
| 4K 显示器 | 1920×1080 | 2xl | 最大宽度限制、留白 |
|
||||||
|
|
||||||
|
### 测试脚本(Playwright)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/responsive.spec.ts
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
const viewports = [
|
||||||
|
{ name: 'mobile', width: 375, height: 812 },
|
||||||
|
{ name: 'tablet', width: 768, height: 1024 },
|
||||||
|
{ name: 'desktop', width: 1280, height: 800 },
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const vp of viewports) {
|
||||||
|
test(`dashboard layout on ${vp.name}`, async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: vp.width, height: vp.height })
|
||||||
|
await page.goto('/dashboard')
|
||||||
|
|
||||||
|
if (vp.name === 'mobile') {
|
||||||
|
// 移动端:侧边栏默认隐藏
|
||||||
|
await expect(page.locator('aside')).toHaveCSS('transform', 'matrix(1, 0, 0, 1, -256, 0)')
|
||||||
|
// 汉堡按钮可见
|
||||||
|
await expect(page.locator('[aria-label="打开菜单"]')).toBeVisible()
|
||||||
|
} else {
|
||||||
|
// 平板/桌面:侧边栏默认显示
|
||||||
|
await expect(page.locator('aside')).toBeVisible()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 规则
|
||||||
|
|
||||||
|
- 所有新页面必须在 375px / 768px / 1280px 三个断点下验证
|
||||||
|
- 禁止使用 `px` 设置字体大小(用 `rem` 或 Tailwind 文本类)
|
||||||
|
- 触摸目标最小 44×44px
|
||||||
|
- 禁止使用 `:hover` 作为唯一交互反馈(移动端无 hover)
|
||||||
|
- 管理端:所有 `el-dialog` 必须处理移动端 `fullscreen` 属性(用户端使用 Headless UI Dialog)
|
||||||
|
- 图片必须设置 `loading="lazy"` 和 `aspect-ratio`(防止布局抖动 CLS)
|
||||||
|
- 横屏(landscape)模式下的布局需专门测试
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
# 019-modular.mdc (Deep Reference)
|
||||||
|
|
||||||
|
> 该文件为原始详细规范归档,供 Tier 3 按需读取。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# 🧩 Modular Architecture Standards
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
| 原则 | 说明 | 违反案例 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **单一职责** | 一个文件只做一件事 | 1000 行的 `index.vue` 包含业务+状态+样式 |
|
||||||
|
| **高内聚低耦合** | 同模块内紧密,跨模块通过接口 | 直接引用另一模块的内部 Service |
|
||||||
|
| **依赖方向单一** | 只允许 `UI → Service → Repository → Model` | Service 引用 Controller |
|
||||||
|
| **显式边界** | 模块通过公开的 index.ts 暴露 API | `import { InternalHelper } from '../order/utils/internal'` |
|
||||||
|
| **禁止循环依赖** | A 不能引用 B 的同时 B 引用 A | order 模块和 user 模块互相引用 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端模块划分
|
||||||
|
|
||||||
|
### 标准目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── api/ # API 请求层(按模块分文件)
|
||||||
|
│ ├── user.ts # 用户相关接口
|
||||||
|
│ ├── order.ts # 订单相关接口
|
||||||
|
│ ├── permission.ts # 权限相关接口
|
||||||
|
│ └── index.ts # 统一导出
|
||||||
|
│
|
||||||
|
├── composables/ # 可复用逻辑(按功能命名 use*.ts)
|
||||||
|
│ ├── useTable.ts # 表格通用逻辑
|
||||||
|
│ ├── useForm.ts # 表单通用逻辑
|
||||||
|
│ ├── useAuth.ts # 认证状态
|
||||||
|
│ ├── useDevice.ts # 设备检测
|
||||||
|
│ ├── usePermission.ts # 权限检查
|
||||||
|
│ └── useWebSocket.ts # WebSocket 管理
|
||||||
|
│
|
||||||
|
├── stores/ # Pinia 状态(按模块分文件)
|
||||||
|
│ ├── user.ts
|
||||||
|
│ ├── menu.ts
|
||||||
|
│ ├── setting.ts
|
||||||
|
│ └── index.ts # 统一导出
|
||||||
|
│
|
||||||
|
├── components/ # 全局公共组件
|
||||||
|
│ ├── ArtTable/ # 每个复杂组件独立目录
|
||||||
|
│ │ ├── index.vue # 主组件
|
||||||
|
│ │ ├── TableToolbar.vue # 子组件(内聚)
|
||||||
|
│ │ ├── TablePagination.vue
|
||||||
|
│ │ └── types.ts # 组件专属类型
|
||||||
|
│ ├── ArtForm/
|
||||||
|
│ └── ArtChart/
|
||||||
|
│
|
||||||
|
├── views/ # 页面(按业务域分目录)
|
||||||
|
│ ├── order/ # 订单业务域
|
||||||
|
│ │ ├── index.vue # 列表页
|
||||||
|
│ │ ├── detail.vue # 详情页
|
||||||
|
│ │ ├── components/ # 页面专属组件(不对外)
|
||||||
|
│ │ │ ├── OrderCard.vue
|
||||||
|
│ │ │ └── OrderTimeline.vue
|
||||||
|
│ │ ├── composables/ # 页面专属 composable
|
||||||
|
│ │ │ └── useOrderFilter.ts
|
||||||
|
│ │ └── types.ts # 页面专属类型
|
||||||
|
│ │
|
||||||
|
│ ├── user/
|
||||||
|
│ ├── permission/
|
||||||
|
│ └── dashboard/
|
||||||
|
│
|
||||||
|
├── utils/ # 纯工具函数(无副作用)
|
||||||
|
│ ├── format.ts # 格式化
|
||||||
|
│ ├── validate.ts # 校验
|
||||||
|
│ ├── crypto.ts # 加密工具
|
||||||
|
│ └── date.ts # 日期工具
|
||||||
|
│
|
||||||
|
├── directives/ # 自定义指令(每个指令独立文件)
|
||||||
|
│ ├── auth.ts # v-auth
|
||||||
|
│ ├── roles.ts # v-roles
|
||||||
|
│ └── index.ts # 统一注册
|
||||||
|
│
|
||||||
|
└── types/ # 全局类型定义
|
||||||
|
├── api.ts # API 响应/请求类型
|
||||||
|
├── models.ts # 业务实体类型
|
||||||
|
└── common.ts # 通用类型
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文件拆分阈值
|
||||||
|
|
||||||
|
| 指标 | 警告 | 必须拆分 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 文件行数 | > 200 行 | > 400 行 |
|
||||||
|
| 组件内 ref 数量 | > 8 个 | > 15 个 |
|
||||||
|
| 单个 composable 行数 | > 100 行 | > 200 行 |
|
||||||
|
| 单个 store 行数 | > 150 行 | > 300 行 |
|
||||||
|
|
||||||
|
### 组件拆分决策树
|
||||||
|
|
||||||
|
```
|
||||||
|
一个 .vue 文件是否应该拆分?
|
||||||
|
├─ 文件 > 200 行? → 考虑拆分
|
||||||
|
├─ 包含多个独立 UI 块? → 拆分为子组件
|
||||||
|
├─ 业务逻辑超过 50 行? → 提取为 composable
|
||||||
|
├─ 该组件在 > 2 个地方复用? → 移至 components/
|
||||||
|
└─ 否 → 保持不变
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 后端模块划分(Hyperf DDD)
|
||||||
|
|
||||||
|
### 标准目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── Controller/ # HTTP 入口层(只做参数接收 + 响应格式化)
|
||||||
|
│ ├── OrderController.php
|
||||||
|
│ ├── UserController.php
|
||||||
|
│ └── Traits/
|
||||||
|
│ └── ApiResponse.php # 响应格式 Trait
|
||||||
|
│
|
||||||
|
├── Service/ # 业务逻辑层(核心业务规则)
|
||||||
|
│ ├── OrderService.php
|
||||||
|
│ ├── UserService.php
|
||||||
|
│ └── Dto/ # 数据传输对象
|
||||||
|
│ ├── OrderCreateDto.php
|
||||||
|
│ └── OrderListDto.php
|
||||||
|
│
|
||||||
|
├── Repository/ # 数据访问层(数据库/缓存操作)
|
||||||
|
│ ├── OrderRepository.php
|
||||||
|
│ ├── UserRepository.php
|
||||||
|
│ └── Contracts/ # Repository 接口
|
||||||
|
│ └── OrderRepositoryInterface.php
|
||||||
|
│
|
||||||
|
├── Model/ # Eloquent ORM 模型
|
||||||
|
│ ├── Order.php
|
||||||
|
│ ├── User.php
|
||||||
|
│ └── Traits/ # 可复用模型 Trait
|
||||||
|
│ ├── HasCreator.php
|
||||||
|
│ ├── HasSoftDeletes.php
|
||||||
|
│ └── HasDataPermission.php
|
||||||
|
│
|
||||||
|
├── Request/ # 表单验证(每个操作独立 Request)
|
||||||
|
│ ├── Order/
|
||||||
|
│ │ ├── CreateOrderRequest.php
|
||||||
|
│ │ ├── UpdateOrderRequest.php
|
||||||
|
│ │ └── ListOrderRequest.php
|
||||||
|
│ └── User/
|
||||||
|
│
|
||||||
|
├── Event/ # 事件定义(命名:名词+动词过去式)
|
||||||
|
│ ├── OrderCreated.php
|
||||||
|
│ ├── OrderStatusChanged.php
|
||||||
|
│ └── UserLoggedIn.php
|
||||||
|
│
|
||||||
|
├── Listener/ # 事件监听器(一个事件一个 Listener)
|
||||||
|
│ ├── SendOrderNotification.php
|
||||||
|
│ ├── LogOrderStatusChange.php
|
||||||
|
│ └── UpdateUserLastLogin.php
|
||||||
|
│
|
||||||
|
├── Middleware/ # 中间件(每个职责独立文件)
|
||||||
|
│ ├── AuthMiddleware.php
|
||||||
|
│ ├── PermissionMiddleware.php
|
||||||
|
│ ├── RateLimitMiddleware.php
|
||||||
|
│ └── AntiScrapingMiddleware.php
|
||||||
|
│
|
||||||
|
├── Exception/ # 异常定义(每类业务一个异常)
|
||||||
|
│ ├── BusinessException.php
|
||||||
|
│ ├── AuthException.php
|
||||||
|
│ ├── PermissionException.php
|
||||||
|
│ └── Handler/
|
||||||
|
│ └── AppExceptionHandler.php
|
||||||
|
│
|
||||||
|
├── Job/ # 异步任务(AsyncQueue)
|
||||||
|
│ ├── SendNotificationJob.php
|
||||||
|
│ └── GenerateReportJob.php
|
||||||
|
│
|
||||||
|
└── Command/ # 命令行工具
|
||||||
|
└── SyncPermissionCommand.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### 各层职责边界
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ✅ Controller — 只做参数绑定 + 调用 Service + 格式化响应
|
||||||
|
class OrderController
|
||||||
|
{
|
||||||
|
public function store(CreateOrderRequest $request): array
|
||||||
|
{
|
||||||
|
$dto = CreateOrderDto::fromRequest($request);
|
||||||
|
$order = $this->orderService->create($dto);
|
||||||
|
return $this->success(OrderResource::make($order));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Service — 只做业务逻辑,调用 Repository 取数据
|
||||||
|
class OrderService
|
||||||
|
{
|
||||||
|
public function create(CreateOrderDto $dto): Order
|
||||||
|
{
|
||||||
|
// 业务规则:库存检查
|
||||||
|
$this->inventoryService->checkStock($dto->productId, $dto->quantity);
|
||||||
|
|
||||||
|
$order = $this->orderRepository->create($dto->toArray());
|
||||||
|
|
||||||
|
// 发事件(不直接发通知,解耦)
|
||||||
|
event(new OrderCreated($order));
|
||||||
|
|
||||||
|
return $order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Repository — 只做数据库操作,不包含业务规则
|
||||||
|
class OrderRepository
|
||||||
|
{
|
||||||
|
public function create(array $data): Order
|
||||||
|
{
|
||||||
|
return Order::create($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findWithItems(int $id): ?Order
|
||||||
|
{
|
||||||
|
return Order::with(['items', 'creator'])->find($id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 反模式:Controller 直接操作 Model
|
||||||
|
class BadController
|
||||||
|
{
|
||||||
|
public function store(Request $request): array
|
||||||
|
{
|
||||||
|
$order = Order::create($request->all()); // ← 跳过 Service 层
|
||||||
|
Mail::to($order->user)->send(new OrderMail($order)); // ← Controller 不应发邮件
|
||||||
|
return ['data' => $order];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模块通信规范
|
||||||
|
|
||||||
|
| 通信方式 | 使用场景 | 实现 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| **直接依赖注入** | 同层级调用(Service → Service) | `__construct` 注入 |
|
||||||
|
| **事件系统** | 跨模块解耦通知 | `event(new OrderCreated($order))` |
|
||||||
|
| **消息队列** | 耗时操作异步化 | `AsyncQueue::push(new SendEmailJob(...))` |
|
||||||
|
| **共享 Repository** | 跨模块读取数据 | 注入对方 Repository(只读) |
|
||||||
|
| **禁止** | 跨模块调用内部方法 | ✘ `$userService->_internalCalc()` |
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ✅ 事件解耦:订单模块不直接依赖通知模块
|
||||||
|
// app/Event/OrderCreated.php
|
||||||
|
class OrderCreated
|
||||||
|
{
|
||||||
|
public function __construct(public readonly Order $order) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// app/Listener/SendOrderNotification.php(通知模块监听)
|
||||||
|
#[Listener]
|
||||||
|
class SendOrderNotification implements ListenerInterface
|
||||||
|
{
|
||||||
|
public function listen(): array
|
||||||
|
{
|
||||||
|
return [OrderCreated::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(object $event): void
|
||||||
|
{
|
||||||
|
$this->notificationService->notify($event->order->user_id, 'order_created', [
|
||||||
|
'order_id' => $event->order->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 禁止循环依赖
|
||||||
|
|
||||||
|
```
|
||||||
|
依赖方向规则(严格单向):
|
||||||
|
|
||||||
|
Controller → Service → Repository → Model
|
||||||
|
↓
|
||||||
|
Event → Listener
|
||||||
|
↓
|
||||||
|
Job(异步)
|
||||||
|
|
||||||
|
✅ Service 可以依赖 Repository
|
||||||
|
✅ Service 可以发布 Event
|
||||||
|
✅ Listener 可以依赖 Service
|
||||||
|
❌ Repository 不能依赖 Service
|
||||||
|
❌ Model 不能依赖 Service/Repository
|
||||||
|
❌ Event 不能依赖 Service(Event 是纯数据)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检测循环依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PHP 项目
|
||||||
|
composer require maglnet/composer-require-checker --dev
|
||||||
|
|
||||||
|
# 前端项目(ESLint 插件)
|
||||||
|
npm install eslint-plugin-import --save-dev
|
||||||
|
# .eslintrc 中配置 import/no-cycle
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vue 组件模块化规范
|
||||||
|
|
||||||
|
### 组件分类
|
||||||
|
|
||||||
|
| 类型 | 位置 | 命名 | 特征 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| **基础组件** | `src/components/` | `Art*` | 无业务逻辑,高复用 |
|
||||||
|
| **业务组件** | `src/views/{domain}/components/` | `{Domain}*` | 包含业务,不跨域复用 |
|
||||||
|
| **页面组件** | `src/views/{domain}/index.vue` | 路由直接映射 | 整合子组件和 store |
|
||||||
|
| **布局组件** | `src/layouts/` | `*Layout` | 全局页面框架 |
|
||||||
|
|
||||||
|
### 单文件组件结构(SFC)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 标准顺序:script → template → style -->
|
||||||
|
<script setup>
|
||||||
|
// 1. 导入(分组排列)
|
||||||
|
import { computed, ref } from 'vue' // Vue core
|
||||||
|
import { useRouter } from 'vue-router' // Router
|
||||||
|
import { storeToRefs } from 'pinia' // Pinia
|
||||||
|
import { useOrderStore } from '@/stores/order' // Stores
|
||||||
|
import { useTable } from '@/composables/useTable' // Composables
|
||||||
|
import { OrderApi } from '@/api/order' // API
|
||||||
|
import OrderCard from './components/OrderCard.vue' // Local components
|
||||||
|
|
||||||
|
// 2. Props & Emits(JS 对象语法)
|
||||||
|
const props = defineProps({
|
||||||
|
domain: { type: String, required: true },
|
||||||
|
readonly: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['updated', 'deleted'])
|
||||||
|
|
||||||
|
// 3. Store
|
||||||
|
const orderStore = useOrderStore()
|
||||||
|
const { orders, loading } = storeToRefs(orderStore)
|
||||||
|
|
||||||
|
// 4. Composable(复杂逻辑提取)
|
||||||
|
const { tableData, pagination, fetchData } = useTable(OrderApi.list)
|
||||||
|
|
||||||
|
// 5. 本地状态(按功能分组,加注释分隔)
|
||||||
|
// --- Dialog state ---
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const currentOrder = ref(null)
|
||||||
|
|
||||||
|
// --- Filter state ---
|
||||||
|
const searchForm = ref({ keyword: '', status: '' })
|
||||||
|
|
||||||
|
// 6. Computed
|
||||||
|
const filteredOrders = computed(() =>
|
||||||
|
orders.value.filter((o) => o.domain === props.domain)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 7. Methods(按功能分组)
|
||||||
|
function openDialog(order) {
|
||||||
|
currentOrder.value = order
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(id) {
|
||||||
|
await orderStore.delete(id)
|
||||||
|
emit('deleted', id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Lifecycle
|
||||||
|
onMounted(() => fetchData())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 模板只做渲染,不包含复杂逻辑 -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 组件作用域样式 */
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Composable 拆分规范
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 单一职责:useOrderFilter 只负责过滤逻辑
|
||||||
|
// src/views/order/composables/useOrderFilter.ts
|
||||||
|
export function useOrderFilter(orders) {
|
||||||
|
const status = ref('')
|
||||||
|
const keyword = ref('')
|
||||||
|
const dateRange = ref(null)
|
||||||
|
|
||||||
|
const filtered = computed(() => {
|
||||||
|
return orders.value.filter((order) => {
|
||||||
|
if (status.value && order.status !== status.value) return false
|
||||||
|
if (keyword.value && !order.orderNo.includes(keyword.value)) return false
|
||||||
|
if (dateRange.value) {
|
||||||
|
const [start, end] = dateRange.value
|
||||||
|
if (order.createdAt < start || order.createdAt > end) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
status.value = ''
|
||||||
|
keyword.value = ''
|
||||||
|
dateRange.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status, keyword, dateRange, filtered, reset }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 反模式:把表格、过滤、编辑逻辑都放在一个 composable
|
||||||
|
export function useOrderPage() { // 太大了
|
||||||
|
// ... 300 行混合逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端导入层级边界
|
||||||
|
|
||||||
|
```
|
||||||
|
导入方向规则(严格单向):
|
||||||
|
|
||||||
|
views/ → composables/ → stores/ → api/ → utils/
|
||||||
|
│ │
|
||||||
|
│ └→ components/(仅消费,不反向引用 views/)
|
||||||
|
└→ components/
|
||||||
|
|
||||||
|
✅ views/ 可以引用 composables/、stores/、api/、components/
|
||||||
|
✅ composables/ 可以引用 stores/、api/、utils/
|
||||||
|
✅ stores/ 可以引用 api/、utils/
|
||||||
|
✅ components/ 可以引用 composables/、utils/(通用组件不引用 stores/)
|
||||||
|
❌ components/core/ 不得引用 stores/(通用组件不依赖业务状态)
|
||||||
|
❌ api/ 不得引用 stores/ 或 views/
|
||||||
|
❌ utils/ 不得引用任何其他层(纯函数,无副作用)
|
||||||
|
❌ 跨业务域直接引用内部组件(通过公开 index.ts 导出)
|
||||||
|
```
|
||||||
|
|
||||||
|
**ESLint 配置建议**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// .eslintrc — import/no-restricted-paths
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'import/no-restricted-paths': ['error', {
|
||||||
|
zones: [
|
||||||
|
{ target: './src/utils', from: './src/stores' },
|
||||||
|
{ target: './src/utils', from: './src/api' },
|
||||||
|
{ target: './src/api', from: './src/stores' },
|
||||||
|
{ target: './src/components/core', from: './src/stores' },
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 规则汇总
|
||||||
|
|
||||||
|
- 单个 `.vue` 文件超过 200 行必须拆分,超过 400 行禁止提交
|
||||||
|
- 业务逻辑超过 50 行必须提取为 composable
|
||||||
|
- Composable 必须以 `use` 开头,返回对象必须解构友好
|
||||||
|
- 每个 API 模块对应一个文件(`src/api/{module}.ts`)
|
||||||
|
- 禁止在 Controller 中操作 Model(必须经过 Service + Repository)
|
||||||
|
- 禁止在 Model 中调用 Service(保持 Model 纯净)
|
||||||
|
- 禁止跨业务域直接引用内部组件(通过公开 `index.ts` 导出)
|
||||||
|
- 事件名必须是:`{域}` + `{动作的过去时}` (如 `OrderCreated`)
|
||||||
|
- 每次 PR 提交前运行 `eslint --rule import/no-cycle` 检查循环依赖
|
||||||
|
- 前端导入方向必须单向:`views → composables → stores → api → utils`
|
||||||
|
- 通用组件 (`components/core/`) 禁止依赖业务状态 (`stores/`)
|
||||||
@@ -0,0 +1,442 @@
|
|||||||
|
# 023-accessibility.mdc (Deep Reference)
|
||||||
|
|
||||||
|
> 该文件为原始详细规范归档,供 Tier 3 按需读取。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# ♿ Accessibility (A11y) — WCAG AA Standards
|
||||||
|
|
||||||
|
> **⚠️ 双前端区分**:本文件中的 `el-*` 组件示例**仅适用于管理端** (`Case-Database-Frontend-admin/`)。
|
||||||
|
> 用户端 (`Case-Database-Frontend-user/`) 使用 Headless UI + Tailwind CSS 实现同等无障碍标准,**禁止引入 Element Plus**。
|
||||||
|
> Headless UI 组件天然内置 ARIA 属性,用户端可直接使用。
|
||||||
|
|
||||||
|
## WCAG AA 基线要求
|
||||||
|
|
||||||
|
| 原则 | 要求 | 检测方法 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **可感知** | 图片有 alt;颜色对比度 ≥ 4.5:1 (文本) / 3:1 (大文本) | Axe DevTools / Lighthouse |
|
||||||
|
| **可操作** | 所有功能可键盘访问;无键盘陷阱;跳转链接 | Tab 键手动测试 |
|
||||||
|
| **可理解** | 语言声明;错误提示清晰;一致导航 | 屏幕阅读器测试 |
|
||||||
|
| **健壮性** | 语义化 HTML;ARIA 正确使用 | W3C Validator |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 语义化 HTML(Vue 3)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- ✅ 语义化页面结构 -->
|
||||||
|
<div id="app">
|
||||||
|
<!-- 跳转链接:键盘用户快速跳过导航 -->
|
||||||
|
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-white focus:rounded">
|
||||||
|
跳转到主要内容
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<header role="banner">
|
||||||
|
<nav role="navigation" aria-label="主导航">
|
||||||
|
<ul>
|
||||||
|
<li><router-link to="/dashboard">首页</router-link></li>
|
||||||
|
<li><router-link to="/orders">订单</router-link></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main-content" role="main" tabindex="-1">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer role="contentinfo">
|
||||||
|
<p>© 2025 Company Name</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 键盘导航规范
|
||||||
|
|
||||||
|
### 焦点管理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/composables/useFocusTrap.ts
|
||||||
|
// 对话框焦点捕获:阻止 Tab 跳出对话框
|
||||||
|
export function useFocusTrap(containerRef: Ref<HTMLElement | null>) {
|
||||||
|
function trapFocus(event: KeyboardEvent): void {
|
||||||
|
if (!containerRef.value || event.key !== 'Tab') return
|
||||||
|
|
||||||
|
const focusable = containerRef.value.querySelectorAll<HTMLElement>(
|
||||||
|
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
)
|
||||||
|
const first = focusable[0]
|
||||||
|
const last = focusable[focusable.length - 1]
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
if (document.activeElement === first) {
|
||||||
|
last.focus()
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (document.activeElement === last) {
|
||||||
|
first.focus()
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('keydown', trapFocus))
|
||||||
|
onUnmounted(() => document.removeEventListener('keydown', trapFocus))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/composables/useFocusReturn.ts
|
||||||
|
// 对话框关闭后,焦点回到触发元素
|
||||||
|
export function useFocusReturn() {
|
||||||
|
const triggerEl = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
function saveTrigger(): void {
|
||||||
|
triggerEl.value = document.activeElement as HTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreFocus(): void {
|
||||||
|
nextTick(() => triggerEl.value?.focus())
|
||||||
|
}
|
||||||
|
|
||||||
|
return { saveTrigger, restoreFocus }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 键盘快捷键
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/composables/useKeyboard.ts
|
||||||
|
export function useKeyboard(handlers: Record<string, () => void>) {
|
||||||
|
function handleKeydown(event: KeyboardEvent): void {
|
||||||
|
const key = [
|
||||||
|
event.ctrlKey && 'Ctrl',
|
||||||
|
event.altKey && 'Alt',
|
||||||
|
event.shiftKey && 'Shift',
|
||||||
|
event.key,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('+')
|
||||||
|
|
||||||
|
handlers[key]?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('keydown', handleKeydown))
|
||||||
|
onUnmounted(() => document.removeEventListener('keydown', handleKeydown))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
useKeyboard({
|
||||||
|
'Escape': () => closeDialog(),
|
||||||
|
'Ctrl+s': () => saveForm(),
|
||||||
|
'Ctrl+/': () => toggleHelp(),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ARIA 模式库(Vue 3 组件)
|
||||||
|
|
||||||
|
### 模态框
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- src/components/ArtDialog/index.vue -->
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="dialog">
|
||||||
|
<div
|
||||||
|
v-if="modelValue"
|
||||||
|
ref="dialogRef"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
:aria-labelledby="titleId"
|
||||||
|
:aria-describedby="descId"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
|
@keydown.esc="$emit('update:modelValue', false)"
|
||||||
|
>
|
||||||
|
<!-- 背景遮罩 -->
|
||||||
|
<div class="absolute inset-0 bg-black/50" aria-hidden="true" @click="$emit('update:modelValue', false)" />
|
||||||
|
|
||||||
|
<!-- 对话框内容 -->
|
||||||
|
<div class="relative bg-white rounded-xl p-6 max-w-md w-full mx-4">
|
||||||
|
<h2 :id="titleId" class="text-xl font-semibold">{{ title }}</h2>
|
||||||
|
<p v-if="description" :id="descId" class="mt-2 text-gray-600">{{ description }}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 focus:ring-2 focus:ring-primary-500 rounded"
|
||||||
|
aria-label="关闭对话框"
|
||||||
|
@click="$emit('update:modelValue', false)"
|
||||||
|
>
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useFocusTrap } from '@/composables/useFocusTrap'
|
||||||
|
import { useFocusReturn } from '@/composables/useFocusReturn'
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: boolean; title: string; description?: string }>()
|
||||||
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||||
|
|
||||||
|
const titleId = useId() // Vue 3.5+ 内置
|
||||||
|
const descId = useId()
|
||||||
|
const dialogRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
useFocusTrap(dialogRef)
|
||||||
|
const { saveTrigger, restoreFocus } = useFocusReturn()
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (isOpen) => {
|
||||||
|
if (isOpen) {
|
||||||
|
saveTrigger()
|
||||||
|
nextTick(() => {
|
||||||
|
// 聚焦第一个可交互元素
|
||||||
|
const firstFocusable = dialogRef.value?.querySelector<HTMLElement>(
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
)
|
||||||
|
firstFocusable?.focus()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
restoreFocus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 加载状态
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- ✅ 加载中的按钮(屏幕阅读器可感知) -->
|
||||||
|
<button
|
||||||
|
:aria-busy="isLoading"
|
||||||
|
:aria-disabled="isLoading"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
<el-icon v-if="isLoading" class="animate-spin" aria-hidden="true"><Loading /></el-icon>
|
||||||
|
<span>{{ isLoading ? '提交中...' : '提交' }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- ✅ 页面级加载状态 -->
|
||||||
|
<div aria-live="polite" aria-atomic="true" class="sr-only">
|
||||||
|
{{ isLoading ? '正在加载数据,请稍候' : '' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ 骨架屏 -->
|
||||||
|
<div v-if="isLoading" role="status" aria-label="内容加载中">
|
||||||
|
<el-skeleton :rows="5" animated />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 表单无障碍
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
novalidate
|
||||||
|
>
|
||||||
|
<!-- ✅ 必填字段标注 -->
|
||||||
|
<el-form-item
|
||||||
|
label="用户名"
|
||||||
|
prop="username"
|
||||||
|
:required="true"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="form.username"
|
||||||
|
:aria-required="true"
|
||||||
|
:aria-describedby="`username-hint ${formErrors.username ? 'username-error' : ''}`"
|
||||||
|
:aria-invalid="!!formErrors.username"
|
||||||
|
/>
|
||||||
|
<p id="username-hint" class="text-sm text-gray-500 mt-1">
|
||||||
|
3-20 位字母、数字或下划线
|
||||||
|
</p>
|
||||||
|
<!-- ✅ 错误提示:aria-live 确保屏幕阅读器读取 -->
|
||||||
|
<p
|
||||||
|
v-if="formErrors.username"
|
||||||
|
id="username-error"
|
||||||
|
role="alert"
|
||||||
|
class="text-sm text-red-500 mt-1"
|
||||||
|
>
|
||||||
|
{{ formErrors.username }}
|
||||||
|
</p>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据表格
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- ✅ 可访问的表格 -->
|
||||||
|
<div role="region" :aria-label="`${title}列表`" aria-live="polite">
|
||||||
|
<el-table
|
||||||
|
:data="tableData"
|
||||||
|
:aria-rowcount="total"
|
||||||
|
@sort-change="handleSort"
|
||||||
|
>
|
||||||
|
<!-- 选择列 -->
|
||||||
|
<el-table-column type="selection" :aria-label="'全选'" width="55" />
|
||||||
|
|
||||||
|
<!-- 数据列 -->
|
||||||
|
<el-table-column prop="name" label="名称" sortable>
|
||||||
|
<template #header>
|
||||||
|
<span>名称</span>
|
||||||
|
<el-tooltip content="按名称升序/降序排列">
|
||||||
|
<el-icon aria-hidden="true"><InfoFilled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
|
<el-table-column label="操作" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<!-- ✅ 操作按钮有明确的 aria-label -->
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:aria-label="`编辑 ${row.name}`"
|
||||||
|
@click="editRow(row)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
:aria-label="`删除 ${row.name}`"
|
||||||
|
@click="deleteRow(row.id)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
:total="total"
|
||||||
|
:aria-label="`分页,当前第 ${currentPage} 页,共 ${Math.ceil(total / pageSize)} 页`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 图标按钮(必须有 aria-label)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ✅ 图标按钮 -->
|
||||||
|
<button aria-label="关闭菜单" @click="closeMenu">
|
||||||
|
<el-icon aria-hidden="true"><Close /></el-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- ✅ 带 tooltip 的图标按钮 -->
|
||||||
|
<el-tooltip content="刷新数据" placement="top">
|
||||||
|
<button aria-label="刷新数据" @click="refresh">
|
||||||
|
<el-icon aria-hidden="true"><Refresh /></el-icon>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<!-- ❌ 无 label 的图标按钮 -->
|
||||||
|
<button @click="closeMenu">
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 颜色对比度
|
||||||
|
|
||||||
|
```scss
|
||||||
|
// 确保颜色对比度 ≥ 4.5:1
|
||||||
|
// 工具:https://webaim.org/resources/contrastchecker/
|
||||||
|
|
||||||
|
// ✅ 正文文字
|
||||||
|
.text-primary { color: #1d4ed8; } // 在白底: 7.5:1 ✓
|
||||||
|
.text-secondary { color: #374151; } // 在白底: 9.4:1 ✓
|
||||||
|
|
||||||
|
// ⚠️ 灰色文字需谨慎
|
||||||
|
.text-muted { color: #6b7280; } // 在白底: 4.6:1 ✓(刚好过 AA)
|
||||||
|
|
||||||
|
// ❌ 禁止使用
|
||||||
|
.text-too-light { color: #9ca3af; } // 在白底: 2.8:1 ✗(不过 AA)
|
||||||
|
|
||||||
|
// 状态颜色需同时用颜色 + 图标/文字(不只靠颜色区分)
|
||||||
|
.status-success {
|
||||||
|
color: #15803d;
|
||||||
|
// ✅ 同时用图标辅助:<el-icon><CircleCheck /></el-icon>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 动效规范(减少动效)
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 尊重用户减少动效偏好 */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/composables/useReducedMotion.ts
|
||||||
|
export function useReducedMotion() {
|
||||||
|
const preferReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')
|
||||||
|
return computed(() => preferReducedMotion.matches)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 自动化测试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/a11y.spec.ts — 使用 axe-playwright
|
||||||
|
import { checkA11y, injectAxe } from 'axe-playwright'
|
||||||
|
|
||||||
|
test('dashboard page has no accessibility violations', async ({ page }) => {
|
||||||
|
await page.goto('/dashboard')
|
||||||
|
await injectAxe(page)
|
||||||
|
await checkA11y(page, undefined, {
|
||||||
|
axeOptions: { runOnly: ['wcag2a', 'wcag2aa'] },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 axe 测试依赖
|
||||||
|
npm install -D axe-playwright @axe-core/playwright
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 规则
|
||||||
|
|
||||||
|
- 所有图片必须有 `alt`(装饰性图片用 `alt=""`)
|
||||||
|
- 所有图标按钮必须有 `aria-label`,图标本身加 `aria-hidden="true"`
|
||||||
|
- 所有表单字段必须有关联的 `<label>` 或 `aria-label`
|
||||||
|
- 错误提示必须使用 `role="alert"` 或 `aria-live="polite"`
|
||||||
|
- 对话框必须有焦点陷阱(focus trap)和 ESC 关闭支持
|
||||||
|
- 不能只用颜色区分状态(必须加文字或图标)
|
||||||
|
- 自动化测试须包含 axe-playwright a11y 扫描
|
||||||
|
- 产品上线前须通过 Lighthouse Accessibility 评分 ≥ 90
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
# 024-monitoring.mdc (Deep Reference)
|
||||||
|
|
||||||
|
> 该文件为原始详细规范归档,供 Tier 3 按需读取。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# 📊 Logging & Error Monitoring Standards (Hyperf + Vue 3)
|
||||||
|
|
||||||
|
## 日志规范
|
||||||
|
|
||||||
|
### 后端结构化日志 (Hyperf Monolog)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/autoload/logger.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Monolog\Formatter\JsonFormatter;
|
||||||
|
use Monolog\Handler\RotatingFileHandler;
|
||||||
|
use Monolog\Level;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'default' => [
|
||||||
|
'handler' => [
|
||||||
|
'class' => RotatingFileHandler::class,
|
||||||
|
'constructor' => [
|
||||||
|
'filename' => BASE_PATH . '/runtime/logs/hyperf.log',
|
||||||
|
'maxFiles' => 30,
|
||||||
|
'level' => Level::Info,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'formatter' => [
|
||||||
|
'class' => JsonFormatter::class,
|
||||||
|
'constructor' => [
|
||||||
|
'batchMode' => JsonFormatter::BATCH_MODE_JSON,
|
||||||
|
'appendNewline' => true,
|
||||||
|
'includeStacktraces' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'sql' => [
|
||||||
|
'handler' => [
|
||||||
|
'class' => RotatingFileHandler::class,
|
||||||
|
'constructor' => [
|
||||||
|
'filename' => BASE_PATH . '/runtime/logs/sql.log',
|
||||||
|
'maxFiles' => 14,
|
||||||
|
'level' => Level::Debug,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日志级别规则
|
||||||
|
|
||||||
|
| 级别 | 使用场景 | 示例 |
|
||||||
|
|------|---------|------|
|
||||||
|
| `debug` | 开发调试,生产禁用 | 函数入参、SQL 查询 |
|
||||||
|
| `info` | 关键业务事件 | 用户登录、订单创建、审批通过 |
|
||||||
|
| `warning` | 非预期但可恢复 | 缓存未命中、重试、Token 即将过期 |
|
||||||
|
| `error` | 需要处理的错误 | API 调用失败、数据库异常、队列失败 |
|
||||||
|
| `critical` | 服务无法继续运行 | 启动失败、关键依赖丢失 |
|
||||||
|
|
||||||
|
### 请求链路追踪 (TraceId)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Middleware/TraceIdMiddleware.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Middleware;
|
||||||
|
|
||||||
|
use Hyperf\Context\Context;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
|
||||||
|
class TraceIdMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
$traceId = $request->getHeaderLine('X-Trace-Id') ?: bin2hex(random_bytes(16));
|
||||||
|
Context::set('trace_id', $traceId);
|
||||||
|
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
|
||||||
|
return $response->withHeader('X-Trace-Id', $traceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Usage in Service
|
||||||
|
$this->logger->info('Order created', [
|
||||||
|
'trace_id' => Context::get('trace_id'),
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'user_id' => Context::get('current_user')?->id,
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 禁止事项
|
||||||
|
|
||||||
|
```
|
||||||
|
✗ 不在日志中记录密码、Token、信用卡号等敏感信息
|
||||||
|
✗ 不使用 var_dump / dd / echo 替代结构化日志
|
||||||
|
✗ 不在循环中记录 debug 日志(性能影响)
|
||||||
|
✗ 不记录完整的请求/响应体(可能含敏感数据)
|
||||||
|
✗ 生产环境禁止 debug 级别日志
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端错误监控 (Vue 3)
|
||||||
|
|
||||||
|
### 全局错误捕获
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/plugins/error-handler.ts
|
||||||
|
import { App } from 'vue'
|
||||||
|
|
||||||
|
export function setupErrorHandler(app) {
|
||||||
|
app.config.errorHandler = (error, instance, info) => {
|
||||||
|
console.error('Vue component error:', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
component: instance?.$options?.name,
|
||||||
|
info,
|
||||||
|
})
|
||||||
|
// Report to Sentry or custom error service
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
console.error('Unhandled promise rejection:', event.reason)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sentry 集成 (Vue 3 版)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/plugins/sentry.ts
|
||||||
|
import * as Sentry from '@sentry/vue'
|
||||||
|
|
||||||
|
export function setupSentry(app, router) {
|
||||||
|
if (import.meta.env.PROD) {
|
||||||
|
Sentry.init({
|
||||||
|
app,
|
||||||
|
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||||||
|
environment: import.meta.env.MODE,
|
||||||
|
integrations: [
|
||||||
|
Sentry.browserTracingIntegration({ router }),
|
||||||
|
],
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
|
beforeSend(event) {
|
||||||
|
if (event.request?.headers) {
|
||||||
|
delete event.request.headers['authorization']
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 服务端监控
|
||||||
|
|
||||||
|
### Swoole 进程监控
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Health check endpoint
|
||||||
|
#[RequestMapping(path: '/admin/health', methods: ['GET'])]
|
||||||
|
public function health(): ResponseInterface
|
||||||
|
{
|
||||||
|
$stats = $this->server->stats();
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'status' => 'ok',
|
||||||
|
'uptime' => time() - $stats['start_time'],
|
||||||
|
'connections' => $stats['connection_num'],
|
||||||
|
'requests' => $stats['request_count'],
|
||||||
|
'coroutine_num' => $stats['coroutine_num'],
|
||||||
|
'worker_num' => $stats['worker_num'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MySQL 慢查询监控
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 启用慢查询日志
|
||||||
|
SET GLOBAL slow_query_log = 'ON';
|
||||||
|
SET GLOBAL long_query_time = 1;
|
||||||
|
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Hyperf SQL 日志(开发环境)
|
||||||
|
// config/autoload/databases.php
|
||||||
|
'commands' => [
|
||||||
|
'gen:model' => [
|
||||||
|
'with_comments' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis 监控
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 关键指标
|
||||||
|
redis-cli INFO stats | grep -E 'keyspace_hits|keyspace_misses|connected_clients'
|
||||||
|
redis-cli INFO memory | grep used_memory_human
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 告警策略
|
||||||
|
|
||||||
|
### 告警优先级
|
||||||
|
|
||||||
|
| 优先级 | 触发条件 | 响应时间 | 通知方式 |
|
||||||
|
|--------|---------|---------|---------|
|
||||||
|
| P0 - 紧急 | 服务不可用、数据丢失 | 15 分钟内 | 电话 + 企业微信 |
|
||||||
|
| P1 - 高 | 错误率 > 5%、P99 > 5s | 1 小时内 | 企业微信 |
|
||||||
|
| P2 - 中 | 错误率 > 1%、异常流量 | 4 小时内 | 邮件 |
|
||||||
|
| P3 - 低 | 资源使用率异常 | 次日 | 日报 |
|
||||||
|
|
||||||
|
### 监控指标
|
||||||
|
|
||||||
|
```
|
||||||
|
# 业务指标
|
||||||
|
- API 请求成功率(目标 > 99.9%)
|
||||||
|
- API P50/P95/P99 响应时间
|
||||||
|
- 每分钟错误数(Error Rate)
|
||||||
|
|
||||||
|
# 系统指标
|
||||||
|
- CPU / 内存使用率(告警阈值:80%)
|
||||||
|
- Swoole Worker 协程数(告警阈值:90%)
|
||||||
|
- MySQL 连接池使用率(告警阈值:70%)
|
||||||
|
- Redis 内存使用率(告警阈值:70%)
|
||||||
|
- 磁盘使用率(告警阈值:85%)
|
||||||
|
- 队列堆积数(告警阈值:1000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 日志保留策略
|
||||||
|
|
||||||
|
| 环境 | 保留周期 | 存储方式 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 开发 | 7 天 | 本地文件 (runtime/logs/) |
|
||||||
|
| 测试 | 30 天 | 对象存储 |
|
||||||
|
| 生产 | 90 天(error 保留 1 年) | ELK / CloudWatch |
|
||||||
|
|
||||||
|
## 检查清单
|
||||||
|
|
||||||
|
- [ ] 所有 API 路由有请求日志(通过 TraceId 中间件)
|
||||||
|
- [ ] 错误捕获后记录完整 stack trace
|
||||||
|
- [ ] traceId 贯穿整个请求链路
|
||||||
|
- [ ] 敏感字段已从日志中过滤
|
||||||
|
- [ ] 生产环境 debug 日志已关闭
|
||||||
|
- [ ] 关键业务事件有对应告警规则
|
||||||
|
- [ ] 前端有全局错误捕获
|
||||||
|
- [ ] Swoole 健康检查端点可用
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# 026-secure-coding.mdc (Deep Reference)
|
||||||
|
|
||||||
|
> 该文件为原始详细规范归档,供 Tier 3 按需读取。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# 🔐 Secure Coding Standards (Project CodeGuard)
|
||||||
|
|
||||||
|
> 安全不是事后审查,而是编码时的默认选择。
|
||||||
|
|
||||||
|
## ❌ 禁用加密算法(绝对禁止)
|
||||||
|
|
||||||
|
以下算法已被证实不安全,**禁止在任何新代码中使用**:
|
||||||
|
|
||||||
|
| 类型 | 禁用 | 替代 |
|
||||||
|
|------|------|------|
|
||||||
|
| 哈希 | MD2、MD4、**MD5**、SHA-0、**SHA-1** | SHA-256、SHA-3 |
|
||||||
|
| 对称加密 | RC2、RC4、Blowfish、DES、3DES、**AES-ECB** | **AES-GCM**、ChaCha20-Poly1305 |
|
||||||
|
| 密钥交换 | 静态 RSA、匿名 DH | ECDHE (X25519) |
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ 禁止
|
||||||
|
$hash = md5($password);
|
||||||
|
$hash = sha1($data);
|
||||||
|
openssl_encrypt($data, 'AES-128-ECB', $key);
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
$hash = password_hash($password, PASSWORD_ARGON2ID);
|
||||||
|
$hash = hash('sha256', $data);
|
||||||
|
openssl_encrypt($data, 'AES-256-GCM', $key, 0, $iv, $tag);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💉 注入防护
|
||||||
|
|
||||||
|
### SQL 注入
|
||||||
|
- **100% 使用参数化查询**,禁止字符串拼接 SQL
|
||||||
|
- 使用 Hyperf ORM 或 PDO 绑定参数
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ 禁止
|
||||||
|
$result = Db::select("SELECT * FROM users WHERE name = '{$name}'");
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
$result = Db::select('SELECT * FROM users WHERE name = ?', [$name]);
|
||||||
|
// 或
|
||||||
|
User::where('name', $name)->first();
|
||||||
|
```
|
||||||
|
|
||||||
|
### OS 命令注入
|
||||||
|
- 优先使用内置函数替代 shell 调用
|
||||||
|
- 禁止将用户输入直接传入 `exec()`、`system()`、`shell_exec()`
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ 禁止
|
||||||
|
exec("convert {$userFile} output.png");
|
||||||
|
|
||||||
|
// ✅ 正确(使用内置库)
|
||||||
|
$image = new Imagick($sanitizedPath);
|
||||||
|
```
|
||||||
|
|
||||||
|
### TypeScript Prototype Pollution
|
||||||
|
- 使用 `Map` / `Set` 替代对象字面量存储用户数据
|
||||||
|
- 合并对象时拦截 `__proto__`、`constructor`、`prototype` 键
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 禁止
|
||||||
|
function merge(target, source) {
|
||||||
|
Object.assign(target, source) // 可能导致 prototype pollution
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
const safe = Object.create(null)
|
||||||
|
const blocked = ['__proto__', 'constructor', 'prototype']
|
||||||
|
Object.keys(source).filter(k => !blocked.includes(k)).forEach(k => { safe[k] = source[k] })
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔑 密码存储
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ 禁止(bcrypt 有 72 字节截断限制)
|
||||||
|
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||||
|
|
||||||
|
// ✅ 推荐(Argon2id:更安全,无长度限制)
|
||||||
|
$hash = password_hash($password, PASSWORD_ARGON2ID, [
|
||||||
|
'memory_cost' => 65536, // 64 MiB
|
||||||
|
'time_cost' => 2,
|
||||||
|
'threads' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 验证(常量时间比较,防时序攻击)
|
||||||
|
$valid = password_verify($input, $hash);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 访问控制(IDOR / Mass Assignment 防护)
|
||||||
|
|
||||||
|
### IDOR 防护
|
||||||
|
- 永远不要只凭用户传入的 ID 查询资源,必须附加所有权校验
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ 禁止 — 用户可访问任意 ID 的数据
|
||||||
|
$order = Order::find($request->input('id'));
|
||||||
|
|
||||||
|
// ✅ 正确 — 限定当前用户范围
|
||||||
|
$order = $this->user->orders()->findOrFail($request->input('id'));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mass Assignment 防护
|
||||||
|
- Hyperf FormRequest 必须声明 `rules()` 白名单
|
||||||
|
- Model 必须使用 `$fillable` 而非 `$guarded = []`
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ 禁止
|
||||||
|
$user->fill($request->all());
|
||||||
|
|
||||||
|
// ✅ 正确(只允许明确字段)
|
||||||
|
$user->fill($request->only(['name', 'email', 'avatar']));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🍪 Session 和 Cookie 安全
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 登录成功后必须轮转 Session ID(防 Session Fixation)
|
||||||
|
session_regenerate_id(true);
|
||||||
|
|
||||||
|
// Cookie 必须设置安全属性
|
||||||
|
setcookie('session', $id, [
|
||||||
|
'secure' => true, // 仅 HTTPS
|
||||||
|
'httponly' => true, // 禁止 JS 访问
|
||||||
|
'samesite' => 'Strict', // 防 CSRF
|
||||||
|
'path' => '/',
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Session 超时:高风险操作 5 分钟,常规 30 分钟,绝对上限 8 小时
|
||||||
|
- **禁止在 `localStorage` / `sessionStorage` 存储 Session Token**(XSS 可窃取)
|
||||||
|
|
||||||
|
## 🌐 客户端安全(Vue 3 / TypeScript)
|
||||||
|
|
||||||
|
### XSS 防护
|
||||||
|
```typescript
|
||||||
|
// ❌ 禁止 — 直接使用 v-html 且无净化
|
||||||
|
// <div v-html="userContent"></div>
|
||||||
|
|
||||||
|
// ✅ 正确 — 使用 DOMPurify 净化
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
const clean = DOMPurify.sanitize(userContent, {
|
||||||
|
ALLOWED_TAGS: ['b', 'i', 'p', 'a', 'ul', 'li'],
|
||||||
|
ALLOWED_ATTR: ['href'],
|
||||||
|
})
|
||||||
|
// <div v-html="clean"></div>
|
||||||
|
|
||||||
|
// ❌ 禁止 — 动态代码执行
|
||||||
|
eval(userCode)
|
||||||
|
new Function(userCode)()
|
||||||
|
setTimeout(userString, 100)
|
||||||
|
|
||||||
|
// ❌ 禁止 — 直接 DOM 写入
|
||||||
|
element.innerHTML = userInput
|
||||||
|
document.write(userInput)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 外链安全
|
||||||
|
```html
|
||||||
|
<!-- ✅ 所有 target="_blank" 必须加防护 -->
|
||||||
|
<a href="..." target="_blank" rel="noopener noreferrer">外链</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSRF 防护
|
||||||
|
- 所有状态变更请求(POST/PUT/DELETE/PATCH)必须携带 CSRF Token
|
||||||
|
- 禁止用 GET 请求执行状态变更操作
|
||||||
|
|
||||||
|
## 📁 文件上传安全
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ✅ 文件上传安全检查清单
|
||||||
|
|
||||||
|
// 1. 白名单扩展名(不是黑名单)
|
||||||
|
$allowed = ['jpg', 'png', 'gif', 'pdf'];
|
||||||
|
$ext = strtolower(pathinfo($file->getClientFilename(), PATHINFO_EXTENSION));
|
||||||
|
if (!in_array($ext, $allowed)) throw new ValidationException('不允许的文件类型');
|
||||||
|
|
||||||
|
// 2. 验证 magic bytes(不信任 Content-Type)
|
||||||
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||||
|
$mimeType = $finfo->file($tmpPath);
|
||||||
|
|
||||||
|
// 3. 生成随机文件名(不使用用户提供的文件名)
|
||||||
|
$safeName = Str::uuid() . '.' . $ext;
|
||||||
|
|
||||||
|
// 4. 存储在 webroot 外
|
||||||
|
$storagePath = BASE_PATH . '/storage/uploads/' . $safeName;
|
||||||
|
|
||||||
|
// 5. 设置文件大小限制
|
||||||
|
if ($file->getSize() > 10 * 1024 * 1024) throw new ValidationException('文件过大');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 安全日志规范
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ 禁止记录敏感信息
|
||||||
|
Log::info('用户登录', ['password' => $password, 'token' => $token]);
|
||||||
|
|
||||||
|
// ✅ 正确 — 只记录安全信息,用 hash 标识 session
|
||||||
|
Log::info('用户登录', [
|
||||||
|
'user_id' => $userId,
|
||||||
|
'ip' => $request->getServerParams()['REMOTE_ADDR'],
|
||||||
|
'user_agent' => substr($request->getHeaderLine('user-agent'), 0, 200),
|
||||||
|
'session_id' => substr(hash('sha256', $sessionId), 0, 16), // 前16位,不泄露原值
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ API 安全
|
||||||
|
|
||||||
|
- **禁止在 URL 参数中传递敏感数据**(会出现在日志里)
|
||||||
|
- GraphQL:生产环境禁用 introspection
|
||||||
|
- SSRF:所有外部 HTTP 请求必须验证目标域名白名单
|
||||||
|
|
||||||
|
```php
|
||||||
|
// SSRF 防护示例
|
||||||
|
$allowedDomains = ['api.trusted.com', 'cdn.trusted.com'];
|
||||||
|
$parsedUrl = parse_url($userProvidedUrl);
|
||||||
|
if (!in_array($parsedUrl['host'], $allowedDomains)) {
|
||||||
|
throw new SecurityException('不允许的目标地址');
|
||||||
|
}
|
||||||
|
// 还需阻断私有 IP 范围:10.x, 172.16.x, 192.168.x, 127.x
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ 编码时自查清单
|
||||||
|
|
||||||
|
每次提交前确认:
|
||||||
|
- [ ] 无 MD5/SHA1/DES/AES-ECB 使用
|
||||||
|
- [ ] SQL 查询 100% 参数化
|
||||||
|
- [ ] 资源查询已限定所有权范围(无 IDOR 风险)
|
||||||
|
- [ ] 无 `$request->all()` 直接绑定模型
|
||||||
|
- [ ] Cookie 包含 Secure + HttpOnly + SameSite 属性
|
||||||
|
- [ ] `v-html` 使用了 DOMPurify 净化
|
||||||
|
- [ ] 文件上传使用 UUID 文件名 + magic bytes 验证
|
||||||
|
- [ ] 日志未包含明文密码/Token/Session ID
|
||||||
|
|
||||||
|
> 📚 深度参考:`.cursor/skills/security-audit/references/codeguard/`
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
Hyperf 后端模块脚手架技能。新建 API 端点、Controller、Service、
|
||||||
|
Repository 或完整后端业务模块时激活。含分层约定、异常体系和事务管理。
|
||||||
|
globs:
|
||||||
|
- "**/Controller/**/*.php"
|
||||||
|
- "**/Service/**/*.php"
|
||||||
|
- "**/Repository/**/*.php"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Backend Scaffold (API + Service)
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、模板和深度参考见:
|
||||||
|
> Read `.cursor/skills/api-scaffold/SKILL.md`(API 端点)
|
||||||
|
> Read `.cursor/skills/hyperf-service/SKILL.md`(Service 模块)
|
||||||
|
|
||||||
|
> **适用性**(双模式):
|
||||||
|
> - **新建**后端模块/API 端点:走完整执行流程(Step 1-8)
|
||||||
|
> - **修改**已有接口或 Service:跳过脚手架步骤,完成后走「验证」清单
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
1. 加载规范:Read `013-backend.mdc`、`016-swoole.mdc`
|
||||||
|
2. 确认模块规格(资源名称、HTTP 方法、路由路径、是否需要 Repository/事件/数据权限)
|
||||||
|
3. 扫描项目约定:`app/Controller/`、`app/Service/` 现有模式
|
||||||
|
4. **配置依赖检查**(使用事件/队列/缓存时必须检查):
|
||||||
|
- 使用异步队列:检查 `config/autoload/async_queue.php` 是否存在
|
||||||
|
- 使用 Redis 缓存:检查 `config/autoload/redis.php` 连接池配置
|
||||||
|
- 使用 JWT:检查 `config/autoload/jwt.php` 和环境变量 `JWT_SECRET`
|
||||||
|
5. 生成文件:Controller → Service → Repository(如需)→ Model → FormRequest → Event(可选)→ 路由注册
|
||||||
|
6. Service 使用 `Db::transaction` 包裹写操作
|
||||||
|
7. Repository 实现 `applyFilters`、`applyDataScope`、分页上限
|
||||||
|
8. 集成 HasLogger Trait,关键操作有结构化日志
|
||||||
|
9. 外部 API 调用使用 RetryHelper 指数退避重试
|
||||||
|
|
||||||
|
## 分层约定
|
||||||
|
|
||||||
|
Controller(接收请求)→ Service(业务逻辑)→ Repository(数据访问)→ Model
|
||||||
|
|
||||||
|
## 异常体系
|
||||||
|
|
||||||
|
- `BusinessException` — 可预期业务错误(404/403/422)
|
||||||
|
- `ValidationException` — FormRequest 校验失败
|
||||||
|
- `SystemException` — 500 系统故障,记录堆栈
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] `php -l` 编译无错误
|
||||||
|
- [ ] FormRequest rules 覆盖所有请求字段
|
||||||
|
- [ ] 错误处理覆盖 400/401/403/404/422/500
|
||||||
|
- [ ] Service 使用事务包裹写操作
|
||||||
|
- [ ] 路由遵循 RESTful 命名(复数名词、kebab-case)
|
||||||
|
- [ ] 中间件正确挂载(认证 + 权限)
|
||||||
|
- [ ] 分页有 page_size 上限
|
||||||
|
- [ ] 关键操作有结构化日志
|
||||||
|
- [ ] 外部 API 用 RetryHelper
|
||||||
|
|
||||||
|
## 深度参考
|
||||||
|
|
||||||
|
- `.cursor/skills/api-scaffold/references/code-templates.md`
|
||||||
|
- `.cursor/skills/api-scaffold/references/exception-handling.md`
|
||||||
|
- `.cursor/skills/hyperf-service/references/service-templates.md`
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
代码审查技能。当用户要求 review 代码、检查质量、审查 PR、
|
||||||
|
或执行预提交检查时激活。从六个维度系统化审查。
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code Review — 六维度审查
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、模板和深度参考见:
|
||||||
|
> Read `.cursor/skills/code-review/SKILL.md`
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 确定审查范围
|
||||||
|
|
||||||
|
- 具体文件 → 审查指定文件
|
||||||
|
- 目录范围 → 审查目录下所有变更
|
||||||
|
- Git diff → `git diff HEAD~1` 审查最近变更
|
||||||
|
|
||||||
|
### 2. 六维度审查
|
||||||
|
|
||||||
|
按顺序逐一审查:
|
||||||
|
|
||||||
|
1. **安全性** — 硬编码密钥?SQL 注入/XSS?未验证输入?不安全认证?
|
||||||
|
2. **正确性** — 逻辑正确?边界条件?错误处理?null/undefined 安全?
|
||||||
|
3. **可维护性** — 命名清晰?函数 < 50 行?单一职责?逻辑已提取到纯函数?
|
||||||
|
4. **性能** — 不必要渲染?N+1 查询?大数据未分页?缺少缓存?
|
||||||
|
5. **可测试性** — 纯函数已提取到 .utils.ts?composable 可独立测试?有 data-testid?
|
||||||
|
6. **一致性** — 命名风格统一?遵循项目模式?import 顺序?错误处理模式?
|
||||||
|
|
||||||
|
### 3. 预提交检查联动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PHP 后端
|
||||||
|
vendor/bin/phpstan analyse --level=5 --no-progress
|
||||||
|
vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||||
|
|
||||||
|
# Vue 前端
|
||||||
|
npx eslint --quiet src/
|
||||||
|
npx vitest run --reporter=verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 输出格式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 代码审查报告
|
||||||
|
|
||||||
|
**范围**: <文件/目录/PR>
|
||||||
|
**总体评价**: 建议合并 | 需修改后合并 | 需重写
|
||||||
|
|
||||||
|
### 发现
|
||||||
|
|
||||||
|
#### Must Fix
|
||||||
|
1. **[SEC]** `file.ts:23` — 问题描述 → 修复建议
|
||||||
|
|
||||||
|
#### Should Fix
|
||||||
|
2. **[MAINT]** `service.ts:45` — 问题描述 → 修复建议
|
||||||
|
|
||||||
|
#### Suggestion
|
||||||
|
3. **[PERF]** `list.vue:12` — 问题描述 → 修复建议
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] 六个维度全部覆盖
|
||||||
|
- [ ] 每个发现有具体文件和行号
|
||||||
|
- [ ] 每个发现有修复建议
|
||||||
|
- [ ] 发现按严重程度分级
|
||||||
|
- [ ] 预提交检查已执行
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
Vue 3 组件脚手架技能。新建 Vue SFC 组件、拆分子组件、
|
||||||
|
创建复合组件时激活。含模板生成、命名规范、Prop 设计。
|
||||||
|
globs:
|
||||||
|
- "**/components/**/*.vue"
|
||||||
|
- "**/composables/**/*.ts"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Component Scaffold
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、模板和深度参考见:
|
||||||
|
> Read `.cursor/skills/component-scaffold/SKILL.md`
|
||||||
|
|
||||||
|
> **适用性**(双模式):
|
||||||
|
> - **新建**组件/重大拆分:走完整执行流程(Step 1-7)
|
||||||
|
> - **修改**已有组件:跳过脚手架步骤,完成后走「验证」清单
|
||||||
|
|
||||||
|
依赖技能:`vue-testing`(Cursor 已通过 `skill-vue-testing` 规则自动加载)
|
||||||
|
|
||||||
|
## 前端识别(必须先确认)
|
||||||
|
|
||||||
|
- **管理端** (`Case-Database-Frontend-admin/`):Element Plus + Tailwind
|
||||||
|
- **用户端** (`Case-Database-Frontend-user/`):Headless UI + Tailwind,**禁止 Element Plus**
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
1. 加载规范:Read `010-typescript.mdc`、`011-vue.mdc`、`019-modular.mdc`
|
||||||
|
2. 扫描 `src/components/core/` 和 `src/components/custom/`,避免重复造轮子
|
||||||
|
3. 重复 UI 检测:同一结构 >=3 次 → 提取基础组件
|
||||||
|
4. **输出文件结构**(写代码前必须先输出):
|
||||||
|
```
|
||||||
|
src/components/<type>/<ComponentName>/
|
||||||
|
├── <ComponentName>.vue (行数限制见 011-vue.mdc)
|
||||||
|
├── <ComponentName>.test.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
5. 确认组件规格(名称、类型、目录、Props/Emits)
|
||||||
|
6. 生成组件代码(`<script setup lang="ts">` + `defineProps`)
|
||||||
|
7. 生成测试和 barrel export
|
||||||
|
|
||||||
|
## 质量约束
|
||||||
|
|
||||||
|
行数限制和设计决策树已下沉到 `011-vue.mdc`(编辑任何 .vue 文件时自动生效),
|
||||||
|
此处不再重复。脚手架生成的组件同样受 `011-vue.mdc` 约束。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] Props 使用对象语法 `defineProps({ key: { type, default } })`;**script 中若不访问 `props.xxx` 则不保存返回值**(直接写 `defineProps(...)` 而非 `const props = defineProps(...)`,避免 `unused-vars` lint 报错)
|
||||||
|
- [ ] **`catch` 块中不使用异常参数时写 `catch {}` 而非 `catch (e) {}`**,避免 `unused-vars` 报错
|
||||||
|
- [ ] 包含 `data-testid`
|
||||||
|
- [ ] 测试至少一个渲染测试
|
||||||
|
- [ ] barrel export 正确
|
||||||
|
- [ ] 符合 `011-vue.mdc` 的 SFC 行数限制和设计决策规则
|
||||||
|
- [ ] composable 参数有类型注解、ref 有泛型
|
||||||
|
|
||||||
|
## 深度参考
|
||||||
|
|
||||||
|
- `.cursor/skills/component-scaffold/references/component-templates.md`
|
||||||
|
- `.cursor/skills/component-scaffold/references/naming-conventions.md`
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
Hyperf 数据库迁移技能。数据库 Schema 变更(增删改表/字段/索引)时激活。
|
||||||
|
确保迁移安全可回滚。
|
||||||
|
globs:
|
||||||
|
- "**/database/migrations/**/*.php"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Database Migration
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、模板和深度参考见:
|
||||||
|
> Read `.cursor/skills/database-migration/SKILL.md`
|
||||||
|
|
||||||
|
> **适用性**:本技能用于**数据库 Schema 变更**。
|
||||||
|
> 仅查询数据库或读取 Model 时可忽略本技能。
|
||||||
|
|
||||||
|
安全等级:ORANGE — 执行前必须确认。
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
1. 每次变更都是迁移 — 禁止手动 DDL
|
||||||
|
2. Schema 与 Data 严格分离 — DDL 和 DML 分文件
|
||||||
|
3. 迁移部署后不可变 — 已执行迁移禁止修改
|
||||||
|
4. 新字段安全 — 新增字段必须 nullable 或有默认值
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
1. 加载规范:Read `014-database.mdc`
|
||||||
|
2. 确认变更需求(变更内容、是否涉及数据迁移、是否可逆、数据量级)
|
||||||
|
3. 生成迁移:`php bin/hyperf.php gen:migration <name>`
|
||||||
|
4. 编写迁移(Schema::create / Schema::table,down() 必须完整可逆)
|
||||||
|
5. 高并发表设计检查(主键 BIGINT、utf8mb4、金额 DECIMAL、索引 <=6)
|
||||||
|
6. 执行:`migrate` → `migrate:status` → 更新 Model、Service、data-model.md
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] migrate 无错误
|
||||||
|
- [ ] migrate:rollback 可回滚
|
||||||
|
- [ ] Model $fillable / $casts 正确
|
||||||
|
- [ ] 外键有索引
|
||||||
|
- [ ] 新字段 nullable 或有默认值
|
||||||
|
- [ ] Schema 与 Data 迁移分文件
|
||||||
|
- [ ] data-model.md 已更新
|
||||||
|
|
||||||
|
## 深度参考
|
||||||
|
|
||||||
|
- `.cursor/skills/database-migration/references/migration-patterns.md`
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
系统化调试技能。当用户报告 bug、错误、异常、崩溃,
|
||||||
|
提供截图指出界面问题,或描述功能与预期不符时激活。
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Debugging — 七步法
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、模板和深度参考见:
|
||||||
|
> Read `.cursor/skills/debugging/SKILL.md`
|
||||||
|
|
||||||
|
## -1. 截图前置守卫(Scan 阶段硬检查)
|
||||||
|
|
||||||
|
收到用户消息时,**先检查输入形式**,再分析文字意图:
|
||||||
|
|
||||||
|
| 输入形式 | 问题关键词 | 判定 |
|
||||||
|
|---------|-----------|------|
|
||||||
|
| 包含截图/图片 | 不合理 / 不对 / 有问题 / 调整 / 修复 / 偏了 / 太大 / 太小 / 空白 / 溢出 / 错位 | → 视觉 Bug,激活完整调试流程 |
|
||||||
|
| 包含截图/图片 | 新增 / 添加 / 改为 / 参考这个 / 照着做 | → 需求变更,走正常开发流程 |
|
||||||
|
|
||||||
|
**硬规则**:无论判定结果是哪条路径,都**必须在回复开头输出守卫判定块**:
|
||||||
|
|
||||||
|
```
|
||||||
|
## 截图守卫判定
|
||||||
|
- 输入形式:包含截图 ✓
|
||||||
|
- 关键词扫描:「XXX」→ 命中 [视觉 Bug / 需求变更] 类
|
||||||
|
- 判定:[激活调试流程 / 走正常开发流程]
|
||||||
|
```
|
||||||
|
|
||||||
|
**视觉 Bug 路径规则**:
|
||||||
|
- 截图 + 问题描述 = 视觉 Bug,**不得降级为 L1 直接执行**,最低 L2
|
||||||
|
- 必须走完信号解析 → 修复 → 回归验证(含 chrome-devtools 截图对比)全流程
|
||||||
|
|
||||||
|
**需求变更路径规则**:
|
||||||
|
- 截图用于"参考设计"或"说明改动目标",走正常开发流程
|
||||||
|
- 复杂度按 001-workflow 标准判定(可为 L1)
|
||||||
|
|
||||||
|
## 0. 信号解析
|
||||||
|
|
||||||
|
从用户描述中提取(缺失则追问):
|
||||||
|
|
||||||
|
| 信号 | 重要性 |
|
||||||
|
|------|--------|
|
||||||
|
| 错误消息 / 堆栈追踪 | 必需 |
|
||||||
|
| 复现步骤 | 必需 |
|
||||||
|
| 截图标注的异常区域 | 必需(有截图时) |
|
||||||
|
| 影响区域(前端/后端/API/DB) | 重要 |
|
||||||
|
| 环境信息 / 上次正常版本 | 有用 |
|
||||||
|
|
||||||
|
## 0.5 调试策略路由
|
||||||
|
|
||||||
|
| 影响区域 | 首选方式 | 测试工具 |
|
||||||
|
|----------|---------|---------|
|
||||||
|
| PHP Service | 断点 + 日志 | PHPUnit |
|
||||||
|
| API 端点 | curl + 日志 | Hyperf HttpClient |
|
||||||
|
| 数据库查询 | SQL 日志 + EXPLAIN | PHPUnit + SQLite |
|
||||||
|
| Vue 组件渲染 | Vue DevTools | Vitest + VTU |
|
||||||
|
| 样式/布局 | Chrome DevTools | 视觉回归 |
|
||||||
|
|
||||||
|
## 1-6. 核心流程
|
||||||
|
|
||||||
|
1. **复现** — 确认稳定复现步骤,记录复现概率和条件
|
||||||
|
2. **收集** — 读取源码、检查 git log、查看日志和已有测试
|
||||||
|
3. **假设** — 列出 2-3 个最可能原因,按概率排序,指向具体文件和行
|
||||||
|
4. **验证** — 从最高概率假设开始,每次只验证一个
|
||||||
|
5. **修复** — 修复根因,修改尽可能小,添加防御性代码
|
||||||
|
6. **回归** — 确认修复、运行测试套件、编写回归测试防复发
|
||||||
|
|
||||||
|
## 硬停止条件
|
||||||
|
|
||||||
|
需要真实第三方凭证、竞态条件不可靠复现、需要生产数据、超过 3 次假设全部否定 → 立即停止,告知用户需要额外条件。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] 提取了所有关键信号
|
||||||
|
- [ ] 选择了正确的调试策略
|
||||||
|
- [ ] 原始问题已修复
|
||||||
|
- [ ] 所有现有测试通过
|
||||||
|
- [ ] 添加了回归测试
|
||||||
|
- [ ] 修复范围最小化
|
||||||
|
|
||||||
|
深度参考:`.cursor/skills/debugging/references/common-errors.md`
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
端到端功能开发技能。当需要从数据库到 API 到 UI 全链路开发完整功能时激活。
|
||||||
|
编排 database-migration、api-scaffold、component-scaffold 等技能协同工作。
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Full Feature Workflow
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、模板和深度参考见:
|
||||||
|
> Read `.cursor/skills/full-feature/SKILL.md`
|
||||||
|
|
||||||
|
依赖技能:`component-scaffold`、`vue-testing`
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### Phase 1: 规划
|
||||||
|
|
||||||
|
拆分功能为子任务:
|
||||||
|
- 数据层:数据模型和数据库变更
|
||||||
|
- 后端 API:Controller / Service / Repository
|
||||||
|
- 前端 UI:组件和页面
|
||||||
|
- 测试:每层测试
|
||||||
|
|
||||||
|
输出执行计划后确认再开始。
|
||||||
|
|
||||||
|
### Phase 2: 数据层
|
||||||
|
|
||||||
|
1. 生成迁移:`php bin/hyperf.php gen:migration create_<table>_table`
|
||||||
|
2. 编写迁移(遵循高并发表设计规范)
|
||||||
|
3. 执行迁移 + 生成 Model
|
||||||
|
4. 补充 Model 关联和类型转换
|
||||||
|
|
||||||
|
参考:Cursor 已通过 `skill-database-migration` 规则自动加载。
|
||||||
|
如需模板细节,Read `.cursor/skills/database-migration/references/migration-patterns.md`
|
||||||
|
|
||||||
|
### Phase 3: 后端 API 层
|
||||||
|
|
||||||
|
1. Controller(接收请求)→ Service(业务逻辑)→ Repository → FormRequest
|
||||||
|
2. 注册路由 + 挂载中间件
|
||||||
|
3. 编写 PHPUnit 测试
|
||||||
|
|
||||||
|
参考:Cursor 已通过 `skill-backend-scaffold` 规则自动加载。
|
||||||
|
如需模板细节,Read `.cursor/skills/api-scaffold/references/code-templates.md`
|
||||||
|
|
||||||
|
### Phase 4: 前端 UI 层
|
||||||
|
|
||||||
|
1. 封装 API 接口(`src/api/<module>/`)
|
||||||
|
2. 创建列表页 + 表单组件 + 详情页
|
||||||
|
3. 配置 Vue Router 路由
|
||||||
|
4. 连接 Pinia Store(如需跨页面状态)
|
||||||
|
|
||||||
|
参考:Cursor 已通过 `skill-component-scaffold` 和 `skill-vue-page` 规则自动加载。
|
||||||
|
如需模板细节,Read `.cursor/skills/component-scaffold/references/component-templates.md`
|
||||||
|
|
||||||
|
### Phase 5: 集成验证
|
||||||
|
|
||||||
|
1. 后端测试:`composer test`
|
||||||
|
2. 前端 Lint:`npm run lint`
|
||||||
|
3. 手动测试 CRUD 全路径
|
||||||
|
|
||||||
|
### Phase 6: 收尾
|
||||||
|
|
||||||
|
更新文档(data-model.md、api-contracts.md)→ Git commit
|
||||||
|
|
||||||
|
## 执行原则
|
||||||
|
|
||||||
|
- **自底向上**:数据层 → 后端 API → 前端 UI
|
||||||
|
- **每步验证**:每个 Phase 完成后运行测试
|
||||||
|
- **可中断**:每个 Phase 独立可提交
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] 后端测试全部通过
|
||||||
|
- [ ] 前端 ESLint 无报错
|
||||||
|
- [ ] 功能端到端可用(CRUD 全路径)
|
||||||
|
- [ ] 代码遵循项目现有模式
|
||||||
|
- [ ] 文档已更新
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
国际化技能。当需要添加多语言支持、翻译内容、管理语言包
|
||||||
|
或配置 vue-i18n 国际化时激活。含键命名规范和路由国际化。
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Internationalization (i18n)
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、模板和深度参考见:
|
||||||
|
> Read `.cursor/skills/i18n/SKILL.md`
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
1. 检测现有 i18n 方案(`vue-i18n` 是否已安装)
|
||||||
|
2. 初始化(如未配置):`npm install vue-i18n@9`,创建 `src/locales/` 目录
|
||||||
|
3. 配置 i18n 实例(`legacy: false`、fallbackLocale、localStorage 持久化)
|
||||||
|
4. 翻译键命名:使用扁平 dot notation `{feature}.{context}.{action|status|label}`
|
||||||
|
5. 组件中使用 `useI18n()` 的 `t()` 函数
|
||||||
|
6. 管理端同步 Element Plus locale(用户端不适用)
|
||||||
|
7. 添加新语言时确保所有 key 一致
|
||||||
|
|
||||||
|
## 键命名公式
|
||||||
|
|
||||||
|
`{feature}.{context}.{action|status|label}`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
'order.list.title': '订单列表',
|
||||||
|
'order.form.submit': '提交订单',
|
||||||
|
'common.action.save': '保存',
|
||||||
|
```
|
||||||
|
|
||||||
|
## 标准命名空间
|
||||||
|
|
||||||
|
common | auth | menu | validation | error | {module}
|
||||||
|
|
||||||
|
新增命名空间时必须登记。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] 所有 locale 文件的 key 结构一致
|
||||||
|
- [ ] 切换语言后页面正确翻译
|
||||||
|
- [ ] 键命名遵循公式
|
||||||
|
- [ ] 使用扁平 dot notation
|
||||||
|
- [ ] 无键冲突
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
Hyperf 模块化脚手架技能。在 modules/ 下新建业务模块、创建 module、
|
||||||
|
添加模块化功能时激活。基于 Composer path repository + ConfigProvider 机制。
|
||||||
|
globs:
|
||||||
|
- "**/modules/*/src/**/*.php"
|
||||||
|
- "**/modules/*/composer.json"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Module Scaffold
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、模板和深度参考见:
|
||||||
|
> Read `.cursor/skills/module-scaffold/SKILL.md`
|
||||||
|
|
||||||
|
> **适用性**(双模式):
|
||||||
|
> - **新建**模块:走完整执行流程(Step 1-7)
|
||||||
|
> - **修改**已有模块代码:跳过脚手架步骤,完成后走「验证」清单
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
1. 确认模块规格(名称、描述、接口类型:`api` / `admin` / `both`、所需子层)
|
||||||
|
2. 生成目录结构(**Controller 按接口类型分子目录,见下方规则**)
|
||||||
|
3. 生成模块 `composer.json`(name: `modules/{kebab-case}`, namespace: `Modules\{PascalCase}`, extra.hyperf.config)
|
||||||
|
4. 生成 `ConfigProvider.php`(注册 dependencies + annotations scan + publish)
|
||||||
|
5. 生成 Controller(根据接口类型选择正确子目录和模板)
|
||||||
|
6. **配置依赖检查**(使用事件/队列/缓存时必须检查):
|
||||||
|
- 使用异步队列:检查主项目 `config/autoload/async_queue.php` 是否存在
|
||||||
|
- 使用 Redis 缓存:检查主项目 `config/autoload/redis.php` 连接池配置
|
||||||
|
- 使用 JWT:检查主项目 `config/autoload/jwt.php` 和环境变量 `JWT_SECRET`
|
||||||
|
7. **无需修改主项目 `composer.json`**:ModuleLoader 在启动时自动扫描发现
|
||||||
|
8. 验证:`require vendor/autoload.php` 后检查 ConfigProvider 是否被发现
|
||||||
|
|
||||||
|
## Controller 与 Request 目录规则(核心)
|
||||||
|
|
||||||
|
根据接口路由前缀,Controller 和 Request **均须放入对应子目录**:
|
||||||
|
|
||||||
|
| 接口类型 | 路由前缀 | Controller 目录 | Request 目录 | 类名规范 |
|
||||||
|
|----------|----------|-----------------|--------------|----------|
|
||||||
|
| 用户端 | `/api/{module}` | `Http/Controller/Api/` | `Http/Request/Api/` | `{Name}Controller` / `{Name}Request` |
|
||||||
|
| 管理端 | `/admin/{module}` | `Http/Controller/Admin/` | `Http/Request/Admin/` | `{Name}Controller` / `{Name}Request` |
|
||||||
|
| 两者 | 各自前缀 | 两组子目录都创建 | 两组子目录都创建 | 各自子目录,类名相同 |
|
||||||
|
|
||||||
|
**额外约定**:
|
||||||
|
- `Admin/` 下的控制器**自动添加** `#[Middleware(JwtAuthMiddleware::class)]` 注解
|
||||||
|
- **类名不加接口类型前缀**,`Api/` 和 `Admin/` 下同名类通过命名空间区分
|
||||||
|
- Controller 中 `use` 的 Request 路径必须与子目录命名空间一致
|
||||||
|
- 路由前缀中 `/api/` 和 `/admin/` 是判断依据,不能混放
|
||||||
|
- 未明确时,**主动询问**接口类型而不是猜测
|
||||||
|
|
||||||
|
## 命名约定
|
||||||
|
|
||||||
|
| 位置 | 格式 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| 目录名 | PascalCase | `modules/UserCenter/` |
|
||||||
|
| Composer 包名 | kebab-case | `modules/user-center` |
|
||||||
|
| PHP 命名空间 | PascalCase | `Modules\UserCenter` |
|
||||||
|
| 用户端路由前缀 | `/api/kebab-case` | `/api/user-center` |
|
||||||
|
| 管理端路由前缀 | `/admin/kebab-case` | `/admin/user-center` |
|
||||||
|
|
||||||
|
## 关键机制
|
||||||
|
|
||||||
|
- **ModuleLoader 自动发现**:`app/Support/ModuleLoader.php` 注册在 `autoload.files`,在 `vendor/autoload.php` 执行时扫描 `modules/*/composer.json`,通过反射注入 `Hyperf\Support\Composer::$extra`,让 `ProviderConfig::load()` 发现模块 ConfigProvider。**无需修改 `composer.json` require**
|
||||||
|
- **composer-merge-plugin**:合并模块 `composer.json` 的 `autoload` 和第三方 `require` 到主项目(仅在模块有外部依赖时才需要 `composer update`)
|
||||||
|
- **ConfigProvider**:模块自注册 DI 绑定、注解扫描路径、可发布配置
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] **Controller 按接口类型放入正确子目录**(`Api/` 或 `Admin/`,不能平铺在 `Controller/` 根目录)
|
||||||
|
- [ ] **Request 按接口类型放入正确子目录**(`Request/Api/` 或 `Request/Admin/`,不能平铺在 `Request/` 根目录)
|
||||||
|
- [ ] Controller 命名空间含 `\Controller\Api\` 或 `\Controller\Admin\`
|
||||||
|
- [ ] Request 命名空间含 `\Request\Api\` 或 `\Request\Admin\`
|
||||||
|
- [ ] Controller 中 `use` 的 Request 路径与子目录命名空间一致
|
||||||
|
- [ ] `Admin/` 下:Controller 自动加 `JwtAuthMiddleware`;类名与 `Api/` 保持一致,**不加 `Admin` 前缀**
|
||||||
|
- [ ] 模块结构完整(composer.json + ConfigProvider.php + Http/Controller/{Api|Admin}/ + Http/Request/{Api|Admin}/)
|
||||||
|
- [ ] composer.json name 为 `modules/{kebab-case}`
|
||||||
|
- [ ] namespace 为 `Modules\{PascalCase}`
|
||||||
|
- [ ] ConfigProvider 注册 `__DIR__` 扫描路径
|
||||||
|
- [ ] extra.hyperf.config 指向正确 ConfigProvider
|
||||||
|
- [ ] **无需修改主 composer.json**,ModuleLoader 自动发现
|
||||||
|
- [ ] Hyperf ProviderConfig 能发现模块 ConfigProvider
|
||||||
|
- [ ] `php -l` 无语法错误
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
安全审计技能。当用户要求安全审查、漏洞扫描、安全加固、
|
||||||
|
OWASP 检查或合规性检查时激活。遵循 OWASP Top 10 + CodeGuard 框架。
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Security Audit
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、命令和深度参考见:
|
||||||
|
> Read `.cursor/skills/security-audit/SKILL.md`
|
||||||
|
|
||||||
|
安全等级:RED — 审计结果包含敏感信息。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 依赖安全扫描
|
||||||
|
|
||||||
|
`npm audit --audit-level=high`、`composer audit`
|
||||||
|
|
||||||
|
### 2. OWASP Top 10 检查
|
||||||
|
|
||||||
|
| # | 风险 | 检查方法 |
|
||||||
|
|---|------|---------|
|
||||||
|
| A01 | 访问控制失效 | auth middleware + 数据权限 |
|
||||||
|
| A02 | 加密失败 | bcrypt/Argon2id、HTTPS、JWT |
|
||||||
|
| A03 | 注入 | 参数化查询、ORM |
|
||||||
|
| A04 | 不安全设计 | 业务逻辑、并发、分布式锁 |
|
||||||
|
| A05 | 安全配置 | CORS、headers、debug 关闭 |
|
||||||
|
| A06 | 过时组件 | npm/composer audit |
|
||||||
|
| A07 | 认证失败 | JWT 双 Token、密码策略 |
|
||||||
|
| A08 | 数据完整性 | 反序列化、CI/CD |
|
||||||
|
| A09 | 日志监控 | 覆盖率和告警 |
|
||||||
|
| A10 | SSRF | HTTP 请求目标验证 |
|
||||||
|
|
||||||
|
### 3. PHP / 前端 / 密钥扫描
|
||||||
|
|
||||||
|
PHPStan、危险函数、v-html/eval/innerHTML、密钥格式检测。
|
||||||
|
|
||||||
|
### 4. 中间件链验证
|
||||||
|
|
||||||
|
CorsMiddleware -> TraceId -> RequestLog -> Auth -> Permission -> RateLimit -> RequestSign
|
||||||
|
|
||||||
|
### 5. 输出报告
|
||||||
|
|
||||||
|
Critical/High/Medium/Low 分级。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] OWASP Top 10 逐项完成
|
||||||
|
- [ ] npm/composer audit 无 critical/high
|
||||||
|
- [ ] 无硬编码密钥
|
||||||
|
- [ ] 安全头已配置
|
||||||
|
- [ ] 中间件链完整
|
||||||
|
|
||||||
|
## 深度参考
|
||||||
|
|
||||||
|
- `.cursor/skills/security-audit/references/audit-commands.md`
|
||||||
|
- `.cursor/skills/security-audit/references/security-headers.md`
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
Vue 3 + Vue Router 页面脚手架技能。新建路由页面、
|
||||||
|
含布局/加载状态/错误处理时激活。
|
||||||
|
globs:
|
||||||
|
- "**/router/**/*.ts"
|
||||||
|
- "**/views/**/*.vue"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vue Page Scaffold
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、模板和深度参考见:
|
||||||
|
> Read `.cursor/skills/vue-page/SKILL.md`
|
||||||
|
|
||||||
|
> **适用性**(双模式):
|
||||||
|
> - **新建**路由页面:走完整执行流程(Step 1-6)
|
||||||
|
> - **修改**已有页面:跳过脚手架步骤,完成后走「验证」清单
|
||||||
|
|
||||||
|
依赖技能:`vue-testing`(Cursor 已通过 `skill-vue-testing` 规则自动加载)
|
||||||
|
|
||||||
|
## 前端识别
|
||||||
|
|
||||||
|
- **管理端**:Element Plus 布局
|
||||||
|
- **用户端**:Headless UI + Tailwind,**禁止 Element Plus**
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
1. 加载规范:Read `010-typescript.mdc`、`011-vue.mdc`、`019-modular.mdc`
|
||||||
|
2. **生成前强制拆分分析**(写代码前必须输出):
|
||||||
|
- 多视图检测:>=2 个排斥视图 → 每个视图独立组件
|
||||||
|
- 行数预估:template > 80 行 → 拆子组件,script > 60 行 → 提取 composable
|
||||||
|
- 重复 UI 检测:同一结构 >=3 次 → 提取基础组件
|
||||||
|
3. 输出拆分方案:
|
||||||
|
```
|
||||||
|
src/views/<module>/<page>/
|
||||||
|
├── index.vue <= 60 行(编排层)
|
||||||
|
├── components/
|
||||||
|
│ ├── <ViewA>.vue <= 120 行
|
||||||
|
│ └── <ViewB>.vue <= 120 行
|
||||||
|
└── composables/
|
||||||
|
└── use<PageName>.ts <= 80 行
|
||||||
|
```
|
||||||
|
4. 确认页面规格(路由路径、标题、模块、类型)
|
||||||
|
5. 逐文件生成代码
|
||||||
|
6. 配置路由(path、name、component 懒加载、meta)
|
||||||
|
|
||||||
|
## 页面类型
|
||||||
|
|
||||||
|
list | detail | form | dashboard | blank | multi-view
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] 页面正常渲染,document.title 正确
|
||||||
|
- [ ] Loading 骨架屏 + 错误提示
|
||||||
|
- [ ] meta.requiresAuth、meta.permission
|
||||||
|
- [ ] index.vue <= 60 行
|
||||||
|
- [ ] 无子组件超过 150 行
|
||||||
|
- [ ] composable 参数有类型注解、ref 有泛型
|
||||||
|
|
||||||
|
## 深度参考
|
||||||
|
|
||||||
|
- `.cursor/skills/vue-page/references/page-templates.md`
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
description: >
|
||||||
|
Vue 3 + Vitest 前端测试技能。新建或更新测试文件、
|
||||||
|
编写单元/组件/E2E 测试时激活。
|
||||||
|
globs:
|
||||||
|
- "**/*.test.*"
|
||||||
|
- "**/__tests__/**"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vue Testing
|
||||||
|
|
||||||
|
> 本文件是精简执行摘要。完整流程、模板和深度参考见:
|
||||||
|
> Read `.cursor/skills/vue-testing/SKILL.md`
|
||||||
|
|
||||||
|
> **适用性**:本技能用于**新建或更新测试文件**。
|
||||||
|
> 非测试相关的代码修改可忽略本技能。
|
||||||
|
|
||||||
|
## 测试决策树(每次写测试前必须先走)
|
||||||
|
|
||||||
|
```
|
||||||
|
纯转换逻辑(parse/format/validate/compute)→ 提取到 .utils.ts,Vitest 单元测试
|
||||||
|
涉及复杂 UI 交互 → Vue Test Utils 组件测试
|
||||||
|
能提取为纯函数或 composable → 提取后写单元测试
|
||||||
|
跨页面流程、关键业务路径 → Playwright E2E
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试类型
|
||||||
|
|
||||||
|
| 类型 | 工具 | 文件命名 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 单元测试 | Vitest | *.test.ts |
|
||||||
|
| 组件测试 | Vitest + Vue Test Utils | *.test.ts |
|
||||||
|
| E2E | Playwright | *.spec.ts |
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
1. **逻辑提取** — 业务逻辑提取到 .utils.ts 纯函数
|
||||||
|
2. **穷举排列** — 有效/无效/空值/边界/安全输入
|
||||||
|
3. **AAA 模式** — Arrange-Act-Assert
|
||||||
|
4. **单一行为** — 每个 it() 只验证一个行为
|
||||||
|
5. **命名** — `should <行为> when <条件>`
|
||||||
|
|
||||||
|
## 增量式工作流
|
||||||
|
|
||||||
|
工具函数 → Composables → 简单组件 → 复杂组件 → 集成。
|
||||||
|
逐个文件:写测试 → vitest run → PASS 才下一个。
|
||||||
|
|
||||||
|
## 覆盖率目标
|
||||||
|
|
||||||
|
Function 100% | Statement 100% | Branch > 95% | Line > 95%
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- [ ] 走过决策树
|
||||||
|
- [ ] 逻辑已提取到 .utils.ts
|
||||||
|
- [ ] 穷举排列覆盖
|
||||||
|
- [ ] 命名 should...when...
|
||||||
|
- [ ] vitest run 全部通过
|
||||||
|
- [ ] 覆盖率达标
|
||||||
|
|
||||||
|
## 深度参考
|
||||||
|
|
||||||
|
- `.cursor/skills/vue-testing/references/testing-templates.md`
|
||||||
|
- `.cursor/skills/vue-testing/references/mock-patterns.md`
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: anti-scraping
|
||||||
|
version: 4.0.0
|
||||||
|
description: "为 PHP Hyperf + Vue 3 应用设计反爬虫防护。当需要防御 Bot、限流或保护 API 时使用。覆盖五个威胁层级。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🛡️ Anti-Scraping Protection — 全栈防爬体系
|
||||||
|
|
||||||
|
## 威胁分级与对应策略
|
||||||
|
|
||||||
|
| 等级 | 爬虫类型 | 典型工具 | 主要特征 | 防护策略 |
|
||||||
|
|------|---------|---------|---------|---------|
|
||||||
|
| **T1** | 简单脚本 | curl, wget, Python requests | 无 JS 执行,缺失必要 Header | UA 过滤 + Header 检查 |
|
||||||
|
| **T2** | 爬虫框架 | Scrapy, Playwright (无配置), httpx | 自动处理 Cookie 但行为机械 | Header 指纹 + 速率限制 |
|
||||||
|
| **T3** | 无头浏览器 | Puppeteer, Playwright (配置过) | 执行 JS 但 Canvas/WebGL 异常 | 浏览器指纹 + 行为分析 |
|
||||||
|
| **T4** | 分布式集群 | 自建集群 + 代理池 + UA 轮换 | 跨 IP 协同,单 IP 请求少 | 关联分析 + 蜜罐 + PoW |
|
||||||
|
| **T5** | AI 代理 | LLM 控制的浏览器 / GPT 插件 | 接近真实用户但行为规律性高 | 多维指纹 + CV 分析 + 挑战 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户询问防爬虫、限流、Bot 防护、爬虫识别、AI 爬虫、Scrapy 等关键词。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### Phase 0:威胁评估
|
||||||
|
|
||||||
|
在实施前回答以下问题,确定防护等级:
|
||||||
|
|
||||||
|
| 问题 | 回答影响 |
|
||||||
|
|------|---------|
|
||||||
|
| 保护目标?(API / 页面内容 / 数据资产) | 决定防护位置(Nginx / Middleware / Frontend) |
|
||||||
|
| 可接受的误伤率?(0.1% / 0.5% / 1%) | 决定阈值设定的松紧 |
|
||||||
|
| 是否有 CDN/WAF? | 可借用 WAF 能力,减少自研成本 |
|
||||||
|
| 业务是否允许验证码? | 影响 CAPTCHA 降级策略 |
|
||||||
|
| 需要保护登录后内容还是公开内容? | 决定是否使用前端指纹 Token |
|
||||||
|
|
||||||
|
### Phase 1–4:前置与基础防护
|
||||||
|
|
||||||
|
1. **Phase 1 Nginx** — UA 黑名单 map、limit_req/limit_conn,直接拒绝已知 Bot
|
||||||
|
2. **Phase 2 指纹** — RequestFingerprintMiddleware 检查 UA/Header/Accept/Referer,输出 risk_score
|
||||||
|
3. **Phase 3 限速** — RateLimitService 分层限速 + 请求间隔变异系数分析(CV)+ 子网关联分析
|
||||||
|
4. **Phase 4 IP** — IpIntelligenceService 黑/白名单、TOR、数据中心、爬取广度(HyperLogLog)
|
||||||
|
|
||||||
|
### Phase 5–6:高级识别
|
||||||
|
|
||||||
|
5. **Phase 5 浏览器指纹** — 前端采集 Canvas/WebGL/音频/字体/鼠标,后端校验无头特征
|
||||||
|
6. **Phase 6 AI 代理** — 请求间隔 CV、只读模式、速度、UA 与语言不匹配
|
||||||
|
|
||||||
|
### Phase 7–9:对抗与响应
|
||||||
|
|
||||||
|
7. **Phase 7 PoW** — 工作量证明挑战,真实用户 JS 自动计算
|
||||||
|
8. **Phase 8 蜜罐** — 前端隐藏字段 + 后端蜜罐路由
|
||||||
|
9. **Phase 9 综合** — AntiScrapingMiddleware 加权评分,差异化响应(封禁 / PoW / 延迟 / 假数据)
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
1. [ ] `curl` 请求 3 次内触发 403 或 429
|
||||||
|
2. [ ] Python `requests` 默认 UA 被 Nginx 直接拒绝
|
||||||
|
3. [ ] Scrapy 爬取 100+ 页面被封 IP
|
||||||
|
4. [ ] Puppeteer(无反指纹)指纹得分 ≥ 60,触发挑战
|
||||||
|
5. [ ] 均匀间隔请求(CV < 0.2)被 AI 行为分析识别
|
||||||
|
6. [ ] 蜜罐路由访问后 IP 被封
|
||||||
|
7. [ ] PoW 挑战前端正确求解(difficulty=4,约 2 秒内完成)
|
||||||
|
8. [ ] 同 /24 子网 500+ 请求触发代理池标记
|
||||||
|
9. [ ] 正常用户误触率 < 0.2%
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `references/implementation-phases.md` | Phase 1–9 完整实现代码与 Redis 监控 |
|
||||||
|
| `references/anti-scraping-patterns.md` | 反爬模式与策略速查 |
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
# Anti-Scraping 参考模式库
|
||||||
|
|
||||||
|
## 1. 常见爬虫特征指纹
|
||||||
|
|
||||||
|
### User-Agent 黑名单(正则)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const BOT_UA_PATTERNS = [
|
||||||
|
// 爬虫框架
|
||||||
|
/python-requests/i,
|
||||||
|
/scrapy/i,
|
||||||
|
/beautifulsoup/i,
|
||||||
|
/selenium/i,
|
||||||
|
/playwright/i,
|
||||||
|
/puppeteer/i,
|
||||||
|
/mechanize/i,
|
||||||
|
/httpclient/i,
|
||||||
|
/java\/\d/i,
|
||||||
|
/go-http-client/i,
|
||||||
|
/ruby/i,
|
||||||
|
|
||||||
|
// 命令行工具
|
||||||
|
/curl\//i,
|
||||||
|
/wget\//i,
|
||||||
|
/httpie/i,
|
||||||
|
/insomnia/i,
|
||||||
|
|
||||||
|
// 无头浏览器特征
|
||||||
|
/headlesschrome/i,
|
||||||
|
/phantomjs/i,
|
||||||
|
/slimerjs/i,
|
||||||
|
|
||||||
|
// 已知数据采集
|
||||||
|
/dataprovider/i,
|
||||||
|
/yandexbot/i,
|
||||||
|
/mj12bot/i,
|
||||||
|
/ahrefsbot/i,
|
||||||
|
/semrushbot/i,
|
||||||
|
/dotbot/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function isBotUA(ua: string): boolean {
|
||||||
|
return BOT_UA_PATTERNS.some(p => p.test(ua));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 允许的搜索引擎爬虫白名单
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 合法爬虫:需要验证真实性(反向 DNS 查找)
|
||||||
|
const ALLOWED_BOTS = [
|
||||||
|
{ name: 'Googlebot', ua: /googlebot/i, rdns: 'googlebot.com' },
|
||||||
|
{ name: 'Bingbot', ua: /bingbot/i, rdns: 'search.msn.com' },
|
||||||
|
{ name: 'Baidu Spider', ua: /baiduspider/i, rdns: 'crawl.baidu.com' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function isLegitimateBot(ua: string, ip: string): Promise<boolean> {
|
||||||
|
const bot = ALLOWED_BOTS.find(b => b.ua.test(ua));
|
||||||
|
if (!bot) return false;
|
||||||
|
|
||||||
|
// 反向 DNS 验证(防止伪造 UA)
|
||||||
|
const hostname = await reverseDNS(ip);
|
||||||
|
return hostname?.endsWith(bot.rdns) ?? false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 风险评分算法
|
||||||
|
|
||||||
|
### 综合评分模型
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface RiskFactors {
|
||||||
|
fingerprintScore: number; // 0-100
|
||||||
|
rateScore: number; // 0-100(超速时增加)
|
||||||
|
ipScore: number; // 0-100(数据中心/Tor/VPN)
|
||||||
|
behaviorScore: number; // 0-100(行为异常)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRiskScore(factors: RiskFactors): number {
|
||||||
|
const weights = {
|
||||||
|
fingerprint: 0.35,
|
||||||
|
rate: 0.30,
|
||||||
|
ip: 0.25,
|
||||||
|
behavior: 0.10,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Math.min(
|
||||||
|
Math.round(
|
||||||
|
factors.fingerprintScore * weights.fingerprint +
|
||||||
|
factors.rateScore * weights.rate +
|
||||||
|
factors.ipScore * weights.ip +
|
||||||
|
factors.behaviorScore * weights.behavior
|
||||||
|
),
|
||||||
|
100
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应策略
|
||||||
|
function getResponseStrategy(score: number): 'allow' | 'slowdown' | 'challenge' | 'block' {
|
||||||
|
if (score >= 80) return 'block';
|
||||||
|
if (score >= 50) return 'challenge';
|
||||||
|
if (score >= 30) return 'slowdown';
|
||||||
|
return 'allow';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Redis 数据结构设计
|
||||||
|
|
||||||
|
```
|
||||||
|
# 速率限制(滑动窗口)
|
||||||
|
ZSET rl:{ip} → { timestamp: score }
|
||||||
|
ZSET rl:{ip}:{endpoint} → { timestamp: score }
|
||||||
|
|
||||||
|
# IP 黑白名单
|
||||||
|
SET ip:blocklist → { ip1, ip2, ... }
|
||||||
|
SET ip:allowlist → { ip1, ip2, ... }(合法爬虫白名单)
|
||||||
|
SET ip:tor-exit → { ip1, ip2, ... }
|
||||||
|
SET ip:datacenter → { ip1, ip2, ... }
|
||||||
|
|
||||||
|
# 蜜罐触发记录
|
||||||
|
HASH honeypot:hits → { ip: count }
|
||||||
|
|
||||||
|
# CAPTCHA 通过记录(防止重复挑战)
|
||||||
|
STRING captcha:passed:{ip} → "1"(TTL 1 小时)
|
||||||
|
|
||||||
|
# 行为画像
|
||||||
|
HASH behavior:{ip} → {
|
||||||
|
first_seen: timestamp,
|
||||||
|
request_count: number,
|
||||||
|
path_entropy: number, # 访问路径多样性(低=爬虫)
|
||||||
|
referer_missing_ratio: number, # 缺少 Referer 比例(高=爬虫)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Nginx 层防护(可选,性能最佳)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/conf.d/anti-scraping.conf
|
||||||
|
|
||||||
|
# 限速区域定义
|
||||||
|
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
|
||||||
|
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
|
||||||
|
limit_conn_zone $binary_remote_addr zone=conn:10m;
|
||||||
|
|
||||||
|
server {
|
||||||
|
# 连接数限制(单 IP 最多 20 并发)
|
||||||
|
limit_conn conn 20;
|
||||||
|
|
||||||
|
# UA 黑名单
|
||||||
|
if ($http_user_agent ~* "(python|curl|wget|scrapy|selenium)") {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 空 UA 拒绝
|
||||||
|
if ($http_user_agent = "") {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
limit_req zone=api burst=10 nodelay;
|
||||||
|
limit_req_status 429;
|
||||||
|
|
||||||
|
proxy_pass http://app;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/auth/ {
|
||||||
|
limit_req zone=login burst=2 nodelay;
|
||||||
|
limit_req_status 429;
|
||||||
|
|
||||||
|
proxy_pass http://app;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 蜜罐路由(真实用户不会访问)
|
||||||
|
location /admin-backup/ {
|
||||||
|
access_log /var/log/nginx/honeypot.log;
|
||||||
|
# 记录访问者 IP 并返回假数据
|
||||||
|
return 200 '{"status":"ok"}';
|
||||||
|
add_header Content-Type application/json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Cloudflare Workers 方案(Edge 层,最推荐)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// workers/anti-scraping.ts
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
const ip = request.headers.get('CF-Connecting-IP') ?? '';
|
||||||
|
const ua = request.headers.get('User-Agent') ?? '';
|
||||||
|
|
||||||
|
// 利用 Cloudflare 的威胁评分
|
||||||
|
const cfThreatScore = Number(request.headers.get('CF-Threat-Score') ?? 0);
|
||||||
|
if (cfThreatScore > 30) {
|
||||||
|
return new Response('Forbidden', { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 利用 Cloudflare 的 Bot 管理分数(需开启 Bot Management)
|
||||||
|
const cfBotScore = Number(request.headers.get('CF-Bot-Score') ?? 100);
|
||||||
|
if (cfBotScore < 30) {
|
||||||
|
// 低分 = 高爬虫可能性
|
||||||
|
return new Response('Forbidden', { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义限速(使用 Durable Objects 或 KV)
|
||||||
|
const rateLimitKey = `rl:${ip}`;
|
||||||
|
const count = Number(await env.RATE_LIMIT.get(rateLimitKey) ?? 0);
|
||||||
|
if (count > 60) {
|
||||||
|
return new Response('Too Many Requests', {
|
||||||
|
status: 429,
|
||||||
|
headers: { 'Retry-After': '60' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await env.RATE_LIMIT.put(rateLimitKey, String(count + 1), { expirationTtl: 60 });
|
||||||
|
return fetch(request);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 监控 Dashboard(Datadog / Grafana 指标)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/metrics.ts — 关键埋点
|
||||||
|
export const antiScrapingMetrics = {
|
||||||
|
// 请求被拦截
|
||||||
|
blocked: (reason: 'fingerprint' | 'rate' | 'honeypot' | 'ip' | 'captcha') => {
|
||||||
|
metrics.increment('anti_scraping.blocked', { reason });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 风险评分分布
|
||||||
|
scoreDistribution: (score: number) => {
|
||||||
|
metrics.histogram('anti_scraping.risk_score', score);
|
||||||
|
},
|
||||||
|
|
||||||
|
// CAPTCHA 展示与通过
|
||||||
|
captchaImpressed: () => metrics.increment('anti_scraping.captcha.impressed'),
|
||||||
|
captchaPassed: () => metrics.increment('anti_scraping.captcha.passed'),
|
||||||
|
captchaFailed: () => metrics.increment('anti_scraping.captcha.failed'),
|
||||||
|
|
||||||
|
// 误伤监控
|
||||||
|
falsePositive: (userId: string) => {
|
||||||
|
metrics.increment('anti_scraping.false_positive');
|
||||||
|
logger.warn({ userId }, 'Possible false positive in anti-scraping');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 测试用例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// __tests__/anti-scraping.test.ts
|
||||||
|
describe('Anti-Scraping', () => {
|
||||||
|
describe('Fingerprint Analysis', () => {
|
||||||
|
it('should flag Python requests as high risk', () => {
|
||||||
|
const score = analyzeFingerpring(mockRequest({
|
||||||
|
'user-agent': 'python-requests/2.28.0',
|
||||||
|
}));
|
||||||
|
expect(score).toBeGreaterThanOrEqual(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not flag normal Chrome browser', () => {
|
||||||
|
const score = analyzeFingerpring(mockRequest({
|
||||||
|
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||||
|
'accept-language': 'zh-CN,zh;q=0.9',
|
||||||
|
'accept-encoding': 'gzip, deflate, br',
|
||||||
|
'accept': 'text/html,application/xhtml+xml',
|
||||||
|
}));
|
||||||
|
expect(score).toBeLessThan(20);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Rate Limiting', () => {
|
||||||
|
it('should block after exceeding limit', async () => {
|
||||||
|
const ip = '192.168.1.100';
|
||||||
|
// 发送 61 次请求
|
||||||
|
for (let i = 0; i < 61; i++) {
|
||||||
|
await checkRateLimit(ip, { windowMs: 60_000, limit: 60 });
|
||||||
|
}
|
||||||
|
const { allowed } = await checkRateLimit(ip);
|
||||||
|
expect(allowed).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Honeypot', () => {
|
||||||
|
it('should block IP that triggers honeypot', async () => {
|
||||||
|
const ip = '10.0.0.1';
|
||||||
|
await triggerHoneypot(ip);
|
||||||
|
const isBlocked = await redis.sismember('ip:blocklist', ip);
|
||||||
|
expect(isBlocked).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# Anti-Scraping — 完整实现代码
|
||||||
|
|
||||||
|
> 主流程与决策逻辑见 SKILL.md,本文档为 Phase 1–9 的深度实现细节。
|
||||||
|
|
||||||
|
## Phase 1:Nginx 前置拦截
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/conf.d/anti-scraping.conf
|
||||||
|
map $http_user_agent $is_bot {
|
||||||
|
default 0;
|
||||||
|
~*python-requests|python-urllib|httpx|aiohttp|scrapy|mechanize 1;
|
||||||
|
~*curl|wget|libwww-perl|Go-http-client|Java/|okhttp|axios 1;
|
||||||
|
~*HeadlessChrome|headless|PhantomJS|Playwright|Puppeteer|Selenium|webdriver 1;
|
||||||
|
~*GPTBot|ChatGPT-User|Claude-Web|PerplexityBot|anthropic-ai|CCBot|DataForSeoBot|SemrushBot 1;
|
||||||
|
~*Googlebot/|~*Bingbot/ 0;
|
||||||
|
"" 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/m;
|
||||||
|
limit_req_zone $binary_remote_addr zone=page_limit:10m rate=60r/m;
|
||||||
|
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
|
||||||
|
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
|
||||||
|
|
||||||
|
server {
|
||||||
|
if ($is_bot) { return 403; }
|
||||||
|
if ($http_user_agent = "") { return 444; }
|
||||||
|
location /api/ {
|
||||||
|
limit_req zone=api_limit burst=10 nodelay;
|
||||||
|
limit_conn conn_limit 20;
|
||||||
|
proxy_pass http://hyperf_upstream;
|
||||||
|
}
|
||||||
|
location /api/auth/ {
|
||||||
|
limit_req zone=login_limit burst=2 nodelay;
|
||||||
|
limit_req_status 429;
|
||||||
|
proxy_pass http://hyperf_upstream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 2:HTTP 请求指纹识别
|
||||||
|
|
||||||
|
见 `RequestFingerprintMiddleware.php`:
|
||||||
|
- `BROWSER_REQUIRED_HEADERS`: accept, accept-language, accept-encoding
|
||||||
|
- `BROWSER_SECURITY_HEADERS`: sec-fetch-site, sec-fetch-mode, sec-fetch-dest, sec-ch-ua
|
||||||
|
- `SUSPICIOUS_ACCEPT_PATTERNS`: */*, application/json, text/html,*/*;q=0.9
|
||||||
|
- 评分逻辑:UA 缺失 +80,爬虫工具 +70,无头特征 +60,缺失 Header 每项 +15,缺失 sec-fetch +25,可疑 Accept +20,API 无 Referer +15
|
||||||
|
|
||||||
|
## Phase 3:速率限制与行为分析
|
||||||
|
|
||||||
|
`RateLimitService`:
|
||||||
|
- `IP_RULES`: global/api/search/export/login/register 分层限速
|
||||||
|
- `analyzeRequestPattern()`: 请求间隔变异系数 CV,CV < 0.3 或平均间隔 < 200ms 判定异常
|
||||||
|
- `analyzeSubnetPattern()`: /24 子网 1 分钟 > 500 请求判定代理池
|
||||||
|
- Redis key: `rl:{rule}:{ip}` (ZSET), `req_ts:{session}` (LIST)
|
||||||
|
|
||||||
|
## Phase 4:IP 信誉与代理检测
|
||||||
|
|
||||||
|
`IpIntelligenceService`:
|
||||||
|
- `DATACENTER_PREFIXES`: AWS/GCP/Azure/Cloudflare/DigitalOcean/Vultr 等
|
||||||
|
- `classify()`: blocklist → whitelist → tor → datacenter → residential
|
||||||
|
- `trackCrawlBreadth()`: HyperLogLog 1 小时内 > 200 唯一路径判定爬虫
|
||||||
|
|
||||||
|
## Phase 5:浏览器指纹识别
|
||||||
|
|
||||||
|
前端 `collectFingerprint()`: Canvas/WebGL/音频/鼠标/字体 指纹
|
||||||
|
后端 `BrowserFingerprintService.analyze()`: SwiftShader/llvmpipe/ANGLE +30,无音频 +20,字体全相同 +25,无鼠标 +20,4核0内存 +15
|
||||||
|
|
||||||
|
## Phase 6:AI 代理行为识别
|
||||||
|
|
||||||
|
`AiBotDetectionService`: CV < 0.2 加 45,纯只读 GET +25,平均间隔 < 300ms +30,UA 与语言不匹配 +15;总分 ≥ 60 判定 AI Bot
|
||||||
|
|
||||||
|
## Phase 7:工作量证明 PoW
|
||||||
|
|
||||||
|
前端 `solvePoW()`: SHA-256 前缀匹配,difficulty=4 约 1 万次
|
||||||
|
后端 `PoWService`: generate() 生成 nonce+id,verify() 校验并防重放
|
||||||
|
|
||||||
|
## Phase 8:蜜罐
|
||||||
|
|
||||||
|
前端:CSS 隐藏 input,随机字段名,提交时检测 honeypot 有值则 reportBot
|
||||||
|
后端:`/api/internal/user-export-all` 蜜罐路由,访问即封 IP 1 小时
|
||||||
|
|
||||||
|
## Phase 9:综合评分与差异化响应
|
||||||
|
|
||||||
|
`AntiScrapingMiddleware`: 加权汇总 headerScore*0.2 + ipRisk*0.15 + rateScore*0.25 + breadthScore*0.15 + patternScore*0.15 + subnetScore*0.05 + fpScore*0.05
|
||||||
|
- total ≥ 90: 封 IP 2 小时
|
||||||
|
- total ≥ 70: 要求 PoW 挑战
|
||||||
|
- total ≥ 50: 延迟 0.5–2 秒
|
||||||
|
- total ≥ 30: 敏感接口返回空数据
|
||||||
|
|
||||||
|
## 监控与 Redis 清理
|
||||||
|
|
||||||
|
| 指标 | 告警阈值 |
|
||||||
|
|------|---------|
|
||||||
|
| 403 响应率 | > 3% |
|
||||||
|
| 429 响应率 | > 5% |
|
||||||
|
| PoW 触发量 | > 100/小时 |
|
||||||
|
| 蜜罐触发量 | > 50/天 |
|
||||||
|
| IP 封禁量 | > 200/天 |
|
||||||
|
|
||||||
|
Redis TTL: rl/req_ts/req_log/crawl_breadth/subnet_req/pow 均设 TTL,ip:blocklist 定期清理过期封禁。
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
name: api-scaffold
|
||||||
|
version: 3.0.0
|
||||||
|
description: "生成 Hyperf API 端点脚手架(Controller/Service/FormRequest/路由)。当需要新建 API 接口或 CRUD 资源时使用。含分级异常体系。"
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/skill-backend-scaffold.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Hyperf API Endpoint Scaffold
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求创建新 API 端点、路由、接口、REST 资源或 CRUD 操作。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 0. 加载规范(⚠️ 必须最先执行)
|
||||||
|
|
||||||
|
依次使用 `Read` 工具读取:
|
||||||
|
- `.cursor/rules/013-backend.mdc` — ApiResponse 格式、FormRequest、路由、认证
|
||||||
|
- `.cursor/rules/019-modular.mdc` — Controller→Service→Repository→Model 依赖方向、各层职责
|
||||||
|
|
||||||
|
### 1. 确认端点规格
|
||||||
|
|
||||||
|
| 字段 | 必填 | 默认值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| 资源名称 | ✅ | — |
|
||||||
|
| HTTP 方法 | ✅ | — |
|
||||||
|
| 路由路径 | ✅ | — |
|
||||||
|
| 请求体结构 | GET 除外 | — |
|
||||||
|
| 响应结构 | ✅ | — |
|
||||||
|
| 需要认证? | ❌ | true |
|
||||||
|
| 需要权限? | ❌ | true |
|
||||||
|
| 分页? | ❌ | GET 列表自动加 |
|
||||||
|
|
||||||
|
### 2. 生成文件
|
||||||
|
|
||||||
|
生成 Controller、Service、FormRequest、Model(如不存在)、路由。分层约定与完整模板见 **Tier 3**。
|
||||||
|
|
||||||
|
### 3. 遵循项目约定
|
||||||
|
|
||||||
|
生成前扫描:`app/Controller/` 现有模式、`AbstractController` 基类、`AppExceptionHandler`、中间件注册方式。
|
||||||
|
|
||||||
|
### 4. 异常体系
|
||||||
|
|
||||||
|
- `BusinessException` — 404/403/409/422 等可预期业务错误
|
||||||
|
- `ValidationException` — FormRequest 校验失败
|
||||||
|
- `SystemException` — 500 系统故障,记录堆栈并通知运维
|
||||||
|
|
||||||
|
异常选择决策与 `AppExceptionHandler` 实现见 **Tier 3**。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] `php -l` 编译无错误
|
||||||
|
2. [ ] FormRequest rules 覆盖所有请求字段
|
||||||
|
3. [ ] 错误处理覆盖 400/401/403/404/422/500
|
||||||
|
4. [ ] Service 使用事务包裹写操作
|
||||||
|
5. [ ] 路由遵循 RESTful 命名(复数名词、kebab-case)
|
||||||
|
6. [ ] 中间件正确挂载(认证 + 权限)
|
||||||
|
7. [ ] 可预期错误使用 `BusinessException`
|
||||||
|
8. [ ] 不可预期错误使用 `SystemException`
|
||||||
|
9. [ ] `SystemException` 不向客户端暴露内部细节
|
||||||
|
10. [ ] `AppExceptionHandler` 已注册在 `config/autoload/exceptions.php`
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `references/code-templates.md` | Controller/Service/FormRequest/路由完整模板 |
|
||||||
|
| `references/exception-handling.md` | 分级异常体系与 AppExceptionHandler 实现 |
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# API 统一响应格式
|
||||||
|
|
||||||
|
## 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": { ... },
|
||||||
|
"meta": {
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 20,
|
||||||
|
"total": 100,
|
||||||
|
"totalPages": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Human-readable error message",
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"details": [ ... ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP 状态码
|
||||||
|
|
||||||
|
| 状态码 | 场景 |
|
||||||
|
|--------|------|
|
||||||
|
| 200 | 查询/更新成功 |
|
||||||
|
| 201 | 创建成功 |
|
||||||
|
| 204 | 删除成功(无响应体) |
|
||||||
|
| 400 | 请求参数验证失败 |
|
||||||
|
| 401 | 未认证 |
|
||||||
|
| 403 | 已认证但无权限 |
|
||||||
|
| 404 | 资源不存在 |
|
||||||
|
| 409 | 资源冲突(如重复创建) |
|
||||||
|
| 422 | 业务逻辑错误 |
|
||||||
|
| 429 | 请求频率超限 |
|
||||||
|
| 500 | 服务器内部错误 |
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# API Scaffold — 代码模板与异常体系
|
||||||
|
|
||||||
|
> 主流程见 SKILL.md,本文档为 Controller/Service/FormRequest/路由 的完整代码模板。
|
||||||
|
|
||||||
|
## Hyperf 分层目录约定
|
||||||
|
|
||||||
|
| 文件 | 路径 |
|
||||||
|
|------|------|
|
||||||
|
| Controller | `app/Controller/Admin/<Resource>Controller.php` |
|
||||||
|
| Service | `app/Service/<Module>/<Resource>Service.php` |
|
||||||
|
| FormRequest | `app/Request/<Module>/Create<Resource>Request.php` |
|
||||||
|
| Model | `app/Model/<Module>/<Resource>.php`(如不存在) |
|
||||||
|
| Route | `app/Http/Admin/Router/<resource>.php` |
|
||||||
|
|
||||||
|
## Controller 模板
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Request\{{Module}}\Create{{Resource}}Request;
|
||||||
|
use App\Service\{{Module}}\{{Resource}}Service;
|
||||||
|
use Hyperf\Di\Annotation\Inject;
|
||||||
|
|
||||||
|
#[Controller(prefix: '/admin/{{route-path}}')]
|
||||||
|
class {{Resource}}Controller extends AbstractController
|
||||||
|
{
|
||||||
|
#[Inject]
|
||||||
|
protected {{Resource}}Service $service;
|
||||||
|
|
||||||
|
#[RequestMapping(path: '', methods: ['GET'])]
|
||||||
|
public function list(RequestInterface $request): ResponseInterface {
|
||||||
|
$params = $request->all();
|
||||||
|
$result = $this->service->getPageList($params);
|
||||||
|
return $this->success($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[RequestMapping(path: '{id:\d+}', methods: ['GET'])]
|
||||||
|
public function detail(int $id): ResponseInterface {
|
||||||
|
$result = $this->service->getById($id);
|
||||||
|
return $this->success($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[RequestMapping(path: '', methods: ['POST'])]
|
||||||
|
public function create(Create{{Resource}}Request $request): ResponseInterface {
|
||||||
|
$data = $request->validated();
|
||||||
|
$result = $this->service->create($data);
|
||||||
|
return $this->success($result, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[RequestMapping(path: '{id:\d+}', methods: ['PUT'])]
|
||||||
|
public function update(int $id, Create{{Resource}}Request $request): ResponseInterface {
|
||||||
|
$data = $request->validated();
|
||||||
|
$result = $this->service->update($id, $data);
|
||||||
|
return $this->success($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[RequestMapping(path: '{id:\d+}', methods: ['DELETE'])]
|
||||||
|
public function delete(int $id): ResponseInterface {
|
||||||
|
$this->service->delete($id);
|
||||||
|
return $this->success(null, message: 'Deleted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service 模板
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace App\Service\{{Module}};
|
||||||
|
|
||||||
|
use App\Model\{{Module}}\{{Resource}};
|
||||||
|
use App\Exception\BusinessException;
|
||||||
|
use Hyperf\DbConnection\Db;
|
||||||
|
|
||||||
|
class {{Resource}}Service
|
||||||
|
{
|
||||||
|
public function getPageList(array $params): array {
|
||||||
|
$query = {{Resource}}::query();
|
||||||
|
if (!empty($params['status'])) $query->where('status', $params['status']);
|
||||||
|
$page = (int) ($params['page'] ?? 1);
|
||||||
|
$pageSize = (int) ($params['page_size'] ?? 10);
|
||||||
|
$total = $query->count();
|
||||||
|
$items = $query->orderByDesc('id')->offset(($page - 1) * $pageSize)->limit($pageSize)->get();
|
||||||
|
return ['items' => $items, 'total' => $total];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getById(int $id): {{Resource}} {
|
||||||
|
$record = {{Resource}}::find($id);
|
||||||
|
if (!$record) throw new BusinessException(404, '{{Resource}} not found');
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(array $data): {{Resource}} {
|
||||||
|
return Db::transaction(fn () => {{Resource}}::create($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(int $id, array $data): {{Resource}} {
|
||||||
|
$record = $this->getById($id);
|
||||||
|
return Db::transaction(function () use ($record, $data) {
|
||||||
|
$record->update($data);
|
||||||
|
return $record->refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(int $id): void {
|
||||||
|
$record = $this->getById($id);
|
||||||
|
$record->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## FormRequest 模板
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace App\Request\{{Module}};
|
||||||
|
|
||||||
|
use Hyperf\Validation\Request\FormRequest;
|
||||||
|
|
||||||
|
class Create{{Resource}}Request extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool { return true; }
|
||||||
|
public function rules(): array {
|
||||||
|
return [ /* Define validation rules based on resource fields */ ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 路由注册
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Http/Admin/Router/{{resource}}.php
|
||||||
|
use App\Controller\Admin\{{Resource}}Controller;
|
||||||
|
use Hyperf\HttpServer\Router\Router;
|
||||||
|
|
||||||
|
Router::addGroup('/admin/{{route-path}}', function () {
|
||||||
|
Router::get('', [{{Resource}}Controller::class, 'list']);
|
||||||
|
Router::get('/{id:\d+}', [{{Resource}}Controller::class, 'detail']);
|
||||||
|
Router::post('', [{{Resource}}Controller::class, 'create']);
|
||||||
|
Router::put('/{id:\d+}', [{{Resource}}Controller::class, 'update']);
|
||||||
|
Router::delete('/{id:\d+}', [{{Resource}}Controller::class, 'delete']);
|
||||||
|
}, ['middleware' => [AccessTokenMiddleware::class, PermissionMiddleware::class]]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 统一响应格式
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function success(mixed $data = null, int $code = 200, string $message = 'ok'): ResponseInterface {
|
||||||
|
return $this->response->json(['code' => $code, 'message' => $message, 'data' => $data]);
|
||||||
|
}
|
||||||
|
protected function error(string $message, int $code = 500, mixed $data = null): ResponseInterface {
|
||||||
|
return $this->response->json(['code' => $code, 'message' => $message, 'data' => $data]);
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# API Scaffold — 分级异常体系
|
||||||
|
|
||||||
|
> 主流程见 SKILL.md,本文档为异常分级与 AppExceptionHandler 完整实现。
|
||||||
|
|
||||||
|
## 异常分级
|
||||||
|
|
||||||
|
| 异常类 | 场景 | HTTP 状态码 | 日志级别 | 是否通知运维 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `BusinessException` | 可预期的业务错误 | 400/403/404/409/422 | warning | ❌ |
|
||||||
|
| `ValidationException` | 参数校验失败 | 422 | info | ❌ |
|
||||||
|
| `SystemException` | 不可预期的系统故障 | 500/502/503 | error | ✅ |
|
||||||
|
|
||||||
|
## BusinessException / SystemException 定义
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Exception/BusinessException.php
|
||||||
|
class BusinessException extends ServerException
|
||||||
|
{
|
||||||
|
public function __construct(int $code = 400, string $message = 'Business error', public readonly bool $isOperational = true)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// app/Exception/SystemException.php
|
||||||
|
class SystemException extends ServerException
|
||||||
|
{
|
||||||
|
public function __construct(string $message = 'System error', int $code = 500, public readonly bool $isOperational = false, ?\Throwable $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## AppExceptionHandler
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace App\Exception\Handler;
|
||||||
|
|
||||||
|
use App\Exception\BusinessException;
|
||||||
|
use App\Exception\SystemException;
|
||||||
|
use Hyperf\ExceptionHandler\ExceptionHandler;
|
||||||
|
use Hyperf\HttpMessage\Stream\SwooleStream;
|
||||||
|
use Hyperf\Validation\ValidationException;
|
||||||
|
|
||||||
|
class AppExceptionHandler extends ExceptionHandler
|
||||||
|
{
|
||||||
|
public function handle(\Throwable $throwable, ResponseInterface $response): ResponseInterface
|
||||||
|
{
|
||||||
|
$this->stopPropagation();
|
||||||
|
$body = match (true) {
|
||||||
|
$throwable instanceof ValidationException => ['code' => 422, 'message' => 'Validation failed', 'data' => $throwable->validator->errors()->toArray()],
|
||||||
|
$throwable instanceof BusinessException => ['code' => $throwable->getCode(), 'message' => $throwable->getMessage(), 'data' => null],
|
||||||
|
$throwable instanceof SystemException => $this->handleSystemException($throwable),
|
||||||
|
default => $this->handleUnexpected($throwable),
|
||||||
|
};
|
||||||
|
$statusCode = ($body['code'] >= 100 && $body['code'] < 600) ? $body['code'] : 500;
|
||||||
|
return $response->withStatus($statusCode)->withHeader('Content-Type', 'application/json')
|
||||||
|
->withBody(new SwooleStream(json_encode($body, JSON_UNESCAPED_UNICODE)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleSystemException(SystemException $e): array {
|
||||||
|
$this->logger->error('System exception', ['message' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
||||||
|
return ['code' => 500, 'message' => 'Internal server error', 'data' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleUnexpected(\Throwable $e): array {
|
||||||
|
$this->logger->critical('Unexpected exception', ['class' => get_class($e), 'message' => $e->getMessage()]);
|
||||||
|
return ['code' => 500, 'message' => 'Internal server error', 'data' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isValid(\Throwable $throwable): bool { return true; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 异常选择决策
|
||||||
|
|
||||||
|
```
|
||||||
|
抛出异常?
|
||||||
|
├─ 用户输入/请求导致的?
|
||||||
|
│ ├─ 参数格式错误 → ValidationException(FormRequest 自动抛出)
|
||||||
|
│ ├─ 资源不存在 → BusinessException(404)
|
||||||
|
│ ├─ 无权限 → BusinessException(403)
|
||||||
|
│ └─ 业务规则冲突 → BusinessException(409/422)
|
||||||
|
└─ 系统/环境导致的?
|
||||||
|
├─ 数据库连接失败 → SystemException(503)
|
||||||
|
├─ 第三方 API 超时 → SystemException(502)
|
||||||
|
└─ 未知异常 → AppExceptionHandler 兜底
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service 使用示例
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Predictable: resource not found
|
||||||
|
throw new BusinessException(404, 'Order not found');
|
||||||
|
|
||||||
|
// Unpredictable: external system failure
|
||||||
|
throw new SystemException(message: 'ERP sync failed: connection timeout', previous: $e);
|
||||||
|
```
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
---
|
||||||
|
name: bug-reproduce
|
||||||
|
version: 1.1.0
|
||||||
|
description: "结构化 Bug 复现框架,系统化定位和复现 Bug。当需要从 Bug 报告出发编写复现步骤和回归测试时使用。仅复现,不修复。"
|
||||||
|
requires: [vue-testing]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 复现框架(PHP Hyperf + Vue 3)
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户提供 Bug 报告、Issue 描述,要求复现问题或编写回归测试。
|
||||||
|
|
||||||
|
## ⚠️ 核心原则
|
||||||
|
|
||||||
|
- **只复现,不修复** — 输出是失败的测试用例,不是修复代码
|
||||||
|
- **最多 3 次尝试** — 3 次无法复现即标记为 UNCONFIRMED
|
||||||
|
- **遇到硬停止条件立即放弃** — 不浪费时间在不可能的事上
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 信号解析(Parse Signals)
|
||||||
|
|
||||||
|
从用户提供的 Bug 报告中提取关键信号:
|
||||||
|
|
||||||
|
```
|
||||||
|
□ 错误消息 / 堆栈追踪
|
||||||
|
□ 复现步骤(操作序列)
|
||||||
|
□ 影响区域(后端服务 / 前端组件 / API / 数据库)
|
||||||
|
□ 环境信息(PHP 版本 / 浏览器 / OS)
|
||||||
|
□ 何时开始出现 / 上次正常的版本
|
||||||
|
□ 是否可稳定复现 / 偶发
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 路由到测试策略(Route to Test Layer)
|
||||||
|
|
||||||
|
根据影响区域,选择对应的测试层级和工具:
|
||||||
|
|
||||||
|
| 影响区域 | 测试层 | 工具 | 关键目录 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| PHP Service/Repository | 单元测试 | PHPUnit + Mockery | `Case-Database-Backend/test/Unit/` |
|
||||||
|
| Controller/API 端点 | 集成测试 | Hyperf Testing HttpClient | `Case-Database-Backend/test/Feature/` |
|
||||||
|
| 中间件/认证 | 集成测试 | Hyperf Testing | `Case-Database-Backend/test/Feature/` |
|
||||||
|
| 数据库 Migration/Model | 单元测试 | PHPUnit + SQLite | `Case-Database-Backend/test/Unit/` |
|
||||||
|
| Vue 组件逻辑 | 单元测试 | Vitest | `frontend-*/src/**/*.test.ts` |
|
||||||
|
| Vue 组件交互 | 组件测试 | Vitest + Vue Test Utils | `frontend-*/src/**/*.test.ts` |
|
||||||
|
| 跨页面流程 | E2E 测试 | Playwright | `frontend-*/e2e/` |
|
||||||
|
| API ↔ 前端联调 | E2E 测试 | Playwright | `frontend-*/e2e/` |
|
||||||
|
|
||||||
|
### 3. 定位源码(Locate Source Files)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 搜索相关文件
|
||||||
|
rg -l "ClassName\|functionName" --type php --glob '!vendor/**'
|
||||||
|
rg -l "ComponentName" --glob '*.vue' --glob '!node_modules/**'
|
||||||
|
|
||||||
|
# 查看最近变更
|
||||||
|
git log --oneline -10 -- <affected-path>
|
||||||
|
|
||||||
|
# 查看 diff
|
||||||
|
git diff HEAD~5 -- <affected-path>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 追踪代码路径(Trace Code Path)
|
||||||
|
|
||||||
|
从入口点到失败点,追踪完整的调用链:
|
||||||
|
|
||||||
|
**后端路径示例**:
|
||||||
|
```
|
||||||
|
Route → Middleware → Controller → FormRequest → Service → Repository → Model → DB
|
||||||
|
```
|
||||||
|
|
||||||
|
**前端路径示例**:
|
||||||
|
```
|
||||||
|
用户操作 → Event Handler → Composable/Store → API Request → 响应处理 → UI 更新
|
||||||
|
```
|
||||||
|
|
||||||
|
记录每个节点的输入输出和异常处理情况。
|
||||||
|
|
||||||
|
### 5. 形成假设(Hypothesize)
|
||||||
|
|
||||||
|
陈述清晰、可测试的假设:
|
||||||
|
|
||||||
|
```
|
||||||
|
假设 A (70%): 当 [输入/条件] 时,代码在 [位置] 执行了 [错误行为],
|
||||||
|
因为 [根因分析]。
|
||||||
|
假设 B (20%): ...
|
||||||
|
假设 C (10%): ...
|
||||||
|
```
|
||||||
|
|
||||||
|
每个假设必须指向具体的代码行。
|
||||||
|
|
||||||
|
### 6. 查找测试模式(Find Test Patterns)
|
||||||
|
|
||||||
|
在编写测试前,先查找同区域的已有测试作为参考:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查找 PHP 测试
|
||||||
|
rg -l "class.*Test" --type php Case-Database-Backend/test/
|
||||||
|
|
||||||
|
# 查找 Vue 测试
|
||||||
|
rg -l "describe\|it\(" --glob '*.test.ts' Case-Database-Frontend-*/src/
|
||||||
|
```
|
||||||
|
|
||||||
|
使用相同的 mock 模式和 setup 结构保持一致性。
|
||||||
|
|
||||||
|
### 7. 编写失败测试(Write Failing Test)
|
||||||
|
|
||||||
|
测试必须满足:
|
||||||
|
- 使用步骤 6 中找到的已有模式
|
||||||
|
- 断言**正确行为**(测试在当前代码上会失败)
|
||||||
|
- 包含 Bug 引用注释
|
||||||
|
- 同时包含一个 happy path 测试(证明 setup 正确)
|
||||||
|
|
||||||
|
**PHP 测试模板**:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace HyperfTest\Unit;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class OrderServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
// Happy path — proves setup works
|
||||||
|
public function testCreateOrderWithValidData(): void
|
||||||
|
{
|
||||||
|
$service = new OrderService($this->mockRepo);
|
||||||
|
$result = $service->create(['amount' => 100]);
|
||||||
|
$this->assertNotNull($result->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regression — reproduces the bug
|
||||||
|
// @see https://github.com/org/repo/issues/123
|
||||||
|
public function testCreateOrderRejectsNegativeAmount(): void
|
||||||
|
{
|
||||||
|
$service = new OrderService($this->mockRepo);
|
||||||
|
$this->expectException(BusinessException::class);
|
||||||
|
$service->create(['amount' => -1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vitest 测试模板**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getStatusText } from './OrderStatus.utils'
|
||||||
|
|
||||||
|
describe('OrderStatus (Bug #123)', () => {
|
||||||
|
// Happy path
|
||||||
|
it('should return correct status for paid order', () => {
|
||||||
|
expect(getStatusText('paid', false)).toBe('待发货')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Regression — reproduces the bug
|
||||||
|
// @see https://github.com/org/repo/issues/123
|
||||||
|
it('should handle null status without crashing', () => {
|
||||||
|
expect(() => getStatusText(null, false)).not.toThrow()
|
||||||
|
expect(getStatusText(null, false)).toBe('待支付')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. 运行并评分(Run & Score)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PHP
|
||||||
|
cd Case-Database-Backend && composer test -- --filter=OrderServiceTest
|
||||||
|
|
||||||
|
# Vue
|
||||||
|
cd Case-Database-Frontend-user && npx vitest run OrderStatus.utils.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
根据结果评定置信度:
|
||||||
|
|
||||||
|
| 置信度 | 判定条件 | 后续动作 |
|
||||||
|
|---|---|---|
|
||||||
|
| **CONFIRMED** | 测试稳定失败,失败模式与假设吻合 | 输出复现报告 |
|
||||||
|
| **LIKELY** | 测试失败,但失败方式与预期略有偏差 | 报告 + 附注偏差原因 |
|
||||||
|
| **UNCONFIRMED** | 无法触发失败 | 报告已尝试的方法 |
|
||||||
|
| **SKIPPED** | 遇到硬停止条件 | 报告停止原因 |
|
||||||
|
| **ALREADY_FIXED** | Bug 在当前代码上不可复现 | 报告何时修复的 |
|
||||||
|
|
||||||
|
### 9. 迭代或放弃(Iterate or Bail)
|
||||||
|
|
||||||
|
**UNCONFIRMED 后的迭代**(最多 3 次):
|
||||||
|
1. 重新审视假设 — 重读代码路径
|
||||||
|
2. 尝试不同的测试方式或层级
|
||||||
|
3. 第 3 次仍失败 → 标记 UNCONFIRMED
|
||||||
|
|
||||||
|
**硬停止条件**(遇到即立即 SKIP):
|
||||||
|
- 需要真实第三方 API 凭证(支付网关、短信服务等)
|
||||||
|
- 竞态条件/时序依赖(需要精确的并发控制)
|
||||||
|
- 需要特定云基础设施(AWS/阿里云特定服务)
|
||||||
|
- 需要无法脚本化的手动 UI 操作
|
||||||
|
- 需要生产数据库中的真实数据
|
||||||
|
|
||||||
|
## 输出:复现报告
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 🐛 Bug 复现报告
|
||||||
|
|
||||||
|
**Issue**: [ID] — [标题]
|
||||||
|
**置信度**: CONFIRMED | LIKELY | UNCONFIRMED | SKIPPED | ALREADY_FIXED
|
||||||
|
|
||||||
|
### 根因分析
|
||||||
|
[1-2 句话解释 Bug 产生的机制]
|
||||||
|
|
||||||
|
### 定位
|
||||||
|
| 文件 | 行号 | 问题 |
|
||||||
|
|---|---|---|
|
||||||
|
| `path/to/file.php` | XX-YY | 描述 |
|
||||||
|
|
||||||
|
### 失败测试
|
||||||
|
`path/to/test/file` — X/Y 个测试失败:
|
||||||
|
1. `testName` — [失败描述]
|
||||||
|
|
||||||
|
### 修复提示
|
||||||
|
[修复方向的伪代码或文字描述,不写完整修复代码]
|
||||||
|
|
||||||
|
### 尝试记录
|
||||||
|
| 次数 | 假设 | 测试方法 | 结果 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | [假设 A] | [测试方式] | FAIL/PASS |
|
||||||
|
| 2 | [假设 B] | [测试方式] | FAIL/PASS |
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 从 Bug 报告中提取了所有关键信号
|
||||||
|
2. [ ] 选择了正确的测试层级
|
||||||
|
3. [ ] 失败测试包含 Bug 引用注释
|
||||||
|
4. [ ] 同时有 happy path 测试证明 setup 正确
|
||||||
|
5. [ ] 置信度评分有据可依
|
||||||
|
6. [ ] 测试文件已保留(未删除)
|
||||||
|
7. [ ] 输出了结构化复现报告
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
---
|
||||||
|
name: code-review
|
||||||
|
version: 2.0.0
|
||||||
|
description: "从六个维度系统化代码审查(安全/正确/可维护/性能/可测试/一致性)。当需要 Review 代码、检查质量或预提交检查时使用。"
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/skill-code-review.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Code Review
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求审查代码、检查 PR、评估代码质量或执行预提交检查。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 确定审查范围
|
||||||
|
|
||||||
|
- 具体文件 → 审查指定文件
|
||||||
|
- 目录范围 → 审查目录下所有变更
|
||||||
|
- Git diff → `git diff HEAD~1` 审查最近变更
|
||||||
|
|
||||||
|
### 2. 六维度审查
|
||||||
|
|
||||||
|
按以下顺序逐一审查:
|
||||||
|
|
||||||
|
**🔴 安全性 (Security)**
|
||||||
|
- 硬编码密钥/凭证?
|
||||||
|
- SQL 注入 / XSS 风险?
|
||||||
|
- 未验证的用户输入?
|
||||||
|
- 不安全的认证/授权?
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ BAD: 直接拼接用户输入到 SQL
|
||||||
|
$users = Db::select("SELECT * FROM users WHERE name = '{$name}'");
|
||||||
|
|
||||||
|
// ✅ GOOD: 参数绑定
|
||||||
|
$users = Db::select("SELECT * FROM users WHERE name = ?", [$name]);
|
||||||
|
// ✅ GOOD: Hyperf ORM
|
||||||
|
$users = User::where('name', $name)->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ BAD: 直接渲染用户输入的 HTML -->
|
||||||
|
<div v-html="userInput" />
|
||||||
|
|
||||||
|
<!-- ✅ GOOD: 文本插值自动转义 -->
|
||||||
|
<div>{{ userInput }}</div>
|
||||||
|
|
||||||
|
<!-- ✅ GOOD: 如必须用 v-html,先经过 DOMPurify 清洗 -->
|
||||||
|
<div v-html="sanitize(userInput)" />
|
||||||
|
```
|
||||||
|
|
||||||
|
**🟠 正确性 (Correctness)**
|
||||||
|
- 逻辑是否正确?
|
||||||
|
- 边界条件处理?
|
||||||
|
- 错误处理完善?
|
||||||
|
- null/undefined 安全?
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ BAD: 无空值保护
|
||||||
|
$userName = $user->profile->name;
|
||||||
|
|
||||||
|
// ✅ GOOD: 空安全访问
|
||||||
|
$userName = $user?->profile?->name ?? 'Unknown';
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: 解构可能为 null 的响应
|
||||||
|
const { data } = await api.getUser(id)
|
||||||
|
|
||||||
|
// ✅ GOOD: 防御性解构
|
||||||
|
const response = await api.getUser(id)
|
||||||
|
const data = response?.data ?? null
|
||||||
|
```
|
||||||
|
|
||||||
|
**🟡 可维护性 (Maintainability)**
|
||||||
|
- 命名清晰?
|
||||||
|
- 函数长度合理(< 50 行)?
|
||||||
|
- 单一职责?
|
||||||
|
- 业务逻辑是否已提取到纯函数/composable?
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ BAD: 组件内嵌大量业务逻辑 -->
|
||||||
|
<script setup>
|
||||||
|
const result = computed(() => {
|
||||||
|
// 30 行复杂计算逻辑...
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- ✅ GOOD: 逻辑提取到 .utils.ts -->
|
||||||
|
<script setup>
|
||||||
|
import { calculateResult } from './MyComponent.utils'
|
||||||
|
const result = computed(() => calculateResult(props.data))
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**🔵 性能 (Performance)**
|
||||||
|
- 不必要的渲染?
|
||||||
|
- N+1 查询?
|
||||||
|
- 大数据集未分页?
|
||||||
|
- 缺少缓存机会?
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ BAD: N+1 查询
|
||||||
|
$orders = Order::all();
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
echo $order->user->name; // each iteration queries DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: 预加载关联
|
||||||
|
$orders = Order::with('user')->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- ❌ BAD: v-for 内使用复杂计算 -->
|
||||||
|
<div v-for="item in list" :key="item.id">
|
||||||
|
{{ heavyCompute(item) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ GOOD: 预计算或使用 computed -->
|
||||||
|
<div v-for="item in computedList" :key="item.id">
|
||||||
|
{{ item.computedValue }}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**🟣 可测试性 (Testability)**
|
||||||
|
- 纯函数是否已提取到 `.utils.ts`?
|
||||||
|
- composable 是否可独立测试?
|
||||||
|
- 关键路径是否有 `data-testid`?
|
||||||
|
- 是否有过度耦合使测试困难?
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: 逻辑耦合在组件中,无法独立测试
|
||||||
|
// 必须 mount 整个组件才能测试 formatDate
|
||||||
|
|
||||||
|
// ✅ GOOD: 提取为可独立测试的纯函数
|
||||||
|
// date.utils.ts
|
||||||
|
export function formatDate(date, format = 'YYYY-MM-DD') { ... }
|
||||||
|
// date.utils.test.ts
|
||||||
|
it('should format date correctly', () => { ... })
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚪ 一致性 (Consistency)**
|
||||||
|
- 命名风格统一?
|
||||||
|
- 遵循项目模式?
|
||||||
|
- import 顺序统一?
|
||||||
|
- 错误处理模式统一?
|
||||||
|
|
||||||
|
### 3. 预提交检查联动
|
||||||
|
|
||||||
|
审查代码时,同步执行以下自动化检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PHP 后端
|
||||||
|
cd Case-Database-Backend
|
||||||
|
vendor/bin/phpstan analyse --level=5 --no-progress # 静态分析
|
||||||
|
vendor/bin/php-cs-fixer fix --dry-run --diff # 代码格式
|
||||||
|
|
||||||
|
# Vue 前端
|
||||||
|
cd Case-Database-Frontend-user
|
||||||
|
npx eslint --quiet src/ # ESLint
|
||||||
|
npx vitest run --reporter=verbose # 单元测试
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
如果项目配置了 husky + lint-staged,确认 `.husky/pre-commit` 钩子覆盖:
|
||||||
|
- [ ] ESLint (前端)
|
||||||
|
- [ ] PHPStan (后端)
|
||||||
|
- [ ] 代码格式检查 (Prettier / PHP CS Fixer)
|
||||||
|
|
||||||
|
### 4. 输出格式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 代码审查报告
|
||||||
|
|
||||||
|
**范围**: <文件/目录/PR>
|
||||||
|
**总体评价**: ✅ 建议合并 | ⚠️ 需修改后合并 | ❌ 需重写
|
||||||
|
|
||||||
|
### 自动化检查
|
||||||
|
- ESLint: ✅ 通过 | ❌ N 个错误
|
||||||
|
- PHPStan: ✅ 通过 | ❌ N 个错误
|
||||||
|
- 测试: ✅ 通过 | ❌ N 个失败
|
||||||
|
|
||||||
|
### 发现
|
||||||
|
|
||||||
|
#### 🔴 Must Fix
|
||||||
|
1. **[SEC]** `file.ts:23` — 硬编码 API Key
|
||||||
|
→ 修复:移入环境变量
|
||||||
|
|
||||||
|
#### 🟡 Should Fix
|
||||||
|
2. **[MAINT]** `service.ts:45` — 函数过长(87 行)
|
||||||
|
→ 建议:拆分为 3 个私有方法
|
||||||
|
3. **[TEST]** `OrderForm.vue` — 价格计算逻辑未提取到 .utils.ts
|
||||||
|
→ 建议:提取为纯函数并添加单元测试
|
||||||
|
|
||||||
|
#### 🟢 Suggestion
|
||||||
|
4. **[PERF]** `list.vue:12` — 列表项未使用 `v-memo`
|
||||||
|
→ 建议:添加 `v-memo="[item.id]"` 减少重渲染
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 六个维度全部覆盖(含可测试性)
|
||||||
|
2. [ ] 每个发现有具体文件和行号
|
||||||
|
3. [ ] 每个发现有修复建议(含 BAD/GOOD 代码对比)
|
||||||
|
4. [ ] 发现按严重程度分级
|
||||||
|
5. [ ] 预提交检查已执行(ESLint + PHPStan + 测试)
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
name: component-scaffold
|
||||||
|
version: 4.1.0
|
||||||
|
description: "生成 Vue 3 SFC 组件脚手架,含单元测试和类型安全 Props。当需要新建组件、拆分子组件或创建复合组件时使用。管理端用 Element Plus,用户端用 Headless UI。"
|
||||||
|
requires: [vue-testing]
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/skill-component-scaffold.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Vue 3 Component Scaffold
|
||||||
|
|
||||||
|
> **⚠️ 前端识别**:生成组件前必须确认目标前端。
|
||||||
|
> - **管理端** (`Case-Database-Frontend-admin/`):使用 Element Plus + Tailwind
|
||||||
|
> - **用户端** (`Case-Database-Frontend-user/`):使用 Headless UI + Tailwind,**禁止 Element Plus**
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求创建新的 Vue 组件、页面组件、UI 元素或交互模块。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 0. 加载规范(⚠️ 必须最先执行)
|
||||||
|
|
||||||
|
依次读取 `.cursor/rules/010-typescript.mdc`、`.cursor/rules/011-vue.mdc`、`.cursor/rules/019-modular.mdc`,提取类型注解要求(隐式 any 禁令、ref 泛型规范、Composable 类型规范)、script setup、defineProps/Emits、组件分类、拆分阈值、Composable 提取规则。
|
||||||
|
|
||||||
|
> **Tier 3**:Vue 3 完整 API 见 `references/vue-api-reference.md`。
|
||||||
|
|
||||||
|
### 0.5 ⚠️ 生成前强制结构规划(禁止跳过)
|
||||||
|
|
||||||
|
**写代码前必须先输出文件结构和组件职责说明。**
|
||||||
|
|
||||||
|
#### A. 检查是否已有可复用的基础组件
|
||||||
|
|
||||||
|
在生成新组件前,先扫描以下路径,**避免重复造轮子**:
|
||||||
|
- `src/components/core/` — 通用基础组件(按钮、输入框、卡片等)
|
||||||
|
- `src/components/custom/` — 业务定制组件
|
||||||
|
|
||||||
|
若已有 `FormInput.vue`、`BaseCard.vue` 等,**直接复用,不重新生成**。
|
||||||
|
|
||||||
|
#### B. 用户端表单输入:优先复用 FormInput 模式
|
||||||
|
|
||||||
|
用户端页面中表单输入框若出现 ≥3 个,必须使用 `FormInput` 基础组件:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- src/components/core/FormInput/FormInput.vue -->
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
modelValue: { type: String, default: '' },
|
||||||
|
type: { type: String, default: 'text' },
|
||||||
|
placeholder: { type: String, default: '' },
|
||||||
|
icon: { type: Object, default: null }, // Lucide 图标组件
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
error: { type: String, default: '' },
|
||||||
|
})
|
||||||
|
defineEmits(['update:modelValue'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<component :is="icon" v-if="icon"
|
||||||
|
class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500 w-5 h-5" />
|
||||||
|
<input
|
||||||
|
:type="type"
|
||||||
|
:value="modelValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="disabled"
|
||||||
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
|
:class="[
|
||||||
|
'w-full border-b py-3 outline-none transition-colors',
|
||||||
|
icon ? 'pl-10 pr-4' : 'px-4',
|
||||||
|
'bg-transparent border-gray-300 dark:border-[#333333]',
|
||||||
|
'text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-600',
|
||||||
|
'focus:border-[#C41E3A] dark:focus:border-[#C41E3A]',
|
||||||
|
error ? 'border-red-500' : '',
|
||||||
|
]"
|
||||||
|
data-testid="form-input"
|
||||||
|
/>
|
||||||
|
<p v-if="error" class="mt-1 text-xs text-red-500">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
> 若项目中尚无 `FormInput`,在生成使用它的页面前先生成该核心组件。
|
||||||
|
|
||||||
|
#### C. 重复 UI 模式检测
|
||||||
|
|
||||||
|
检查需求中是否存在重复结构:
|
||||||
|
|
||||||
|
| 场景 | 检测标准 | 操作 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 表单输入项 | ≥3 个相同结构的 input 组 | 提取 `FormInput.vue` |
|
||||||
|
| 内容卡片 | ≥3 个相同布局的卡片 | 提取 `CaseCard.vue` / `DesignerCard.vue` |
|
||||||
|
| 操作按钮组 | ≥2 处相同按钮组合 | 提取 `ActionGroup.vue` |
|
||||||
|
| 阶段/Tab 面板 | ≥2 个结构相同的面板 | 提取 `StagePanel.vue` |
|
||||||
|
|
||||||
|
#### D. 输出组件文件结构(代码前必须输出)
|
||||||
|
|
||||||
|
```
|
||||||
|
# 本次将生成以下文件:
|
||||||
|
src/components/<type>/<ComponentName>/
|
||||||
|
├── <ComponentName>.vue ← 主组件(目标 ≤ 120 行)
|
||||||
|
├── <ComponentName>.test.ts ← 单元测试
|
||||||
|
└── index.ts ← barrel export
|
||||||
|
|
||||||
|
# 依赖(已存在 / 需新建):
|
||||||
|
- FormInput.vue: [已存在 / 需新建]
|
||||||
|
- BaseCard.vue: [已存在 / 不需要]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. 确认组件规格
|
||||||
|
|
||||||
|
| 字段 | 必填 | 默认值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| 组件名 | ✅ | — |
|
||||||
|
| 组件类型 | ❌ | UI 组件 |
|
||||||
|
| 所在目录 | ❌ | `src/components/` |
|
||||||
|
| Props / Emits | ❌ | 根据需求推断 |
|
||||||
|
|
||||||
|
类型与目录:`core`→`src/components/core/`、`custom`→`custom/`、`layout`→`layouts/`、`page`→`views/<module>/components/`、`form`→`custom/`。
|
||||||
|
|
||||||
|
### 2. 扫描项目模式
|
||||||
|
|
||||||
|
读取 `src/components/` 确认 Props 声明、样式方案、命名前缀约定、已有的 core 组件列表。
|
||||||
|
|
||||||
|
### 3. 生成文件结构
|
||||||
|
|
||||||
|
按步骤 0.5 输出的结构逐文件生成,**禁止将多个组件合并到一个文件**。
|
||||||
|
|
||||||
|
```
|
||||||
|
src/components/<type>/<ComponentName>/
|
||||||
|
├── <ComponentName>.vue
|
||||||
|
├── <ComponentName>.test.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 组件模板
|
||||||
|
|
||||||
|
根据类型选择:基础 UI / 表格 / 表单对话框 / 复合组件。完整模板与设计决策树见 **Tier 3**。
|
||||||
|
|
||||||
|
**单组件行数限制**:
|
||||||
|
- template 区域 ≤ 80 行
|
||||||
|
- script 区域 ≤ 60 行(超出提取 composable)
|
||||||
|
- 整个 SFC ≤ 150 行(超出必须拆分子组件)
|
||||||
|
|
||||||
|
### 4.5 设计决策树(必须执行)
|
||||||
|
|
||||||
|
- ≥3 布尔 prop?→ 显式变体组件
|
||||||
|
- 多子组件共享状态?→ provide/inject
|
||||||
|
- script 逻辑 > 60 行?→ 提取 `use<ComponentName>.ts` composable(**必须遵循 `010-typescript.mdc` 的 Composable 类型规范**:参数有类型、`ref` 有泛型、业务类型用 `import type`)
|
||||||
|
- >2 处复用?→ `src/components/` + slot
|
||||||
|
- 同一 UI 结构重复 ≥3 次?→ 提取基础组件(见步骤 0.5C)
|
||||||
|
- 否则 → 基础 UI 模板
|
||||||
|
|
||||||
|
### 5. 测试与 Barrel export
|
||||||
|
|
||||||
|
测试至少包含渲染测试。`index.ts` 导出:`export { default as ComponentName } from './ComponentName.vue'`。详细测试见 `vue-testing` 技能。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] ESLint 无报错
|
||||||
|
2. [ ] Props 使用对象语法 `defineProps({ key: { type, default } })`,**若模板可直接访问 props 字段则不保存返回值**(避免 `unused-vars` 报错;仅当 script 中需要访问 `props.xxx` 时才保存:`const props = defineProps(...)`)
|
||||||
|
3. [ ] Emits 使用数组语法,事件名使用 camelCase(非 kebab-case)
|
||||||
|
4. [ ] 包含 `data-testid`
|
||||||
|
5. [ ] 测试至少一个渲染测试
|
||||||
|
6. [ ] 管理端:Element Plus 用于组件,Tailwind 用于布局;用户端:Headless UI + Tailwind(禁止 Element Plus)
|
||||||
|
7. [ ] barrel export 正确
|
||||||
|
8. [ ] **单 SFC ≤ 150 行**
|
||||||
|
9. [ ] **重复 UI 结构(≥3 次)已提取为基础组件**
|
||||||
|
10. [ ] **表单输入 ≥3 个已复用 FormInput.vue**(用户端)
|
||||||
|
11. [ ] **子组件模板中无 `v-model` 直接绑定 prop 嵌套属性**(参见 `011-vue.mdc` Props 数据流模式)
|
||||||
|
12. [ ] **提取的 composable(use*.ts)参数有类型注解、`ref([])` / `ref(null)` 有泛型标注**(参见 `010-typescript.mdc` Composable 类型规范)
|
||||||
|
|
||||||
|
### Red Flags(触发则停止并重构)
|
||||||
|
|
||||||
|
- ❌ 未输出文件结构直接写代码 → 停止,先输出步骤 0.5D 的结构
|
||||||
|
- ❌ 单 SFC > 150 行 → 拆分子组件
|
||||||
|
- ❌ 相同 Tailwind 输入框结构写了 ≥3 次 → 提取 FormInput.vue
|
||||||
|
- ❌ Options API → 改用 `<script setup>`
|
||||||
|
- ❌ `const props = defineProps(...)` 但 script 中从不访问 `props.xxx` → 去掉 `const props =`,避免 `unused-vars` lint 报错
|
||||||
|
- ❌ `catch (e) { ... }` 但 catch 块中不使用 `e` → 改为 `catch { ... }`(无参 catch),避免 `unused-vars` 报错
|
||||||
|
- ❌ 直接修改 props → 通过 emit(含隐蔽变体:`v-model="prop.field"`、`v-model="prop[key]"`、事件回调中 `prop[k] = v`)
|
||||||
|
- ❌ watch deep:true 滥用 → 精准监听
|
||||||
|
- ❌ script 逻辑 > 60 行未提取 → 拆分为 use*.ts
|
||||||
|
- ❌ composable 参数无类型 / `ref([])` 无泛型 → 补全类型注解(`strict: true` 下必报错)
|
||||||
|
- ❌ ≥3 布尔 prop 控制渲染 → 拆分变体
|
||||||
|
- ❌ prop drilling 传递同状态 → 改用 provide/inject
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `references/component-templates.md` | 基础/表格/表单/复合组件模板、决策树、Prop 反模式、错误处理、UI 反模式 |
|
||||||
|
| `references/vue-api-reference.md` | Vue 3 完整 API 索引 |
|
||||||
|
| `references/naming-conventions.md` | 组件命名规范 |
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Component Scaffold — 组件模板
|
||||||
|
|
||||||
|
> 主流程见 SKILL.md,本文档为各类 Vue 3 组件的完整模板。
|
||||||
|
>
|
||||||
|
> **⚠️ 双前端区分**:本文件中使用 `el-*` 组件的模板**仅适用于管理端** (`Case-Database-Frontend-admin/`)。
|
||||||
|
> 用户端 (`Case-Database-Frontend-user/`) 使用 Headless UI + Tailwind CSS,**禁止引入 Element Plus**。
|
||||||
|
|
||||||
|
## 基础 UI 组件
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
loading: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="rounded-lg border border-gray-200 p-4" data-testid="component-name">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">{{ props.title }}</h3>
|
||||||
|
<slot />
|
||||||
|
<el-button v-if="props.loading" :loading="true" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 表格组件 (Element Plus)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { useTable } from '@/hooks/useTable'
|
||||||
|
const props = defineProps({ apiUrl: { type: String, required: true }, columns: { type: Array, required: true } })
|
||||||
|
const { loading, dataList, pagination, loadData } = useTable((params) => request.get(props.apiUrl, { params }))
|
||||||
|
onMounted(() => loadData())
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<el-table v-loading="loading" :data="dataList" border stripe data-testid="data-table">
|
||||||
|
<el-table-column v-for="col in columns" :key="col.prop" v-bind="col" />
|
||||||
|
<el-table-column label="操作" fixed="right" width="180">
|
||||||
|
<template #default="{ row }"><slot name="actions" :row="row" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.size" :total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next" @current-change="loadData" @size-change="loadData" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 表单对话框组件
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({ visible: { type: Boolean, required: true }, title: { type: String, required: true }, formData: { type: Object, default: () => ({}) } })
|
||||||
|
const emit = defineEmits(['update:visible', 'submit'])
|
||||||
|
const formRef = ref()
|
||||||
|
const form = reactive({ ...props.formData })
|
||||||
|
const rules = { name: [{ required: true, message: '请输入名称', trigger: 'blur' }] }
|
||||||
|
async function handleSubmit() { await formRef.value?.validate(); emit('submit', { ...form }) }
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<el-dialog :model-value="visible" :title="title" width="600px" @update:model-value="emit('update:visible', $event)">
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<el-form-item label="名称" prop="name"><el-input v-model="form.name" placeholder="请输入" /></el-form-item>
|
||||||
|
<slot name="form-fields" :form="form" />
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="emit('update:visible', false)">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确认</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 复合组件 (Compound + provide/inject)
|
||||||
|
|
||||||
|
Provider 组件:provide 状态 + actions。子组件:inject 消费。keys.ts 定义 Symbol + useDataPanel()。消费者组合:`<DataPanelProvider :fetch-fn="orderApi.list"><DataPanelContent /></DataPanelProvider>`
|
||||||
|
|
||||||
|
## 组件设计决策树
|
||||||
|
|
||||||
|
```
|
||||||
|
≥3 个布尔 prop 控制渲染?→ 拆分为显式变体组件
|
||||||
|
多个子组件共享状态?→ provide/inject 复合组件
|
||||||
|
业务逻辑 > 50 行?→ 提取 composable
|
||||||
|
>2 处复用?→ 放入 src/components/,使用 slot
|
||||||
|
否则 → 基础 UI 组件模板
|
||||||
|
```
|
||||||
|
|
||||||
|
## 逻辑提取规则 (CRITICAL)
|
||||||
|
|
||||||
|
纯转换逻辑必须提取到 `ComponentName.utils.ts`,组件只保留 UI 和事件。文件结构:`ComponentName.vue` + `ComponentName.utils.ts` + `ComponentName.utils.test.ts` + `ComponentName.test.ts`(可选)+ `index.ts`
|
||||||
|
|
||||||
|
## 复杂度阈值
|
||||||
|
|
||||||
|
| 指标 | 阈值 | 动作 |
|
||||||
|
|---|---|---|
|
||||||
|
| 组件总行数 | > 200 | ⚠️ 考虑拆分 |
|
||||||
|
| 组件总行数 | > 400 | 🔴 必须拆分 |
|
||||||
|
| script 逻辑行数 | > 50 | 提取 composable |
|
||||||
|
| 布尔 Props | ≥ 3 | 拆分为变体组件 |
|
||||||
|
|
||||||
|
## Prop 反模式与显式变体
|
||||||
|
|
||||||
|
❌ 4 个布尔 = 16 种状态,v-if 地狱。✅ 显式变体:`AdminEditPanel` / `PublicReadonlyPanel` / `DraftPreviewPanel`,通过 slot 复用共享子组件。Slot vs Prop:简单数据用 Prop,自定义渲染用 Slot,控制显示用有无 slot 内容。
|
||||||
|
|
||||||
|
## 错误处理规范
|
||||||
|
|
||||||
|
| 场景 | 组件 |
|
||||||
|
|------|------|
|
||||||
|
| 操作成功/失败即时反馈 | ElMessage |
|
||||||
|
| 需用户确认 | ElMessageBox |
|
||||||
|
| 异步持续状态 | ElNotification |
|
||||||
|
| 页面级加载失败 | 内联错误 UI + 重试按钮 |
|
||||||
|
|
||||||
|
## UI 设计反模式
|
||||||
|
|
||||||
|
居中一切、紫色渐变、大圆角、过度阴影、空白页大插图、过多动画 → 以 Element Plus 为基准,优先功能清晰度。
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 组件命名规范
|
||||||
|
|
||||||
|
## 文件命名
|
||||||
|
|
||||||
|
| 类型 | 命名 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| 组件文件 | PascalCase.vue | `UserProfile.vue` |
|
||||||
|
| 测试文件 | PascalCase.test.ts | `UserProfile.test.ts` |
|
||||||
|
| 样式文件 | 内联 `<style scoped>` | `UserProfile.vue` |
|
||||||
|
| Composable | use + PascalCase.ts | `useUserProfile.ts` |
|
||||||
|
| 工具函数 | camelCase.ts | `formatDate.ts` |
|
||||||
|
|
||||||
|
## Props 接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good: use JSDoc typedef for props-like structure
|
||||||
|
/**
|
||||||
|
* @typedef {Object} UserProfileProps
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ❌ Bad: ambiguous generic name
|
||||||
|
/** @typedef {Object} Props */
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件类型
|
||||||
|
|
||||||
|
| 类型 | 目录 | 特征 |
|
||||||
|
|------|------|------|
|
||||||
|
| UI 组件 | `components/ui/` | 无业务逻辑,纯展示 |
|
||||||
|
| Feature 组件 | `components/features/` | 包含业务逻辑 |
|
||||||
|
| Layout 组件 | `components/layout/` | 页面布局 |
|
||||||
|
| Form 组件 | `components/forms/` | 表单及验证 |
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
# Vue.ts 3 API Reference
|
||||||
|
|
||||||
|
Quick reference with links to official Vue.ts documentation.
|
||||||
|
|
||||||
|
## Official Documentation
|
||||||
|
|
||||||
|
**Main Site:** https://vuejs.org
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Getting Started](#getting-started)
|
||||||
|
2. [Essentials](#essentials)
|
||||||
|
3. [Components In-Depth](#components-in-depth)
|
||||||
|
4. [Reusability](#reusability)
|
||||||
|
5. [Built-in Components](#built-in-components)
|
||||||
|
6. [API Reference](#api-reference)
|
||||||
|
7. [TypeScript Patterns](#typescript-patterns)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Introduction
|
||||||
|
**URL:** https://vuejs.org/guide/introduction.html
|
||||||
|
- What is Vue?
|
||||||
|
- Progressive Framework
|
||||||
|
- Single-File Components
|
||||||
|
- API Styles (Options vs Composition)
|
||||||
|
- Which to choose?
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
**URL:** https://vuejs.org/guide/quick-start.html
|
||||||
|
- Creating a Vue Application
|
||||||
|
- Using Vue from CDN
|
||||||
|
- With build tools (Vite)
|
||||||
|
- IDE Setup
|
||||||
|
|
||||||
|
### Creating an Application
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/application.html
|
||||||
|
- Application instance
|
||||||
|
- Root component
|
||||||
|
- Mounting the app
|
||||||
|
- App configurations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Essentials
|
||||||
|
|
||||||
|
### Template Syntax
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/template-syntax.html
|
||||||
|
- Text interpolation
|
||||||
|
- Raw HTML (v-html)
|
||||||
|
- Attribute bindings (v-bind)
|
||||||
|
- TypeScript expressions
|
||||||
|
- Directives
|
||||||
|
|
||||||
|
### Reactivity Fundamentals
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/reactivity-fundamentals.html
|
||||||
|
- `ref()` - Reactive state for primitives
|
||||||
|
- `reactive()` - Reactive objects
|
||||||
|
- Reactive proxy vs original
|
||||||
|
- `nextTick()` - DOM update timing
|
||||||
|
|
||||||
|
### Computed Properties
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/computed.html
|
||||||
|
- Basic usage
|
||||||
|
- Computed caching vs methods
|
||||||
|
- Writable computed
|
||||||
|
- Best practices
|
||||||
|
|
||||||
|
### Class and Style Bindings
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/class-and-style.html
|
||||||
|
- Binding HTML classes
|
||||||
|
- Binding inline styles
|
||||||
|
- Object syntax
|
||||||
|
- Array syntax
|
||||||
|
|
||||||
|
### Conditional Rendering
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/conditional.html
|
||||||
|
- `v-if`, `v-else-if`, `v-else`
|
||||||
|
- `v-show`
|
||||||
|
- v-if vs v-show
|
||||||
|
- v-if with v-for
|
||||||
|
|
||||||
|
### List Rendering
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/list.html
|
||||||
|
- `v-for` with arrays
|
||||||
|
- `v-for` with objects
|
||||||
|
- `v-for` with ranges
|
||||||
|
- Maintaining state with `key`
|
||||||
|
- Array change detection
|
||||||
|
|
||||||
|
### Event Handling
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/event-handling.html
|
||||||
|
- Listening to events (`v-on` / `@`)
|
||||||
|
- Method handlers
|
||||||
|
- Inline handlers
|
||||||
|
- Event modifiers (.stop, .prevent, etc.)
|
||||||
|
- Key modifiers
|
||||||
|
|
||||||
|
### Form Input Bindings
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/forms.html
|
||||||
|
- `v-model` basics
|
||||||
|
- Text, textarea inputs
|
||||||
|
- Checkboxes, radio buttons
|
||||||
|
- Select dropdowns
|
||||||
|
- Modifiers (.lazy, .number, .trim)
|
||||||
|
|
||||||
|
### Lifecycle Hooks
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/lifecycle.html
|
||||||
|
- Lifecycle diagram
|
||||||
|
- `onMounted()`, `onUpdated()`, `onUnmounted()`
|
||||||
|
- `onBeforeMount()`, `onBeforeUpdate()`, `onBeforeUnmount()`
|
||||||
|
- `onErrorCaptured()`, `onActivated()`, `onDeactivated()`
|
||||||
|
|
||||||
|
### Watchers
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/watchers.html
|
||||||
|
- `watch()` - Watch specific sources
|
||||||
|
- `watchEffect()` - Auto-track dependencies
|
||||||
|
- Deep watchers
|
||||||
|
- Eager watchers (immediate)
|
||||||
|
- Callback flush timing
|
||||||
|
- Stopping watchers
|
||||||
|
|
||||||
|
### Template Refs
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/template-refs.html
|
||||||
|
- Accessing DOM elements
|
||||||
|
- Refs inside v-for
|
||||||
|
- Function refs
|
||||||
|
- Component refs
|
||||||
|
|
||||||
|
### Components Basics
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/component-basics.html
|
||||||
|
- Defining components
|
||||||
|
- Using components
|
||||||
|
- Passing props
|
||||||
|
- Listening to events
|
||||||
|
- Slots
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Components In-Depth
|
||||||
|
|
||||||
|
### Registration
|
||||||
|
**URL:** https://vuejs.org/guide/components/registration.html
|
||||||
|
- Global registration
|
||||||
|
- Local registration
|
||||||
|
- Component name casing
|
||||||
|
|
||||||
|
### Props
|
||||||
|
**URL:** https://vuejs.org/guide/components/props.html
|
||||||
|
- Props declaration
|
||||||
|
- Prop types and validation
|
||||||
|
- Prop passing details
|
||||||
|
- One-way data flow
|
||||||
|
- Boolean casting
|
||||||
|
- Prop validation
|
||||||
|
|
||||||
|
### Events
|
||||||
|
**URL:** https://vuejs.org/guide/components/events.html
|
||||||
|
- Emitting and listening to events
|
||||||
|
- Event arguments
|
||||||
|
- Declaring emitted events
|
||||||
|
- Events validation
|
||||||
|
- Usage with v-model
|
||||||
|
|
||||||
|
### Component v-model
|
||||||
|
**URL:** https://vuejs.org/guide/components/v-model.html
|
||||||
|
- Basic usage
|
||||||
|
- v-model arguments
|
||||||
|
- Multiple v-model bindings
|
||||||
|
- Custom modifiers
|
||||||
|
|
||||||
|
### Fallthrough Attributes
|
||||||
|
**URL:** https://vuejs.org/guide/components/attrs.html
|
||||||
|
- Attribute inheritance
|
||||||
|
- Disabling inheritance
|
||||||
|
- Accessing fallthrough attributes
|
||||||
|
- Multi-root nodes
|
||||||
|
|
||||||
|
### Slots
|
||||||
|
**URL:** https://vuejs.org/guide/components/slots.html
|
||||||
|
- Default slot content
|
||||||
|
- Named slots
|
||||||
|
- Scoped slots
|
||||||
|
- Renderless components
|
||||||
|
|
||||||
|
### Provide / Inject
|
||||||
|
**URL:** https://vuejs.org/guide/components/provide-inject.html
|
||||||
|
- Basic usage
|
||||||
|
- App-level provide
|
||||||
|
- Working with reactivity
|
||||||
|
- Working with Symbol keys
|
||||||
|
|
||||||
|
### Async Components
|
||||||
|
**URL:** https://vuejs.org/guide/components/async.html
|
||||||
|
- Basic usage
|
||||||
|
- Loading and error states
|
||||||
|
- Using with Suspense
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reusability
|
||||||
|
|
||||||
|
### Composables
|
||||||
|
**URL:** https://vuejs.org/guide/reusability/composables.html
|
||||||
|
- What is a composable?
|
||||||
|
- Mouse tracker example
|
||||||
|
- Async state example
|
||||||
|
- Conventions and best practices
|
||||||
|
- Usage restrictions
|
||||||
|
- Extracting composables
|
||||||
|
|
||||||
|
### Custom Directives
|
||||||
|
**URL:** https://vuejs.org/guide/reusability/custom-directives.html
|
||||||
|
- Introduction
|
||||||
|
- Directive hooks
|
||||||
|
- Hook arguments
|
||||||
|
- Function shorthand
|
||||||
|
- Object literals
|
||||||
|
- Usage on components
|
||||||
|
|
||||||
|
### Plugins
|
||||||
|
**URL:** https://vuejs.org/guide/reusability/plugins.html
|
||||||
|
- Introduction
|
||||||
|
- Writing a plugin
|
||||||
|
- Plugin options
|
||||||
|
- Provide / inject with plugins
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Built-in Components
|
||||||
|
|
||||||
|
### Transition
|
||||||
|
**URL:** https://vuejs.org/guide/built-ins/transition.html
|
||||||
|
- Basic usage
|
||||||
|
- CSS-based transitions
|
||||||
|
- TypeScript hooks
|
||||||
|
- Reusable transitions
|
||||||
|
- Appear on initial render
|
||||||
|
- Transition between elements
|
||||||
|
- Transition modes
|
||||||
|
|
||||||
|
### TransitionGroup
|
||||||
|
**URL:** https://vuejs.org/guide/built-ins/transition-group.html
|
||||||
|
- Basic usage
|
||||||
|
- Move transitions
|
||||||
|
- Staggering list transitions
|
||||||
|
|
||||||
|
### KeepAlive
|
||||||
|
**URL:** https://vuejs.org/guide/built-ins/keep-alive.html
|
||||||
|
- Basic usage
|
||||||
|
- Include / exclude
|
||||||
|
- Max cached instances
|
||||||
|
- Lifecycle of cached instance
|
||||||
|
|
||||||
|
### Teleport
|
||||||
|
**URL:** https://vuejs.org/guide/built-ins/teleport.html
|
||||||
|
- Basic usage
|
||||||
|
- Using with components
|
||||||
|
- Multiple teleports on same target
|
||||||
|
- Disabling teleport
|
||||||
|
|
||||||
|
### Suspense
|
||||||
|
**URL:** https://vuejs.org/guide/built-ins/suspense.html
|
||||||
|
- Async dependencies
|
||||||
|
- Loading state
|
||||||
|
- Error handling
|
||||||
|
- Combining with Transitions
|
||||||
|
- **Note:** Experimental feature
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Global API
|
||||||
|
**URL:** https://vuejs.org/api/application.html
|
||||||
|
- Application API
|
||||||
|
- General API
|
||||||
|
|
||||||
|
### Composition API - Setup
|
||||||
|
**URL:** https://vuejs.org/api/composition-api-setup.html
|
||||||
|
- `setup()` function
|
||||||
|
- `<script setup>` syntax
|
||||||
|
|
||||||
|
### Composition API - Reactivity Core
|
||||||
|
**URL:** https://vuejs.org/api/reactivity-core.html
|
||||||
|
- `ref()`, `computed()`, `reactive()`, `readonly()`
|
||||||
|
- `watchEffect()`, `watchPostEffect()`, `watchSyncEffect()`
|
||||||
|
- `watch()`
|
||||||
|
- `isRef()`, `unref()`, `toRef()`, `toRefs()`, `toValue()`
|
||||||
|
- `isProxy()`, `isReactive()`, `isReadonly()`
|
||||||
|
|
||||||
|
### Composition API - Reactivity Utilities
|
||||||
|
**URL:** https://vuejs.org/api/reactivity-utilities.html
|
||||||
|
- `isRef()`, `unref()`, `toRef()`, `toRefs()`, `toValue()`
|
||||||
|
- `isProxy()`, `isReactive()`, `isReadonly()`
|
||||||
|
- `shallowRef()`, `triggerRef()`, `customRef()`
|
||||||
|
- `shallowReactive()`, `shallowReadonly()`
|
||||||
|
|
||||||
|
### Composition API - Reactivity Advanced
|
||||||
|
**URL:** https://vuejs.org/api/reactivity-advanced.html
|
||||||
|
- `shallowRef()`, `triggerRef()`, `customRef()`
|
||||||
|
- `shallowReactive()`, `shallowReadonly()`
|
||||||
|
- `toRaw()`, `markRaw()`
|
||||||
|
- `effectScope()`, `getCurrentScope()`, `onScopeDispose()`
|
||||||
|
|
||||||
|
### Composition API - Lifecycle Hooks
|
||||||
|
**URL:** https://vuejs.org/api/composition-api-lifecycle.html
|
||||||
|
- `onMounted()`, `onUpdated()`, `onUnmounted()`
|
||||||
|
- `onBeforeMount()`, `onBeforeUpdate()`, `onBeforeUnmount()`
|
||||||
|
- `onErrorCaptured()`, `onRenderTracked()`, `onRenderTriggered()`
|
||||||
|
- `onActivated()`, `onDeactivated()`, `onServerPrefetch()`
|
||||||
|
|
||||||
|
### Composition API - Dependency Injection
|
||||||
|
**URL:** https://vuejs.org/api/composition-api-dependency-injection.html
|
||||||
|
- `provide()`, `inject()`
|
||||||
|
- `hasInjectionContext()`
|
||||||
|
|
||||||
|
### Composition API - Helpers
|
||||||
|
**URL:** https://vuejs.org/api/composition-api-helpers.html
|
||||||
|
- `useAttrs()`, `useSlots()`
|
||||||
|
- `useModel()`, `useTemplateRef()`
|
||||||
|
- `useId()`, `useCssModule()`
|
||||||
|
|
||||||
|
### Options API - State
|
||||||
|
**URL:** https://vuejs.org/api/options-state.html
|
||||||
|
- `data`, `props`, `computed`
|
||||||
|
- `methods`, `watch`, `emits`
|
||||||
|
- `expose`
|
||||||
|
|
||||||
|
### Options API - Rendering
|
||||||
|
**URL:** https://vuejs.org/api/options-rendering.html
|
||||||
|
- `template`, `render`
|
||||||
|
- `compilerOptions`
|
||||||
|
|
||||||
|
### Options API - Lifecycle
|
||||||
|
**URL:** https://vuejs.org/api/options-lifecycle.html
|
||||||
|
- `beforeCreate`, `created`
|
||||||
|
- `beforeMount`, `mounted`
|
||||||
|
- `beforeUpdate`, `updated`
|
||||||
|
- `beforeUnmount`, `unmounted`
|
||||||
|
- `errorCaptured`, `renderTracked`, `renderTriggered`
|
||||||
|
- `activated`, `deactivated`, `serverPrefetch`
|
||||||
|
|
||||||
|
### Options API - Composition
|
||||||
|
**URL:** https://vuejs.org/api/options-composition.html
|
||||||
|
- `provide`, `inject`
|
||||||
|
- `mixins`, `extends`
|
||||||
|
|
||||||
|
### Options API - Misc
|
||||||
|
**URL:** https://vuejs.org/api/options-misc.html
|
||||||
|
- `name`, `inheritAttrs`, `components`, `directives`
|
||||||
|
|
||||||
|
### Component Instance
|
||||||
|
**URL:** https://vuejs.org/api/component-instance.html
|
||||||
|
- `$data`, `$props`, `$el`, `$refs`
|
||||||
|
- `$parent`, `$root`, `$slots`, `$attrs`
|
||||||
|
- `$watch()`, `$emit()`, `$forceUpdate()`, `$nextTick()`
|
||||||
|
|
||||||
|
### Built-in Directives
|
||||||
|
**URL:** https://vuejs.org/api/built-in-directives.html
|
||||||
|
- `v-text`, `v-html`, `v-show`, `v-if`, `v-else`, `v-else-if`, `v-for`
|
||||||
|
- `v-on`, `v-bind`, `v-model`
|
||||||
|
- `v-slot`, `v-pre`, `v-once`, `v-memo`, `v-cloak`
|
||||||
|
|
||||||
|
### Built-in Components
|
||||||
|
**URL:** https://vuejs.org/api/built-in-components.html
|
||||||
|
- `<Transition>`, `<TransitionGroup>`
|
||||||
|
- `<KeepAlive>`, `<Teleport>`, `<Suspense>`
|
||||||
|
|
||||||
|
### Built-in Special Elements
|
||||||
|
**URL:** https://vuejs.org/api/built-in-special-elements.html
|
||||||
|
- `<component>`, `<slot>`
|
||||||
|
|
||||||
|
### Built-in Special Attributes
|
||||||
|
**URL:** https://vuejs.org/api/built-in-special-attributes.html
|
||||||
|
- `key`, `ref`, `is`
|
||||||
|
|
||||||
|
### Single-File Component Syntax
|
||||||
|
**URL:** https://vuejs.org/api/sfc-syntax.html
|
||||||
|
- Language blocks
|
||||||
|
- Automatic name inference
|
||||||
|
- Pre-processors
|
||||||
|
- Src imports
|
||||||
|
- Custom blocks
|
||||||
|
|
||||||
|
### SFC `<script setup>`
|
||||||
|
**URL:** https://vuejs.org/api/sfc-script-setup.html
|
||||||
|
- Basic syntax
|
||||||
|
- Top-level bindings
|
||||||
|
- Using components
|
||||||
|
- Using custom directives
|
||||||
|
- defineProps(), defineEmits()
|
||||||
|
- defineExpose(), defineOptions(), defineSlots(), defineModel()
|
||||||
|
- useSlots(), useAttrs()
|
||||||
|
- Normal `<script>` alongside `<script setup>`
|
||||||
|
|
||||||
|
### SFC CSS Features
|
||||||
|
**URL:** https://vuejs.org/api/sfc-css-features.html
|
||||||
|
- Scoped CSS
|
||||||
|
- CSS modules
|
||||||
|
- `v-bind()` in CSS
|
||||||
|
- `<style>` with `src` imports
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TypeScript Patterns
|
||||||
|
|
||||||
|
### TypeScript with Composition API
|
||||||
|
**URL:** https://vuejs.org/guide/extras/composition-api-faq.html
|
||||||
|
- Define props with runtime object syntax
|
||||||
|
- Define emits with array/object syntax
|
||||||
|
- Use JSDoc for complex shapes
|
||||||
|
- Keep composables in `use*.ts`
|
||||||
|
- Prefer clear runtime validation for public component APIs
|
||||||
|
|
||||||
|
### TypeScript with Options API
|
||||||
|
**URL:** https://vuejs.org/guide/essentials/component-basics.html
|
||||||
|
- Use `props` runtime validation
|
||||||
|
- Keep event names explicit in `emits`
|
||||||
|
- Avoid implicit global properties where possible
|
||||||
|
- Prefer composition API for new code
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
**URL:** https://vuejs.org/guide/scaling-up/tooling.html
|
||||||
|
- IDE support
|
||||||
|
- Linting and formatting integration
|
||||||
|
- Volar extension basics
|
||||||
|
- Build-tool and plugin usage notes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
**URL:** https://vuejs.org/guide/best-practices/production-deployment.html
|
||||||
|
- Production deployment
|
||||||
|
- Performance
|
||||||
|
- Accessibility
|
||||||
|
- Security
|
||||||
|
|
||||||
|
### Scaling Up
|
||||||
|
**URL:** https://vuejs.org/guide/scaling-up/sfc.html
|
||||||
|
- Single-File Components
|
||||||
|
- Tooling
|
||||||
|
- Routing
|
||||||
|
- State management
|
||||||
|
- Testing
|
||||||
|
- Server-Side Rendering (SSR)
|
||||||
|
|
||||||
|
### Style Guide
|
||||||
|
**URL:** https://vuejs.org/style-guide/
|
||||||
|
- Priority A: Essential (Error Prevention)
|
||||||
|
- Priority B: Strongly Recommended
|
||||||
|
- Priority C: Recommended
|
||||||
|
- Priority D: Use with Caution
|
||||||
|
|
||||||
|
### Vue Router
|
||||||
|
**URL:** https://router.vuejs.org/
|
||||||
|
- Official routing library
|
||||||
|
|
||||||
|
### Pinia
|
||||||
|
**URL:** https://pinia.vuejs.org/
|
||||||
|
- Official state management library
|
||||||
|
|
||||||
|
### Vite
|
||||||
|
**URL:** https://vitejs.dev/
|
||||||
|
- Recommended build tool
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Search Tips
|
||||||
|
|
||||||
|
When searching official docs:
|
||||||
|
1. Use the search bar at https://vuejs.org
|
||||||
|
2. For API details, go directly to https://vuejs.org/api/
|
||||||
|
3. For guides and concepts, start at https://vuejs.org/guide/
|
||||||
|
4. For examples, check https://vuejs.org/examples/
|
||||||
|
|
||||||
|
All documentation is available in multiple languages using the language selector.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: database-migration
|
||||||
|
version: 3.0.0
|
||||||
|
description: "使用 Hyperf Migrations 安全管理数据库 Schema 变更。当需要创建表、添加字段或修改索引时使用。确保迁移安全可回滚。"
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/skill-database-migration.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Hyperf Database Migration
|
||||||
|
|
||||||
|
## ⚠️ 安全等级:ORANGE — 执行前必须确认
|
||||||
|
|
||||||
|
## 迁移核心原则
|
||||||
|
|
||||||
|
1. **每次变更都是迁移** — 禁止手动 DDL
|
||||||
|
2. **Schema 与 Data 严格分离** — DDL 和 DML 分文件
|
||||||
|
3. **迁移部署后不可变** — 已执行迁移禁止修改
|
||||||
|
4. **生产前向原则** — 回滚用新前向迁移修正
|
||||||
|
5. **新字段安全** — 新增字段必须 nullable 或有默认值
|
||||||
|
6. **迁移前测试** — 大表先在副本验证
|
||||||
|
|
||||||
|
## 目录约定
|
||||||
|
|
||||||
|
```
|
||||||
|
Case-Database-Backend/
|
||||||
|
├── database/
|
||||||
|
│ ├── migrations/ ← 迁移文件(已通过 DI 工厂注册到 Migrator)
|
||||||
|
│ └── seeders/ ← 种子文件(已通过 DI 工厂注册到 Seed)
|
||||||
|
```
|
||||||
|
|
||||||
|
路径配置说明:
|
||||||
|
- Hyperf 默认迁移路径为 `migrations/`,本项目通过 `App\Database\MigratorFactory` 覆盖为 `database/migrations/`
|
||||||
|
- Seeder 路径通过 `App\Database\SeedFactory` 覆盖为 `database/seeders/`
|
||||||
|
- `gen:migration` / `gen:seeder` 路径在 `config/autoload/devtool.php` 中配置
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求修改数据库结构、添加/删除字段、创建/删除表、修改索引或关联。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 0. 加载规范
|
||||||
|
|
||||||
|
读取 `.cursor/rules/014-database.mdc`,提取表设计、索引规则、Migration 写法、高并发注意事项。
|
||||||
|
|
||||||
|
### 1. 理解变更需求
|
||||||
|
|
||||||
|
确认:变更什么、是否涉及数据迁移、是否可逆、是否有线上数据、数据量级、环境阶段。
|
||||||
|
|
||||||
|
### 2. 生成迁移
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/hyperf.php gen:migration create_{{table_name}}_table
|
||||||
|
# 文件自动生成到 database/migrations/ 目录
|
||||||
|
# 命名: create_orders_table | add_status_to_orders | remove_legacy_field_from_users
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 编写迁移
|
||||||
|
|
||||||
|
Schema::create / Schema::table,含 id、audit 字段、索引。down() 必须完整可逆。幂等、Expand-Contract、批量迁移见 **Tier 3**。
|
||||||
|
|
||||||
|
### 4. 高并发表设计检查
|
||||||
|
|
||||||
|
主键 BIGINT UNSIGNED、utf8mb4、金额 DECIMAL、状态 VARCHAR/TINYINT、外键索引、常用 WHERE 索引、复合索引最左前缀、单表索引 ≤6、JSON 仅非查询、避免过多 TEXT。
|
||||||
|
|
||||||
|
### 5. 执行与更新
|
||||||
|
|
||||||
|
`php bin/hyperf.php migrate`、`migrate:status`、`migrate:rollback`。`gen:model {{table_name}}`。更新 Model、Service、Repository、`docs/architecture/data-model.md`。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] `migrate` 无错误
|
||||||
|
2. [ ] `migrate:rollback` 可回滚(开发)
|
||||||
|
3. [ ] Model `$fillable` / `$casts` 正确
|
||||||
|
4. [ ] 外键有索引
|
||||||
|
5. [ ] 新字段 nullable 或有默认值
|
||||||
|
6. [ ] Schema 与 Data 迁移分文件
|
||||||
|
7. [ ] 幂等检查通过
|
||||||
|
8. [ ] SQL 人工审查
|
||||||
|
9. [ ] data-model.md 已更新
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `references/migration-patterns.md` | 幂等、Expand-Contract、批量迁移、大表策略、危险操作 |
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Database Migration — 迁移模式与示例
|
||||||
|
|
||||||
|
> 主流程见 SKILL.md,本文档为幂等迁移、Expand-Contract、批量迁移、大表策略的完整实现。
|
||||||
|
|
||||||
|
## 幂等迁移
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!Schema::hasTable('production_orders')) {
|
||||||
|
Schema::create('production_orders', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->string('order_no', 50)->unique();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Schema::hasColumn('production_orders', 'priority')) {
|
||||||
|
Schema::table('production_orders', function (Blueprint $table) {
|
||||||
|
$table->tinyInteger('priority')->default(0)->after('status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!$this->hasIndex('production_orders', 'idx_orders_priority')) {
|
||||||
|
Schema::table('production_orders', function (Blueprint $table) {
|
||||||
|
$table->index('priority', 'idx_orders_priority');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expand-Contract 三阶段示例(字段重命名 username → display_name)
|
||||||
|
|
||||||
|
Phase 1 EXPAND: 添加 display_name nullable。部署 v2 双写。
|
||||||
|
Phase 2 MIGRATE: 独立迁移文件批量 UPDATE 回填。部署 v3 读新写双。验证一致性。
|
||||||
|
Phase 3 CONTRACT: 删除 username。部署 v4 仅新字段。
|
||||||
|
|
||||||
|
时间线:Day 1 加字段+双写,Day 2 回填,Day 3 读新写双+验证,Day 7 删旧字段。
|
||||||
|
|
||||||
|
## 批量数据迁移模板
|
||||||
|
|
||||||
|
```php
|
||||||
|
$batchSize = 2000;
|
||||||
|
$lastId = 0;
|
||||||
|
while (true) {
|
||||||
|
$affected = Db::update("UPDATE users SET normalized_email = LOWER(email) WHERE id > ? AND normalized_email IS NULL ORDER BY id LIMIT ?", [$lastId, $batchSize]);
|
||||||
|
if ($affected === 0) break;
|
||||||
|
$lastId = Db::selectOne("SELECT MAX(id) AS max_id FROM users WHERE normalized_email IS NOT NULL AND id > ?", [$lastId])->max_id ?? $lastId + $batchSize;
|
||||||
|
usleep(100_000);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
规则:1000–5000 行/批、批次间 sleep、游标 WHERE id > ?、低峰期执行。
|
||||||
|
|
||||||
|
## 大表变更策略(百万级+)
|
||||||
|
|
||||||
|
| 操作 | 风险 | 方案 |
|
||||||
|
|------|------|------|
|
||||||
|
| ADD COLUMN | 低 | 直接执行,nullable 或有默认值 |
|
||||||
|
| ADD INDEX | 中 | ALGORITHM=INPLACE, LOCK=NONE |
|
||||||
|
| DROP COLUMN | 高 | 先移除代码→部署→下版本迁移删除 |
|
||||||
|
| ALTER TYPE | 高 | Expand-Contract |
|
||||||
|
| RENAME COLUMN | 高 | Expand-Contract |
|
||||||
|
| DROP TABLE | 极高 | 先重命名→观察一周→删除 |
|
||||||
|
|
||||||
|
## 危险操作清单
|
||||||
|
|
||||||
|
| 操作 | 缓解 |
|
||||||
|
|------|------|
|
||||||
|
| DROP TABLE | 先备份 |
|
||||||
|
| DROP COLUMN | 先移除代码引用 |
|
||||||
|
| ALTER TYPE | Expand-Contract |
|
||||||
|
| TRUNCATE | 禁止生产 |
|
||||||
|
| NOT NULL 无默认值 | 先 nullable→回填→再加约束 |
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 数据库回滚模式 (Hyperf Migration)
|
||||||
|
|
||||||
|
## 安全回滚策略
|
||||||
|
|
||||||
|
### 添加字段(可回滚)
|
||||||
|
```php
|
||||||
|
// Migration up()
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('avatar_url')->nullable()->after('email');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migration down()
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('avatar_url');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 删除字段(不可逆 — 需预备份)
|
||||||
|
```sql
|
||||||
|
-- 删除前先备份
|
||||||
|
CREATE TABLE _backup_users_phone AS
|
||||||
|
SELECT id, phone FROM users WHERE phone IS NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重命名字段(分步迁移)
|
||||||
|
```php
|
||||||
|
// Step 1: 添加新列
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('display_name')->nullable()->after('name');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: 迁移数据
|
||||||
|
DB::statement('UPDATE users SET display_name = name');
|
||||||
|
|
||||||
|
// Step 3: 删除旧列(下一个迁移文件)
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('name');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 类型变更(分步迁移)
|
||||||
|
```
|
||||||
|
Step 1: 添加新列 → Step 2: 迁移数据 → Step 3: 删除旧列
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回滚命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 回滚最近一次迁移
|
||||||
|
php bin/hyperf.php migrate:rollback
|
||||||
|
|
||||||
|
# 回滚最近 N 次迁移
|
||||||
|
php bin/hyperf.php migrate:rollback --step=3
|
||||||
|
|
||||||
|
# 查看迁移状态
|
||||||
|
php bin/hyperf.php migrate:status
|
||||||
|
```
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
---
|
||||||
|
name: debugging
|
||||||
|
version: 2.0.0
|
||||||
|
description: "七步法系统化调试工作流。当用户报告 Bug、错误、异常、截图指出界面问题或功能与预期不符时使用。含信号解析和结构化调试报告。"
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/skill-debugging.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Debugging Workflow
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户报告错误、异常、意外行为,或请求排查问题。包含但不限于:
|
||||||
|
|
||||||
|
| 来源 | 典型表现 | 示例 |
|
||||||
|
|---|---|---|
|
||||||
|
| 运行时错误 | 控制台报错、崩溃、白屏 | "点击按钮报 500" |
|
||||||
|
| 视觉/UI 异常 | 组件显示与预期不符 | "暗色模式下输入框是白色" |
|
||||||
|
| 截图指出问题 | 用户提供截图标注异常区域 | 附图 + "这里不对" |
|
||||||
|
| 功能失效 | 操作无响应或结果错误 | "筛选没有效果" |
|
||||||
|
| 样式回归 | 改动后样式错乱 | "更新后布局乱了" |
|
||||||
|
|
||||||
|
> ⚠️ **截图 = Bug 报告**:用户提供截图并指出问题,本质是视觉 Bug 报告,必须激活本技能。
|
||||||
|
|
||||||
|
## 七步法
|
||||||
|
|
||||||
|
### -1. 截图前置守卫 (Screenshot Guard)
|
||||||
|
|
||||||
|
收到用户消息时,**先检查输入形式**,再分析文字意图:
|
||||||
|
|
||||||
|
| 输入形式 | 问题关键词 | 判定 |
|
||||||
|
|---------|-----------|------|
|
||||||
|
| 包含截图/图片 | 不合理 / 不对 / 有问题 / 调整 / 修复 / 偏了 / 太大 / 太小 / 空白 / 溢出 / 错位 | → 视觉 Bug,激活完整调试流程 |
|
||||||
|
| 包含截图/图片 | 新增 / 添加 / 改为 / 参考这个 / 照着做 | → 需求变更,走正常开发流程 |
|
||||||
|
|
||||||
|
**硬规则**:无论判定结果是哪条路径,都**必须在回复开头输出守卫判定块**:
|
||||||
|
|
||||||
|
```
|
||||||
|
## 截图守卫判定
|
||||||
|
- 输入形式:包含截图 ✓
|
||||||
|
- 关键词扫描:「XXX」→ 命中 [视觉 Bug / 需求变更] 类
|
||||||
|
- 判定:[激活调试流程 / 走正常开发流程]
|
||||||
|
```
|
||||||
|
|
||||||
|
**视觉 Bug 路径规则**:
|
||||||
|
- 截图 + 问题描述 = 视觉 Bug,**不得降级为 L1 直接执行**,最低 L2
|
||||||
|
- 必须走完信号解析 → 修复 → 回归验证(含 chrome-devtools 截图对比)全流程
|
||||||
|
|
||||||
|
**需求变更路径规则**:
|
||||||
|
- 截图用于"参考设计"或"说明改动目标",走正常开发流程
|
||||||
|
- 复杂度按 001-workflow 标准判定(可为 L1)
|
||||||
|
|
||||||
|
### 0. 信号解析 (Parse Signals)
|
||||||
|
|
||||||
|
从用户描述中**主动提取**以下关键信号(缺失的主动追问):
|
||||||
|
|
||||||
|
| 信号 | 来源 | 重要性 |
|
||||||
|
|---|---|---|
|
||||||
|
| 错误消息 / 堆栈追踪 | 控制台、日志、浏览器 DevTools | 🔴 必需 |
|
||||||
|
| 复现步骤 | 用户操作序列 | 🔴 必需 |
|
||||||
|
| 截图标注的异常区域 | 用户截图 + 文字描述 | 🔴 必需(有截图时) |
|
||||||
|
| 影响区域 | 后端服务 / 前端组件 / API / DB | 🟠 重要 |
|
||||||
|
| 环境信息 | PHP 版本 / 浏览器 / OS | 🟡 有用 |
|
||||||
|
| 上次正常的时间/版本 | Git log / 部署记录 | 🟡 有用 |
|
||||||
|
| 偶发 vs 稳定 | 复现概率 | 🟡 有用 |
|
||||||
|
|
||||||
|
### 0.5 路由到调试策略 (Route to Debug Strategy)
|
||||||
|
|
||||||
|
根据影响区域选择最高效的调试工具:
|
||||||
|
|
||||||
|
| 影响区域 | 首选调试方式 | 测试工具 | 关键位置 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| PHP Service 逻辑 | 断点 + 日志 | PHPUnit | `Case-Database-Backend/test/Unit/` |
|
||||||
|
| API 端点 | Postman / curl + 日志 | Hyperf HttpClient | `Case-Database-Backend/test/Feature/` |
|
||||||
|
| 数据库查询 | SQL 日志 + `EXPLAIN` | PHPUnit + SQLite | `Case-Database-Backend/test/Unit/` |
|
||||||
|
| Vue 组件渲染 | Vue DevTools | Vitest + VTU | `frontend-*/src/**/*.test.ts` |
|
||||||
|
| Vue 状态管理 | Pinia DevTools | Vitest | `frontend-*/src/**/*.test.ts` |
|
||||||
|
| API 请求/响应 | 浏览器 Network 面板 | Mock + Vitest | `frontend-*/src/**/*.test.ts` |
|
||||||
|
| 样式/布局 | Chrome DevTools Elements | 视觉回归 | — |
|
||||||
|
| Swoole 协程 | `var_dump` + Hyperf 日志 | PHPUnit | `Case-Database-Backend/test/` |
|
||||||
|
| WebSocket | 浏览器 WS 面板 + 服务端日志 | Swoole Testing | `Case-Database-Backend/test/` |
|
||||||
|
|
||||||
|
### 1. 复现 (Reproduce)
|
||||||
|
|
||||||
|
- 获取准确的错误信息、堆栈追踪
|
||||||
|
- 确认可稳定复现的步骤
|
||||||
|
- 确认环境:浏览器/Node 版本、OS
|
||||||
|
- **关键**:如果无法稳定复现,记录复现概率和条件
|
||||||
|
|
||||||
|
### 2. 收集 (Collect)
|
||||||
|
|
||||||
|
- 读取相关源码文件
|
||||||
|
- 检查最近的 git 变更:`git log --oneline -10 -- <affected-path>`
|
||||||
|
- 查看相关日志
|
||||||
|
- 检查环境变量配置
|
||||||
|
- **新增**:查看该区域的已有测试,了解预期行为
|
||||||
|
|
||||||
|
### 3. 假设 (Hypothesize)
|
||||||
|
|
||||||
|
列出 2-3 个最可能的原因,按概率排序:
|
||||||
|
```
|
||||||
|
假设 A (70%): <描述> — 因为 <证据>
|
||||||
|
假设 B (20%): <描述> — 因为 <证据>
|
||||||
|
假设 C (10%): <描述> — 因为 <证据>
|
||||||
|
```
|
||||||
|
|
||||||
|
每个假设必须指向**具体文件和代码行**。
|
||||||
|
|
||||||
|
### 4. 验证 (Verify)
|
||||||
|
|
||||||
|
从最高概率假设开始验证。每次只验证一个假设:
|
||||||
|
- 添加 `console.log` / 断点 / Hyperf 日志
|
||||||
|
- 写最小复现测试用例(参考 `bug-reproduce` 技能)
|
||||||
|
- 检查相关数据状态
|
||||||
|
|
||||||
|
### 5. 修复 (Fix)
|
||||||
|
|
||||||
|
- 修复根因,不是症状
|
||||||
|
- 修改尽可能小
|
||||||
|
- 添加防御性代码防止复发
|
||||||
|
|
||||||
|
### 6. 回归 (Regress)
|
||||||
|
|
||||||
|
- 确认原始问题已修复
|
||||||
|
- 运行相关测试套件
|
||||||
|
- 检查是否引入新问题
|
||||||
|
- **新增**:编写回归测试防止复发
|
||||||
|
|
||||||
|
## 硬停止条件
|
||||||
|
|
||||||
|
遇到以下情况时**立即停止调试**,告知用户需要额外条件:
|
||||||
|
|
||||||
|
| 条件 | 原因 | 建议 |
|
||||||
|
|---|---|---|
|
||||||
|
| 需要真实第三方 API 凭证 | 无法在本地模拟 | 提供测试凭证或 Mock 服务 |
|
||||||
|
| 竞态条件/时序依赖 | 不可靠的复现 | 添加日志收集并发数据 |
|
||||||
|
| 需要生产数据 | 本地无法重建状态 | 导出脱敏数据子集 |
|
||||||
|
| 需要特定基础设施 | 本地无法模拟 | 在测试环境调试 |
|
||||||
|
| 超过 3 次假设全部否定 | 信息不足 | 收集更多日志和上下文 |
|
||||||
|
|
||||||
|
## 调试报告模板
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 🔧 调试报告
|
||||||
|
|
||||||
|
**问题**: [一句话描述]
|
||||||
|
**状态**: ✅ 已修复 | ⏳ 进行中 | ❌ 需要更多信息
|
||||||
|
|
||||||
|
### 信号
|
||||||
|
- 错误: [错误消息]
|
||||||
|
- 区域: [影响区域]
|
||||||
|
- 复现: [步骤]
|
||||||
|
|
||||||
|
### 根因
|
||||||
|
[1-2 句话解释 Bug 产生的机制]
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
| 文件 | 修改 |
|
||||||
|
|---|---|
|
||||||
|
| `path/to/file` | [修改说明] |
|
||||||
|
|
||||||
|
### 回归测试
|
||||||
|
- `path/to/test` — [测试说明]
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
- [ ] 原始问题已修复
|
||||||
|
- [ ] 回归测试通过
|
||||||
|
- [ ] 无新问题引入
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题速查
|
||||||
|
|
||||||
|
详见 `references/common-errors.md`。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 提取了所有关键信号(错误消息、复现步骤、影响区域)
|
||||||
|
2. [ ] 选择了正确的调试策略和工具
|
||||||
|
3. [ ] 原始问题已修复
|
||||||
|
4. [ ] 所有现有测试通过
|
||||||
|
5. [ ] 添加了防止复发的回归测试
|
||||||
|
6. [ ] 修复范围最小化
|
||||||
|
7. [ ] 输出了结构化调试报告
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 常见错误速查表
|
||||||
|
|
||||||
|
## Vue 3
|
||||||
|
|
||||||
|
| 错误 | 原因 | 修复 |
|
||||||
|
|------|------|------|
|
||||||
|
| `[Vue warn] Missing required prop` | 必填 prop 未传入 | 检查父组件传参或设置默认值 |
|
||||||
|
| `Maximum recursive updates` | 响应式数据在 watch 中循环修改 | 添加条件判断或用 `watchEffect` 替代 |
|
||||||
|
| `inject() can only be called inside setup()` | 依赖注入在非 setup 中调用 | 移入 `<script setup>` 或 `setup()` 函数 |
|
||||||
|
| `Extraneous non-props attributes` | 组件未声明 `inheritAttrs: false` | 添加 `defineOptions({ inheritAttrs: false })` |
|
||||||
|
| `Component is missing template` | 组件缺少 template 或 render | 检查 .vue 文件是否有 `<template>` |
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
|
||||||
|
| 错误 | 原因 | 修复 |
|
||||||
|
|------|------|------|
|
||||||
|
| `Cannot read properties of undefined` | 访问未定义对象属性 | 增加空值判断或使用可选链 `?.` |
|
||||||
|
| `Unexpected token 'export'` | 运行环境与模块格式不匹配 | 检查 ESM/CJS 配置与运行命令 |
|
||||||
|
| `x is not a function` | 导入或变量类型错误 | 检查导出方式与调用处参数 |
|
||||||
|
|
||||||
|
## PHP Hyperf
|
||||||
|
|
||||||
|
| 错误 | 原因 | 修复 |
|
||||||
|
|------|------|------|
|
||||||
|
| `Class not found` | DI 容器未注册或命名空间错误 | 检查 namespace、运行 `composer dump-autoload` |
|
||||||
|
| `Connection pool exhausted` | 连接池已满,协程等待超时 | 增大 `max_connections` 或减少连接占用时间 |
|
||||||
|
| `Coroutine context destroyed` | 在协程外访问协程上下文 | 使用 `Context::get()` 前确保在协程环境 |
|
||||||
|
| `Table not found` | 迁移未执行 | 运行 `php bin/hyperf.php migrate` |
|
||||||
|
| `Allowed memory exhausted` | 内存泄漏或大数据未分块 | 使用 `chunk()` 处理大数据集 |
|
||||||
|
|
||||||
|
## MySQL
|
||||||
|
|
||||||
|
| 错误 | 原因 | 修复 |
|
||||||
|
|------|------|------|
|
||||||
|
| Duplicate entry (1062) | 插入重复唯一键数据 | 用 `INSERT ... ON DUPLICATE KEY UPDATE` 或先查询 |
|
||||||
|
| Lock wait timeout (1205) | 事务死锁或长事务 | 缩短事务范围,添加合适索引 |
|
||||||
|
| Too many connections (1040) | 连接数超限 | 检查连接池配置,增大 `max_connections` |
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
name: documentation
|
||||||
|
version: 1.0.0
|
||||||
|
description: "生成并更新项目文档。当需要撰写技术文档、README、API 文档或架构决策记录时使用。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Documentation Generator
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求创建、更新或改进项目文档。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 确定文档类型
|
||||||
|
|
||||||
|
| 类型 | 输出位置 | 模板来源 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| PRD | `docs/vision/PRD.md` | 已有模板,直接填写 |
|
||||||
|
| README | `README.md` | 项目概述 + 安装 + 使用 |
|
||||||
|
| API 文档 | `docs/architecture/api-contracts.md` | 端点 + 请求/响应 |
|
||||||
|
| 架构文档 | `docs/architecture/system-design.md` | 已有模板,直接填写 |
|
||||||
|
| 数据模型 | `docs/architecture/data-model.md` | 已有模板,直接填写 |
|
||||||
|
| ADR | `docs/architecture/decisions/NNN-*.md` | 复制 `_template.md` 后填写 |
|
||||||
|
| 组件文档 | JSDoc / Storybook | Props + 示例 |
|
||||||
|
| 变更日志 | `CHANGELOG.md` | Conventional Changelog |
|
||||||
|
|
||||||
|
> **模板路径**: 创建 ADR 时,先读取 `docs/architecture/decisions/_template.md` 作为基础。
|
||||||
|
> PRD、架构文档、数据模型已在 `docs/` 下有完整模板,优先填充而非重新创建。
|
||||||
|
|
||||||
|
### 2. 收集上下文
|
||||||
|
|
||||||
|
1. 读取相关源码理解功能
|
||||||
|
2. 读取已有文档确认风格
|
||||||
|
3. 读取 `package.json` 获取项目信息
|
||||||
|
4. 读取 `docs/guides/style-guide.md` 确认文档规范
|
||||||
|
|
||||||
|
### 3. 生成文档
|
||||||
|
|
||||||
|
遵循以下写作原则:
|
||||||
|
- **简明**:一句话说清楚,不要冗余描述
|
||||||
|
- **可执行**:代码示例必须可运行
|
||||||
|
- **可维护**:避免写死版本号和路径
|
||||||
|
- **受众明确**:新人 onboarding vs 日常参考 vs 架构决策
|
||||||
|
|
||||||
|
### 4. ADR 创建流程
|
||||||
|
|
||||||
|
1. 复制模板:`docs/architecture/decisions/_template.md` → `docs/architecture/decisions/NNN-title.md`
|
||||||
|
2. 替换 `XXX` 为递增编号(检查已有 ADR 文件确定下一个编号)
|
||||||
|
3. 填写所有章节:状态、上下文(含问题陈述和约束)、决策、备选方案、理由、影响
|
||||||
|
4. 标注记录人和审核人
|
||||||
|
|
||||||
|
### 5. JSDoc 规范
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 一句话描述。
|
||||||
|
*
|
||||||
|
* @param paramName - 参数说明
|
||||||
|
* @returns 返回值说明
|
||||||
|
* @throws {ErrorType} 何时抛出
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const result = functionName('input');
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 文档无拼写错误
|
||||||
|
2. [ ] 代码示例可运行
|
||||||
|
3. [ ] 链接有效
|
||||||
|
4. [ ] 遵循项目文档风格
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# 文档写作指南
|
||||||
|
|
||||||
|
## 写作原则
|
||||||
|
|
||||||
|
1. **一句话规则**:每个概念用一句话解释清楚
|
||||||
|
2. **Show, Don't Tell**:用代码示例代替文字描述
|
||||||
|
3. **链接优于重复**:引用已有文档而非复制粘贴
|
||||||
|
4. **面向受众**:明确文档读者是谁
|
||||||
|
|
||||||
|
## Markdown 规范
|
||||||
|
|
||||||
|
- 标题层级不超过 4 级(`#` 到 `####`)
|
||||||
|
- 代码块指定语言(```typescript / ```php / ```bash 等,而非 ```)
|
||||||
|
- 表格对齐(使用 Prettier)
|
||||||
|
- 链接使用相对路径
|
||||||
|
|
||||||
|
## README 结构
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# 项目名
|
||||||
|
|
||||||
|
一句话描述。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
## 功能特性
|
||||||
|
## 安装
|
||||||
|
## 使用
|
||||||
|
## 配置
|
||||||
|
## 贡献指南
|
||||||
|
## 许可证
|
||||||
|
```
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
---
|
||||||
|
name: env-setup
|
||||||
|
version: 2.0.0
|
||||||
|
description: "初始化 PHP Hyperf + Vue 3 双栈开发环境。当需要项目初始化、环境配置或安装依赖时使用。含 Docker Compose 和数据库初始化。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔧 Environment Setup (PHP Hyperf + Vue 3 Dual Stack)
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求初始化项目环境、配置开发工具链、设置环境变量。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 检测系统依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "=== System Dependencies ==="
|
||||||
|
|
||||||
|
# PHP
|
||||||
|
php -v 2>/dev/null && echo "✅ PHP installed" || echo "❌ PHP not found (need >= 8.1)"
|
||||||
|
|
||||||
|
# Swoole
|
||||||
|
php -m 2>/dev/null | grep -i swoole && echo "✅ Swoole installed" || echo "❌ Swoole not found (need >= 5.0)"
|
||||||
|
|
||||||
|
# Composer
|
||||||
|
composer --version 2>/dev/null && echo "✅ Composer installed" || echo "❌ Composer not found"
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
node -v 2>/dev/null && echo "✅ Node.js installed" || echo "❌ Node.js not found (need >= 20)"
|
||||||
|
|
||||||
|
# npm
|
||||||
|
npm -v 2>/dev/null && echo "✅ npm installed" || echo "❌ npm not found"
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker --version 2>/dev/null && echo "✅ Docker installed" || echo "⚠️ Docker not found (optional for local dev)"
|
||||||
|
|
||||||
|
# Docker Compose
|
||||||
|
docker compose version 2>/dev/null && echo "✅ Docker Compose installed" || echo "⚠️ Docker Compose not found"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 后端初始化 (PHP Hyperf)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Case-Database-Backend
|
||||||
|
|
||||||
|
# 安装 PHP 依赖
|
||||||
|
composer install
|
||||||
|
|
||||||
|
# 复制环境变量
|
||||||
|
if [ ! -f ".env" ] && [ -f ".env.example" ]; then
|
||||||
|
cp .env.example .env
|
||||||
|
echo "✅ .env created from .env.example"
|
||||||
|
echo "⚠️ Please fill in required values: DB_HOST, DB_DATABASE, REDIS_HOST, JWT_SECRET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 验证 Hyperf 启动
|
||||||
|
php bin/hyperf.php start --dry-run 2>/dev/null || php bin/hyperf.php di:init-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 前端初始化 (Vue 3 + Vite)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for dir in Case-Database-Frontend-user Case-Database-Frontend-admin; do
|
||||||
|
echo "=== $dir ==="
|
||||||
|
cd $dir
|
||||||
|
|
||||||
|
# 安装 Node 依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 复制环境变量
|
||||||
|
if [ ! -f ".env.local" ] && [ -f ".env.example" ]; then
|
||||||
|
cp .env.example .env.local
|
||||||
|
echo "✅ .env.local created from .env.example"
|
||||||
|
fi
|
||||||
|
cd ..
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 数据库初始化
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Case-Database-Backend
|
||||||
|
|
||||||
|
# 运行迁移
|
||||||
|
php bin/hyperf.php migrate
|
||||||
|
|
||||||
|
# 运行种子(如有)
|
||||||
|
php bin/hyperf.php db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Docker Compose 启动(可选)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 一键启动全部服务
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 验证服务状态
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 初始化工具链
|
||||||
|
|
||||||
|
| 工具 | 配置文件 | 前端/后端 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| jsconfig | `jsconfig.json` | 前端 |
|
||||||
|
| ESLint | `.eslintrc.*` / `eslint.config.*` | 前端 |
|
||||||
|
| Prettier | `.prettierrc` | 前端 |
|
||||||
|
| Husky | `.husky/` | 全栈 |
|
||||||
|
| PHPStan | `phpstan.neon` | 后端 |
|
||||||
|
| PHP CS Fixer | `.php-cs-fixer.php` | 后端 |
|
||||||
|
|
||||||
|
### 7. 验证环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "=== Backend Verification ==="
|
||||||
|
cd Case-Database-Backend
|
||||||
|
php -v
|
||||||
|
composer --version
|
||||||
|
php bin/hyperf.php --version 2>/dev/null || echo "Run: php bin/hyperf.php start"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Frontend Verification ==="
|
||||||
|
for dir in Case-Database-Frontend-user Case-Database-Frontend-admin; do
|
||||||
|
echo "=== $dir Verification ==="
|
||||||
|
cd $dir
|
||||||
|
node -v
|
||||||
|
npm -v
|
||||||
|
npm run build --dry-run 2>/dev/null || echo "Run: npm run dev"
|
||||||
|
cd ..
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Services ==="
|
||||||
|
docker compose ps 2>/dev/null || echo "Docker Compose not running"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键环境变量
|
||||||
|
|
||||||
|
### 后端 (.env)
|
||||||
|
|
||||||
|
| 变量 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `APP_NAME` | 应用名称 | `MyApp` |
|
||||||
|
| `APP_ENV` | 环境 | `dev` / `production` |
|
||||||
|
| `DB_HOST` | MySQL 主机 | `127.0.0.1` |
|
||||||
|
| `DB_PORT` | MySQL 端口 | `3306` |
|
||||||
|
| `DB_DATABASE` | 数据库名 | `myapp` |
|
||||||
|
| `DB_USERNAME` | 数据库用户 | `root` |
|
||||||
|
| `DB_PASSWORD` | 数据库密码 | *(Secret)* |
|
||||||
|
| `REDIS_HOST` | Redis 主机 | `127.0.0.1` |
|
||||||
|
| `REDIS_PORT` | Redis 端口 | `6379` |
|
||||||
|
| `JWT_SECRET` | JWT 密钥 | *(auto-generate)* |
|
||||||
|
|
||||||
|
### 前端 (.env.local)
|
||||||
|
|
||||||
|
| 变量 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `VITE_API_BASE_URL` | API 地址 | `http://localhost:9501` |
|
||||||
|
| `VITE_WS_URL` | WebSocket 地址 | `ws://localhost:9502` |
|
||||||
|
| `VITE_APP_TITLE` | 应用标题 | `MyApp` |
|
||||||
|
|
||||||
|
## 密钥管理最佳实践
|
||||||
|
|
||||||
|
### 原则
|
||||||
|
|
||||||
|
- **永不硬编码** — 密钥不进入代码仓库
|
||||||
|
- **运行时注入** — 密钥在部署/启动时注入,不写入磁盘文件
|
||||||
|
- **最小权限** — 每个服务只拥有必需的密钥访问权限
|
||||||
|
- **定期轮换** — 密钥和 Token 设置过期时间并定期轮换
|
||||||
|
|
||||||
|
### 推荐方案(按场景选择)
|
||||||
|
|
||||||
|
| 场景 | 方案 | 复杂度 |
|
||||||
|
|---|---|---|
|
||||||
|
| 本地开发 | `.env` 文件 (已在 .gitignore) | 🟢 低 |
|
||||||
|
| CI/CD | GitHub Secrets / GitLab Variables | 🟡 中 |
|
||||||
|
| 生产环境 | HashiCorp Vault / 云 KMS | 🔴 高 |
|
||||||
|
| Docker 部署 | Docker Secrets / Compose env_file | 🟡 中 |
|
||||||
|
|
||||||
|
### 本地开发密钥管理
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ✅ .env 文件必须在 .gitignore 中
|
||||||
|
echo ".env" >> .gitignore
|
||||||
|
echo ".env.local" >> .gitignore
|
||||||
|
|
||||||
|
# ✅ 提供 .env.example 作为模板(无真实密钥)
|
||||||
|
cp .env .env.example
|
||||||
|
# 手动清空 .env.example 中的敏感值
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 部署密钥注入
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ❌ BAD: docker-compose.yml 中硬编码密钥
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
environment:
|
||||||
|
DB_PASSWORD: "my_secret_password"
|
||||||
|
|
||||||
|
# ✅ GOOD: 通过 .env 文件注入
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JWT 密钥生成
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成强随机 JWT 密钥
|
||||||
|
openssl rand -base64 64
|
||||||
|
|
||||||
|
# 或使用 PHP
|
||||||
|
php -r "echo bin2hex(random_bytes(32));"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查密钥安全
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查是否有密钥意外提交到仓库
|
||||||
|
rg -rn "(?i)(api.?key|secret|password|token)\s*[=:]\s*['\"][a-zA-Z0-9]{8,}" \
|
||||||
|
--glob '!vendor/**' --glob '!node_modules/**' --glob '!*.lock' --glob '!.env*'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题排查
|
||||||
|
|
||||||
|
| 问题 | 解决方案 |
|
||||||
|
|------|---------|
|
||||||
|
| Swoole 未安装 | `pecl install swoole` 或使用 Docker |
|
||||||
|
| Composer 依赖失败 | `composer clear-cache && composer install` |
|
||||||
|
| Node 版本不匹配 | 使用 `nvm use 20` 切换版本 |
|
||||||
|
| MySQL 连接失败 | 检查 `.env` 中 DB_HOST 和端口,确认 MySQL 正在运行 |
|
||||||
|
| Redis 连接失败 | 检查 `.env` 中 REDIS_HOST,确认 Redis 正在运行 |
|
||||||
|
| Hyperf 启动失败 | 检查 `runtime/` 目录权限,运行 `php bin/hyperf.php di:init-proxy` |
|
||||||
|
| 端口冲突 9501 | `lsof -i :9501` 查看占用进程 |
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
- [ ] PHP >= 8.1 且 Swoole >= 5.0 扩展已安装
|
||||||
|
- [ ] Composer 依赖安装成功
|
||||||
|
- [ ] Node.js >= 20 且 npm 安装成功
|
||||||
|
- [ ] `php bin/hyperf.php start` 可正常启动 (HTTP 9501)
|
||||||
|
- [ ] `npm run dev` 可正常启动 (Vite dev server)
|
||||||
|
- [ ] MySQL 和 Redis 连接正常
|
||||||
|
- [ ] 数据库迁移成功
|
||||||
|
- [ ] 所有环境变量已配置
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
name: full-feature
|
||||||
|
version: 2.1.0
|
||||||
|
description: "Vue 3 + Hyperf 端到端功能开发工作流。当需要从数据库到 API 到 UI 全链路开发完整功能时使用。编排多个技能协同工作。"
|
||||||
|
requires: [component-scaffold, vue-testing]
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/skill-full-feature.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Full Feature Workflow (Vue 3 + Hyperf)
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求开发一个完整功能(前端 + 后端 + 数据库 + 测试)。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### Phase 1: 规划
|
||||||
|
|
||||||
|
1. **拆分功能为子任务**:
|
||||||
|
- 数据层:需要什么数据模型和数据库变更?
|
||||||
|
- 后端 API:需要哪些 Controller/Service/Repository?
|
||||||
|
- 前端 UI:需要哪些组件和页面?
|
||||||
|
- 测试:每层需要什么测试?
|
||||||
|
|
||||||
|
2. **确认技术决策**(向用户确认有争议的选择)
|
||||||
|
|
||||||
|
3. **输出执行计划**:
|
||||||
|
|
||||||
|
```
|
||||||
|
## 功能: <功能名称>
|
||||||
|
|
||||||
|
### 数据层
|
||||||
|
- [ ] Hyperf Migration — 创建/修改表
|
||||||
|
- [ ] Model 定义 — 关联、Casts、Fillable
|
||||||
|
|
||||||
|
### 后端 API 层
|
||||||
|
- [ ] POST /admin/<resources> — 创建
|
||||||
|
- [ ] GET /admin/<resources> — 列表
|
||||||
|
- [ ] GET /admin/<resources>/:id — 详情
|
||||||
|
- [ ] PUT /admin/<resources>/:id — 更新
|
||||||
|
- [ ] DELETE /admin/<resources>/:id — 删除
|
||||||
|
- [ ] Service 业务逻辑
|
||||||
|
- [ ] FormRequest 参数验证
|
||||||
|
- [ ] 路由注册 + 中间件
|
||||||
|
|
||||||
|
### 前端 UI 层
|
||||||
|
- [ ] API 接口封装 (src/api/<module>)
|
||||||
|
- [ ] 列表页 (src/views/<module>/<resource>/index.vue)
|
||||||
|
- [ ] 表单组件 (src/components/custom/<Resource>Form.vue)
|
||||||
|
- [ ] 详情页 (可选)
|
||||||
|
- [ ] 路由配置 (src/router/routes/<module>.ts)
|
||||||
|
- [ ] Store 模块 (如需跨页面状态)
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
- [ ] PHPUnit — Service 层测试
|
||||||
|
- [ ] Vitest — 组件渲染测试
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: 数据层(使用 database-migration 技能)
|
||||||
|
|
||||||
|
1. 生成迁移: `php bin/hyperf.php gen:migration create_<table>_table`
|
||||||
|
2. 编写迁移(遵循高并发表设计规范)
|
||||||
|
3. 执行迁移: `php bin/hyperf.php migrate`
|
||||||
|
4. 生成 Model: `php bin/hyperf.php gen:model <table>`
|
||||||
|
5. 补充 Model 关联和类型转换
|
||||||
|
|
||||||
|
### Phase 3: 后端 API 层(使用 api-scaffold + hyperf-service 技能)
|
||||||
|
|
||||||
|
1. 创建 Controller(接收请求、调用 Service)
|
||||||
|
2. 创建 Service(核心业务逻辑、事务管理)
|
||||||
|
3. 创建 Repository(可选,复杂查询时使用)
|
||||||
|
4. 创建 FormRequest(输入验证)
|
||||||
|
5. 注册路由 + 挂载中间件
|
||||||
|
6. 编写 PHPUnit 测试
|
||||||
|
|
||||||
|
### Phase 4: 前端 UI 层(使用 component-scaffold + vue-page 技能)
|
||||||
|
|
||||||
|
1. 封装 API 接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/api/<module>/<resource>.ts
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export const {{resource}}Api = {
|
||||||
|
list: (params) => request.get('/admin/{{resources}}', { params }),
|
||||||
|
getById: (id) => request.get(`/admin/{{resources}}/${id}`),
|
||||||
|
create: (data) => request.post('/admin/{{resources}}', data),
|
||||||
|
update: (id, data) => request.put(`/admin/{{resources}}/${id}`, data),
|
||||||
|
delete: (id) => request.delete(`/admin/{{resources}}/${id}`),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 创建列表页(管理端:Element Plus 表格 + 分页;用户端:Tailwind 卡片/表格 + 自定义分页,禁止 Element Plus)
|
||||||
|
3. 创建表单组件(管理端:Element Plus Form + 验证;用户端:Headless UI + Tailwind 表单,禁止 Element Plus)
|
||||||
|
4. 创建详情页(如需要)
|
||||||
|
5. 配置 Vue Router 路由
|
||||||
|
6. 连接 API 到 Pinia Store(如需跨页面状态)
|
||||||
|
|
||||||
|
### Phase 5: 集成验证
|
||||||
|
|
||||||
|
1. 后端测试: `composer test`
|
||||||
|
2. 后端静态分析: `composer analyse`
|
||||||
|
3. 前端 Lint 检查: `npm run lint`
|
||||||
|
5. 手动测试功能流程(CRUD 全路径)
|
||||||
|
|
||||||
|
### Phase 6: 收尾
|
||||||
|
|
||||||
|
1. 更新相关文档(data-model.md、api-contracts.md)
|
||||||
|
2. Git commit(遵循 Conventional Commits)
|
||||||
|
3. 汇报完成状态
|
||||||
|
|
||||||
|
## 执行原则
|
||||||
|
|
||||||
|
- **自底向上**:先数据层,再后端 API,再前端 UI
|
||||||
|
- **每步验证**:每个 Phase 完成后运行测试
|
||||||
|
- **可中断**:每个 Phase 独立可提交
|
||||||
|
- **用已有技能**:尽量调用其他 Skill 而非从零写
|
||||||
|
- **前后端分离**:API 契约先行,前后端可并行开发
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 后端测试全部通过
|
||||||
|
2. [ ] 前端 ESLint 无报错
|
||||||
|
3. [ ] 功能端到端可用(创建、列表、详情、编辑、删除)
|
||||||
|
4. [ ] 代码遵循项目现有模式
|
||||||
|
5. [ ] 文档已更新
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
name: hyperf-service
|
||||||
|
version: 2.0.0
|
||||||
|
description: "生成 Hyperf 后端服务模块脚手架(Controller/Service/Repository/Model)。当需要新建后端业务模块时使用。支持 DI 注入和事务管理。"
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/skill-backend-scaffold.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Hyperf Service Module Scaffold
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求创建后端服务模块、业务 Service 类、或完整的后端业务层。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 0. 加载规范(⚠️ 必须最先执行)
|
||||||
|
|
||||||
|
依次读取 `.cursor/rules/013-backend.mdc`、`016-swoole.mdc`、`019-modular.mdc`,提取 DI、事务、协程安全、依赖方向、事件解耦。
|
||||||
|
|
||||||
|
### 1. 确认模块规格
|
||||||
|
|
||||||
|
| 字段 | 必填 | 默认值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| 模块名称 | ✅ | — |
|
||||||
|
| 所属业务域 | ❌ | 推断 |
|
||||||
|
| 需要 Repository? | ❌ | 复杂查询时 true |
|
||||||
|
| 需要事件? | ❌ | false |
|
||||||
|
| 需要数据权限? | ❌ | true |
|
||||||
|
| 需要缓存? | ❌ | false |
|
||||||
|
|
||||||
|
### 2. 生成文件结构
|
||||||
|
|
||||||
|
Controller/Admin、Service、Repository、Model、Request、Event、Listener(可选)。
|
||||||
|
|
||||||
|
### 3. Service / Repository
|
||||||
|
|
||||||
|
Service 使用 `Db::transaction` 包裹写操作,Repository 实现 `applyFilters`、`applyDataScope`、分页上限。完整模板见 **Tier 3**。
|
||||||
|
|
||||||
|
### 4. 结构化日志
|
||||||
|
|
||||||
|
Service 集成 HasLogger Trait,create/update/delete 至少 info 级别。RequestId 中间件全局注入 request_id。
|
||||||
|
|
||||||
|
### 5. 重试机制
|
||||||
|
|
||||||
|
外部 API 调用使用 RetryHelper 指数退避重试,retryOn 指定可重试异常类型。
|
||||||
|
|
||||||
|
### 6. 遵循项目约定
|
||||||
|
|
||||||
|
扫描 `app/Service/`、AbstractService、BusinessException、数据权限、事件注册、HasLogger、RetryHelper。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] `php -l` 无错误
|
||||||
|
2. [ ] 依赖注入正确
|
||||||
|
3. [ ] 写操作使用 `Db::transaction()`
|
||||||
|
4. [ ] 分页有 page_size 上限
|
||||||
|
5. [ ] 异常使用 BusinessException
|
||||||
|
6. [ ] applyDataScope 已实现(如需)
|
||||||
|
7. [ ] 关键操作有结构化日志
|
||||||
|
8. [ ] 外部 API 用 RetryHelper
|
||||||
|
9. [ ] Logger channel 使用类名
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `references/service-templates.md` | Service/Repository/Event/Logger/Retry 完整代码 |
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# Hyperf Service — 代码模板
|
||||||
|
|
||||||
|
> 主流程见 SKILL.md,本文档为 Service/Repository/Event/Logger/Retry 完整实现。
|
||||||
|
|
||||||
|
## Service 模板
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace App\Service\{{Domain}};
|
||||||
|
|
||||||
|
use App\Model\{{Domain}}\{{Module}};
|
||||||
|
use App\Repository\{{Domain}}\{{Module}}Repository;
|
||||||
|
use App\Exception\BusinessException;
|
||||||
|
use Hyperf\DbConnection\Db;
|
||||||
|
use Hyperf\Di\Annotation\Inject;
|
||||||
|
|
||||||
|
class {{Module}}Service
|
||||||
|
{
|
||||||
|
#[Inject]
|
||||||
|
protected {{Module}}Repository $repository;
|
||||||
|
|
||||||
|
public function getPageList(array $params): array { return $this->repository->getPageList($params); }
|
||||||
|
|
||||||
|
public function getById(int $id): {{Module}} {
|
||||||
|
$record = {{Module}}::find($id);
|
||||||
|
if (!$record) throw new BusinessException(404, '{{Module}} not found');
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(array $data): {{Module}} {
|
||||||
|
return Db::transaction(function () use ($data) {
|
||||||
|
$record = {{Module}}::create($data);
|
||||||
|
return $record;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(int $id, array $data): {{Module}} {
|
||||||
|
$record = $this->getById($id);
|
||||||
|
return Db::transaction(function () use ($record, $data) {
|
||||||
|
$record->update($data);
|
||||||
|
return $record->refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(int $id): void {
|
||||||
|
$record = $this->getById($id);
|
||||||
|
Db::transaction(fn () => $record->delete());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository 模板(含 applyFilters / applyDataScope)
|
||||||
|
|
||||||
|
```php
|
||||||
|
class {{Module}}Repository
|
||||||
|
{
|
||||||
|
public function getPageList(array $params): array {
|
||||||
|
$query = {{Module}}::query();
|
||||||
|
$this->applyFilters($query, $params);
|
||||||
|
$this->applyDataScope($query);
|
||||||
|
$page = (int) ($params['page'] ?? 1);
|
||||||
|
$pageSize = min((int) ($params['page_size'] ?? 10), 100);
|
||||||
|
$total = $query->count();
|
||||||
|
$items = $query->with($this->getDefaultRelations())->orderByDesc('id')
|
||||||
|
->offset(($page - 1) * $pageSize)->limit($pageSize)->get();
|
||||||
|
return ['items' => $items, 'total' => $total];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function applyFilters(Builder $query, array $params): void {
|
||||||
|
if (!empty($params['keyword'])) $query->where('name', 'like', "%{$params['keyword']}%");
|
||||||
|
if (!empty($params['status'])) $query->where('status', $params['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function applyDataScope(Builder $query): void { /* 数据权限 */ }
|
||||||
|
protected function getDefaultRelations(): array { return []; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event + Listener 模板
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Event
|
||||||
|
class {{Module}}Created { public function __construct(public readonly {{Module}} $record) {} }
|
||||||
|
|
||||||
|
// Listener
|
||||||
|
#[Listener]
|
||||||
|
class {{Module}}CreatedListener implements ListenerInterface
|
||||||
|
{
|
||||||
|
public function listen(): array { return [{{Module}}Created::class]; }
|
||||||
|
public function process(object $event): void { /* 发送通知、更新缓存等 */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## HasLogger Trait
|
||||||
|
|
||||||
|
```php
|
||||||
|
trait HasLogger {
|
||||||
|
protected ?LoggerInterface $logger = null;
|
||||||
|
protected function logger(): LoggerInterface { /* 按类名获取 channel */ }
|
||||||
|
protected function logContext(array $extra = []): array { return array_merge(['request_id' => Context::get('request_id'), 'user_id' => Context::get('current_user_id')], $extra); }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## RetryHelper
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function withRetry(callable $fn, int $maxRetries = 3, int $baseDelayMs = 1000, array $retryOn = []): mixed {
|
||||||
|
for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
|
||||||
|
try { return $fn(); }
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
if ($attempt < $maxRetries) Coroutine::sleep(($baseDelayMs * (2 ** $attempt) + jitter) / 1000);
|
||||||
|
else throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## RequestIdMiddleware
|
||||||
|
|
||||||
|
在 process 中:`Context::set('request_id', $requestId)`,响应头加 `X-Request-ID`。
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
---
|
||||||
|
name: i18n
|
||||||
|
version: 3.0.0
|
||||||
|
description: "使用 vue-i18n 管理 Vue 3 应用的国际化。当需要添加多语言支持或管理翻译时使用。含语言包管理和路由国际化。"
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/skill-i18n.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Internationalization (i18n) — Vue 3 + vue-i18n
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求添加多语言支持、翻译内容、配置国际化路由。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 检测现有 i18n 方案
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查是否已有 i18n 库
|
||||||
|
grep -E "vue-i18n|@intlify" package.json 2>/dev/null
|
||||||
|
ls src/i18n* src/locales* 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 初始化 i18n(如未配置)
|
||||||
|
|
||||||
|
推荐方案:`vue-i18n`(Vue 3 官方国际化方案)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install vue-i18n@9
|
||||||
|
```
|
||||||
|
|
||||||
|
创建目录结构:
|
||||||
|
```
|
||||||
|
src/locales/
|
||||||
|
├── index.ts # i18n 实例配置
|
||||||
|
├── zh-CN.ts # 中文(简体)— 使用 JS 扁平对象格式
|
||||||
|
├── en.ts # 英文 — 使用 JS 扁平对象格式
|
||||||
|
└── modules/ # 按模块拆分(大型项目,同样使用 JS 格式)
|
||||||
|
├── common.zh-CN.ts
|
||||||
|
└── common.en.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
> **格式选择**:统一使用 `.ts` 文件(`export default { 'key': 'value' }`),
|
||||||
|
> 而非 `.json`。原因:JS 格式支持注释、支持函数(复数规则)、支持扁平键名中的特殊字符,
|
||||||
|
> 且与 vue-i18n v9 的 Composition API 配合更灵活。
|
||||||
|
> 若项目已使用 `.json`,保持现有格式,不强制迁移。
|
||||||
|
|
||||||
|
### 3. i18n 配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/locales/index.ts
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import zhCN from './zh-CN.ts'
|
||||||
|
import en from './en.ts'
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: localStorage.getItem('locale') || 'zh-CN',
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
messages: {
|
||||||
|
'zh-CN': zhCN,
|
||||||
|
en,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/main.ts
|
||||||
|
import i18n from './locales'
|
||||||
|
app.use(i18n)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 翻译键命名规范
|
||||||
|
|
||||||
|
**使用扁平 dot notation**(不使用嵌套对象),避免深层路径冲突:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 扁平 dot notation — 清晰、无歧义、工具友好
|
||||||
|
export default {
|
||||||
|
'common.action.save': '保存',
|
||||||
|
'common.action.cancel': '取消',
|
||||||
|
'common.action.delete': '删除',
|
||||||
|
'common.status.loading': '加载中...',
|
||||||
|
'auth.action.login': '登录',
|
||||||
|
'auth.action.logout': '退出登录',
|
||||||
|
'auth.action.signUp': '注册',
|
||||||
|
'home.hero.title': '欢迎',
|
||||||
|
'home.hero.description': '...',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 嵌套对象 — 容易产生路径冲突
|
||||||
|
export default {
|
||||||
|
common: { save: '保存' },
|
||||||
|
pages: { home: { title: '...' } },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**命名公式**:`{feature}.{context}.{action|status|label}`
|
||||||
|
|
||||||
|
| 段 | 含义 | 示例 |
|
||||||
|
|---|---|---|
|
||||||
|
| `feature` | 业务功能/模块 | `order`、`user`、`common` |
|
||||||
|
| `context` | 所在 UI 区域 | `list`、`form`、`dialog`、`action` |
|
||||||
|
| `action\|status\|label` | 具体语义 | `submit`、`loading`、`title` |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 示例
|
||||||
|
'order.list.title': '订单列表',
|
||||||
|
'order.form.submit': '提交订单',
|
||||||
|
'order.status.pending': '待处理',
|
||||||
|
'order.dialog.deleteConfirm': '确认删除该订单?',
|
||||||
|
'user.profile.edit': '编辑资料',
|
||||||
|
```
|
||||||
|
|
||||||
|
**键冲突预防**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ 冲突:'order.edit' 同时是叶子节点和父节点前缀
|
||||||
|
'order.edit': '编辑',
|
||||||
|
'order.edit.title': '编辑订单',
|
||||||
|
|
||||||
|
// ✅ 修正:给叶子节点加上语义后缀
|
||||||
|
'order.edit.action': '编辑',
|
||||||
|
'order.edit.title': '编辑订单',
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数化翻译**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 语言包
|
||||||
|
'order.total': '共 {count} 件商品,合计 {amount}',
|
||||||
|
'user.greeting': '你好,{name}',
|
||||||
|
|
||||||
|
// 组件中使用
|
||||||
|
t('order.total', { count: 5, amount: '¥100.00' })
|
||||||
|
t('user.greeting', { name: user.name })
|
||||||
|
```
|
||||||
|
|
||||||
|
**标准命名空间**(根据项目模块预定义,禁止随意创建):
|
||||||
|
|
||||||
|
| 命名空间 | 用途 | 文件 |
|
||||||
|
|----------|------|------|
|
||||||
|
| `common` | 通用 UI(按钮、状态、提示) | `common.ts` |
|
||||||
|
| `auth` | 登录/注册/权限 | `auth.ts` |
|
||||||
|
| `menu` | 侧边栏/导航菜单 | `menu.ts` |
|
||||||
|
| `validation` | 表单校验提示 | `validation.ts` |
|
||||||
|
| `error` | 错误码/错误提示 | `error.ts` |
|
||||||
|
| `{module}` | 各业务模块 (order/user/...) | `{module}.ts` |
|
||||||
|
|
||||||
|
新增命名空间时必须在此表中登记。
|
||||||
|
|
||||||
|
### 5. 组件中使用
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
|
function switchLanguage(lang) {
|
||||||
|
locale.value = lang
|
||||||
|
localStorage.setItem('locale', lang)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 管理端 -->
|
||||||
|
<el-button @click="switchLanguage('en')">English</el-button>
|
||||||
|
<el-button @click="switchLanguage('zh-CN')">中文</el-button>
|
||||||
|
|
||||||
|
<!-- 用户端(禁止 Element Plus) -->
|
||||||
|
<button class="px-3 py-1 rounded border" @click="switchLanguage('en')">English</button>
|
||||||
|
<button class="px-3 py-1 rounded border" @click="switchLanguage('zh-CN')">中文</button>
|
||||||
|
|
||||||
|
<!-- 基础用法 -->
|
||||||
|
<p>{{ t('common.action.save') }}</p>
|
||||||
|
|
||||||
|
<!-- 参数化翻译 -->
|
||||||
|
<p>{{ t('order.total', { count: orderCount, amount: totalAmount }) }}</p>
|
||||||
|
|
||||||
|
<!-- 管理端:在 Element Plus 组件中使用 -->
|
||||||
|
<el-button type="primary">{{ t('common.action.save') }}</el-button>
|
||||||
|
<el-input :placeholder="t('order.form.searchPlaceholder')" />
|
||||||
|
|
||||||
|
<!-- 用户端:在 Tailwind 组件中使用 -->
|
||||||
|
<button class="bg-blue-600 text-white px-4 py-2 rounded-lg">{{ t('common.action.save') }}</button>
|
||||||
|
<input class="border rounded-lg px-3 py-2" :placeholder="t('order.form.searchPlaceholder')" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Element Plus 国际化(仅管理端)
|
||||||
|
|
||||||
|
> 以下内容仅适用于 `Case-Database-Frontend-admin/`,用户端无需配置 Element Plus 国际化。
|
||||||
|
|
||||||
|
`main.ts` 不在 setup 上下文中,无法使用 `computed` 做响应式切换。正确方案:`main.ts` 静态初始化,`App.vue` 中通过 `computed` + `ElConfigProvider` 动态响应:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/main.ts
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
import en from 'element-plus/es/locale/lang/en'
|
||||||
|
|
||||||
|
// 初始化时根据 locale 选择语言包
|
||||||
|
const elLocaleMap = { 'zh-CN': zhCn, en }
|
||||||
|
const initLocale = localStorage.getItem('locale') || 'zh-CN'
|
||||||
|
|
||||||
|
app.use(ElementPlus, { locale: elLocaleMap[initLocale] ?? zhCn })
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 在根组件 App.vue 中动态响应语言切换 -->
|
||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
import en from 'element-plus/es/locale/lang/en'
|
||||||
|
|
||||||
|
const { locale } = useI18n()
|
||||||
|
const elLocaleMap = { 'zh-CN': zhCn, en }
|
||||||
|
|
||||||
|
// 通过 ElConfigProvider 的 locale prop 动态切换
|
||||||
|
const elLocale = computed(() => elLocaleMap[locale.value] ?? zhCn)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-config-provider :locale="elLocale">
|
||||||
|
<router-view />
|
||||||
|
</el-config-provider>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 添加新语言
|
||||||
|
|
||||||
|
1. 在 `src/locales/` 下创建新的 locale JS 文件(如 `fr.ts`)
|
||||||
|
2. 在 `src/locales/index.ts` 中注册新语言
|
||||||
|
3. 翻译所有已有 key
|
||||||
|
4. 管理端:同步 Element Plus 对应的 locale(用户端跳过此步)
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 所有 locale 文件的 key 结构一致(无遗漏)
|
||||||
|
2. [ ] 切换语言后页面正确翻译
|
||||||
|
3. [ ] 管理端:Element Plus 组件语言同步切换(用户端不适用)
|
||||||
|
4. [ ] 日期/数字格式随 locale 变化
|
||||||
|
5. [ ] 键命名遵循 `{feature}.{context}.{action|status|label}` 公式
|
||||||
|
6. [ ] 使用扁平 dot notation,不使用嵌套对象
|
||||||
|
7. [ ] 无键冲突(同一前缀不同时作为叶子节点和父节点前缀)
|
||||||
|
8. [ ] 参数化翻译使用 `{variableName}` 语法
|
||||||
|
9. [ ] 新命名空间已在标准命名空间表中登记
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
name: mcp-builder
|
||||||
|
version: 1.1.0
|
||||||
|
description: "构建 MCP Server,让 LLM 通过 Tool 与外部服务交互。当需要创建 MCP 工具或封装 API 为 MCP 时使用。推荐 TypeScript + MCP SDK。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# MCP Server 开发指南
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户需要构建自定义 MCP Server 以封装 API 或服务为 LLM 可调用的 Tool。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### Phase 1:研究与规划
|
||||||
|
|
||||||
|
#### 1.1 理解目标 API
|
||||||
|
|
||||||
|
- 梳理目标服务的 API 端点、认证方式、数据模型
|
||||||
|
- 使用 WebFetch 或 WebSearch 获取 API 文档
|
||||||
|
- 列出需要封装的端点,按优先级排序(最常用操作优先)
|
||||||
|
|
||||||
|
#### 1.2 学习 MCP 协议
|
||||||
|
|
||||||
|
**加载 MCP 规范**:
|
||||||
|
- 起始页:`https://modelcontextprotocol.io/sitemap.xml`
|
||||||
|
- 获取特定页面时添加 `.md` 后缀获取 Markdown 格式
|
||||||
|
|
||||||
|
**加载 MCP SDK 文档**:
|
||||||
|
- WebFetch: `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md`
|
||||||
|
- SDK 名称虽含 "typescript",但完全支持纯 TypeScript 项目
|
||||||
|
|
||||||
|
#### 1.3 设计 Tool
|
||||||
|
|
||||||
|
| 设计原则 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 命名清晰 | 使用 `prefix_action_target` 格式,如 `github_create_issue` |
|
||||||
|
| 描述精准 | 简洁说明功能 + 参数含义 + 返回值结构 |
|
||||||
|
| 错误可操作 | 错误信息应引导 LLM 找到解决方案 |
|
||||||
|
| 结果聚焦 | 返回精准、相关的数据,支持分页/过滤 |
|
||||||
|
| API 覆盖优先 | 不确定时优先提供全面的 API 覆盖,而非少量高层 Workflow Tool |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2:实现
|
||||||
|
|
||||||
|
**推荐技术栈**:
|
||||||
|
- **语言**:TypeScript(MCP SDK 原生支持,无需编译,直接运行)
|
||||||
|
- **传输**:远程服务用 Streamable HTTP(无状态 JSON),本地用 stdio
|
||||||
|
|
||||||
|
#### 2.1 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp-server-{{name}}/
|
||||||
|
├── src/
|
||||||
|
│ ├── index.ts # Server entry + tool registration
|
||||||
|
│ ├── client.ts # API client with auth
|
||||||
|
│ └── tools/ # One file per tool group
|
||||||
|
│ ├── resources.ts
|
||||||
|
│ └── actions.ts
|
||||||
|
├── package.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 基础设施
|
||||||
|
|
||||||
|
实现共享工具:
|
||||||
|
- API Client(认证、请求头、Base URL)
|
||||||
|
- 错误处理 Helper(统一格式,包含 actionable 信息)
|
||||||
|
- 响应格式化(JSON 结构化 + 文本摘要)
|
||||||
|
- 分页支持
|
||||||
|
|
||||||
|
#### 2.3 Tool 实现
|
||||||
|
|
||||||
|
每个 Tool 需要:
|
||||||
|
|
||||||
|
**输入 Schema**(Zod):
|
||||||
|
- Zod 在纯 TypeScript 中同样可用,提供运行时参数校验
|
||||||
|
- 包含约束和清晰的字段描述
|
||||||
|
- 在 description 中添加示例值
|
||||||
|
|
||||||
|
**输出 Schema**(推荐定义):
|
||||||
|
- 使用 `outputSchema` + `structuredContent` 返回结构化数据
|
||||||
|
|
||||||
|
**Tool 注解**:
|
||||||
|
- `readOnlyHint`: 是否只读
|
||||||
|
- `destructiveHint`: 是否有破坏性
|
||||||
|
- `idempotentHint`: 是否幂等
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3:审查与测试
|
||||||
|
|
||||||
|
#### 3.1 代码质量检查
|
||||||
|
- 无重复代码(DRY)
|
||||||
|
- 一致的错误处理
|
||||||
|
- 关键函数和参数有 JSDoc 注释
|
||||||
|
- 清晰的 Tool 描述
|
||||||
|
|
||||||
|
#### 3.2 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 直接运行验证语法
|
||||||
|
node src/index.ts
|
||||||
|
|
||||||
|
# 使用 MCP Inspector 交互式测试
|
||||||
|
npx @modelcontextprotocol/inspector
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4:集成到项目
|
||||||
|
|
||||||
|
将构建好的 MCP Server 注册到 `.cursor/mcp.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"{{name}}": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["path/to/mcp-server-{{name}}/src/index.ts"],
|
||||||
|
"env": {
|
||||||
|
"API_KEY": "{{env_var}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] `node src/index.ts` 启动无错误
|
||||||
|
2. [ ] MCP Inspector 可以列出所有 Tool
|
||||||
|
3. [ ] 每个 Tool 的输入 Schema 有 Zod 校验
|
||||||
|
4. [ ] 每个 Tool 的 description 清晰、包含参数说明
|
||||||
|
5. [ ] 错误信息包含具体的修复建议(actionable)
|
||||||
|
6. [ ] 需要分页的 Tool 支持 `cursor`/`limit` 参数
|
||||||
|
7. [ ] 环境变量用于凭证/密钥,不硬编码
|
||||||
|
8. [ ] `.cursor/mcp.json` 注册正确
|
||||||
|
|
||||||
|
## Tier 3 参考资料(按需读取)
|
||||||
|
|
||||||
|
- `references/mcp_best_practices.md` — MCP 最佳实践指南
|
||||||
|
- `references/node_mcp_server.md` — TypeScript/Node.js 实现详细指南
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
# MCP Server Best Practices
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Server Naming
|
||||||
|
- **Python**: `{service}_mcp` (e.g., `slack_mcp`)
|
||||||
|
- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`)
|
||||||
|
|
||||||
|
### Tool Naming
|
||||||
|
- Use snake_case with service prefix
|
||||||
|
- Format: `{service}_{action}_{resource}`
|
||||||
|
- Example: `slack_send_message`, `github_create_issue`
|
||||||
|
|
||||||
|
### Response Formats
|
||||||
|
- Support both JSON and Markdown formats
|
||||||
|
- JSON for programmatic processing
|
||||||
|
- Markdown for human readability
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
- Always respect `limit` parameter
|
||||||
|
- Return `has_more`, `next_offset`, `total_count`
|
||||||
|
- Default to 20-50 items
|
||||||
|
|
||||||
|
### Transport
|
||||||
|
- **Streamable HTTP**: For remote servers, multi-client scenarios
|
||||||
|
- **stdio**: For local integrations, command-line tools
|
||||||
|
- Avoid SSE (deprecated in favor of streamable HTTP)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server Naming Conventions
|
||||||
|
|
||||||
|
Follow these standardized naming patterns:
|
||||||
|
|
||||||
|
**Python**: Use format `{service}_mcp` (lowercase with underscores)
|
||||||
|
- Examples: `slack_mcp`, `github_mcp`, `jira_mcp`
|
||||||
|
|
||||||
|
**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens)
|
||||||
|
- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server`
|
||||||
|
|
||||||
|
The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Naming and Design
|
||||||
|
|
||||||
|
### Tool Naming
|
||||||
|
|
||||||
|
1. **Use snake_case**: `search_users`, `create_project`, `get_channel_info`
|
||||||
|
2. **Include service prefix**: Anticipate that your MCP server may be used alongside other MCP servers
|
||||||
|
- Use `slack_send_message` instead of just `send_message`
|
||||||
|
- Use `github_create_issue` instead of just `create_issue`
|
||||||
|
3. **Be action-oriented**: Start with verbs (get, list, search, create, etc.)
|
||||||
|
4. **Be specific**: Avoid generic names that could conflict with other servers
|
||||||
|
|
||||||
|
### Tool Design
|
||||||
|
|
||||||
|
- Tool descriptions must narrowly and unambiguously describe functionality
|
||||||
|
- Descriptions must precisely match actual functionality
|
||||||
|
- Provide tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
|
||||||
|
- Keep tool operations focused and atomic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Response Formats
|
||||||
|
|
||||||
|
All tools that return data should support multiple formats:
|
||||||
|
|
||||||
|
### JSON Format (`response_format="json"`)
|
||||||
|
- Machine-readable structured data
|
||||||
|
- Include all available fields and metadata
|
||||||
|
- Consistent field names and types
|
||||||
|
- Use for programmatic processing
|
||||||
|
|
||||||
|
### Markdown Format (`response_format="markdown"`, typically default)
|
||||||
|
- Human-readable formatted text
|
||||||
|
- Use headers, lists, and formatting for clarity
|
||||||
|
- Convert timestamps to human-readable format
|
||||||
|
- Show display names with IDs in parentheses
|
||||||
|
- Omit verbose metadata
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
For tools that list resources:
|
||||||
|
|
||||||
|
- **Always respect the `limit` parameter**
|
||||||
|
- **Implement pagination**: Use `offset` or cursor-based pagination
|
||||||
|
- **Return pagination metadata**: Include `has_more`, `next_offset`/`next_cursor`, `total_count`
|
||||||
|
- **Never load all results into memory**: Especially important for large datasets
|
||||||
|
- **Default to reasonable limits**: 20-50 items is typical
|
||||||
|
|
||||||
|
Example pagination response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total": 150,
|
||||||
|
"count": 20,
|
||||||
|
"offset": 0,
|
||||||
|
"items": [...],
|
||||||
|
"has_more": true,
|
||||||
|
"next_offset": 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Transport Options
|
||||||
|
|
||||||
|
### Streamable HTTP
|
||||||
|
|
||||||
|
**Best for**: Remote servers, web services, multi-client scenarios
|
||||||
|
|
||||||
|
**Characteristics**:
|
||||||
|
- Bidirectional communication over HTTP
|
||||||
|
- Supports multiple simultaneous clients
|
||||||
|
- Can be deployed as a web service
|
||||||
|
- Enables server-to-client notifications
|
||||||
|
|
||||||
|
**Use when**:
|
||||||
|
- Serving multiple clients simultaneously
|
||||||
|
- Deploying as a cloud service
|
||||||
|
- Integration with web applications
|
||||||
|
|
||||||
|
### stdio
|
||||||
|
|
||||||
|
**Best for**: Local integrations, command-line tools
|
||||||
|
|
||||||
|
**Characteristics**:
|
||||||
|
- Standard input/output stream communication
|
||||||
|
- Simple setup, no network configuration needed
|
||||||
|
- Runs as a subprocess of the client
|
||||||
|
|
||||||
|
**Use when**:
|
||||||
|
- Building tools for local development environments
|
||||||
|
- Integrating with desktop applications
|
||||||
|
- Single-user, single-session scenarios
|
||||||
|
|
||||||
|
**Note**: stdio servers should NOT log to stdout (use stderr for logging)
|
||||||
|
|
||||||
|
### Transport Selection
|
||||||
|
|
||||||
|
| Criterion | stdio | Streamable HTTP |
|
||||||
|
|-----------|-------|-----------------|
|
||||||
|
| **Deployment** | Local | Remote |
|
||||||
|
| **Clients** | Single | Multiple |
|
||||||
|
| **Complexity** | Low | Medium |
|
||||||
|
| **Real-time** | No | Yes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### Authentication and Authorization
|
||||||
|
|
||||||
|
**OAuth 2.1**:
|
||||||
|
- Use secure OAuth 2.1 with certificates from recognized authorities
|
||||||
|
- Validate access tokens before processing requests
|
||||||
|
- Only accept tokens specifically intended for your server
|
||||||
|
|
||||||
|
**API Keys**:
|
||||||
|
- Store API keys in environment variables, never in code
|
||||||
|
- Validate keys on server startup
|
||||||
|
- Provide clear error messages when authentication fails
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
|
||||||
|
- Sanitize file paths to prevent directory traversal
|
||||||
|
- Validate URLs and external identifiers
|
||||||
|
- Check parameter sizes and ranges
|
||||||
|
- Prevent command injection in system calls
|
||||||
|
- Use schema validation (Pydantic/Zod) for all inputs
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- Don't expose internal errors to clients
|
||||||
|
- Log security-relevant errors server-side
|
||||||
|
- Provide helpful but not revealing error messages
|
||||||
|
- Clean up resources after errors
|
||||||
|
|
||||||
|
### DNS Rebinding Protection
|
||||||
|
|
||||||
|
For streamable HTTP servers running locally:
|
||||||
|
- Enable DNS rebinding protection
|
||||||
|
- Validate the `Origin` header on all incoming connections
|
||||||
|
- Bind to `127.0.0.1` rather than `0.0.0.0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Annotations
|
||||||
|
|
||||||
|
Provide annotations to help clients understand tool behavior:
|
||||||
|
|
||||||
|
| Annotation | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `readOnlyHint` | boolean | false | Tool does not modify its environment |
|
||||||
|
| `destructiveHint` | boolean | true | Tool may perform destructive updates |
|
||||||
|
| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect |
|
||||||
|
| `openWorldHint` | boolean | true | Tool interacts with external entities |
|
||||||
|
|
||||||
|
**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Use standard JSON-RPC error codes
|
||||||
|
- Report tool errors within result objects (not protocol-level errors)
|
||||||
|
- Provide helpful, specific error messages with suggested next steps
|
||||||
|
- Don't expose internal implementation details
|
||||||
|
- Clean up resources properly on errors
|
||||||
|
|
||||||
|
Example error handling:
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
const result = performOperation();
|
||||||
|
return { content: [{ type: "text", text: result }] };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
isError: true,
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Error: ${error.message}. Try using filter='active_only' to reduce results.`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
Comprehensive testing should cover:
|
||||||
|
|
||||||
|
- **Functional testing**: Verify correct execution with valid/invalid inputs
|
||||||
|
- **Integration testing**: Test interaction with external systems
|
||||||
|
- **Security testing**: Validate auth, input sanitization, rate limiting
|
||||||
|
- **Performance testing**: Check behavior under load, timeouts
|
||||||
|
- **Error handling**: Ensure proper error reporting and cleanup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Requirements
|
||||||
|
|
||||||
|
- Provide clear documentation of all tools and capabilities
|
||||||
|
- Include working examples (at least 3 per major feature)
|
||||||
|
- Document security considerations
|
||||||
|
- Specify required permissions and access levels
|
||||||
|
- Document rate limits and performance characteristics
|
||||||
@@ -0,0 +1,970 @@
|
|||||||
|
# Node/TypeScript MCP Server Implementation Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document provides Node/TypeScript-specific best practices and examples for implementing MCP servers using the MCP TypeScript SDK. It covers project structure, server setup, tool registration patterns, input validation with Zod, error handling, and complete working examples.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Key Imports
|
||||||
|
```typescript
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.ts";
|
||||||
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.ts";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.ts";
|
||||||
|
import express from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Initialization
|
||||||
|
```typescript
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "service-mcp-server",
|
||||||
|
version: "1.0.0"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tool Registration Pattern
|
||||||
|
```typescript
|
||||||
|
server.registerTool(
|
||||||
|
"tool_name",
|
||||||
|
{
|
||||||
|
title: "Tool Display Name",
|
||||||
|
description: "What the tool does",
|
||||||
|
inputSchema: { param: z.string() },
|
||||||
|
outputSchema: { result: z.string() }
|
||||||
|
},
|
||||||
|
async ({ param }) => {
|
||||||
|
const output = { result: `Processed: ${param}` };
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(output) }],
|
||||||
|
structuredContent: output // Modern pattern for structured data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP TypeScript SDK
|
||||||
|
|
||||||
|
The official MCP TypeScript SDK provides:
|
||||||
|
- `McpServer` class for server initialization
|
||||||
|
- `registerTool` method for tool registration
|
||||||
|
- Zod schema integration for runtime input validation
|
||||||
|
- Type-safe tool handler implementations
|
||||||
|
|
||||||
|
**IMPORTANT - Use Modern APIs Only:**
|
||||||
|
- **DO use**: `server.registerTool()`, `server.registerResource()`, `server.registerPrompt()`
|
||||||
|
- **DO NOT use**: Old deprecated APIs such as `server.tool()`, `server.setRequestHandler(ListToolsRequestSchema, ...)`, or manual handler registration
|
||||||
|
- The `register*` methods provide better type safety, automatic schema handling, and are the recommended approach
|
||||||
|
|
||||||
|
See the MCP SDK documentation in the references for complete details.
|
||||||
|
|
||||||
|
## Server Naming Convention
|
||||||
|
|
||||||
|
Node/TypeScript MCP servers must follow this naming pattern:
|
||||||
|
- **Format**: `{service}-mcp-server` (lowercase with hyphens)
|
||||||
|
- **Examples**: `github-mcp-server`, `jira-mcp-server`, `stripe-mcp-server`
|
||||||
|
|
||||||
|
The name should be:
|
||||||
|
- General (not tied to specific features)
|
||||||
|
- Descriptive of the service/API being integrated
|
||||||
|
- Easy to infer from the task description
|
||||||
|
- Without version numbers or dates
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
Create the following structure for Node/TypeScript MCP servers:
|
||||||
|
|
||||||
|
```
|
||||||
|
{service}-mcp-server/
|
||||||
|
├── package.json
|
||||||
|
├── tsconfig.json
|
||||||
|
├── README.md
|
||||||
|
├── src/
|
||||||
|
│ ├── index.ts # Main entry point with McpServer initialization
|
||||||
|
│ ├── types.ts # TypeScript type definitions and interfaces
|
||||||
|
│ ├── tools/ # Tool implementations (one file per domain)
|
||||||
|
│ ├── services/ # API clients and shared utilities
|
||||||
|
│ ├── schemas/ # Zod validation schemas
|
||||||
|
│ └── constants.ts # Shared constants (API_URL, CHARACTER_LIMIT, etc.)
|
||||||
|
└── dist/ # Built TypeScript files (entry point: dist/index.ts)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tool Implementation
|
||||||
|
|
||||||
|
### Tool Naming
|
||||||
|
|
||||||
|
Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names.
|
||||||
|
|
||||||
|
**Avoid Naming Conflicts**: Include the service context to prevent overlaps:
|
||||||
|
- Use "slack_send_message" instead of just "send_message"
|
||||||
|
- Use "github_create_issue" instead of just "create_issue"
|
||||||
|
- Use "asana_list_tasks" instead of just "list_tasks"
|
||||||
|
|
||||||
|
### Tool Structure
|
||||||
|
|
||||||
|
Tools are registered using the `registerTool` method with the following requirements:
|
||||||
|
- Use Zod schemas for runtime input validation and type safety
|
||||||
|
- The `description` field must be explicitly provided - JSDoc comments are NOT automatically extracted
|
||||||
|
- Explicitly provide `title`, `description`, `inputSchema`, and `annotations`
|
||||||
|
- The `inputSchema` must be a Zod schema object (not a JSON schema)
|
||||||
|
- Type all parameters and return values explicitly
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.ts";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "example-mcp",
|
||||||
|
version: "1.0.0"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Zod schema for input validation
|
||||||
|
const UserSearchInputSchema = z.object({
|
||||||
|
query: z.string()
|
||||||
|
.min(2, "Query must be at least 2 characters")
|
||||||
|
.max(200, "Query must not exceed 200 characters")
|
||||||
|
.describe("Search string to match against names/emails"),
|
||||||
|
limit: z.number()
|
||||||
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.max(100)
|
||||||
|
.default(20)
|
||||||
|
.describe("Maximum results to return"),
|
||||||
|
offset: z.number()
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.default(0)
|
||||||
|
.describe("Number of results to skip for pagination"),
|
||||||
|
response_format: z.nativeEnum(ResponseFormat)
|
||||||
|
.default(ResponseFormat.MARKDOWN)
|
||||||
|
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
|
||||||
|
}).strict();
|
||||||
|
|
||||||
|
// Type definition from Zod schema
|
||||||
|
type UserSearchInput = z.infer<typeof UserSearchInputSchema>;
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"example_search_users",
|
||||||
|
{
|
||||||
|
title: "Search Example Users",
|
||||||
|
description: `Search for users in the Example system by name, email, or team.
|
||||||
|
|
||||||
|
This tool searches across all user profiles in the Example platform, supporting partial matches and various search filters. It does NOT create or modify users, only searches existing ones.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- query (string): Search string to match against names/emails
|
||||||
|
- limit (number): Maximum results to return, between 1-100 (default: 20)
|
||||||
|
- offset (number): Number of results to skip for pagination (default: 0)
|
||||||
|
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
For JSON format: Structured data with schema:
|
||||||
|
{
|
||||||
|
"total": number, // Total number of matches found
|
||||||
|
"count": number, // Number of results in this response
|
||||||
|
"offset": number, // Current pagination offset
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": string, // User ID (e.g., "U123456789")
|
||||||
|
"name": string, // Full name (e.g., "John Doe")
|
||||||
|
"email": string, // Email address
|
||||||
|
"team": string, // Team name (optional)
|
||||||
|
"active": boolean // Whether user is active
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"has_more": boolean, // Whether more results are available
|
||||||
|
"next_offset": number // Offset for next page (if has_more is true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- Use when: "Find all marketing team members" -> params with query="team:marketing"
|
||||||
|
- Use when: "Search for John's account" -> params with query="john"
|
||||||
|
- Don't use when: You need to create a user (use example_create_user instead)
|
||||||
|
|
||||||
|
Error Handling:
|
||||||
|
- Returns "Error: Rate limit exceeded" if too many requests (429 status)
|
||||||
|
- Returns "No users found matching '<query>'" if search returns empty`,
|
||||||
|
inputSchema: UserSearchInputSchema,
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: true,
|
||||||
|
destructiveHint: false,
|
||||||
|
idempotentHint: true,
|
||||||
|
openWorldHint: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async (params: UserSearchInput) => {
|
||||||
|
try {
|
||||||
|
// Input validation is handled by Zod schema
|
||||||
|
// Make API request using validated parameters
|
||||||
|
const data = await makeApiRequest<any>(
|
||||||
|
"users/search",
|
||||||
|
"GET",
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
q: params.query,
|
||||||
|
limit: params.limit,
|
||||||
|
offset: params.offset
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const users = data.users || [];
|
||||||
|
const total = data.total || 0;
|
||||||
|
|
||||||
|
if (!users.length) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `No users found matching '${params.query}'`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare structured output
|
||||||
|
const output = {
|
||||||
|
total,
|
||||||
|
count: users.length,
|
||||||
|
offset: params.offset,
|
||||||
|
users: users.map((user: any) => ({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
...(user.team ? { team: user.team } : {}),
|
||||||
|
active: user.active ?? true
|
||||||
|
})),
|
||||||
|
has_more: total > params.offset + users.length,
|
||||||
|
...(total > params.offset + users.length ? {
|
||||||
|
next_offset: params.offset + users.length
|
||||||
|
} : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format text representation based on requested format
|
||||||
|
let textContent: string;
|
||||||
|
if (params.response_format === ResponseFormat.MARKDOWN) {
|
||||||
|
const lines = [`# User Search Results: '${params.query}'`, "",
|
||||||
|
`Found ${total} users (showing ${users.length})`, ""];
|
||||||
|
for (const user of users) {
|
||||||
|
lines.push(`## ${user.name} (${user.id})`);
|
||||||
|
lines.push(`- **Email**: ${user.email}`);
|
||||||
|
if (user.team) lines.push(`- **Team**: ${user.team}`);
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
textContent = lines.join("\n");
|
||||||
|
} else {
|
||||||
|
textContent = JSON.stringify(output, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: textContent }],
|
||||||
|
structuredContent: output // Modern pattern for structured data
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: handleApiError(error)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zod Schemas for Input Validation
|
||||||
|
|
||||||
|
Zod provides runtime type validation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Basic schema with validation
|
||||||
|
const CreateUserSchema = z.object({
|
||||||
|
name: z.string()
|
||||||
|
.min(1, "Name is required")
|
||||||
|
.max(100, "Name must not exceed 100 characters"),
|
||||||
|
email: z.string()
|
||||||
|
.email("Invalid email format"),
|
||||||
|
age: z.number()
|
||||||
|
.int("Age must be a whole number")
|
||||||
|
.min(0, "Age cannot be negative")
|
||||||
|
.max(150, "Age cannot be greater than 150")
|
||||||
|
}).strict(); // Use .strict() to forbid extra fields
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
enum ResponseFormat {
|
||||||
|
MARKDOWN = "markdown",
|
||||||
|
JSON = "json"
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchSchema = z.object({
|
||||||
|
response_format: z.nativeEnum(ResponseFormat)
|
||||||
|
.default(ResponseFormat.MARKDOWN)
|
||||||
|
.describe("Output format")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optional fields with defaults
|
||||||
|
const PaginationSchema = z.object({
|
||||||
|
limit: z.number()
|
||||||
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.max(100)
|
||||||
|
.default(20)
|
||||||
|
.describe("Maximum results to return"),
|
||||||
|
offset: z.number()
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.default(0)
|
||||||
|
.describe("Number of results to skip")
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format Options
|
||||||
|
|
||||||
|
Support multiple output formats for flexibility:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum ResponseFormat {
|
||||||
|
MARKDOWN = "markdown",
|
||||||
|
JSON = "json"
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputSchema = z.object({
|
||||||
|
query: z.string(),
|
||||||
|
response_format: z.nativeEnum(ResponseFormat)
|
||||||
|
.default(ResponseFormat.MARKDOWN)
|
||||||
|
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Markdown format**:
|
||||||
|
- Use headers, lists, and formatting for clarity
|
||||||
|
- Convert timestamps to human-readable format
|
||||||
|
- Show display names with IDs in parentheses
|
||||||
|
- Omit verbose metadata
|
||||||
|
- Group related information logically
|
||||||
|
|
||||||
|
**JSON format**:
|
||||||
|
- Return complete, structured data suitable for programmatic processing
|
||||||
|
- Include all available fields and metadata
|
||||||
|
- Use consistent field names and types
|
||||||
|
|
||||||
|
## Pagination Implementation
|
||||||
|
|
||||||
|
For tools that list resources:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const ListSchema = z.object({
|
||||||
|
limit: z.number().int().min(1).max(100).default(20),
|
||||||
|
offset: z.number().int().min(0).default(0)
|
||||||
|
});
|
||||||
|
|
||||||
|
async function listItems(params: z.infer<typeof ListSchema>) {
|
||||||
|
const data = await apiRequest(params.limit, params.offset);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
total: data.total,
|
||||||
|
count: data.items.length,
|
||||||
|
offset: params.offset,
|
||||||
|
items: data.items,
|
||||||
|
has_more: data.total > params.offset + data.items.length,
|
||||||
|
next_offset: data.total > params.offset + data.items.length
|
||||||
|
? params.offset + data.items.length
|
||||||
|
: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(response, null, 2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Character Limits and Truncation
|
||||||
|
|
||||||
|
Add a CHARACTER_LIMIT constant to prevent overwhelming responses:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// At module level in constants.ts
|
||||||
|
export const CHARACTER_LIMIT = 25000; // Maximum response size in characters
|
||||||
|
|
||||||
|
async function searchTool(params: SearchInput) {
|
||||||
|
let result = generateResponse(data);
|
||||||
|
|
||||||
|
// Check character limit and truncate if needed
|
||||||
|
if (result.length > CHARACTER_LIMIT) {
|
||||||
|
const truncatedData = data.slice(0, Math.max(1, data.length / 2));
|
||||||
|
response.data = truncatedData;
|
||||||
|
response.truncated = true;
|
||||||
|
response.truncation_message =
|
||||||
|
`Response truncated from ${data.length} to ${truncatedData.length} items. ` +
|
||||||
|
`Use 'offset' parameter or add filters to see more results.`;
|
||||||
|
result = JSON.stringify(response, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Provide clear, actionable error messages:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
|
||||||
|
function handleApiError(error: unknown): string {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
if (error.response) {
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 404:
|
||||||
|
return "Error: Resource not found. Please check the ID is correct.";
|
||||||
|
case 403:
|
||||||
|
return "Error: Permission denied. You don't have access to this resource.";
|
||||||
|
case 429:
|
||||||
|
return "Error: Rate limit exceeded. Please wait before making more requests.";
|
||||||
|
default:
|
||||||
|
return `Error: API request failed with status ${error.response.status}`;
|
||||||
|
}
|
||||||
|
} else if (error.code === "ECONNABORTED") {
|
||||||
|
return "Error: Request timed out. Please try again.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shared Utilities
|
||||||
|
|
||||||
|
Extract common functionality into reusable functions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Shared API request function
|
||||||
|
async function makeApiRequest<T>(
|
||||||
|
endpoint: string,
|
||||||
|
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
|
||||||
|
data?: any,
|
||||||
|
params?: any
|
||||||
|
): Promise<T> {
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
method,
|
||||||
|
url: `${API_BASE_URL}/${endpoint}`,
|
||||||
|
data,
|
||||||
|
params,
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async/Await Best Practices
|
||||||
|
|
||||||
|
Always use async/await for network requests and I/O operations:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good: Async network request
|
||||||
|
async function fetchData(resourceId: string): Promise<ResourceData> {
|
||||||
|
const response = await axios.get(`${API_URL}/resource/${resourceId}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad: Promise chains
|
||||||
|
function fetchData(resourceId: string): Promise<ResourceData> {
|
||||||
|
return axios.get(`${API_URL}/resource/${resourceId}`)
|
||||||
|
.then(response => response.data); // Harder to read and maintain
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript Best Practices
|
||||||
|
|
||||||
|
1. **Use Strict TypeScript**: Enable strict mode in tsconfig.json
|
||||||
|
2. **Define Interfaces**: Create clear interface definitions for all data structures
|
||||||
|
3. **Avoid `any`**: Use proper types or `unknown` instead of `any`
|
||||||
|
4. **Zod for Runtime Validation**: Use Zod schemas to validate external data
|
||||||
|
5. **Type Guards**: Create type guard functions for complex type checking
|
||||||
|
6. **Error Handling**: Always use try-catch with proper error type checking
|
||||||
|
7. **Null Safety**: Use optional chaining (`?.`) and nullish coalescing (`??`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good: Type-safe with Zod and interfaces
|
||||||
|
interface UserResponse {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
team?: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
team: z.string().optional(),
|
||||||
|
active: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
type User = z.infer<typeof UserSchema>;
|
||||||
|
|
||||||
|
async function getUser(id: string): Promise<User> {
|
||||||
|
const data = await apiCall(`/users/${id}`);
|
||||||
|
return UserSchema.parse(data); // Runtime validation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad: Using any
|
||||||
|
async function getUser(id: string): Promise<any> {
|
||||||
|
return await apiCall(`/users/${id}`); // No type safety
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Configuration
|
||||||
|
|
||||||
|
### package.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "{service}-mcp-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MCP server for {Service} API integration",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node dist/index.ts",
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.6.1",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.0",
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### tsconfig.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* MCP Server for Example Service.
|
||||||
|
*
|
||||||
|
* This server provides tools to interact with Example API, including user search,
|
||||||
|
* project management, and data export capabilities.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.ts";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.ts";
|
||||||
|
import { z } from "zod";
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const API_BASE_URL = "https://api.example.com/v1";
|
||||||
|
const CHARACTER_LIMIT = 25000;
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
enum ResponseFormat {
|
||||||
|
MARKDOWN = "markdown",
|
||||||
|
JSON = "json"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zod schemas
|
||||||
|
const UserSearchInputSchema = z.object({
|
||||||
|
query: z.string()
|
||||||
|
.min(2, "Query must be at least 2 characters")
|
||||||
|
.max(200, "Query must not exceed 200 characters")
|
||||||
|
.describe("Search string to match against names/emails"),
|
||||||
|
limit: z.number()
|
||||||
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.max(100)
|
||||||
|
.default(20)
|
||||||
|
.describe("Maximum results to return"),
|
||||||
|
offset: z.number()
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.default(0)
|
||||||
|
.describe("Number of results to skip for pagination"),
|
||||||
|
response_format: z.nativeEnum(ResponseFormat)
|
||||||
|
.default(ResponseFormat.MARKDOWN)
|
||||||
|
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
|
||||||
|
}).strict();
|
||||||
|
|
||||||
|
type UserSearchInput = z.infer<typeof UserSearchInputSchema>;
|
||||||
|
|
||||||
|
// Shared utility functions
|
||||||
|
async function makeApiRequest<T>(
|
||||||
|
endpoint: string,
|
||||||
|
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
|
||||||
|
data?: any,
|
||||||
|
params?: any
|
||||||
|
): Promise<T> {
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
method,
|
||||||
|
url: `${API_BASE_URL}/${endpoint}`,
|
||||||
|
data,
|
||||||
|
params,
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleApiError(error: unknown): string {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
if (error.response) {
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 404:
|
||||||
|
return "Error: Resource not found. Please check the ID is correct.";
|
||||||
|
case 403:
|
||||||
|
return "Error: Permission denied. You don't have access to this resource.";
|
||||||
|
case 429:
|
||||||
|
return "Error: Rate limit exceeded. Please wait before making more requests.";
|
||||||
|
default:
|
||||||
|
return `Error: API request failed with status ${error.response.status}`;
|
||||||
|
}
|
||||||
|
} else if (error.code === "ECONNABORTED") {
|
||||||
|
return "Error: Request timed out. Please try again.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create MCP server instance
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "example-mcp",
|
||||||
|
version: "1.0.0"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register tools
|
||||||
|
server.registerTool(
|
||||||
|
"example_search_users",
|
||||||
|
{
|
||||||
|
title: "Search Example Users",
|
||||||
|
description: `[Full description as shown above]`,
|
||||||
|
inputSchema: UserSearchInputSchema,
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: true,
|
||||||
|
destructiveHint: false,
|
||||||
|
idempotentHint: true,
|
||||||
|
openWorldHint: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async (params: UserSearchInput) => {
|
||||||
|
// Implementation as shown above
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Main function
|
||||||
|
// For stdio (local):
|
||||||
|
async function runStdio() {
|
||||||
|
if (!process.env.EXAMPLE_API_KEY) {
|
||||||
|
console.error("ERROR: EXAMPLE_API_KEY environment variable is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.error("MCP server running via stdio");
|
||||||
|
}
|
||||||
|
|
||||||
|
// For streamable HTTP (remote):
|
||||||
|
async function runHTTP() {
|
||||||
|
if (!process.env.EXAMPLE_API_KEY) {
|
||||||
|
console.error("ERROR: EXAMPLE_API_KEY environment variable is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.post('/mcp', async (req, res) => {
|
||||||
|
const transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: undefined,
|
||||||
|
enableJsonResponse: true
|
||||||
|
});
|
||||||
|
res.on('close', () => transport.close());
|
||||||
|
await server.connect(transport);
|
||||||
|
await transport.handleRequest(req, res, req.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = parseInt(process.env.PORT || '3000');
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.error(`MCP server running on http://localhost:${port}/mcp`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose transport based on environment
|
||||||
|
const transport = process.env.TRANSPORT || 'stdio';
|
||||||
|
if (transport === 'http') {
|
||||||
|
runHTTP().catch(error => {
|
||||||
|
console.error("Server error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
runStdio().catch(error => {
|
||||||
|
console.error("Server error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced MCP Features
|
||||||
|
|
||||||
|
### Resource Registration
|
||||||
|
|
||||||
|
Expose data as resources for efficient, URI-based access:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/types.ts";
|
||||||
|
|
||||||
|
// Register a resource with URI template
|
||||||
|
server.registerResource(
|
||||||
|
{
|
||||||
|
uri: "file://documents/{name}",
|
||||||
|
name: "Document Resource",
|
||||||
|
description: "Access documents by name",
|
||||||
|
mimeType: "text/plain"
|
||||||
|
},
|
||||||
|
async (uri: string) => {
|
||||||
|
// Extract parameter from URI
|
||||||
|
const match = uri.match(/^file:\/\/documents\/(.+)$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error("Invalid URI format");
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentName = match[1];
|
||||||
|
const content = await loadDocument(documentName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: [{
|
||||||
|
uri,
|
||||||
|
mimeType: "text/plain",
|
||||||
|
text: content
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// List available resources dynamically
|
||||||
|
server.registerResourceList(async () => {
|
||||||
|
const documents = await getAvailableDocuments();
|
||||||
|
return {
|
||||||
|
resources: documents.map(doc => ({
|
||||||
|
uri: `file://documents/${doc.name}`,
|
||||||
|
name: doc.name,
|
||||||
|
mimeType: "text/plain",
|
||||||
|
description: doc.description
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use Resources vs Tools:**
|
||||||
|
- **Resources**: For data access with simple URI-based parameters
|
||||||
|
- **Tools**: For complex operations requiring validation and business logic
|
||||||
|
- **Resources**: When data is relatively static or template-based
|
||||||
|
- **Tools**: When operations have side effects or complex workflows
|
||||||
|
|
||||||
|
### Transport Options
|
||||||
|
|
||||||
|
The TypeScript SDK supports two main transport mechanisms:
|
||||||
|
|
||||||
|
#### Streamable HTTP (Recommended for Remote Servers)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.ts";
|
||||||
|
import express from "express";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.post('/mcp', async (req, res) => {
|
||||||
|
// Create new transport for each request (stateless, prevents request ID collisions)
|
||||||
|
const transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: undefined,
|
||||||
|
enableJsonResponse: true
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('close', () => transport.close());
|
||||||
|
|
||||||
|
await server.connect(transport);
|
||||||
|
await transport.handleRequest(req, res, req.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3000);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### stdio (For Local Integrations)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.ts";
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transport selection:**
|
||||||
|
- **Streamable HTTP**: Web services, remote access, multiple clients
|
||||||
|
- **stdio**: Command-line tools, local development, subprocess integration
|
||||||
|
|
||||||
|
### Notification Support
|
||||||
|
|
||||||
|
Notify clients when server state changes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Notify when tools list changes
|
||||||
|
server.notification({
|
||||||
|
method: "notifications/tools/list_changed"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify when resources change
|
||||||
|
server.notification({
|
||||||
|
method: "notifications/resources/list_changed"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Use notifications sparingly - only when server capabilities genuinely change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Best Practices
|
||||||
|
|
||||||
|
### Code Composability and Reusability
|
||||||
|
|
||||||
|
Your implementation MUST prioritize composability and code reuse:
|
||||||
|
|
||||||
|
1. **Extract Common Functionality**:
|
||||||
|
- Create reusable helper functions for operations used across multiple tools
|
||||||
|
- Build shared API clients for HTTP requests instead of duplicating code
|
||||||
|
- Centralize error handling logic in utility functions
|
||||||
|
- Extract business logic into dedicated functions that can be composed
|
||||||
|
- Extract shared markdown or JSON field selection & formatting functionality
|
||||||
|
|
||||||
|
2. **Avoid Duplication**:
|
||||||
|
- NEVER copy-paste similar code between tools
|
||||||
|
- If you find yourself writing similar logic twice, extract it into a function
|
||||||
|
- Common operations like pagination, filtering, field selection, and formatting should be shared
|
||||||
|
- Authentication/authorization logic should be centralized
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
Always build your TypeScript code before running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the project
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Development with auto-reload
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Always ensure `npm run build` completes successfully before considering the implementation complete.
|
||||||
|
|
||||||
|
## Quality Checklist
|
||||||
|
|
||||||
|
Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||||
|
|
||||||
|
### Strategic Design
|
||||||
|
- [ ] Tools enable complete workflows, not just API endpoint wrappers
|
||||||
|
- [ ] Tool names reflect natural task subdivisions
|
||||||
|
- [ ] Response formats optimize for agent context efficiency
|
||||||
|
- [ ] Human-readable identifiers used where appropriate
|
||||||
|
- [ ] Error messages guide agents toward correct usage
|
||||||
|
|
||||||
|
### Implementation Quality
|
||||||
|
- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented
|
||||||
|
- [ ] All tools registered using `registerTool` with complete configuration
|
||||||
|
- [ ] All tools include `title`, `description`, `inputSchema`, and `annotations`
|
||||||
|
- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
|
||||||
|
- [ ] All tools use Zod schemas for runtime input validation with `.strict()` enforcement
|
||||||
|
- [ ] All Zod schemas have proper constraints and descriptive error messages
|
||||||
|
- [ ] All tools have comprehensive descriptions with explicit input/output types
|
||||||
|
- [ ] Descriptions include return value examples and complete schema documentation
|
||||||
|
- [ ] Error messages are clear, actionable, and educational
|
||||||
|
|
||||||
|
### TypeScript Quality
|
||||||
|
- [ ] TypeScript interfaces are defined for all data structures
|
||||||
|
- [ ] Strict TypeScript is enabled in tsconfig.json
|
||||||
|
- [ ] No use of `any` type - use `unknown` or proper types instead
|
||||||
|
- [ ] All async functions have explicit Promise<T> return types
|
||||||
|
- [ ] Error handling uses proper type guards (e.g., `axios.isAxiosError`, `z.ZodError`)
|
||||||
|
|
||||||
|
### Advanced Features (where applicable)
|
||||||
|
- [ ] Resources registered for appropriate data endpoints
|
||||||
|
- [ ] Appropriate transport configured (stdio or streamable HTTP)
|
||||||
|
- [ ] Notifications implemented for dynamic server capabilities
|
||||||
|
- [ ] Type-safe with SDK interfaces
|
||||||
|
|
||||||
|
### Project Configuration
|
||||||
|
- [ ] Package.json includes all necessary dependencies
|
||||||
|
- [ ] Build script produces working TypeScript in dist/ directory
|
||||||
|
- [ ] Main entry point is properly configured as dist/index.ts
|
||||||
|
- [ ] Server name follows format: `{service}-mcp-server`
|
||||||
|
- [ ] tsconfig.json properly configured with strict mode
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- [ ] Pagination is properly implemented where applicable
|
||||||
|
- [ ] Large responses check CHARACTER_LIMIT constant and truncate with clear messages
|
||||||
|
- [ ] Filtering options are provided for potentially large result sets
|
||||||
|
- [ ] All network operations handle timeouts and connection errors gracefully
|
||||||
|
- [ ] Common functionality is extracted into reusable functions
|
||||||
|
- [ ] Return types are consistent across similar operations
|
||||||
|
|
||||||
|
### Testing and Build
|
||||||
|
- [ ] `npm run build` completes successfully without errors
|
||||||
|
- [ ] dist/index.ts created and executable
|
||||||
|
- [ ] Server runs: `node dist/index.ts --help`
|
||||||
|
- [ ] All imports resolve correctly
|
||||||
|
- [ ] Sample tool calls work as expected
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: message-queue
|
||||||
|
version: 1.0.0
|
||||||
|
description: "配置 Hyperf AsyncQueue + Redis 实现后台任务处理。当需要异步任务、消息队列或延迟队列时使用。含重试策略和事件驱动模式。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Hyperf Async Queue (Message Queue)
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户需要异步处理任务:通知发送、数据同步、超时检测、日志记录、批量操作。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### Phase 0: 加载规范
|
||||||
|
|
||||||
|
读取 `.cursor/rules/013-backend.mdc`、`016-swoole.mdc`,提取 Job 命名、DI、协程安全、连接池。
|
||||||
|
|
||||||
|
### Phase 1: 队列配置
|
||||||
|
|
||||||
|
`config/autoload/async_queue.php`:Redis 驱动、channel、retry_seconds 指数退避、handle_timeout、processes、concurrent。可配置 default 与 notification 等多队列。
|
||||||
|
|
||||||
|
### Phase 2: Job 类
|
||||||
|
|
||||||
|
继承 `Hyperf\AsyncQueue\Job`,`maxAttempts`、幂等检查、`handle()` 内 try-catch 记录日志后 re-throw 触发重试。
|
||||||
|
|
||||||
|
### Phase 3: 投递任务
|
||||||
|
|
||||||
|
QueueService 封装 `dispatch`、`dispatchNotification`、`dispatchDelayed`。从 DriverFactory 获取 driver 后 push。
|
||||||
|
|
||||||
|
### Phase 4: 使用场景
|
||||||
|
|
||||||
|
通知发送、超时检测(延迟 30 分钟)、批量处理(chunk 后逐批 dispatch)。
|
||||||
|
|
||||||
|
### Phase 5: 事件驱动
|
||||||
|
|
||||||
|
Event → Listener → QueueService.dispatch。Service 更新数据后 event() 触发,Listener 内异步投递多个 Job。
|
||||||
|
|
||||||
|
### Phase 6: 监控
|
||||||
|
|
||||||
|
队列 health check 命令:Redis 统计 waiting/delayed/failed/timeout,failed > 100 告警。
|
||||||
|
|
||||||
|
完整实现见 **Tier 3**。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 配置使用环境变量
|
||||||
|
2. [ ] Job 幂等
|
||||||
|
3. [ ] maxAttempts、retry_seconds 合理
|
||||||
|
4. [ ] handle_timeout 大于 Job 最大执行时间
|
||||||
|
5. [ ] 关键 Job 有错误日志
|
||||||
|
6. [ ] 延迟队列用于超时检测
|
||||||
|
7. [ ] 消费进程在 supervisor 注册
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `references/queue-implementation.md` | 配置、Job、投递、事件驱动、监控完整代码 |
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Message Queue — 实现细节
|
||||||
|
|
||||||
|
> 主流程见 SKILL.md,本文档为配置、Job、投递、事件驱动、监控的完整代码。
|
||||||
|
|
||||||
|
## 队列配置
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/autoload/async_queue.php
|
||||||
|
return [
|
||||||
|
'default' => [
|
||||||
|
'driver' => Hyperf\AsyncQueue\Driver\RedisDriver::class,
|
||||||
|
'redis' => ['pool' => 'default'],
|
||||||
|
'channel' => env('QUEUE_CHANNEL', '{queue}'),
|
||||||
|
'timeout' => 2,
|
||||||
|
'retry_seconds' => [1, 5, 10, 30, 60],
|
||||||
|
'handle_timeout' => 60,
|
||||||
|
'processes' => 1,
|
||||||
|
'concurrent' => ['limit' => 10],
|
||||||
|
],
|
||||||
|
'notification' => [ /* 独立队列,更高优先级 */ ],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Job 类模板
|
||||||
|
|
||||||
|
```php
|
||||||
|
class {{JobName}}Job extends Job
|
||||||
|
{
|
||||||
|
protected int $maxAttempts = 3;
|
||||||
|
public function __construct(protected readonly int $resourceId, protected readonly array $payload = []) {}
|
||||||
|
public function handle(): void {
|
||||||
|
$resource = $this->getResource();
|
||||||
|
if (!$resource || $this->isAlreadyProcessed($resource)) return;
|
||||||
|
try { $this->process($resource); }
|
||||||
|
catch (\Throwable $e) { logger()->error(...); throw $e; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 投递与事件驱动
|
||||||
|
|
||||||
|
QueueService:`dispatch($job, $delay)`、`dispatchNotification($job)`、`dispatchDelayed($job, $delaySeconds)`。事件驱动:Event → Listener → QueueService->dispatch。示例:OrderStatusChanged → 异步通知、同步外部、更新统计缓存(debounce 5s)。
|
||||||
|
|
||||||
|
## 监控命令
|
||||||
|
|
||||||
|
Redis `{queue}:waiting`、`{queue}:delayed`、`{queue}:failed`、`{queue}:timeout`。failed > 100 告警。
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
---
|
||||||
|
name: module-scaffold
|
||||||
|
version: 2.1.0
|
||||||
|
description: "生成 Hyperf 模块化业务模块脚手架(ConfigProvider/Controller/Service/Model)。当需要新建业务模块、创建 module、添加模块化功能时使用。基于 ModuleLoader 自动发现 + ConfigProvider 机制,无需修改 composer.json。"
|
||||||
|
requires: [hyperf-service]
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/skill-module-scaffold.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Hyperf Module Scaffold
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求创建新的业务模块、在 `modules/` 下新建功能模块、或提到"模块化"、"新模块"、"module"。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
项目已完成模块化架构初始化:
|
||||||
|
- `Case-Database-Backend/modules/` 目录存在
|
||||||
|
- `app/Support/ModuleLoader.php` 已存在并注册到 `composer.json` `autoload.files`
|
||||||
|
- `config/autoload/annotations.php` 已包含 `BASE_PATH . '/modules'` 扫描路径
|
||||||
|
|
||||||
|
若未初始化,先引导用户完成架构搭建。
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
`ModuleLoader.php` 在 `vendor/autoload.php` 加载时(Hyperf DI 容器初始化之前)自动执行:
|
||||||
|
1. 扫描 `modules/*/composer.json`
|
||||||
|
2. 读取每个模块的 `extra.hyperf.config`
|
||||||
|
3. 通过反射注入 `Hyperf\Support\Composer::$extra`
|
||||||
|
4. 同时注册模块的 PSR-4 命名空间到 Composer ClassLoader
|
||||||
|
|
||||||
|
这使得 `ProviderConfig::load()` 能发现所有模块的 ConfigProvider,**无需将模块添加到主项目 `composer.json` 的 `require` 中**。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 确认模块规格
|
||||||
|
|
||||||
|
| 字段 | 必填 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| 模块名称 | ✅ | — | PascalCase,如 `Order`、`UserCenter` |
|
||||||
|
| 模块描述 | ❌ | 推断 | 一句话说明模块职责 |
|
||||||
|
| 接口类型 | ❌ | `api` | `api`(用户端)/ `admin`(管理端)/ `both`(两者都有) |
|
||||||
|
| 需要 Service? | ❌ | true | 业务逻辑层 |
|
||||||
|
| 需要 Model? | ❌ | false | 数据模型 |
|
||||||
|
| 需要 Repository? | ❌ | false | 复杂查询时 |
|
||||||
|
| 需要 Middleware? | ❌ | false | 模块级中间件 |
|
||||||
|
| 需要 Request? | ❌ | false | 表单验证 |
|
||||||
|
| 路由前缀 | ❌ | 见下方规则 | 自动推断,也可手动指定 |
|
||||||
|
|
||||||
|
### 2. Controller 与 Request 目录结构规则(核心)
|
||||||
|
|
||||||
|
根据接口类型,Controller 和 Request **均放入对应子目录**,命名空间随之变更:
|
||||||
|
|
||||||
|
| 接口类型 | 路由前缀 | Controller 目录 | Request 目录 | PHP 命名空间(Controller) |
|
||||||
|
|----------|----------|-----------------|--------------|--------------------------|
|
||||||
|
| 用户端 API(`/api/*`) | `/api/{module-kebab}` | `Http/Controller/Api/` | `Http/Request/Api/` | `Modules\{Name}\Http\Controller\Api` |
|
||||||
|
| 管理端 API(`/admin/*`) | `/admin/{module-kebab}` | `Http/Controller/Admin/` | `Http/Request/Admin/` | `Modules\{Name}\Http\Controller\Admin` |
|
||||||
|
| 两者都有(`both`) | 各自前缀 | 两个 Controller 子目录 | 两个 Request 子目录 | 各自命名空间 |
|
||||||
|
|
||||||
|
**判断规则**:
|
||||||
|
- 路由前缀包含 `/api/` → Controller 放 `Controller/Api/`,Request 放 `Request/Api/`
|
||||||
|
- 路由前缀包含 `/admin/` → Controller 放 `Controller/Admin/`,Request 放 `Request/Admin/`
|
||||||
|
- 用户未指定接口类型时,优先询问,或根据模块职责推断
|
||||||
|
- 若模块同时有用户端和管理端接口,两组子目录都创建
|
||||||
|
- **类名不需要加接口类型前缀**,子目录本身已区分,类名保持简洁
|
||||||
|
|
||||||
|
### 3. 生成目录结构
|
||||||
|
|
||||||
|
**仅有用户端 API(最常见)**:
|
||||||
|
```
|
||||||
|
modules/{ModuleName}/
|
||||||
|
├── composer.json
|
||||||
|
└── src/
|
||||||
|
├── ConfigProvider.php
|
||||||
|
├── Http/
|
||||||
|
│ ├── Controller/
|
||||||
|
│ │ └── Api/ # 用户端控制器
|
||||||
|
│ │ └── {Name}Controller.php
|
||||||
|
│ ├── Request/
|
||||||
|
│ │ └── Api/ # 用户端请求验证
|
||||||
|
│ │ └── {Name}Request.php
|
||||||
|
│ └── Middleware/ # 模块中间件(按需)
|
||||||
|
├── Service/
|
||||||
|
├── Model/ # 按需
|
||||||
|
├── Repository/ # 按需
|
||||||
|
├── Event/ # 按需
|
||||||
|
├── Listener/ # 按需
|
||||||
|
└── Constants/ # 按需
|
||||||
|
```
|
||||||
|
|
||||||
|
**仅有管理端 API**:
|
||||||
|
```
|
||||||
|
modules/{ModuleName}/
|
||||||
|
└── src/
|
||||||
|
├── Http/
|
||||||
|
│ ├── Controller/
|
||||||
|
│ │ └── Admin/ # 管理端控制器
|
||||||
|
│ │ └── {Name}Controller.php
|
||||||
|
│ ├── Request/
|
||||||
|
│ │ └── Admin/ # 管理端请求验证
|
||||||
|
│ │ └── {Name}Request.php
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**同时有用户端 + 管理端(both)**:
|
||||||
|
```
|
||||||
|
modules/{ModuleName}/
|
||||||
|
└── src/
|
||||||
|
├── Http/
|
||||||
|
│ ├── Controller/
|
||||||
|
│ │ ├── Api/ # 用户端控制器
|
||||||
|
│ │ │ └── {Name}Controller.php
|
||||||
|
│ │ └── Admin/ # 管理端控制器
|
||||||
|
│ │ └── {Name}Controller.php
|
||||||
|
│ ├── Request/
|
||||||
|
│ │ ├── Api/ # 用户端请求验证
|
||||||
|
│ │ │ └── {Name}Request.php
|
||||||
|
│ │ └── Admin/ # 管理端请求验证
|
||||||
|
│ │ └── {Name}Request.php
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 生成 composer.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "modules/{module-kebab}",
|
||||||
|
"description": "{模块描述}",
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Modules\\{ModuleName}\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"hyperf": {
|
||||||
|
"config": "Modules\\{ModuleName}\\ConfigProvider"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**命名规则**:
|
||||||
|
- Composer 包名:`modules/{kebab-case}`,如 `modules/user-center`
|
||||||
|
- PHP 命名空间:`Modules\{PascalCase}`,如 `Modules\UserCenter`
|
||||||
|
|
||||||
|
如模块有独立的第三方依赖,可在模块 `composer.json` 的 `require` 中声明,`wikimedia/composer-merge-plugin` 会在下次 `composer update` 时合并到主项目。
|
||||||
|
|
||||||
|
### 5. 生成 ConfigProvider.php
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\{ModuleName};
|
||||||
|
|
||||||
|
class ConfigProvider
|
||||||
|
{
|
||||||
|
public function __invoke(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'dependencies' => [
|
||||||
|
// Interface::class => Implementation::class,
|
||||||
|
],
|
||||||
|
'annotations' => [
|
||||||
|
'scan' => [
|
||||||
|
'paths' => [
|
||||||
|
__DIR__,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'publish' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ConfigProvider 职责**:
|
||||||
|
- `dependencies`:注册模块的 DI 绑定(接口 → 实现)
|
||||||
|
- `annotations.scan.paths`:注册注解扫描路径(`__DIR__` 指向 `src/`)
|
||||||
|
- `publish`:可发布的配置文件(可选)
|
||||||
|
|
||||||
|
### 6. 生成 Controller 模板
|
||||||
|
|
||||||
|
**用户端 API Controller**(放在 `Http/Controller/Api/`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\{ModuleName}\Http\Controller\Api;
|
||||||
|
|
||||||
|
use App\Controller\AbstractController;
|
||||||
|
use App\Support\ResponseTrait;
|
||||||
|
use Hyperf\Di\Annotation\Inject;
|
||||||
|
use Hyperf\HttpServer\Annotation\Controller;
|
||||||
|
use Hyperf\HttpServer\Annotation\GetMapping;
|
||||||
|
use Modules\{ModuleName}\Service\{ModuleName}Service;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
#[Controller(prefix: '/api/{module-kebab}')]
|
||||||
|
class {ModuleName}Controller extends AbstractController
|
||||||
|
{
|
||||||
|
use ResponseTrait;
|
||||||
|
|
||||||
|
#[Inject]
|
||||||
|
protected {ModuleName}Service ${moduleName}Service;
|
||||||
|
|
||||||
|
#[GetMapping(path: '')]
|
||||||
|
public function index(): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->success([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**管理端 API Controller**(放在 `Http/Controller/Admin/`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\{ModuleName}\Http\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Controller\AbstractController;
|
||||||
|
use App\Middleware\JwtAuthMiddleware;
|
||||||
|
use App\Support\ResponseTrait;
|
||||||
|
use Hyperf\Di\Annotation\Inject;
|
||||||
|
use Hyperf\HttpServer\Annotation\Controller;
|
||||||
|
use Hyperf\HttpServer\Annotation\GetMapping;
|
||||||
|
use Hyperf\HttpServer\Annotation\Middleware;
|
||||||
|
use Modules\{ModuleName}\Service\{ModuleName}Service;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
#[Controller(prefix: '/admin/{module-kebab}')]
|
||||||
|
#[Middleware(JwtAuthMiddleware::class)]
|
||||||
|
class {ModuleName}Controller extends AbstractController
|
||||||
|
{
|
||||||
|
use ResponseTrait;
|
||||||
|
|
||||||
|
#[Inject]
|
||||||
|
protected {ModuleName}Service ${moduleName}Service;
|
||||||
|
|
||||||
|
#[GetMapping(path: '')]
|
||||||
|
public function index(): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->success([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键区别**:
|
||||||
|
- `Api/` 下:前缀 `/api/`,命名空间含 `\Api\`
|
||||||
|
- `Admin/` 下:前缀 `/admin/`,命名空间含 `\Admin\`,自动加 `JwtAuthMiddleware`
|
||||||
|
- **两者类名相同**(均为 `{Name}Controller`),由命名空间区分,无需加接口类型前缀
|
||||||
|
|
||||||
|
### 7. 生成 Request 模板
|
||||||
|
|
||||||
|
**用户端 Request**(放在 `Http/Request/Api/`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\{ModuleName}\Http\Request\Api;
|
||||||
|
|
||||||
|
use Hyperf\Validation\Request\FormRequest;
|
||||||
|
|
||||||
|
class {Name}Request extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// 'field' => 'required|string|max:255',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**管理端 Request**(放在 `Http/Request/Admin/`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\{ModuleName}\Http\Request\Admin;
|
||||||
|
|
||||||
|
use Hyperf\Validation\Request\FormRequest;
|
||||||
|
|
||||||
|
class {Name}Request extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// 'field' => 'required|string|max:255',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键区别**:
|
||||||
|
- `Api/` 下:命名空间含 `\Request\Api\`,类名 `{Name}Request`
|
||||||
|
- `Admin/` 下:命名空间含 `\Request\Admin\`,类名 `{Name}Request`
|
||||||
|
- **两者类名相同**,由命名空间区分,无需加接口类型前缀
|
||||||
|
- Controller 中 `use` 路径需对应各自子命名空间
|
||||||
|
|
||||||
|
### 8. 验证模块自动发现
|
||||||
|
|
||||||
|
创建文件后**无需任何 composer 操作**,直接验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec hyperf-skeleton php -r "
|
||||||
|
define('BASE_PATH', '/opt/www');
|
||||||
|
require '/opt/www/vendor/autoload.php';
|
||||||
|
\$extra = Hyperf\Support\Composer::getMergedExtra('hyperf');
|
||||||
|
echo in_array('Modules\\{ModuleName}\\ConfigProvider', \$extra['config'] ?? []) ? 'OK' : 'FAIL';
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模块间依赖
|
||||||
|
|
||||||
|
若模块 A 依赖模块 B 的类:
|
||||||
|
- 通过 DI 注入模块 B 的 Service(不直接依赖 Controller/Model)
|
||||||
|
- 遵循依赖方向:上层模块 → 下层模块,禁止循环依赖
|
||||||
|
- ModuleLoader 的 glob 扫描天然支持多模块并存,无需额外配置
|
||||||
|
|
||||||
|
## 已有模块示例参考
|
||||||
|
|
||||||
|
当前项目 `modules/Auth/` 的实际结构(**参考标准**):
|
||||||
|
|
||||||
|
```
|
||||||
|
modules/Auth/src/Http/Controller/
|
||||||
|
├── Api/ # 用户端接口(推荐新规范)
|
||||||
|
│ ├── AuthController.php # prefix: /api/auth
|
||||||
|
│ ├── CaptchaController.php # prefix: /api/auth/captcha
|
||||||
|
│ └── SmsController.php # prefix: /api/auth/sms
|
||||||
|
└── Admin/ # 管理端接口(推荐新规范)
|
||||||
|
└── AdminAuthController.php # prefix: /admin/auth
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注:当前 Auth 模块尚未按此规范重构(所有 Controller 平铺在 `Controller/` 下),新模块应从一开始遵循子目录规范。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 模块目录结构完整(composer.json + src/ConfigProvider.php + Http/Controller/)
|
||||||
|
2. [ ] **Controller 按接口类型放入正确子目录**:`/api/*` → `Controller/Api/`,`/admin/*` → `Controller/Admin/`
|
||||||
|
3. [ ] Controller 命名空间包含正确子层级:`...\Http\Controller\Api\` 或 `...\Http\Controller\Admin\`
|
||||||
|
4. [ ] `Admin/` 下的 Controller 自动添加 `JwtAuthMiddleware`;类名与 `Api/` 下保持一致,无需加前缀
|
||||||
|
5. [ ] **Request 按接口类型放入正确子目录**:`/api/*` → `Request/Api/`,`/admin/*` → `Request/Admin/`
|
||||||
|
6. [ ] Request 命名空间包含正确子层级:`...\Http\Request\Api\` 或 `...\Http\Request\Admin\`
|
||||||
|
7. [ ] Controller 中 `use` 的 Request 路径与子目录一致
|
||||||
|
8. [ ] composer.json 的 `name` 使用 `modules/{kebab-case}` 格式
|
||||||
|
9. [ ] namespace 使用 `Modules\{PascalCase}` 格式
|
||||||
|
10. [ ] ConfigProvider 注册了 `__DIR__` 注解扫描路径
|
||||||
|
11. [ ] `extra.hyperf.config` 指向正确的 ConfigProvider 类
|
||||||
|
12. [ ] 无需修改主项目 `composer.json`,模块被 ModuleLoader 自动发现
|
||||||
|
13. [ ] `ProviderConfig::load()` 能发现模块 ConfigProvider
|
||||||
|
14. [ ] `php -l` 所有 PHP 文件语法正确
|
||||||
|
15. [ ] Controller 路由前缀遵循 RESTful 命名(kebab-case)
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
---
|
||||||
|
name: nginx-config
|
||||||
|
version: 2.0.0
|
||||||
|
description: "配置 Nginx 作为 Hyperf + Vue 3 的反向代理和静态文件服务。当需要 SSL、负载均衡或 Nginx 优化时使用。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔧 Nginx Config (Hyperf + Vue 3 SPA)
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户需要配置 Nginx 作为 Hyperf API + Vue 3 SPA 前端的反向代理、SSL 终端、静态文件服务器或负载均衡器。
|
||||||
|
|
||||||
|
## Phase 0:场景确认
|
||||||
|
|
||||||
|
| 场景 | 对应配置 |
|
||||||
|
|------|---------|
|
||||||
|
| 开发环境反向代理 | 基础 proxy_pass |
|
||||||
|
| 生产环境单机部署 | proxy_pass + SSL + 安全头 |
|
||||||
|
| 多实例负载均衡 | upstream + health check |
|
||||||
|
| 前端 SPA 静态资源 | try_files + 长期缓存 |
|
||||||
|
| WebSocket 支持 | upgrade + connection 头 |
|
||||||
|
| API 反向代理 | /api -> Hyperf 9501 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1:基础架构配置 (Hyperf + Vue 3 SPA)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/sites-available/myapp.conf
|
||||||
|
|
||||||
|
# ── 后端 Upstream ─────────────────────────
|
||||||
|
upstream hyperf_backend {
|
||||||
|
server 127.0.0.1:9501;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream hyperf_websocket {
|
||||||
|
server 127.0.0.1:9502;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── HTTP -> HTTPS 重定向 ──────────────────
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name example.com www.example.com;
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 主配置 ────────────────────────────────
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name example.com www.example.com;
|
||||||
|
|
||||||
|
# ── SSL 证书 ──────────────────────────
|
||||||
|
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
|
||||||
|
# ── 安全头 ────────────────────────────
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header X-Frame-Options DENY always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# ── 文件上传限制 ──────────────────────
|
||||||
|
client_max_body_size 20M;
|
||||||
|
|
||||||
|
# ── Gzip 压缩 ────────────────────────
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1k;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
application/json
|
||||||
|
application/typescript
|
||||||
|
text/xml
|
||||||
|
application/xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# ── Vue 3 SPA 前端 ───────────────────
|
||||||
|
root /var/www/myapp/Case-Database-Frontend-user/dist;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Vite 产物静态资源(带 hash 长期缓存)──
|
||||||
|
location /assets/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /favicon.ico {
|
||||||
|
expires 30d;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Hyperf API 反向代理 ──────────────
|
||||||
|
location /admin/ {
|
||||||
|
proxy_pass http://hyperf_backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── WebSocket 代理 ───────────────────
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://hyperf_websocket;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
proxy_connect_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 文件上传/下载 ────────────────────
|
||||||
|
location /admin/upload {
|
||||||
|
proxy_pass http://hyperf_backend;
|
||||||
|
client_max_body_size 50M;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 拒绝隐藏文件 ────────────────────
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2:负载均衡配置
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# 多 Hyperf 实例
|
||||||
|
upstream hyperf_backend {
|
||||||
|
least_conn;
|
||||||
|
server 10.0.0.1:9501 weight=3;
|
||||||
|
server 10.0.0.2:9501 weight=3;
|
||||||
|
server 10.0.0.3:9501 backup;
|
||||||
|
keepalive 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 健康检查(需要 nginx-upstream-check 模块或商业版)
|
||||||
|
# 替代方案:K8s Ingress + Pod 健康检查
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3:Let's Encrypt SSL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 Certbot
|
||||||
|
sudo apt install certbot python3-certbot-nginx
|
||||||
|
|
||||||
|
# 自动获取证书
|
||||||
|
sudo certbot --nginx -d example.com -d www.example.com
|
||||||
|
|
||||||
|
# 自动续期(Certbot 自动添加 cron)
|
||||||
|
sudo certbot renew --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4:Docker Compose 中的 Nginx
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml 中 Nginx 服务
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.25-alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/conf.d:/etc/nginx/conf.d
|
||||||
|
- ./Case-Database-Frontend-user/dist:/var/www/myapp/Case-Database-Frontend-user/dist:ro
|
||||||
|
- ./certbot/conf:/etc/letsencrypt:ro
|
||||||
|
depends_on:
|
||||||
|
- hyperf
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] `nginx -t` 配置语法检查通过
|
||||||
|
2. [ ] HTTPS 证书有效
|
||||||
|
3. [ ] SPA 路由刷新正常 (try_files -> /index.html)
|
||||||
|
4. [ ] API 代理 `/admin/*` 正常
|
||||||
|
5. [ ] WebSocket `/ws` 连接正常
|
||||||
|
6. [ ] 静态资源 `/assets/*` 带 Cache-Control
|
||||||
|
7. [ ] 安全头通过 securityheaders.com 检查
|
||||||
|
8. [ ] Gzip 压缩生效
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
name: performance-audit
|
||||||
|
version: 1.0.0
|
||||||
|
description: "分析并优化应用性能。当用户报告页面慢、加载卡或包体积大时使用。涵盖前端渲染、网络、打包和数据库四个维度。"
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/022-performance.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Performance Audit
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户报告性能问题、请求性能优化或要求性能审计。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 性能基线
|
||||||
|
|
||||||
|
收集当前数据:
|
||||||
|
```bash
|
||||||
|
# 前端打包分析 (Vite)
|
||||||
|
for dir in Case-Database-Frontend-user Case-Database-Frontend-admin; do
|
||||||
|
echo "=== $dir ==="
|
||||||
|
cd $dir && npm run build 2>&1 | tail -20
|
||||||
|
du -sh dist/
|
||||||
|
cd ..
|
||||||
|
done
|
||||||
|
|
||||||
|
# 后端状态
|
||||||
|
cd Case-Database-Backend && php bin/hyperf.php describe:routes | wc -l
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 四维度审计
|
||||||
|
|
||||||
|
**前端渲染**
|
||||||
|
- Vue DevTools 分析不必要的重渲染
|
||||||
|
- 检查大列表是否缺少 `v-memo` / `shallowRef`
|
||||||
|
- 检查大列表是否使用虚拟化(`@tanstack/vue-virtual`)
|
||||||
|
- 检查图片是否压缩(WebP 格式 + CDN)
|
||||||
|
- 管理端:Element Plus 是否按需导入(用户端不使用 Element Plus)
|
||||||
|
|
||||||
|
**网络请求**
|
||||||
|
- 是否有瀑布式请求(串行 → 并行)
|
||||||
|
- API 响应是否有 Redis 缓存策略
|
||||||
|
- Axios 是否配置请求/响应拦截
|
||||||
|
- 静态内容是否配置 Nginx 缓存
|
||||||
|
|
||||||
|
**打包体积**
|
||||||
|
- 是否有不必要的大依赖
|
||||||
|
- 是否使用 dynamic import 进行代码分割
|
||||||
|
- 是否 tree-shaking 有效
|
||||||
|
- 检查 barrel exports 是否导致全量导入
|
||||||
|
|
||||||
|
**数据库查询**
|
||||||
|
- N+1 查询检测
|
||||||
|
- 缺少索引的慢查询
|
||||||
|
- 未使用 `select` 限制字段
|
||||||
|
- 大数据集未分页
|
||||||
|
|
||||||
|
### 3. Core Web Vitals 目标
|
||||||
|
|
||||||
|
| 指标 | 目标 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| LCP | < 2.5s | Largest Contentful Paint |
|
||||||
|
| INP | < 200ms | Interaction to Next Paint |
|
||||||
|
| CLS | < 0.1 | Cumulative Layout Shift |
|
||||||
|
| FCP | < 1.8s | First Contentful Paint |
|
||||||
|
| TTFB | < 800ms | Time to First Byte |
|
||||||
|
|
||||||
|
### 4. 输出优化建议
|
||||||
|
|
||||||
|
按影响力排序,每个建议包含:预期收益 + 实施难度 + 具体代码。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 优化前后有可量化对比
|
||||||
|
2. [ ] 未引入功能回归
|
||||||
|
3. [ ] Core Web Vitals 达标
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# 性能优化模式
|
||||||
|
|
||||||
|
## Vue 3 前端优化
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 1. v-memo — 防止不必要的重渲染 -->
|
||||||
|
<template>
|
||||||
|
<div v-memo="[item.id, item.name]">
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 2. computed — 缓存计算结果 -->
|
||||||
|
<script setup>
|
||||||
|
const sorted = computed(() => [...items.value].sort(compareFn))
|
||||||
|
|
||||||
|
// 3. shallowRef — 大型对象避免深层响应
|
||||||
|
const heavyData = shallowRef(null)
|
||||||
|
|
||||||
|
// 4. 异步组件 — 代码分割
|
||||||
|
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))
|
||||||
|
|
||||||
|
// 5. 虚拟化 — 大列表
|
||||||
|
import { useVirtualizer } from '@tanstack/vue-virtual'
|
||||||
|
|
||||||
|
// 6. keep-alive — 页面缓存
|
||||||
|
// <keep-alive :include="cachedViews"><router-view /></keep-alive>
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vite 构建优化
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
target: 'es2015',
|
||||||
|
minify: 'terser',
|
||||||
|
terserOptions: {
|
||||||
|
compress: { drop_console: true, drop_debugger: true },
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||||
|
// 仅管理端:'element-plus': ['element-plus'],
|
||||||
|
'echarts': ['echarts'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hyperf 后端优化
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 1. 协程并发 — 并行无依赖 I/O
|
||||||
|
$parallel = new Parallel(10);
|
||||||
|
$parallel->add(fn() => $this->orderService->getStatistics($id));
|
||||||
|
$parallel->add(fn() => $this->paymentService->getPayments($id));
|
||||||
|
[$stats, $payments] = $parallel->wait();
|
||||||
|
|
||||||
|
// 2. 连接池调优
|
||||||
|
'pool' => [
|
||||||
|
'min_connections' => 5,
|
||||||
|
'max_connections' => 50,
|
||||||
|
'wait_timeout' => 3.0,
|
||||||
|
],
|
||||||
|
|
||||||
|
// 3. 缓存注解
|
||||||
|
#[Cacheable(prefix: 'user', ttl: 3600)]
|
||||||
|
public function getById(int $id): ?User { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据库优化
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 复合索引:遵循最左前缀原则
|
||||||
|
CREATE INDEX idx_orders_status_created ON production_orders(status, created_at);
|
||||||
|
|
||||||
|
-- 覆盖索引:避免回表
|
||||||
|
CREATE INDEX idx_orders_cover ON production_orders(status, total_amount, paid_amount);
|
||||||
|
|
||||||
|
-- 游标分页(百万级数据)
|
||||||
|
SELECT * FROM orders WHERE id > ? ORDER BY id ASC LIMIT 20;
|
||||||
|
```
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: redis-cache
|
||||||
|
version: 3.0.0
|
||||||
|
description: "为 Hyperf/Swoole 应用设计 Redis 缓存策略。当需要缓存、限流或 Session 存储时使用。涵盖 TTL 设计、缓存失效和高并发模式。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔴 Redis Cache (Hyperf + Swoole)
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户需要为应用添加缓存层、会话存储、限流计数器或消息队列。
|
||||||
|
|
||||||
|
## Phase 0:场景确认
|
||||||
|
|
||||||
|
| 场景 | 数据结构 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| API 响应 | STRING | key-value + TTL |
|
||||||
|
| Session/Token | HASH | 多字段 |
|
||||||
|
| 排行榜 | ZSET | 范围查询 |
|
||||||
|
| 限流 | ZSET 滑动窗口 | 精准窗口 |
|
||||||
|
| 标签/权限 | SET | 集合运算 |
|
||||||
|
| 消息队列 | LIST/STREAM | FIFO |
|
||||||
|
| 分布式锁 | STRING + SET NX | 原子性 |
|
||||||
|
| WebSocket | HASH | fd→userId |
|
||||||
|
|
||||||
|
## Phase 1:连接配置
|
||||||
|
|
||||||
|
`config/autoload/redis.php`:default 与 cache 独立连接池,环境变量 REDIS_HOST/PORT/AUTH/DB/CACHE_DB。
|
||||||
|
|
||||||
|
## Phase 2:缓存策略
|
||||||
|
|
||||||
|
Cache-Aside:`withCache($key, $ttl, $fetcher)`,fetcher 内查 DB,setex 加 jitter。invalidate/invalidatePattern。
|
||||||
|
|
||||||
|
CachedRepository:装饰器包装 Repository,getById/getPageList 走缓存,写后 invalidateAll。读写比 > 10:1 用装饰器,< 10:1 用 Service 内 CacheService,热点数据用 Redis 原生。完整实现见 **Tier 3**。
|
||||||
|
|
||||||
|
## Phase 3:TTL 与 Key 规范
|
||||||
|
|
||||||
|
Profile 30m、列表 5m、配置 1h、JWT 2h、Session 24h。Key 格式:`namespace:entity:id:field`。
|
||||||
|
|
||||||
|
## Phase 4:分布式锁
|
||||||
|
|
||||||
|
withLock($lockKey, $ttlMs, $fn),SET NX PX,释放用 Lua 原子校验 value 后 DEL。
|
||||||
|
|
||||||
|
## Phase 5:穿透/击穿防护
|
||||||
|
|
||||||
|
穿透:空值缓存 NULL_PLACEHOLDER。击穿:热点 key 用 withLock 互斥 + double-check。
|
||||||
|
|
||||||
|
## Phase 6:监控
|
||||||
|
|
||||||
|
cache hit/miss 计数,redis-cli INFO stats。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 连接使用环境变量
|
||||||
|
2. [ ] Key 命名规范
|
||||||
|
3. [ ] 所有 Key 有 TTL
|
||||||
|
4. [ ] 失效与更新逻辑同步
|
||||||
|
5. [ ] 锁用 Lua 原子释放
|
||||||
|
6. [ ] 使用连接池
|
||||||
|
7. [ ] 百万级有穿透/击穿防护
|
||||||
|
8. [ ] CachedRepository 写后 invalidateAll
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `references/cache-implementation.md` | 连接、Cache-Aside、CachedRepository、TTL、锁、穿透/击穿完整代码 |
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Redis Cache — 实现细节
|
||||||
|
|
||||||
|
> 主流程见 SKILL.md,本文档为连接、Cache-Aside、CachedRepository、TTL、分布式锁、穿透/击穿防护的完整代码。
|
||||||
|
|
||||||
|
## 连接配置
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/autoload/redis.php
|
||||||
|
return [
|
||||||
|
'default' => ['host' => env('REDIS_HOST'), 'auth' => env('REDIS_AUTH'), 'port' => (int) env('REDIS_PORT', 6379), 'db' => (int) env('REDIS_DB', 0), 'pool' => ['min_connections' => 5, 'max_connections' => 30, ...]],
|
||||||
|
'cache' => ['db' => (int) env('REDIS_CACHE_DB', 1), 'pool' => [...]] // 独立连接池
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cache-Aside 模式
|
||||||
|
|
||||||
|
CacheService:`withCache($key, $ttl, $fetcher)` — get 命中则返回,否则 fetcher() 后 setex,TTL 加 jitter 防 stampede。`invalidate($keys)`、`invalidatePattern($pattern)` 用 scan 分批 del。
|
||||||
|
|
||||||
|
## CachedRepository 装饰器
|
||||||
|
|
||||||
|
实现 RepositoryInterface,包装 baseRepo。getById/getPageList 用 withCache,key 格式 `{entity}:{id}` 和 `{entity}:list:{params_hash}`。invalidateDetail/invalidateList/invalidateAll。DI 注册时用 closure 返回 new CachedRepository(baseRepo, cacheService, 'order', detailTtl, listTtl)。Service 写操作后判断 `$repository instanceof CachedRepository` 则 invalidateAll。
|
||||||
|
|
||||||
|
## TTL 规范
|
||||||
|
|
||||||
|
Profile 30m、列表 5m、配置 1h、JWT 2h、Session 24h、限流与窗口对齐、验证码 5m、锁 任务时长+30s。
|
||||||
|
|
||||||
|
## Key 命名
|
||||||
|
|
||||||
|
`<namespace>:<entity>:<id>:<field>`,如 user:profile:123、orders:list:page:1、rate:limit:ip:api、session:user:456、lock:order:789、ws:connection:fd:100。
|
||||||
|
|
||||||
|
## 分布式锁
|
||||||
|
|
||||||
|
RedisLockService:withLock($lockKey, $ttlMs, $fn)。SET NX PX。释放用 Lua 脚本原子校验 value 后 DEL。
|
||||||
|
|
||||||
|
## 缓存穿透/击穿
|
||||||
|
|
||||||
|
穿透:不存在 key 缓存 NULL_PLACEHOLDER(TTL 30s)。击穿:热点 key 用 withLock 互斥,double-check 后 fetcher 再 setex。
|
||||||
|
|
||||||
|
## 装饰器 vs 直接缓存 决策
|
||||||
|
|
||||||
|
读写比 > 10:1 → CachedRepository。读写比 < 10:1 → Service 内 CacheService。热点数据(排行榜)→ Redis 原生 ZSET/INCR。
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
name: refactoring
|
||||||
|
version: 1.1.0
|
||||||
|
description: "基于测试保障的安全重构工作流,行为不变前提下改善代码结构。当需要重构、清理代码或消除技术债时使用。"
|
||||||
|
requires: [vue-testing]
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/033-refactoring.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Refactoring Workflow
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求重构代码、清理技术债、优化代码结构或简化复杂逻辑。
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
**重构 = 不改变外部行为的前提下改善内部结构**
|
||||||
|
|
||||||
|
- 每一步都要小到"显然正确"
|
||||||
|
- 每一步之后测试必须全部通过
|
||||||
|
- 如果测试不足,先补测试再重构
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 评估现状
|
||||||
|
|
||||||
|
1. 读取目标代码,识别"代码坏味道"
|
||||||
|
2. 运行现有测试确认基线:`npm test -- --related`
|
||||||
|
3. 如果测试覆盖不足,**先补充测试**
|
||||||
|
|
||||||
|
### 2. 制定重构计划
|
||||||
|
|
||||||
|
常见重构模式:
|
||||||
|
|
||||||
|
| 坏味道 | 重构手法 | 风险 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| 函数过长(>50行) | Extract Function | 低 |
|
||||||
|
| 重复代码 | Extract + Consolidate | 低 |
|
||||||
|
| 过长参数列表 | Introduce Parameter Object | 低 |
|
||||||
|
| Switch/If 过多 | Replace with Polymorphism | 中 |
|
||||||
|
| 大类 | Extract Class | 中 |
|
||||||
|
| Feature Envy | Move Method | 中 |
|
||||||
|
| 深层嵌套 | Replace Nested with Guard Clauses | 低 |
|
||||||
|
|
||||||
|
### 3. 小步执行
|
||||||
|
|
||||||
|
对每一步:
|
||||||
|
1. 做一个**原子级**的重构变更
|
||||||
|
2. 运行测试 → 全部通过
|
||||||
|
3. Git commit(可选,但建议频繁提交)
|
||||||
|
4. 继续下一步
|
||||||
|
|
||||||
|
### 4. 验证
|
||||||
|
|
||||||
|
1. 运行完整测试套件
|
||||||
|
2. ESLint 无报错
|
||||||
|
3. 功能行为未改变
|
||||||
|
|
||||||
|
## 禁止事项
|
||||||
|
|
||||||
|
- ❌ 不要在重构中同时添加新功能
|
||||||
|
- ❌ 不要一次改太多(改完跑不了测试)
|
||||||
|
- ❌ 不要在没有测试的情况下重构核心逻辑
|
||||||
|
- ❌ 不要重构正在被其他人修改的代码
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 所有测试通过
|
||||||
|
2. [ ] ESLint 无报错
|
||||||
|
3. [ ] 外部行为未改变
|
||||||
|
4. [ ] 代码可读性提升(主观判断)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
name: security-audit
|
||||||
|
version: 3.0.0
|
||||||
|
description: "遵循 OWASP Top 10 对 PHP Hyperf + Vue 3 应用进行安全审查和加固。当需要安全扫描、漏洞检测或安全加固时使用。"
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/skill-security-audit.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# 🔒 Security Audit (PHP Hyperf + Vue 3)
|
||||||
|
|
||||||
|
## ⚠️ 安全等级:RED — 审计结果包含敏感信息
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求安全审查、漏洞扫描、安全加固或合规性检查。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 依赖安全扫描
|
||||||
|
|
||||||
|
`npm audit --audit-level=high`、`composer audit`。
|
||||||
|
|
||||||
|
### 2. OWASP Top 10 检查
|
||||||
|
|
||||||
|
| # | 风险 | 检查方法 |
|
||||||
|
|---|------|---------|
|
||||||
|
| A01 | 访问控制失效 | auth middleware + 数据权限 |
|
||||||
|
| A02 | 加密失败 | bcrypt/Argon2id、HTTPS、JWT 强度 |
|
||||||
|
| A03 | 注入 | 参数化查询、ORM、用户输入 |
|
||||||
|
| A04 | 不安全设计 | 业务逻辑、并发、分布式锁 |
|
||||||
|
| A05 | 安全配置 | CORS、headers、debug 关闭 |
|
||||||
|
| A06 | 过时组件 | npm/composer audit |
|
||||||
|
| A07 | 认证失败 | JWT 双 Token、密码策略、登录限流 |
|
||||||
|
| A08 | 数据完整性 | 反序列化、CI/CD |
|
||||||
|
| A09 | 日志监控 | 覆盖率和告警 |
|
||||||
|
| A10 | SSRF | HTTP 请求目标验证 |
|
||||||
|
|
||||||
|
### 3–5. PHP / 前端 / 密钥扫描
|
||||||
|
|
||||||
|
PHPStan、危险函数(eval/exec/system 等)、反序列化、文件包含、SQL 拼接。前端 v-html、eval、innerHTML、localStorage Token、target=_blank 无 noopener。密钥格式:AWS/GitHub/Stripe/Google/JWT/私钥/连接串。完整命令见 **Tier 3**。
|
||||||
|
|
||||||
|
### 6. 安全头与 CORS
|
||||||
|
|
||||||
|
CSP、HSTS、X-Content-Type-Options、X-Frame-Options、X-XSS-Protection、Referrer-Policy。CORS 必须在中间件集中配置,生产无 Allow-Origin: *。
|
||||||
|
|
||||||
|
### 6.5–6.7. CodeGuard 增强
|
||||||
|
|
||||||
|
密码哈希 Argon2id、IDOR、Mass Assignment、Session Cookie、禁用算法、文件上传、SSRF。完整检查与示例见 **Tier 3**。
|
||||||
|
|
||||||
|
### 7. 中间件链验证
|
||||||
|
|
||||||
|
CorsMiddleware → TraceId → RequestLog → Auth → Permission → RateLimit → RequestSign。
|
||||||
|
|
||||||
|
### 8. 输出报告
|
||||||
|
|
||||||
|
Critical/High/Medium/Low 分级。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] OWASP Top 10 逐项完成
|
||||||
|
2. [ ] npm/composer audit 无 critical/high
|
||||||
|
3. [ ] PHPStan 无错误
|
||||||
|
4. [ ] 无硬编码密钥
|
||||||
|
5. [ ] 安全头已配置
|
||||||
|
6. [ ] 中间件链完整
|
||||||
|
7. [ ] 无危险函数
|
||||||
|
8. [ ] 无 IDOR、Mass Assignment
|
||||||
|
9. [ ] 无禁用算法,密码 Argon2id
|
||||||
|
10. [ ] Session Cookie Secure+HttpOnly+SameSite
|
||||||
|
11. [ ] 文件上传 UUID + magic bytes
|
||||||
|
12. [ ] 外部请求有白名单
|
||||||
|
13. [ ] CORS 集中配置
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `references/audit-commands.md` | PHP/前端/密钥/CodeGuard 完整检查命令与最佳实践 |
|
||||||
|
| `references/codeguard/*` | CodeGuard 各维度详细策略 |
|
||||||
|
| `references/security-headers.md` | Nginx 安全响应头 |
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Security Audit — 检查命令与代码示例
|
||||||
|
|
||||||
|
> 主流程见 SKILL.md,本文档为 PHP/前端/密钥/CodeGuard 的完整检查命令和最佳实践。
|
||||||
|
|
||||||
|
## 依赖扫描
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm audit --audit-level=high
|
||||||
|
composer audit
|
||||||
|
```
|
||||||
|
|
||||||
|
## PHP 检查命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vendor/bin/phpstan analyse --level=max
|
||||||
|
rg -n "eval\(|exec\(|system\(|passthru\(|shell_exec\(|popen\(" --type php --glob '!vendor/**'
|
||||||
|
rg -n "unserialize\(" --type php --glob '!vendor/**'
|
||||||
|
rg -n "include\s*\\\$|require\s*\\\$" --type php --glob '!vendor/**'
|
||||||
|
rg -n "Db::raw\(|DB::select\(.*\\\$" --type php --glob '!vendor/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 前端检查命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "v-html" --glob '*.vue' --glob '*.ts'
|
||||||
|
rg -n "eval\(|new Function\(" --glob '*.ts' --glob '!node_modules/**'
|
||||||
|
rg -n "\.innerHTML\s*=|\.outerHTML\s*=" --glob '*.vue' --glob '!node_modules/**'
|
||||||
|
rg -n "localStorage\.(set|get)Item.*[Tt]oken" --glob '*.ts' --glob '!node_modules/**'
|
||||||
|
rg -n "target=\"_blank\"" --glob '*.vue' | rg -v "noopener"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 密钥扫描命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 通用、AWS、GitHub、Stripe、Google、JWT、私钥、数据库连接串
|
||||||
|
rg -rn "(?i)(api.?key|secret|password|token)\s*[=:]\s*['\"][a-zA-Z0-9]{8,}" --glob '!vendor/**' --glob '!node_modules/**'
|
||||||
|
rg -rn "A(KIA|GPA|IDA|ROA)[0-9A-Z]{16}" ...
|
||||||
|
rg -rn "gh[pousr]_[a-zA-Z0-9]{36}" ...
|
||||||
|
rg -rn "sk_live_|pk_live_|sk_test_[a-zA-Z0-9]{24}" ...
|
||||||
|
rg -rn "AIza[a-zA-Z0-9_\-]{35}" ...
|
||||||
|
rg -rn "eyJ[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+" ...
|
||||||
|
rg -rn "BEGIN\s+(RSA\s+)?PRIVATE\s+KEY" ...
|
||||||
|
rg -rn "(mysql|mongodb|redis|postgres)://[^:]+:[^@]+" ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS 集中配置
|
||||||
|
|
||||||
|
❌ Controller 内单独设置 Allow-Origin: *。✅ CorsMiddleware 集中配置,env('CORS_ORIGIN'),Allow-Credentials 与 Allow-Origin: * 不共存。
|
||||||
|
|
||||||
|
## 密码哈希
|
||||||
|
|
||||||
|
❌ md5/sha1、PASSWORD_BCRYPT 无 cost。✅ PASSWORD_ARGON2ID(memory_cost 65536, time_cost 4)或 PASSWORD_BCRYPT cost=12。验证用 password_verify,升级用 password_needs_rehash。
|
||||||
|
|
||||||
|
## CodeGuard 增强
|
||||||
|
|
||||||
|
IDOR:find/findOrFail($request/$id) 未附加所有权 → 应 $user->orders()->findOrFail($id)。Mass Assignment:fill($request->all())、create/update($request->all())。Session:setcookie 需 Secure+HttpOnly+SameSite。禁用算法:md5/sha1、AES-ECB。文件上传:不用 getClientFilename() 原始名,用 UUID;MIME 用 finfo magic bytes。SSRF:Guzzle/curl 对用户 URL 需域名白名单。
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
description: API & Web services security (REST/GraphQL/SOAP), schema validation, authn/z, SSRF
|
||||||
|
languages:
|
||||||
|
- c
|
||||||
|
- go
|
||||||
|
- java
|
||||||
|
- typescript
|
||||||
|
- php
|
||||||
|
- python
|
||||||
|
- ruby
|
||||||
|
- xml
|
||||||
|
- yaml
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
rule_id: codeguard-0-api-web-services
|
||||||
|
|
||||||
|
## API & Web Services Security
|
||||||
|
|
||||||
|
Secure REST, GraphQL, and SOAP/WS services end‑to‑end: transport, authn/z, schema validation, SSRF controls, DoS limits, and microservice‑safe patterns.
|
||||||
|
|
||||||
|
### Transport and TLS
|
||||||
|
- HTTPS only; consider mTLS for high‑value/internal services. Validate certs (CN/SAN, revocation) and prevent mixed content.
|
||||||
|
|
||||||
|
### Authentication and Tokens
|
||||||
|
- Use standard flows (OAuth2/OIDC) for clients; avoid custom schemes. For services, use mTLS or signed service tokens.
|
||||||
|
- JWTs: pin algorithms; validate iss/aud/exp/nbf; short lifetimes; rotation; denylist on logout/revoke. Prefer opaque tokens when revocation is required and central store is available.
|
||||||
|
- API keys: scope narrowly; rate limit; monitor usage; do not use alone for sensitive operations.
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
- Enforce per‑endpoint, per‑resource checks server‑side; deny by default.
|
||||||
|
- For microservices, authorize at gateway (coarse) and service (fine) layers; propagate signed internal identity, not external tokens.
|
||||||
|
|
||||||
|
### Input and Content Handling
|
||||||
|
- Validate inputs via contracts: OpenAPI/JSON Schema, GraphQL SDL, XSD. Reject unknown fields and oversize payloads; set limits.
|
||||||
|
- Content types: enforce explicit Content‑Type/Accept; reject unsupported combinations. Harden XML parsers against XXE/expansion.
|
||||||
|
|
||||||
|
### SQL/Injection Safety in Resolvers and Handlers
|
||||||
|
- Use parameterized queries/ORM bind parameters; never concatenate user input into queries or commands.
|
||||||
|
|
||||||
|
### GraphQL‑Specific Controls
|
||||||
|
- Limit query depth and overall complexity; enforce pagination; timeouts on execution; disable introspection and IDEs in production.
|
||||||
|
- Implement field/object‑level authorization to prevent IDOR/BOLA; validate batching and rate limit per object type.
|
||||||
|
|
||||||
|
### SSRF Prevention for Outbound Calls
|
||||||
|
- Do not accept raw URLs. Validate domains/IPs using libraries; restrict to HTTP/HTTPS only (block file://, gopher://, ftp://, etc.).
|
||||||
|
- Case 1 (fixed partners): strict allow‑lists; disable redirects; network egress allow‑lists.
|
||||||
|
- Case 2 (arbitrary): block private/link‑local/localhost ranges; resolve and verify all IPs are public; require signed tokens from the target where feasible.
|
||||||
|
|
||||||
|
### SOAP/WS and XML Safety
|
||||||
|
- Validate SOAP payloads with XSD; limit message sizes; enable XML signatures/encryption where required.
|
||||||
|
- Configure parsers against XXE, entity expansion, and recursive payloads; scan attachments.
|
||||||
|
|
||||||
|
### Rate Limiting and DoS
|
||||||
|
- Apply per‑IP/user/client limits, circuit breakers, and timeouts. Use server‑side batching and caching to reduce load.
|
||||||
|
|
||||||
|
### Management Endpoints
|
||||||
|
- Do not expose over the Internet. Require strong auth (MFA), network restrictions, and separate ports/hosts.
|
||||||
|
|
||||||
|
### Testing and Assessment
|
||||||
|
- Maintain formal API definitions; drive contract tests and fuzzing from specs.
|
||||||
|
- Assess endpoints for authn/z bypass, SSRF, injection, and information leakage; log token validation failures.
|
||||||
|
|
||||||
|
### Microservices Practices
|
||||||
|
- Policy‑as‑code with embedded decision points; sidecar or library PDPs.
|
||||||
|
- Service identity via mTLS or signed tokens; never reuse external tokens internally.
|
||||||
|
- Centralized structured logging with correlation IDs; sanitize sensitive data.
|
||||||
|
|
||||||
|
### Implementation Checklist
|
||||||
|
- HTTPS/mTLS configured; certs managed; no mixed content.
|
||||||
|
- Contract validation at the edge and service; unknown fields rejected; size/time limits enforced.
|
||||||
|
- Strong authn/z per endpoint; GraphQL limits applied; introspection disabled in prod.
|
||||||
|
- SSRF protections at app and network layers; redirects disabled; allow‑lists where possible.
|
||||||
|
- Rate limiting, circuit breakers, and resilient patterns in place.
|
||||||
|
- Management endpoints isolated and strongly authenticated.
|
||||||
|
- Logs structured and privacy‑safe with correlation IDs.
|
||||||
|
|
||||||
|
### Test Plan
|
||||||
|
- Contract tests for schema adherence; fuzzing with schema‑aware tools.
|
||||||
|
- Pen tests for SSRF, IDOR/BOLA, and authz bypass; performance tests for DoS limits.
|
||||||
|
- Test all HTTP methods per endpoint; discover parameters in URL paths, headers, and structured data beyond obvious query strings.
|
||||||
|
- Automated checks for token validation and revocation behavior.
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
description: Authentication and MFA best practices (passwords, MFA, OAuth/OIDC, SAML, recovery, tokens)
|
||||||
|
languages:
|
||||||
|
- c
|
||||||
|
- go
|
||||||
|
- java
|
||||||
|
- typescript
|
||||||
|
- kotlin
|
||||||
|
- matlab
|
||||||
|
- php
|
||||||
|
- python
|
||||||
|
- ruby
|
||||||
|
- swift
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
rule_id: codeguard-0-authentication-mfa
|
||||||
|
|
||||||
|
## Authentication & MFA
|
||||||
|
|
||||||
|
Build a resilient, user-friendly authentication system that resists credential attacks, protects secrets, and supports strong, phishing-resistant MFA and secure recovery.
|
||||||
|
|
||||||
|
### Account Identifiers and UX
|
||||||
|
- Use non-public, random, and unique internal user identifiers. Allow login via verified email or username.
|
||||||
|
- Always return generic error messages (e.g., "Invalid username or password"). Keep timing consistent to prevent account enumeration.
|
||||||
|
- Support password managers: `<input type="password">`, allow paste, no JS blocks.
|
||||||
|
|
||||||
|
### Password Policy
|
||||||
|
- Accept passphrases and full Unicode; minimum 8 characters; avoid composition rules. Set only a reasonable maximum length (64+).
|
||||||
|
- Check new passwords against breach corpora (e.g., k‑anonymity APIs); reject breached/common passwords.
|
||||||
|
|
||||||
|
### Password Storage (Hashing)
|
||||||
|
- Hash, do not encrypt. Use slow, memory‑hard algorithms with unique per‑user salts and constant‑time comparison.
|
||||||
|
- Preferred order and parameters (tune to your hardware; target <1s on server):
|
||||||
|
- Argon2id: m=19–46 MiB, t=2–1, p=1 (or equivalent security trade‑offs)
|
||||||
|
- scrypt: N=2^17, r=8, p=1 (or equivalent)
|
||||||
|
- bcrypt (legacy only): cost ≥10, be aware of 72‑byte input limit
|
||||||
|
- PBKDF2 (FIPS): PBKDF2‑HMAC‑SHA‑256 ≥600k, or SHA‑1 ≥1.3M
|
||||||
|
- Optional pepper: store outside DB (KMS/HSM); if used, apply via HMAC or pre‑hashing. Plan for user resets if pepper rotates.
|
||||||
|
- Unicode and null bytes must be supported end‑to‑end by the library.
|
||||||
|
|
||||||
|
### Authentication Flow Hardening
|
||||||
|
- Enforce TLS for all auth endpoints and token transport; enable HSTS.
|
||||||
|
- Implement rate limits per IP, account, and globally; add proof‑of‑work or CAPTCHA only as last resort.
|
||||||
|
- Lockouts/throttling: progressive backoff; avoid permanent lockout via resets/alerts.
|
||||||
|
- Uniform responses and code paths to reduce oracle/timing signals.
|
||||||
|
|
||||||
|
### Multi‑Factor Authentication (MFA)
|
||||||
|
- Adopt phishing‑resistant factors by default for sensitive accounts: passkeys/WebAuthn (FIDO2) or hardware U2F.
|
||||||
|
- Acceptable: TOTP (app‑based), smart cards with PIN. Avoid for sensitive use: SMS/voice, email codes; never rely on security questions.
|
||||||
|
- Require MFA for: login, password/email changes, disabling MFA, privilege elevation, high‑value transactions, new devices/locations.
|
||||||
|
- Risk‑based MFA signals: new device, geo‑velocity, IP reputation, unusual time, breached credentials.
|
||||||
|
- MFA recovery: provide single‑use backup codes, encourage multiple factors, and require strong identity verification for resets.
|
||||||
|
- Handle failed MFA: offer alternative enrolled methods, notify users of failures, and log context (no secrets).
|
||||||
|
|
||||||
|
### Federation and Protocols (OAuth 2.0 / OIDC / SAML)
|
||||||
|
- Use standard protocols only; do not build your own.
|
||||||
|
- OAuth 2.0/OIDC:
|
||||||
|
- Prefer Authorization Code with PKCE for public/native apps; avoid Implicit and ROPC.
|
||||||
|
- Validate state and nonce; use exact redirect URI matching; prevent open redirects.
|
||||||
|
- Constrain tokens to audience/scope; use DPoP or mTLS for sender‑constraining when possible.
|
||||||
|
- Rotate refresh tokens; revoke on logout or risk signals.
|
||||||
|
- SAML:
|
||||||
|
- TLS 1.2+; sign responses/assertions; encrypt sensitive assertions.
|
||||||
|
- Validate issuers, InResponseTo, timestamps (NotBefore/NotOnOrAfter), Recipient; verify against trusted keys.
|
||||||
|
- Prevent XML signature wrapping with strict schema validation and hardened XPath selection.
|
||||||
|
- Keep response lifetimes short; prefer SP‑initiated flows; validate RelayState; implement replay detection.
|
||||||
|
|
||||||
|
### Tokens (JWT and Opaque)
|
||||||
|
- Prefer opaque server‑managed tokens for simplicity and revocation. If using JWTs:
|
||||||
|
- Explicitly pin algorithms; reject "none"; validate iss/aud/exp/iat/nbf; use short lifetimes and rotation.
|
||||||
|
- Store secrets/keys securely (KMS/HSM). Use strong HMAC secrets or asymmetric keys; never hardcode.
|
||||||
|
- Consider binding tokens to a client context (e.g., fingerprint hash in cookie) to reduce replay.
|
||||||
|
- Implement denylist/allowlist for revocation on logout and critical events.
|
||||||
|
|
||||||
|
### Recovery and Reset
|
||||||
|
- Return the same response for existing and non‑existing accounts (no enumeration). Normalize timing.
|
||||||
|
- Generate 32+ byte, CSPRNG tokens; single‑use; store as hashes; short expiry.
|
||||||
|
- Use HTTPS reset links to pinned, trusted domains; add referrer policy (no‑referrer) on UI.
|
||||||
|
- After reset: require re‑authentication, rotate sessions, and do not auto‑login.
|
||||||
|
- Never lock accounts due to reset attempts; rate‑limit and monitor instead.
|
||||||
|
|
||||||
|
### Administrative and Internal Accounts
|
||||||
|
- Separate admin login from public forms; enforce stronger MFA, device posture checks, IP allowlists, and step‑up auth.
|
||||||
|
- Use distinct session contexts and stricter timeouts for admin operations.
|
||||||
|
|
||||||
|
### Monitoring and Signals
|
||||||
|
- Log auth events (failures/successes, MFA enroll/verify, resets, lockouts) with stable fields and correlation IDs; never log secrets or raw tokens.
|
||||||
|
- Detect credential stuffing: high failure rates, many IPs/agents, impossible travel. Notify users of new device logins.
|
||||||
|
|
||||||
|
### Implementation Checklist
|
||||||
|
- Passwords: Argon2id (preferred) with per‑user salt, constant‑time verify; breached password checks on change/set.
|
||||||
|
- MFA: WebAuthn/passkeys or hardware tokens for high‑risk; TOTP as fallback; secure recovery with backup codes.
|
||||||
|
- Federation: Authorization Code + PKCE; strict redirect URI validation; audience/scope enforced; token rotation.
|
||||||
|
- Tokens: short‑lived, sender‑constrained where possible; revocation implemented; secrets in KMS/HSM.
|
||||||
|
- Recovery: single‑use, hashed, time‑boxed tokens; consistent responses; re‑auth required after reset; sessions rotated.
|
||||||
|
- Abuse: rate limits, throttling, and anomaly detection on auth endpoints; uniform error handling.
|
||||||
|
- Admin: isolated flows with stricter policies and device checks.
|
||||||
|
|
||||||
|
### Test Plan
|
||||||
|
- Unit/integration tests for login, MFA enroll/verify, resets, and lockouts with uniform errors.
|
||||||
|
- Protocol tests: PKCE, state/nonce, redirect URI validation, token audience/scope.
|
||||||
|
- Dynamic tests for credential stuffing resistance and token replay; validate revocation after logout and role change.
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
description: Authorization and access control (RBAC/ABAC/ReBAC, IDOR, mass assignment, transaction auth)
|
||||||
|
languages:
|
||||||
|
- c
|
||||||
|
- go
|
||||||
|
- java
|
||||||
|
- typescript
|
||||||
|
- php
|
||||||
|
- python
|
||||||
|
- ruby
|
||||||
|
- yaml
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
rule_id: codeguard-0-authorization-access-control
|
||||||
|
|
||||||
|
## Authorization & Access Control
|
||||||
|
|
||||||
|
Enforce least privilege and precise access decisions for every request and resource, prevent IDOR and mass assignment, and provide strong transaction authorization where necessary.
|
||||||
|
|
||||||
|
### Core Principles
|
||||||
|
1. Deny by Default: The default for any access request should be 'deny'. Explicitly grant permissions to roles or users rather than explicitly denying them. When no allow rule matches, return HTTP 403 Forbidden.
|
||||||
|
2. Principle of Least Privilege: Grant users the minimum level of access required to perform their job functions. Regularly audit permissions to ensure they are not excessive.
|
||||||
|
3. Validate Permissions on Every Request: Check authorization for every single request, regardless of source (AJAX, API, direct). Use middleware/filters to ensure consistent enforcement.
|
||||||
|
4. Prefer ABAC/ReBAC over RBAC: Use Attribute-Based Access Control (ABAC) or Relationship-Based Access Control (ReBAC) for fine-grained permissions instead of simple role-based access control.
|
||||||
|
|
||||||
|
### Systemic Controls
|
||||||
|
- Centralize authorization at service boundaries via middleware/policies/filters.
|
||||||
|
- Model permissions at the resource level (ownership/tenancy) and enforce scoping in data queries.
|
||||||
|
- Return generic 403/404 responses to avoid leaking resource existence.
|
||||||
|
- Log all denials with user, action, resource identifier (non-PII), and rationale code.
|
||||||
|
|
||||||
|
### Preventing IDOR
|
||||||
|
- Never trust user-supplied identifiers alone. Always verify access to each object instance.
|
||||||
|
- Resolve resources through user-scoped queries or server-side lookups. Example: `currentUser.projects.find(id)` instead of `Project.find(id)`.
|
||||||
|
- Use non-enumerable identifiers (UUIDs/random) as defense-in-depth. Do not rely on obscurity alone.
|
||||||
|
|
||||||
|
### Preventing Mass Assignment
|
||||||
|
- Do not bind request bodies directly to domain objects containing sensitive fields.
|
||||||
|
- Expose only safe, editable fields via DTOs. Maintain explicit allow-lists for patch/update.
|
||||||
|
- Use framework features to block-list sensitive fields if allow-listing is infeasible.
|
||||||
|
|
||||||
|
### Transaction Authorization (Step-Up)
|
||||||
|
- Require a second factor for sensitive actions (wire transfers, privilege elevation, data export). Apply What‑You‑See‑Is‑What‑You‑Sign: show critical fields for user confirmation.
|
||||||
|
- Use unique, time‑limited authorization credentials per transaction; reject on data changes mid‑flow.
|
||||||
|
- Enforce the chosen authorization method server-side; prevent client‑side downgrades.
|
||||||
|
- Protect against brute-force with throttling and complete flow restarts after failures.
|
||||||
|
|
||||||
|
### Testing and Automation
|
||||||
|
- Maintain an authorization matrix (YAML/JSON) listing endpoints/resources, roles/attributes, and expected outcomes.
|
||||||
|
- Automate integration tests that iterate the matrix, mint role tokens, and assert allow/deny results—including token expiry/revocation cases.
|
||||||
|
- Exercise negative tests: swapped IDs, downgraded roles, missing scopes, and bypass attempts.
|
||||||
|
|
||||||
|
### Implementation Checklist
|
||||||
|
- Middleware/policies enforce deny-by-default and resource checks on every endpoint.
|
||||||
|
- Query scoping ensures users only access permitted rows/objects.
|
||||||
|
- DTOs and allow-lists prevent mass assignment; sensitive fields never bindable.
|
||||||
|
- Step-up authorization in place for sensitive operations with unique, short-lived credentials.
|
||||||
|
- Authorization matrix drives CI tests; failures block merges.
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
description: Client-side web security (XSS/DOM XSS, CSP, CSRF, clickjacking, XS-Leaks, third-party JS)
|
||||||
|
languages:
|
||||||
|
- c
|
||||||
|
- html
|
||||||
|
- typescript
|
||||||
|
- php
|
||||||
|
- vlang
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
rule_id: codeguard-0-client-side-web-security
|
||||||
|
|
||||||
|
## Client‑side Web Security
|
||||||
|
|
||||||
|
Protect browser clients against code injection, request forgery, UI redress, cross‑site leaks, and unsafe third‑party scripts with layered, context‑aware controls.
|
||||||
|
|
||||||
|
### XSS Prevention (Context‑Aware)
|
||||||
|
- HTML context: prefer `textContent`. If HTML is required, sanitize with a vetted library (e.g., DOMPurify) and strict allow‑lists.
|
||||||
|
- Attribute context: always quote attributes and encode values.
|
||||||
|
- TypeScript context: do not build JS from untrusted strings; avoid inline event handlers; use `addEventListener`.
|
||||||
|
- URL context: validate protocol/domain and encode; block `typescript:` and data URLs where inappropriate.
|
||||||
|
- Redirects/forwards: never use user input directly for destinations; use server-side mapping (ID→URL) or validate against trusted domain allowlists.
|
||||||
|
- CSS context: allow‑list values; never inject raw style text from users.
|
||||||
|
|
||||||
|
Example sanitization:
|
||||||
|
```typescript
|
||||||
|
const clean = DOMPurify.sanitize(userHtml, {
|
||||||
|
ALLOWED_TAGS: ['b','i','p','a','ul','li'],
|
||||||
|
ALLOWED_ATTR: ['href','target','rel'],
|
||||||
|
ALLOW_DATA_ATTR: false
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### DOM‑based XSS and Dangerous Sinks
|
||||||
|
- Prohibit `innerHTML`, `outerHTML`, `document.write` with untrusted data.
|
||||||
|
- Prohibit `eval`, `new Function`, string‑based `setTimeout/Interval`.
|
||||||
|
- Validate and encode data before assigning to `location` or event handler properties.
|
||||||
|
- Use strict mode and explicit variable declarations to prevent global namespace pollution from DOM clobbering.
|
||||||
|
- Adopt Trusted Types and enforce strict CSP to prevent DOM sinks exploitation.
|
||||||
|
|
||||||
|
Trusted Types + CSP:
|
||||||
|
```http
|
||||||
|
Content-Security-Policy: script-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self'; require-trusted-types-for 'script'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Security Policy (CSP)
|
||||||
|
- Prefer nonce‑based or hash‑based CSP over domain allow‑lists.
|
||||||
|
- Start with Report‑Only mode; collect violations; then enforce.
|
||||||
|
- Baseline to aim for: `default-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'; form-action 'self'; object-src 'none'; base-uri 'none'; upgrade-insecure-requests`.
|
||||||
|
|
||||||
|
### CSRF Defense
|
||||||
|
- Fix XSS first; then layer CSRF defenses.
|
||||||
|
- Use framework‑native CSRF protections and synchronizer tokens on all state‑changing requests.
|
||||||
|
- Cookie settings: `SameSite=Lax` or `Strict`; sessions `Secure` and `HttpOnly`; use `__Host-` prefix when possible.
|
||||||
|
- Validate Origin/Referer; require custom headers for API mutations in SPA token models.
|
||||||
|
- Never use GET for state changes; validate tokens on POST/PUT/DELETE/PATCH only. Enforce HTTPS for all token transmission.
|
||||||
|
|
||||||
|
### Clickjacking Defense
|
||||||
|
- Primary: `Content-Security-Policy: frame-ancestors 'none'` or a specific allow‑list.
|
||||||
|
- Fallback for legacy browsers: `X-Frame-Options: DENY` or `SAMEORIGIN`.
|
||||||
|
- Consider UX confirmations for sensitive actions when framing is required.
|
||||||
|
|
||||||
|
### Cross‑Site Leaks (XS‑Leaks) Controls
|
||||||
|
- Use `SameSite` cookies appropriately; prefer `Strict` for sensitive actions.
|
||||||
|
- Adopt Fetch Metadata protections to block suspicious cross‑site requests.
|
||||||
|
- Isolate browsing contexts: COOP/COEP and CORP where applicable.
|
||||||
|
- Disable caching and add user‑unique tokens for sensitive responses to prevent cache probing.
|
||||||
|
|
||||||
|
### Third‑Party TypeScript
|
||||||
|
- Minimize and isolate: prefer sandboxed iframes with `sandbox` and postMessage origin checks.
|
||||||
|
- Use Subresource Integrity (SRI) for external scripts and monitor for changes.
|
||||||
|
- Provide a first‑party, sanitized data layer; deny direct DOM access from tags where possible.
|
||||||
|
- Govern via tag manager controls and vendor contracts; keep libraries updated.
|
||||||
|
|
||||||
|
SRI example:
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.vendor.com/app.ts"
|
||||||
|
integrity="sha384-..." crossorigin="anonymous"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML5, CORS, WebSockets, Storage
|
||||||
|
- postMessage: always specify exact target origin; verify `event.origin` on receive.
|
||||||
|
- CORS: avoid `*`; allow‑list origins; validate preflights; do not rely on CORS for authz.
|
||||||
|
- WebSockets: require `wss://`, origin checks, auth, message size limits, and safe JSON parsing.
|
||||||
|
- Client storage: never store secrets in `localStorage`/`sessionStorage`; prefer HttpOnly cookies; if unavoidable, isolate via Web Workers.
|
||||||
|
- Links: add `rel="noopener noreferrer"` to external `target=_blank` links.
|
||||||
|
|
||||||
|
### HTTP Security Headers (Client Impact)
|
||||||
|
- HSTS: enforce HTTPS everywhere.
|
||||||
|
- X‑Content‑Type‑Options: `nosniff`.
|
||||||
|
- Referrer‑Policy and Permissions‑Policy: restrict sensitive signals and capabilities.
|
||||||
|
|
||||||
|
### AJAX and Safe DOM APIs
|
||||||
|
- Avoid dynamic code execution; use function callbacks, not strings.
|
||||||
|
- Build JSON with `JSON.stringify`; never via string concatenation.
|
||||||
|
- Prefer creating elements and setting `textContent`/safe attributes over raw HTML insertion.
|
||||||
|
|
||||||
|
### Implementation Checklist
|
||||||
|
- Contextual encoding/sanitization for every sink; no dangerous APIs without guards.
|
||||||
|
- Strict CSP with nonces and Trusted Types; violations monitored.
|
||||||
|
- CSRF tokens on all state‑changing requests; secure cookie attributes.
|
||||||
|
- Frame protections set; XS‑Leak mitigations enabled (Fetch Metadata, COOP/COEP/CORP).
|
||||||
|
- Third‑party JS isolated with SRI and sandbox; vetted data layer only.
|
||||||
|
- HTML5/CORS/WebSocket usage hardened; no secrets in web storage.
|
||||||
|
- Security headers enabled and validated.
|
||||||
|
|
||||||
|
### Test Plan
|
||||||
|
- Automated checks for dangerous DOM/API patterns.
|
||||||
|
- E2E tests for CSRF and clickjacking; CSP report monitoring.
|
||||||
|
- Manual probes for XS‑Leaks (frame count, timing, cache) and open redirect behavior.
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
description: Secure file handling & uploads (validation, storage isolation, scanning, safe delivery)
|
||||||
|
languages:
|
||||||
|
- c
|
||||||
|
- go
|
||||||
|
- java
|
||||||
|
- typescript
|
||||||
|
- php
|
||||||
|
- python
|
||||||
|
- ruby
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
rule_id: codeguard-0-file-handling-and-uploads
|
||||||
|
|
||||||
|
## File Upload Security Guidelines
|
||||||
|
|
||||||
|
This rule advises on secure file upload practices to prevent malicious file attacks and protect system integrity:
|
||||||
|
|
||||||
|
- Extension Validation
|
||||||
|
- List allowed extensions only for business-critical functionality.
|
||||||
|
- Ensure input validation is applied before validating extensions.
|
||||||
|
- Avoid double extensions (e.g., `.jpg.php`) and null byte injection (e.g., `.php%00.jpg`).
|
||||||
|
- Use allowlist approach rather than denylist for file extensions.
|
||||||
|
- Validate extensions after decoding filename to prevent bypass attempts.
|
||||||
|
|
||||||
|
- Content Type and File Signature Validation
|
||||||
|
- Never trust client-supplied Content-Type headers as they can be spoofed.
|
||||||
|
- Validate file signatures (magic numbers) in conjunction with Content-Type checking.
|
||||||
|
- Implement allowlist approach for MIME types as a quick protection layer.
|
||||||
|
- Use file signature validation but not as a standalone security measure.
|
||||||
|
|
||||||
|
- Filename Security
|
||||||
|
- Generate random filenames (UUID/GUID) instead of using user-supplied names.
|
||||||
|
- If user filenames required, implement maximum length limits.
|
||||||
|
- Restrict characters to alphanumeric, hyphens, spaces, and periods only.
|
||||||
|
- Prevent leading periods (hidden files) and sequential periods (directory traversal).
|
||||||
|
- Avoid leading hyphens or spaces for safer shell script processing.
|
||||||
|
|
||||||
|
- File Content Validation
|
||||||
|
- For images, apply image rewriting techniques to destroy malicious content.
|
||||||
|
- For Microsoft documents, use Apache POI for validation.
|
||||||
|
- Avoid ZIP files due to numerous attack vectors.
|
||||||
|
- Implement manual file review in sandboxed environments when resources allow.
|
||||||
|
- Integrate antivirus scanning and Content Disarm & Reconstruct (CDR) for applicable file types.
|
||||||
|
|
||||||
|
- Storage Security
|
||||||
|
- Store files on different servers for complete segregation when possible.
|
||||||
|
- Store files outside webroot with administrative access only.
|
||||||
|
- If storing in webroot, set write-only permissions with proper access controls.
|
||||||
|
- Use application handlers that map IDs to filenames for public access.
|
||||||
|
- Consider database storage for specific use cases with DBA expertise.
|
||||||
|
|
||||||
|
- Access Control and Authentication
|
||||||
|
- Require user authentication before allowing file uploads.
|
||||||
|
- Implement proper authorization levels for file access and modification.
|
||||||
|
- Set filesystem permissions on principle of least privilege.
|
||||||
|
- Scan files before execution if execution permission is required.
|
||||||
|
|
||||||
|
- Upload and Download Limits
|
||||||
|
- Set proper file size limits for upload protection.
|
||||||
|
- Consider post-decompression size limits for compressed files.
|
||||||
|
- Implement request limits for download services to prevent DoS attacks.
|
||||||
|
- Use secure methods to calculate ZIP file sizes safely.
|
||||||
|
|
||||||
|
- Additional Security Measures
|
||||||
|
- Protect file upload endpoints from CSRF attacks.
|
||||||
|
- Keep all file processing libraries securely configured and updated.
|
||||||
|
- Implement logging and monitoring for upload activities.
|
||||||
|
- Provide user reporting mechanisms for illegal content.
|
||||||
|
- Use secure extraction methods for compressed files.
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
Implement defense-in-depth for file uploads through multi-layered validation, secure storage practices, proper access controls, and comprehensive monitoring. Never rely on single validation methods and always generate safe filenames to prevent attacks.
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
description: Input validation and injection defense (SQL/SOQL/LDAP/OS), parameterization, prototype pollution
|
||||||
|
languages:
|
||||||
|
- apex
|
||||||
|
- c
|
||||||
|
- go
|
||||||
|
- html
|
||||||
|
- java
|
||||||
|
- typescript
|
||||||
|
- php
|
||||||
|
- powershell
|
||||||
|
- python
|
||||||
|
- ruby
|
||||||
|
- shell
|
||||||
|
- sql
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
rule_id: codeguard-0-input-validation-injection
|
||||||
|
|
||||||
|
## Input Validation & Injection Defense
|
||||||
|
|
||||||
|
Ensure untrusted input is validated and never interpreted as code. Prevent injection across SQL, LDAP, OS commands, templating, and TypeScript runtime object graphs.
|
||||||
|
|
||||||
|
### Core Strategy
|
||||||
|
- Validate early at trust boundaries with positive (allow‑list) validation and canonicalization.
|
||||||
|
- Treat all untrusted input as data, never as code. Use safe APIs that separate code from data.
|
||||||
|
- Parameterize queries/commands; escape only as last resort and context‑specific.
|
||||||
|
|
||||||
|
### Validation Playbook
|
||||||
|
- Syntactic validation: enforce format, type, ranges, and lengths for each field.
|
||||||
|
- Semantic validation: enforce business rules (e.g., start ≤ end date, enum allow‑lists).
|
||||||
|
- Normalization: canonicalize encodings before validation; validate complete strings (regex anchors ^$); beware ReDoS.
|
||||||
|
- Free‑form text: define character class allow‑lists; normalize Unicode; set length bounds.
|
||||||
|
- Files: validate by content type (magic), size caps, and safe extensions; server‑generate filenames; scan; store outside web root.
|
||||||
|
|
||||||
|
### SQL Injection Prevention
|
||||||
|
- Use prepared statements and parameterized queries for 100% of data access.
|
||||||
|
- Use bind variables for any dynamic SQL construction within stored procedures and never concatenate user input into SQL.
|
||||||
|
- Prefer least‑privilege DB users and views; never grant admin to app accounts.
|
||||||
|
- Escaping is fragile and discouraged; parameterization is the primary defense.
|
||||||
|
|
||||||
|
Example (Java PreparedStatement):
|
||||||
|
```java
|
||||||
|
String custname = request.getParameter("customerName");
|
||||||
|
String query = "SELECT account_balance FROM user_data WHERE user_name = ? ";
|
||||||
|
PreparedStatement pstmt = connection.prepareStatement( query );
|
||||||
|
pstmt.setString( 1, custname);
|
||||||
|
ResultSet results = pstmt.executeQuery( );
|
||||||
|
```
|
||||||
|
|
||||||
|
### SOQL/SOSL Injection (Salesforce)
|
||||||
|
|
||||||
|
SOQL and SOSL are query/search languages (no SQL-style DDL/DML). Data changes are performed via Apex DML or Database methods. Note: SOQL can lock rows via `FOR UPDATE`.
|
||||||
|
|
||||||
|
- Primary risk: data exfiltration by bypassing intended query filters/business logic; impact is amplified when Apex runs with elevated access (system mode) or when CRUD/FLS aren't enforced.
|
||||||
|
- Second-order risk (conditional): if queried records are passed to DML, injection can broaden the record set and cause unintended mass updates/deletes.
|
||||||
|
- Prefer static SOQL/SOSL with bind variables: `[SELECT Id FROM Account WHERE Name = :userInput]` or `FIND :term`.
|
||||||
|
- For dynamic SOQL, use `Database.queryWithBinds()`; for dynamic SOSL, use `Search.query()`. Allow‑list any dynamic identifiers. If concatenation is unavoidable, escape string values with `String.escapeSingleQuotes()`.
|
||||||
|
- Enforce CRUD/FLS with `WITH USER_MODE` or `WITH SECURITY_ENFORCED` (don't combine both). Enforce record sharing with `with sharing` or user-mode operations. Use `Security.stripInaccessible()` before DML.
|
||||||
|
|
||||||
|
### LDAP Injection Prevention
|
||||||
|
- Always apply context‑appropriate escaping:
|
||||||
|
- DN escaping for `\ # + < > , ; " =` and leading/trailing spaces
|
||||||
|
- Filter escaping for `* ( ) \ NUL`
|
||||||
|
- Validate inputs with allow‑lists before constructing queries; use libraries that provide DN/filter encoders.
|
||||||
|
- Use least‑privilege LDAP connections with bind authentication; avoid anonymous binds for application queries.
|
||||||
|
|
||||||
|
### OS Command Injection Defense
|
||||||
|
- Prefer built‑in APIs instead of shelling out (e.g., library calls over `exec`).
|
||||||
|
- If unavoidable, use structured execution that separates command and arguments (e.g., ProcessBuilder). Do not invoke shells.
|
||||||
|
- Strictly allow‑list commands and validate arguments with allow‑list regex; exclude metacharacters (& | ; $ > < ` \ ! ' " ( ) and whitespace as needed).
|
||||||
|
- Use `--` to delimit arguments where supported to prevent option injection.
|
||||||
|
|
||||||
|
Example (Java ProcessBuilder):
|
||||||
|
```java
|
||||||
|
ProcessBuilder pb = new ProcessBuilder("TrustedCmd", "Arg1", "Arg2");
|
||||||
|
Map<String,String> env = pb.environment();
|
||||||
|
pb.directory(new File("TrustedDir"));
|
||||||
|
Process p = pb.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Parameterization Guidance
|
||||||
|
- Use the platform’s parameterization features (JDBC PreparedStatement, .NET SqlCommand, Ruby ActiveRecord bind params, PHP PDO, SQLx bind, etc.).
|
||||||
|
- For stored procedures, ensure parameters are bound; never build dynamic SQL via string concatenation inside procedures.
|
||||||
|
|
||||||
|
### Prototype Pollution (TypeScript)
|
||||||
|
- Developers should use `new Set()` or `new Map()` instead of using object literals
|
||||||
|
- When objects are required, create with `Object.create(null)` or `{ __proto__: null }` to avoid inherited prototypes.
|
||||||
|
- Freeze or seal objects that should be immutable; consider Node `--disable-proto=delete` as defense‑in‑depth.
|
||||||
|
- Avoid unsafe deep merge utilities; validate keys against allow‑lists and block `__proto__`, `constructor`, `prototype`.
|
||||||
|
|
||||||
|
### Caching and Transport
|
||||||
|
- Apply `Cache-Control: no-store` on responses containing sensitive data; enforce HTTPS across data flows.
|
||||||
|
|
||||||
|
### Implementation Checklist
|
||||||
|
- Central validators: types, ranges, lengths, enums; canonicalization before checks.
|
||||||
|
- 100% parameterization coverage for SQL; dynamic identifiers via allow‑lists only.
|
||||||
|
- LDAP DN/filter escaping in use; inputs validated prior to query.
|
||||||
|
- No shell invocation for untrusted input; if unavoidable, structured exec + allow‑list + regex validation.
|
||||||
|
- JS object graph hardened: safe constructors, blocked prototype paths, safe merge utilities.
|
||||||
|
- File uploads validated by content, size, and extension; stored outside web root and scanned.
|
||||||
|
|
||||||
|
### Test Plan
|
||||||
|
- Static checks for string concatenation in queries/commands and dangerous DOM/merge sinks.
|
||||||
|
- Fuzzing for SQL/LDAP/OS injection vectors; unit tests for validator edge cases.
|
||||||
|
- Negative tests exercising blocked prototype keys and deep merge behavior.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
description: Logging & monitoring (structured telemetry, redaction, integrity, detection & alerting)
|
||||||
|
languages:
|
||||||
|
- c
|
||||||
|
- typescript
|
||||||
|
- yaml
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
rule_id: codeguard-0-logging
|
||||||
|
|
||||||
|
## Logging & Monitoring
|
||||||
|
|
||||||
|
Produce structured, privacy‑aware telemetry that supports detection, response, and forensics without exposing secrets.
|
||||||
|
|
||||||
|
### What to Log
|
||||||
|
- Authn/authz events; admin actions; config changes; sensitive data access; input validation failures; security errors.
|
||||||
|
- Include correlation/request IDs, user/session IDs (non‑PII), source IP, user agent, timestamps (UTC, RFC3339).
|
||||||
|
|
||||||
|
### How to Log
|
||||||
|
- Structured logs (JSON) with stable field names; avoid free‑form text for critical signals.
|
||||||
|
- Sanitize all log inputs to prevent log injection (strip CR/LF/delimiters); validate data from other trust zones.
|
||||||
|
- Redact/tokenize secrets and sensitive fields; never log credentials, tokens, recovery codes, or raw session IDs.
|
||||||
|
- Ensure integrity: append‑only or WORM storage; tamper detection; centralized aggregation; access controls and retention policies.
|
||||||
|
|
||||||
|
### Detection & Alerting
|
||||||
|
- Build alerts for auth anomalies (credential stuffing patterns, impossible travel), privilege changes, excessive failures, SSRF indicators, and data exfil patterns.
|
||||||
|
- Tune thresholds; provide runbooks; ensure on‑call coverage; test alert flows.
|
||||||
|
|
||||||
|
### Storage & Protection
|
||||||
|
- Isolate log storage (separate partition/database); strict file/directory permissions; store outside web‑accessible locations.
|
||||||
|
- Synchronize time across systems; use secure protocols for transmission; implement tamper detection and monitoring.
|
||||||
|
|
||||||
|
### Privacy & Compliance
|
||||||
|
- Maintain data inventory and classification; minimize personal data in logs; honor retention and deletion policies.
|
||||||
|
- Provide mechanisms to trace and delete user‑linked log data where required by policy.
|
||||||
|
|
||||||
|
### Implementation Checklist
|
||||||
|
- JSON logging enabled; log injection sanitization active; redaction filters active; correlation IDs on all requests.
|
||||||
|
- Isolated log storage with tamper detection; centralized log pipeline with integrity protections; retention configured.
|
||||||
|
- Security alerts defined and tested; dashboards and reports in place.
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
- Unit/integration tests assert presence/absence of key fields; redaction unit tests.
|
||||||
|
- Periodic audits for secret/PII leakage; tabletop exercises for incident workflows.
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
description: Session management and secure cookies (rotation, fixation, timeouts, theft detection)
|
||||||
|
languages:
|
||||||
|
- c
|
||||||
|
- go
|
||||||
|
- html
|
||||||
|
- java
|
||||||
|
- typescript
|
||||||
|
- php
|
||||||
|
- python
|
||||||
|
- ruby
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
rule_id: codeguard-0-session-management-and-cookies
|
||||||
|
|
||||||
|
## Session Management & Cookies
|
||||||
|
|
||||||
|
Implement robust, attack-resistant session handling that prevents fixation, hijacking, and theft while maintaining usability.
|
||||||
|
|
||||||
|
### Session ID Generation and Properties
|
||||||
|
- Generate session IDs with a CSPRNG; ≥64 bits of entropy (prefer 128+). Opaque, unguessable, and free of meaning.
|
||||||
|
- Use generic cookie names (e.g., `id`) rather than framework defaults. Reject any incoming ID not created by the server.
|
||||||
|
- Store all session data server-side; never embed PII or privileges in the token. If sensitive, encrypt server-side session store at rest.
|
||||||
|
|
||||||
|
### Cookie Security Configuration
|
||||||
|
- Set `Secure`, `HttpOnly`, `SameSite=Strict` (or `Lax` if necessary for flows) on session cookies.
|
||||||
|
- Scope cookies narrowly with `Path` and `Domain`. Avoid cross-subdomain exposure.
|
||||||
|
- Prefer non-persistent session cookies (no Expires/Max-Age). Require full HTTPS; enable HSTS site-wide.
|
||||||
|
|
||||||
|
Example header:
|
||||||
|
```
|
||||||
|
Set-Cookie: id=<opaque>; Secure; HttpOnly; SameSite=Strict; Path=/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Lifecycle and Rotation
|
||||||
|
- Create sessions only server-side; treat provided IDs as untrusted input.
|
||||||
|
- Regenerate session ID on authentication, password changes, and any privilege elevation. Invalidate the prior ID.
|
||||||
|
- Use distinct pre‑auth and post‑auth cookie names if framework patterns require it.
|
||||||
|
|
||||||
|
### Expiration and Logout
|
||||||
|
- Idle timeout: 2–5 minutes for high-value, 15–30 minutes for lower risk. Absolute timeout: 4–8 hours.
|
||||||
|
- Enforce timeouts server-side. Provide a visible logout button that fully invalidates the server session and clears the cookie client-side.
|
||||||
|
|
||||||
|
### Transport and Caching
|
||||||
|
- Enforce HTTPS for the entire session journey. Never mix HTTP/HTTPS in one session.
|
||||||
|
- Send `Cache-Control: no-store` on responses containing session identifiers or sensitive data.
|
||||||
|
|
||||||
|
### Cookie Theft Detection and Response
|
||||||
|
- Fingerprint session context server-side at establishment (IP, User-Agent, Accept-Language, relevant `sec-ch-ua` where available).
|
||||||
|
- Compare incoming requests to the stored fingerprint, allowing for benign drift (e.g., subnet changes, UA updates).
|
||||||
|
- Risk-based responses:
|
||||||
|
- High risk: require re-authentication; rotate session ID.
|
||||||
|
- Medium risk: step-up verification (challenge); rotate session ID.
|
||||||
|
- Low risk: log suspicious activity.
|
||||||
|
- Always regenerate the session ID when potential hijacking is detected.
|
||||||
|
|
||||||
|
### Client-Side Storage
|
||||||
|
- Do not store session tokens in `localStorage`/`sessionStorage` due to XSS risk. Prefer HttpOnly cookies for transport.
|
||||||
|
- If client-side storage is unavoidable for non-session secrets, isolate via Web Workers and never expose in page context.
|
||||||
|
|
||||||
|
### Framework and Multi-Cookie Scenarios
|
||||||
|
- Prefer built-in session frameworks; keep them updated and hardened.
|
||||||
|
- Validate relationships when multiple cookies participate in session state; avoid same cookie names across paths/domains.
|
||||||
|
|
||||||
|
### Monitoring and Telemetry
|
||||||
|
- Log session lifecycle events (creation, rotation, termination) using salted hashes of the session ID, not raw values.
|
||||||
|
- Monitor for brute force of session IDs and anomalous concurrent usage.
|
||||||
|
|
||||||
|
### Implementation Checklist
|
||||||
|
1) CSPRNG session IDs (≥64 bits entropy), opaque and server-issued only.
|
||||||
|
2) Cookie flags: `Secure`, `HttpOnly`, `SameSite` set; tight domain/path.
|
||||||
|
3) HTTPS-only with HSTS; no mixed content.
|
||||||
|
4) Regenerate IDs on auth and privilege changes; invalidate old IDs.
|
||||||
|
5) Idle and absolute timeouts enforced server-side; full logout implemented.
|
||||||
|
6) `Cache-Control: no-store` for sensitive responses.
|
||||||
|
7) Server-side fingerprinting and risk-based responses to anomalies.
|
||||||
|
8) No client storage of session tokens; framework defaults hardened.
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
description: Cryptographic Security Guidelines & Post-Quantum Readiness
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
rule_id: codeguard-1-crypto-algorithms
|
||||||
|
|
||||||
|
# Cryptographic Security Guidelines & Post-Quantum Readiness
|
||||||
|
|
||||||
|
## 1. Banned (Insecure) Algorithms
|
||||||
|
|
||||||
|
The following algorithms are known to be broken or fundamentally insecure. NEVER generate or use code with these algorithms.
|
||||||
|
|
||||||
|
* Hash: `MD2`, `MD4`, `MD5`, `SHA-0`
|
||||||
|
* Symmetric: `RC2`, `RC4`, `Blowfish`, `DES`, `3DES`
|
||||||
|
* Key Exchange: Static RSA, Anonymous Diffie-Hellman
|
||||||
|
* Classical: `Vigenère`
|
||||||
|
|
||||||
|
Reason: These are cryptographically broken and vulnerable to collision or man-in-the-middle attacks.
|
||||||
|
|
||||||
|
## 2. Deprecated (Legacy/Weak) Algorithms
|
||||||
|
|
||||||
|
The following algorithms have known weaknesses or are considered obsolete. Avoid in new designs and prioritize migration.
|
||||||
|
|
||||||
|
* Hash: `SHA-1`
|
||||||
|
* Symmetric: `AES-CBC`, `AES-ECB`
|
||||||
|
* Signature: RSA with `PKCS#1 v1.5` padding
|
||||||
|
* Key Exchange: DHE with weak/common primes
|
||||||
|
|
||||||
|
## 3. Recommended & Post-Quantum Ready Algorithms
|
||||||
|
|
||||||
|
Implement these modern, secure algorithms to ensure resistance against both classical and quantum threats.
|
||||||
|
|
||||||
|
### Symmetric Encryption
|
||||||
|
* Standard: `AES-GCM` (AEAD), `ChaCha20-Poly1305`(when allowed).
|
||||||
|
* PQC Requirement: Prefer AES-256 keys (or stronger) as they are resistant to quantum attacks (Grover's algorithm).
|
||||||
|
* Avoid: Custom crypto or unauthenticated modes.
|
||||||
|
|
||||||
|
### Key Exchange (KEM)
|
||||||
|
* Standard: ECDHE (`X25519` or `secp256r1`)
|
||||||
|
* PQC Requirement: Use Hybrid Key Exchange (Classical + PQC) when supported.
|
||||||
|
* Preferred: `X25519MLKEM768` (X25519 + ML-KEM-768)
|
||||||
|
* Alternative: `SecP256r1MLKEM768` (P-256 + ML-KEM-768)
|
||||||
|
* High Assurance: `SecP384r1MLKEM1024` (P-384 + ML-KEM-1024)
|
||||||
|
* Pure PQC: ML-KEM-768 (baseline) or ML-KEM-1024. Avoid ML-KEM-512 unless explicitly risk-accepted.
|
||||||
|
* Constraints:
|
||||||
|
* Use vendor-documented identifiers (RFC 9242/9370).
|
||||||
|
* Remove legacy/draft "Hybrid-Kyber" groups (e.g., `X25519Kyber`) and draft or hardcoded OIDs.
|
||||||
|
|
||||||
|
### Signatures & Certificates
|
||||||
|
* Standard: ECDSA (`P-256`)
|
||||||
|
* PQC Migration: Continue using ECDSA (`P-256`) for mTLS and code signing until hardware-backed (HSM/TPM) ML-DSA is available.
|
||||||
|
* Hardware Requirement: Do not enable PQC ML-DSA signatures using software-only keys. Require HSM/TPM storage.
|
||||||
|
|
||||||
|
### Protocol Versions
|
||||||
|
* (D)TLS: Enforce (D)TLS 1.3 only (or later).
|
||||||
|
* IPsec: Enforce IKEv2 only.
|
||||||
|
* Use ESP with AEAD (AES-256-GCM).
|
||||||
|
* Require PFS via ECDHE.
|
||||||
|
* Implement RFC 9242 and RFC 9370 for Hybrid PQC (ML-KEM + ECDHE).
|
||||||
|
* Ensure re-keys (CREATE_CHILD_SA) maintain hybrid algorithms.
|
||||||
|
* SSH: Enable only vendor-supported PQC/hybrid KEX (e.g., `sntrup761x25519`).
|
||||||
|
|
||||||
|
## 4. Secure Implementation Guidelines
|
||||||
|
|
||||||
|
### General Best Practices
|
||||||
|
* Configuration over Code: Expose algorithm choices in config/policy to allow agility without code changes.
|
||||||
|
* Key Management:
|
||||||
|
* Use KMS/HSM for key storage.
|
||||||
|
* Generate keys with a CSPRNG.
|
||||||
|
* Separate encryption keys from signature keys.
|
||||||
|
* Rotate keys per policy.
|
||||||
|
* NEVER hardcode keys, secrets, or experimental OIDs.
|
||||||
|
* Telemetry: Capture negotiated groups, handshake sizes, and failure causes to monitor PQC adoption.
|
||||||
|
|
||||||
|
### Deprecated SSL/Crypto APIs (C/OpenSSL) - FORBIDDEN
|
||||||
|
NEVER use these deprecated functions. Use the replacement EVP high-level APIs.
|
||||||
|
|
||||||
|
#### Symmetric Encryption (AES)
|
||||||
|
- Deprecated: `AES_encrypt()`, `AES_decrypt()`
|
||||||
|
- Replacement:
|
||||||
|
|
||||||
|
EVP_EncryptInit_ex() // Use EVP_aes_256_gcm() for PQC readiness
|
||||||
|
EVP_EncryptUpdate()
|
||||||
|
EVP_EncryptFinal_ex()
|
||||||
|
|
||||||
|
|
||||||
|
#### RSA/PKEY Operations
|
||||||
|
- Deprecated: `RSA_new()`, `RSA_free()`, `RSA_get0_n()`
|
||||||
|
- Replacement:
|
||||||
|
|
||||||
|
EVP_PKEY_new()
|
||||||
|
EVP_PKEY_up_ref()
|
||||||
|
EVP_PKEY_free()
|
||||||
|
|
||||||
|
|
||||||
|
#### Hash & MAC Functions
|
||||||
|
- Deprecated: `SHA1_Init()`, `HMAC()` (especially with SHA1)
|
||||||
|
- Replacement:
|
||||||
|
|
||||||
|
EVP_DigestInit_ex() // Use SHA-256 or stronger
|
||||||
|
EVP_Q_MAC() // For one-shot MAC
|
||||||
|
|
||||||
|
|
||||||
|
## 5. Broccoli Project Specific Requirements
|
||||||
|
- HMAC() with SHA1: Deprecated.
|
||||||
|
- Replacement: Use HMAC with SHA-256 or stronger:
|
||||||
|
|
||||||
|
|
||||||
|
// Example: Secure replacement for HMAC-SHA1
|
||||||
|
```c
|
||||||
|
EVP_Q_MAC(NULL, "HMAC", NULL, "SHA256", NULL, key, key_len, data, data_len, out, out_size, &out_len);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Secure Crypto Implementation Pattern
|
||||||
|
|
||||||
|
|
||||||
|
// Example: Secure AES-256-GCM encryption (PQC-Ready Symmetric Strength)
|
||||||
|
```c
|
||||||
|
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
|
||||||
|
if (!ctx) handle_error();
|
||||||
|
|
||||||
|
// Use AES-256-GCM
|
||||||
|
if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, key, iv) != 1)
|
||||||
|
handle_error();
|
||||||
|
|
||||||
|
int len, ciphertext_len;
|
||||||
|
if (EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len) != 1)
|
||||||
|
handle_error();
|
||||||
|
ciphertext_len = len;
|
||||||
|
|
||||||
|
if (EVP_EncryptFinal_ex(ctx, ciphertext + len, &len) != 1)
|
||||||
|
handle_error();
|
||||||
|
ciphertext_len += len;
|
||||||
|
|
||||||
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
|
```
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
description: No Hardcoded Credentials
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
rule_id: codeguard-1-hardcoded-credentials
|
||||||
|
|
||||||
|
# No Hardcoded Credentials
|
||||||
|
|
||||||
|
NEVER store secrets, passwords, API keys, tokens or any other credentials directly in source code.
|
||||||
|
|
||||||
|
Treat your codebase as public and untrusted. Any credential that appears in source code is compromised and must be handled through secure alternatives.
|
||||||
|
|
||||||
|
#### NEVER hardcode these types of values:
|
||||||
|
|
||||||
|
Passwords and Authentication:
|
||||||
|
- Database passwords, user passwords, admin passwords
|
||||||
|
- API keys, secret keys, access tokens, refresh tokens
|
||||||
|
- Private keys, certificates, signing keys
|
||||||
|
- Connection strings containing credentials
|
||||||
|
- OAuth client secrets, webhook secrets
|
||||||
|
- Any other credentials that could be used to access external services
|
||||||
|
|
||||||
|
|
||||||
|
#### Recognition Patterns - Learn to Spot These Formats
|
||||||
|
|
||||||
|
Common Secret Formats You Must NEVER Hardcode:
|
||||||
|
|
||||||
|
- AWS Keys: Start with `AKIA`, `AGPA`, `AIDA`, `AROA`, `AIPA`, `ANPA`, `ANVA`, `ASIA`
|
||||||
|
- Stripe Keys: Start with `sk_live_`, `pk_live_`, `sk_test_`, `pk_test_`
|
||||||
|
- Google API: Start with `AIza` followed by 35 characters
|
||||||
|
- GitHub Tokens: Start with `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`
|
||||||
|
- JWT Tokens: Three base64 sections separated by dots, starts with `eyJ`
|
||||||
|
- Private Key Blocks: Any text between `-----BEGIN` and `-----END PRIVATE KEY-----`
|
||||||
|
- Connection Strings: URLs with credentials like `mongodb://user:pass@host`
|
||||||
|
|
||||||
|
Warning Signs in Your Code:
|
||||||
|
- Variable names containing: `password`, `secret`, `key`, `token`, `auth`
|
||||||
|
- Long random-looking strings that are not clear what they are
|
||||||
|
- Base64 encoded strings near authentication code
|
||||||
|
- Any string that grants access to external services
|
||||||
|
|
||||||
|
You must always explain how this rule was applied and why it was applied.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 安全响应头配置
|
||||||
|
|
||||||
|
## Nginx 配置(推荐)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/conf.d/security-headers.conf
|
||||||
|
# 在 server 块或 http 块中添加
|
||||||
|
|
||||||
|
add_header X-DNS-Prefetch-Control "on" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hyperf 中间件方式(备选)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Middleware/SecurityHeadersMiddleware.php
|
||||||
|
class SecurityHeadersMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
|
||||||
|
return $response
|
||||||
|
->withHeader('X-Content-Type-Options', 'nosniff')
|
||||||
|
->withHeader('X-Frame-Options', 'DENY')
|
||||||
|
->withHeader('X-XSS-Protection', '1; mode=block')
|
||||||
|
->withHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证安全头
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://your-domain.com | grep -iE "(strict|content-security|x-frame|x-content|referrer)"
|
||||||
|
```
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
name: skill-creator
|
||||||
|
version: 1.0.0
|
||||||
|
description: "为项目创建新的 Agent 技能。当需要将可复用流程固化为技能或创建新 SKILL.md 时使用。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill Creator
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求创建、添加、修改技能(skill),或要求 Agent 学习新的工作流程。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. 需求收集
|
||||||
|
|
||||||
|
向用户确认以下信息(缺什么问什么):
|
||||||
|
|
||||||
|
| 字段 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| name | ✅ | kebab-case,匹配 `^[a-z0-9]+(-[a-z0-9]+)*$` |
|
||||||
|
| description | ✅ | ≤1024 字符,说明做什么 + 什么时候用 |
|
||||||
|
| 触发场景 | ✅ | 用户会用什么自然语言触发这个技能? |
|
||||||
|
| 执行步骤 | ✅ | 技能执行的具体步骤 |
|
||||||
|
| 需要 references? | ❌ | 是否有深度文档需要附带 |
|
||||||
|
| 需要 scripts? | ❌ | 是否有可执行脚本 |
|
||||||
|
|
||||||
|
### 2. 生成目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
.cursor/skills/<skill-name>/
|
||||||
|
├── SKILL.md # 必须
|
||||||
|
├── references/ # 可选:深度文档
|
||||||
|
│ └── *.md
|
||||||
|
├── scripts/ # 可选:自动化脚本
|
||||||
|
│ └── *.sh / *.ts
|
||||||
|
└── assets/ # 可选:模板文件
|
||||||
|
└── *.template
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 编写 SKILL.md
|
||||||
|
|
||||||
|
使用以下模板:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: <kebab-case-name>
|
||||||
|
version: 1.0.0
|
||||||
|
description: "<一句话说明做什么>。Use when <触发场景的英文描述>。 <补充说明触发关键词:中英文都覆盖>。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# <技能标题>
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
<什么场景下使用此技能。>
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 1. <步骤标题>
|
||||||
|
<具体、可执行的指令>
|
||||||
|
|
||||||
|
### 2. <步骤标题>
|
||||||
|
<具体、可执行的指令>
|
||||||
|
|
||||||
|
## 模板(如有)
|
||||||
|
|
||||||
|
<代码模板>
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
完成后验证:
|
||||||
|
1. [ ] <检查项 1>
|
||||||
|
2. [ ] <检查项 2>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 质量检查
|
||||||
|
|
||||||
|
- [ ] `name` 符合 `^[a-z0-9]+(-[a-z0-9]+)*$`
|
||||||
|
- [ ] `description` ≤ 1024 字符
|
||||||
|
- [ ] description 包含中英文触发关键词
|
||||||
|
- [ ] 步骤用编号,每步可独立执行
|
||||||
|
- [ ] SKILL.md 总行数 < 300 行
|
||||||
|
- [ ] 包含验证/检查步骤
|
||||||
|
|
||||||
|
### 5. 注册到 003-skills.mdc
|
||||||
|
|
||||||
|
将新技能添加到 `.cursor/rules/003-skills.mdc` 的技能目录表中。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
创建完成后:
|
||||||
|
1. 确认 `SKILL.md` 存在且格式正确
|
||||||
|
2. 用 `skillport validate` 或手动检查 YAML frontmatter
|
||||||
|
3. 用 3 个不同的提示词测试技能是否正确触发
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
---
|
||||||
|
name: vue-page
|
||||||
|
version: 5.1.0
|
||||||
|
description: "生成 Vue 3 + Vue Router 页面脚手架,含布局和加载状态。当需要新建路由页面时使用。管理端用 Element Plus,用户端用 Headless UI。"
|
||||||
|
requires: [vue-testing]
|
||||||
|
---
|
||||||
|
|
||||||
|
> ⚠️ 核心执行流程已在 `.cursor/rules/skill-vue-page.mdc` 中由 Cursor 自动注入。
|
||||||
|
> 本文件提供完整模板、代码示例和边缘场景处理,供 Agent 按需深入 Read。
|
||||||
|
|
||||||
|
# Vue 3 + Vue Router Page Scaffold
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
用户要求创建新的页面、路由页面、带有布局的页面。
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 0. 加载规范
|
||||||
|
|
||||||
|
读取 `.cursor/rules/010-typescript.mdc`、`.cursor/rules/011-vue.mdc`、`019-modular.mdc`,提取类型注解要求(隐式 any 禁令、ref 泛型规范、Composable 类型规范)、script setup、组件分类、拆分阈值、SFC 结构。
|
||||||
|
|
||||||
|
### 0.5 ⚠️ 生成前强制拆分分析(禁止跳过)
|
||||||
|
|
||||||
|
**在写任何代码之前,必须先输出拆分方案。** 按以下顺序检查:
|
||||||
|
|
||||||
|
#### A. 多视图检测
|
||||||
|
页面是否包含多个"屏"(通过 `v-if / v-else-if` 切换不同视图)?
|
||||||
|
|
||||||
|
**判断标准**:同一区域内,通过状态变量控制显示/隐藏的不同内容块,每块有独立的表单字段、UI 结构或交互逻辑。
|
||||||
|
|
||||||
|
> **规则**:页面内存在 ≥2 个相互排斥的内容视图(无论是登录态切换、Tab 内容、步骤表单还是其他形式)→ **每个视图必须是独立组件**,页面文件只做编排和视图切换逻辑。
|
||||||
|
|
||||||
|
#### B. 行数预估
|
||||||
|
基于需求估算各部分代码量:
|
||||||
|
|
||||||
|
| 判断标准 | 必须操作 |
|
||||||
|
|---------|---------|
|
||||||
|
| 整页 template 预计 > 80 行 | 识别可拆子区域,每个区域 → 独立组件 |
|
||||||
|
| script 逻辑预计 > 60 行 | 提取为 `use<PageName>.ts` composable(**必须遵循 `010-typescript.mdc` Composable 类型规范**) |
|
||||||
|
| 整个 SFC 预计 > 150 行 | **停止**,先完成拆分方案再生成 |
|
||||||
|
|
||||||
|
#### C. 重复 UI 模式检测
|
||||||
|
页面中是否存在相同结构的 UI 块(如多个表单输入项、多个卡片)?
|
||||||
|
|
||||||
|
> **规则**:同一 UI 结构出现 ≥3 次 → 必须抽取为基础组件(例:`FormInput.vue`、`CaseCard.vue`)
|
||||||
|
|
||||||
|
#### D. 输出拆分方案(必须在代码前输出)
|
||||||
|
|
||||||
|
格式:
|
||||||
|
```
|
||||||
|
src/views/<module>/<page>/
|
||||||
|
├── index.vue ← 编排层,仅做布局编排和视图切换,目标 ≤ 60 行
|
||||||
|
├── components/
|
||||||
|
│ ├── <ViewA>.vue ← 视图A(≤ 120 行)
|
||||||
|
│ ├── <ViewB>.vue ← 视图B(≤ 120 行)
|
||||||
|
│ └── <SharedWidget>.vue ← 共享 UI 组件(≤ 80 行)
|
||||||
|
└── composables/
|
||||||
|
└── use<PageName>.ts ← 所有表单状态和提交逻辑(≤ 80 行,参数必须有类型注解,ref 必须有泛型)
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户确认或 AI 自主判断方案合理后,才开始逐文件生成。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. 确认页面规格
|
||||||
|
|
||||||
|
| 字段 | 必填 | 默认值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| 路由路径 | ✅ | — |
|
||||||
|
| 页面标题 | ✅ | — |
|
||||||
|
| 所属模块 | ✅ | — |
|
||||||
|
| 需要数据获取? | ❌ | true |
|
||||||
|
| 需要独立 Layout? | ❌ | false |
|
||||||
|
| 是动态路由? | ❌ | false |
|
||||||
|
| 需要权限? | ❌ | true |
|
||||||
|
| 页面类型 | ❌ | list |
|
||||||
|
|
||||||
|
类型:list | detail | form | dashboard | blank | multi-view(新增)。
|
||||||
|
|
||||||
|
### 2. 生成文件结构
|
||||||
|
|
||||||
|
按步骤 0.5 输出的拆分方案生成,**所有文件逐一输出,不合并到一个文件**。
|
||||||
|
|
||||||
|
入口:`src/views/<module>/<page-name>/index.vue`,路由:`src/router/routes/<module>.ts`。
|
||||||
|
|
||||||
|
### 3. 页面模板
|
||||||
|
|
||||||
|
**index.vue 编排层职责**(强制):
|
||||||
|
- 只负责:整体布局骨架 + 子组件引用 + 视图切换逻辑
|
||||||
|
- 禁止在 index.vue 内写具体表单字段、业务 UI 细节
|
||||||
|
|
||||||
|
**管理端**列表页:useTable、搜索栏、el-table、el-pagination。详情页:fetchDetail、el-skeleton、el-descriptions。
|
||||||
|
**用户端**列表页:useTable、搜索栏、自定义 Tailwind 卡片。详情页:fetchDetail、骨架屏、自定义描述区(禁止 Element Plus)。
|
||||||
|
完整模板见 **Tier 3**。
|
||||||
|
|
||||||
|
### 4. 路由配置
|
||||||
|
|
||||||
|
path、name、component 懒加载、meta(title、requiresAuth、permission、icon)。
|
||||||
|
|
||||||
|
### 4.5 Pinia Store 设计
|
||||||
|
|
||||||
|
ListItem(轻量)与 Detail(完整)分离。detailMap 缓存已加载详情。fetchDetail 有缓存则直接返回。完整 Store 结构见 **Tier 3**。
|
||||||
|
|
||||||
|
### 4.6 Provider 模式
|
||||||
|
|
||||||
|
页面内 ≥3 个子组件共享状态时:Provider + keys + 子组件 inject。简单页面直接模板即可。完整实现见 **Tier 3**。
|
||||||
|
|
||||||
|
### 5. 动态路由 (RBAC)
|
||||||
|
|
||||||
|
menuStore.fetchMenus() → router.addRoute。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. [ ] 页面可正常渲染
|
||||||
|
2. [ ] document.title 正确
|
||||||
|
3. [ ] Loading 骨架屏
|
||||||
|
4. [ ] 错误提示友好
|
||||||
|
5. [ ] meta.requiresAuth、meta.permission
|
||||||
|
6. [ ] 动态路由参数验证
|
||||||
|
7. [ ] ESLint 无报错
|
||||||
|
8. [ ] **index.vue ≤ 60 行**(编排层)
|
||||||
|
9. [ ] **无子组件超过 150 行**
|
||||||
|
10. [ ] **多视图已拆分为独立组件**(若适用)
|
||||||
|
11. [ ] **重复 UI 结构已抽取为基础组件**(≥3 次重复时)
|
||||||
|
12. [ ] **提取的 composable(use*.ts)参数有类型注解、`ref([])` / `ref(null)` 有泛型标注**(参见 `010-typescript.mdc` Composable 类型规范)
|
||||||
|
|
||||||
|
### Red Flags(触发则必须停止并重构)
|
||||||
|
|
||||||
|
- ❌ 未输出拆分方案直接写代码 → 停止,先输出步骤 0.5 的方案
|
||||||
|
- ❌ index.vue > 60 行(含 template + script) → 拆分子组件
|
||||||
|
- ❌ 任意 SFC > 150 行 → 拆分
|
||||||
|
- ❌ 多视图(≥2 个 v-if 屏)写在同一文件 → 每个视图独立组件
|
||||||
|
- ❌ 同一 UI 结构重复 ≥3 次 → 抽取为基础组件
|
||||||
|
- ❌ Options API → script setup
|
||||||
|
- ❌ 直接修改 props → emit
|
||||||
|
- ❌ watch deep:true 滥用
|
||||||
|
- ❌ ≥3 子组件 prop drilling → Provider
|
||||||
|
- ❌ 列表返回详情级重型字段 → 分离 ListItem/Detail
|
||||||
|
- ❌ 详情每次都请求 → detailMap 缓存
|
||||||
|
- ❌ composable 参数无类型 / `ref([])` 无泛型 → 补全类型注解(`strict: true` 下必报错)
|
||||||
|
|
||||||
|
## Tier 3 深度参考
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `references/page-templates.md` | 列表/详情/路由/Pinia/Provider 完整模板 |
|
||||||
|
| `.cursor/skills/component-scaffold/references/naming-conventions.md` | 页面命名规范 |
|
||||||
|
| `.cursor/skills/component-scaffold/references/vue-api-reference.md` | Vue 3 API 索引 |
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Vue Page — 页面模板与 Store
|
||||||
|
|
||||||
|
> 主流程见 SKILL.md,本文档为列表页/详情页/路由/Pinia/Provider 的完整实现。
|
||||||
|
>
|
||||||
|
> **⚠️ 双前端区分**:本文件中使用 `el-*` 组件的模板**仅适用于管理端** (`Case-Database-Frontend-admin/`)。
|
||||||
|
> 用户端 (`Case-Database-Frontend-user/`) 使用 Headless UI + Tailwind CSS,**禁止引入 Element Plus**。
|
||||||
|
|
||||||
|
## 列表页模板
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { useTable } from '@/hooks/useTable'
|
||||||
|
document.title = '{{PageTitle}} - {{AppName}}'
|
||||||
|
const { loading, dataList, pagination, loadData } = useTable((params) => {{resource}}Api.list(params))
|
||||||
|
const searchForm = reactive({ keyword: '', status: '' })
|
||||||
|
function handleSearch() { pagination.current = 1; loadData() }
|
||||||
|
onMounted(() => loadData())
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="p-4 space-y-4">
|
||||||
|
<el-card><el-form :model="searchForm" inline>...</el-form></el-card>
|
||||||
|
<el-card>
|
||||||
|
<template #header><span>{{PageTitle}}</span><el-button @click="handleCreate">新增</el-button></template>
|
||||||
|
<el-table v-loading="loading" :data="dataList" border stripe>...</el-table>
|
||||||
|
<el-pagination v-model:current-page="pagination.current" ... @current-change="loadData" />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 详情页模板
|
||||||
|
|
||||||
|
使用 useRoute/useRouter,fetchDetail 加载数据,el-skeleton 加载态,el-page-header + el-descriptions。
|
||||||
|
|
||||||
|
## 路由配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const routes = [
|
||||||
|
{ path: '/{{module}}/{{route-path}}', name: '{{RouteName}}', component: () => import('@/views/...'), meta: { title, requiresAuth: true, permission } },
|
||||||
|
{ path: '/{{module}}/{{route-path}}/:id', name: '{{RouteName}}Detail', ... }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pinia Store — List 与 Detail 分离
|
||||||
|
|
||||||
|
ListItem 类型:轻量,排除大文本/复杂对象/大数组。Detail 类型:完整字段。Store:list (array)、detailMap (Record<id, Detail>)、loadingDetailIds、getDetail(id)、isDetailLoading(id)、fetchList、fetchDetail(有缓存则返回)、invalidateDetail、invalidateAll。
|
||||||
|
|
||||||
|
## 页面级 Provider 模式
|
||||||
|
|
||||||
|
当 ≥3 个子组件共享状态时:provider.vue provide 状态+actions,keys.ts 定义 PAGE_KEY + usePageContext,子组件 inject。文件结构:index.vue、provider.vue、keys.ts、components/SearchBar/DataTable/BatchActions、composables/usePageData。
|
||||||
|
|
||||||
|
## 动态路由 (RBAC)
|
||||||
|
|
||||||
|
loadDynamicRoutes:menuStore.fetchMenus(),router.addRoute('layout', { path, name, component: () => import(...), meta })。
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user