ethan-skill 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -24
- package/dist/skills/15-git-workflow.d.ts +3 -0
- package/dist/skills/15-git-workflow.d.ts.map +1 -0
- package/dist/skills/15-git-workflow.js +288 -0
- package/dist/skills/15-git-workflow.js.map +1 -0
- package/dist/skills/16-unit-testing.d.ts +3 -0
- package/dist/skills/16-unit-testing.d.ts.map +1 -0
- package/dist/skills/16-unit-testing.js +298 -0
- package/dist/skills/16-unit-testing.js.map +1 -0
- package/dist/skills/17-system-design.d.ts +3 -0
- package/dist/skills/17-system-design.d.ts.map +1 -0
- package/dist/skills/17-system-design.js +294 -0
- package/dist/skills/17-system-design.js.map +1 -0
- package/dist/skills/18-database-optimize.d.ts +3 -0
- package/dist/skills/18-database-optimize.d.ts.map +1 -0
- package/dist/skills/18-database-optimize.js +294 -0
- package/dist/skills/18-database-optimize.js.map +1 -0
- package/dist/skills/19-docker.d.ts +3 -0
- package/dist/skills/19-docker.d.ts.map +1 -0
- package/dist/skills/19-docker.js +360 -0
- package/dist/skills/19-docker.js.map +1 -0
- package/dist/skills/20-cicd.d.ts +3 -0
- package/dist/skills/20-cicd.d.ts.map +1 -0
- package/dist/skills/20-cicd.js +364 -0
- package/dist/skills/20-cicd.js.map +1 -0
- package/dist/skills/21-performance.d.ts +3 -0
- package/dist/skills/21-performance.d.ts.map +1 -0
- package/dist/skills/21-performance.js +139 -0
- package/dist/skills/21-performance.js.map +1 -0
- package/dist/skills/22-refactoring.d.ts +3 -0
- package/dist/skills/22-refactoring.d.ts.map +1 -0
- package/dist/skills/22-refactoring.js +235 -0
- package/dist/skills/22-refactoring.js.map +1 -0
- package/dist/skills/23-observability.d.ts +3 -0
- package/dist/skills/23-observability.d.ts.map +1 -0
- package/dist/skills/23-observability.js +266 -0
- package/dist/skills/23-observability.js.map +1 -0
- package/dist/skills/24-design-patterns.d.ts +3 -0
- package/dist/skills/24-design-patterns.d.ts.map +1 -0
- package/dist/skills/24-design-patterns.js +258 -0
- package/dist/skills/24-design-patterns.js.map +1 -0
- package/dist/skills/index.d.ts +10 -0
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +41 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/skills.test.js +3 -3
- package/dist/skills/skills.test.js.map +1 -1
- package/dist/templates/templates.test.js +2 -3
- package/dist/templates/templates.test.js.map +1 -1
- package/package.json +1 -1
- package/rules/claude-code/CLAUDE.md +2410 -3
- package/rules/cline/.clinerules +2262 -2
- package/rules/codebuddy/CODEBUDDY.md +2361 -2
- package/rules/continue/.continuerules +2262 -2
- package/rules/copilot/copilot-instructions.md +2331 -2
- package/rules/cursor/.cursorrules +2399 -2
- package/rules/cursor/smart-flow.mdc +2399 -2
- package/rules/jetbrains/smart-flow.md +2331 -2
- package/rules/lingma/smart-flow.md +2352 -3
- package/rules/windsurf/.windsurf/rules/smart-flow.md +2332 -3
- package/rules/zed/smart-flow.rules +2251 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Ethan v1.
|
|
1
|
+
# Ethan v1.8.0
|
|
2
2
|
|
|
3
|
-
> Auto-generated from src/skills/ | 2026-03-
|
|
3
|
+
> Auto-generated from src/skills/ | 2026-03-31T16:55:43.257Z
|
|
4
4
|
> Do not edit manually. Source: src/skills/
|
|
5
5
|
|
|
6
6
|
## Ethan
|
|
7
7
|
|
|
8
|
-
本文件配置了
|
|
8
|
+
本文件配置了 24 个标准化工作流节点(Skill)。当用户输入触发词时,严格按对应 Skill 的步骤执行,输出遵循各 Skill 的格式模板。
|
|
9
9
|
|
|
10
10
|
## 执行原则
|
|
11
11
|
|
|
@@ -1451,4 +1451,2411 @@ docker-compose up -d --no-deps --scale app=2 # 拉起旧版本
|
|
|
1451
1451
|
|
|
1452
1452
|
---
|
|
1453
1453
|
|
|
1454
|
+
### 15. Git 工作流 (`git-workflow`)
|
|
1455
|
+
|
|
1456
|
+
**描述**: 规范 Git 分支策略、提交规范、合并流程,建立团队一致的版本控制工作流
|
|
1457
|
+
|
|
1458
|
+
**触发词**: `Git 工作流`, `git workflow`, `git 规范`, `分支策略`, `branching strategy`, `commit 规范`, `commit convention`, `提交规范`, `PR 规范`, `rebase vs merge`, `冲突解决`, `@ethan git`, `@ethan git-workflow`
|
|
1459
|
+
|
|
1460
|
+
**执行步骤**:
|
|
1461
|
+
|
|
1462
|
+
#### 1. 评估项目特征,选择分支策略
|
|
1463
|
+
|
|
1464
|
+
根据团队规模和发布节奏选择合适的分支策略:
|
|
1465
|
+
|
|
1466
|
+
**GitFlow 适用场景**
|
|
1467
|
+
- 有明确版本号的产品(如 App、SDK、开源库)
|
|
1468
|
+
- 需要维护多个线上版本
|
|
1469
|
+
- 发布周期较长(周/月级别)
|
|
1470
|
+
|
|
1471
|
+
```
|
|
1472
|
+
main ──●────────────────────●── (生产稳定)
|
|
1473
|
+
hotfix/1.0.1 └──●──┘ (紧急修复)
|
|
1474
|
+
release/1.1 └──●──┘ (预发布验证)
|
|
1475
|
+
develop ──●──────●──────●──────●── (集成分支)
|
|
1476
|
+
feature/login └──●──┘ (功能开发)
|
|
1477
|
+
```
|
|
1478
|
+
|
|
1479
|
+
**Trunk-Based Development 适用场景**
|
|
1480
|
+
- 持续部署(CD)体系成熟
|
|
1481
|
+
- 有完善的 Feature Flag 机制
|
|
1482
|
+
- 团队规模适中(≤50 人),发布频率高(日/周)
|
|
1483
|
+
|
|
1484
|
+
```
|
|
1485
|
+
main ──●──●──●──●──●── (直接推送或短命分支 <2天)
|
|
1486
|
+
feat └──●──┘ (短命功能分支,快速合并)
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
**决策矩阵**
|
|
1490
|
+
|
|
1491
|
+
| 维度 | GitFlow | Trunk-Based |
|
|
1492
|
+
|------|---------|-------------|
|
|
1493
|
+
| 发布频率 | 低(周/月) | 高(日/周) |
|
|
1494
|
+
| 团队规模 | 大 | 中小 |
|
|
1495
|
+
| 多版本维护 | 支持 | 不擅长 |
|
|
1496
|
+
| CI/CD 成熟度 | 低要求 | 高要求 |
|
|
1497
|
+
|
|
1498
|
+
#### 2. 制定提交信息规范(Conventional Commits)
|
|
1499
|
+
|
|
1500
|
+
采用 Conventional Commits 规范,格式:`<type>(<scope>): <subject>`
|
|
1501
|
+
|
|
1502
|
+
**类型(type)定义**
|
|
1503
|
+
|
|
1504
|
+
| type | 用途 | 版本影响 |
|
|
1505
|
+
|------|------|---------|
|
|
1506
|
+
| `feat` | 新功能 | MINOR |
|
|
1507
|
+
| `fix` | Bug 修复 | PATCH |
|
|
1508
|
+
| `perf` | 性能优化 | PATCH |
|
|
1509
|
+
| `refactor` | 重构(无功能变化) | — |
|
|
1510
|
+
| `docs` | 文档变更 | — |
|
|
1511
|
+
| `test` | 测试相关 | — |
|
|
1512
|
+
| `chore` | 构建/依赖/工具 | — |
|
|
1513
|
+
| `ci` | CI 配置变更 | — |
|
|
1514
|
+
| `BREAKING CHANGE` | 破坏性变更(Footer) | MAJOR |
|
|
1515
|
+
|
|
1516
|
+
**示例**
|
|
1517
|
+
```bash
|
|
1518
|
+
# 好的提交信息
|
|
1519
|
+
feat(auth): add OAuth2 login with Google provider
|
|
1520
|
+
fix(cart): prevent duplicate item addition on rapid click
|
|
1521
|
+
perf(query): add composite index on (user_id, created_at)
|
|
1522
|
+
refactor(api): extract pagination helper to shared utils
|
|
1523
|
+
docs(readme): update installation steps for Node 20
|
|
1524
|
+
|
|
1525
|
+
# 破坏性变更写法
|
|
1526
|
+
feat(api)!: rename /users endpoint to /accounts
|
|
1527
|
+
|
|
1528
|
+
BREAKING CHANGE: /users endpoint removed, use /accounts instead
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
**工具链配置**
|
|
1532
|
+
```bash
|
|
1533
|
+
# 安装 commitlint
|
|
1534
|
+
npm install -D @commitlint/cli @commitlint/config-conventional
|
|
1535
|
+
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
|
|
1536
|
+
|
|
1537
|
+
# 配合 husky 在 commit-msg 钩子校验
|
|
1538
|
+
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'
|
|
1539
|
+
```
|
|
1540
|
+
|
|
1541
|
+
#### 3. Rebase vs Merge 决策与实践
|
|
1542
|
+
|
|
1543
|
+
**核心原则:黄金法则 — 不要 rebase 已推送的公共分支**
|
|
1544
|
+
|
|
1545
|
+
**何时用 Merge**
|
|
1546
|
+
- 合并长期分支(feature → develop)
|
|
1547
|
+
- 需要保留完整历史记录(审计场景)
|
|
1548
|
+
- 多人协作的共享分支
|
|
1549
|
+
|
|
1550
|
+
```bash
|
|
1551
|
+
# 保留合并记录(推荐用于 PR/MR 合并)
|
|
1552
|
+
git merge --no-ff feature/login
|
|
1553
|
+
|
|
1554
|
+
# 快进合并(适合独立小修改)
|
|
1555
|
+
git merge --ff-only hotfix/typo
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
**何时用 Rebase**
|
|
1559
|
+
- 更新本地功能分支,与主干保持同步
|
|
1560
|
+
- 整理本地提交历史,推送 PR 前清理
|
|
1561
|
+
|
|
1562
|
+
```bash
|
|
1563
|
+
# 将功能分支变基到最新 main
|
|
1564
|
+
git checkout feature/login
|
|
1565
|
+
git rebase origin/main
|
|
1566
|
+
|
|
1567
|
+
# 交互式 rebase:合并/重排/修改最近 3 个提交
|
|
1568
|
+
git rebase -i HEAD~3
|
|
1569
|
+
# 选项: pick / squash(s) / fixup(f) / reword(r) / drop(d)
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
**Squash Merge**(GitHub/GitLab PR 推荐)
|
|
1573
|
+
```bash
|
|
1574
|
+
# 将功能分支所有提交合并为一个干净提交
|
|
1575
|
+
git merge --squash feature/login
|
|
1576
|
+
git commit -m "feat(auth): add login page with form validation"
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
**推荐工作流**
|
|
1580
|
+
1. 本地开发:随意提交,保持节奏
|
|
1581
|
+
2. 推送 PR 前:`git rebase -i origin/main` 整理提交
|
|
1582
|
+
3. PR 合并:使用 Squash Merge 保持主干干净
|
|
1583
|
+
|
|
1584
|
+
#### 4. 冲突解决流程
|
|
1585
|
+
|
|
1586
|
+
**结构化冲突解决步骤**
|
|
1587
|
+
|
|
1588
|
+
```bash
|
|
1589
|
+
# Step 1: 理解冲突来源
|
|
1590
|
+
git log --oneline --graph --all # 查看分支关系
|
|
1591
|
+
git diff HEAD origin/main # 对比差异
|
|
1592
|
+
|
|
1593
|
+
# Step 2: 标记冲突文件分析
|
|
1594
|
+
git status # 查看所有冲突文件
|
|
1595
|
+
# conflict markers: <<<<<<< HEAD ... ======= ... >>>>>>> branch
|
|
1596
|
+
|
|
1597
|
+
# Step 3: 使用工具辅助解决
|
|
1598
|
+
git mergetool # 调用配置的 merge tool(VSCode / IntelliJ)
|
|
1599
|
+
|
|
1600
|
+
# 配置 VSCode 为默认 merge tool
|
|
1601
|
+
git config --global merge.tool vscode
|
|
1602
|
+
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
**三路合并理解(Three-way merge)**
|
|
1606
|
+
```
|
|
1607
|
+
BASE(公共祖先):const timeout = 5000;
|
|
1608
|
+
OURS(当前分支):const timeout = 10000; // 改为10s
|
|
1609
|
+
THEIRS(被合并):const TIMEOUT = 5000; // 改为大写常量名
|
|
1610
|
+
RESULT(手动): const TIMEOUT = 10000; // 两个改动都要
|
|
1611
|
+
```
|
|
1612
|
+
|
|
1613
|
+
**预防冲突的最佳实践**
|
|
1614
|
+
- 功能分支生命周期控制在 1-3 天内
|
|
1615
|
+
- 每日同步主干:`git pull --rebase origin main`
|
|
1616
|
+
- 大文件/自动生成文件加入 `.gitattributes` 配置合并策略
|
|
1617
|
+
```gitattributes
|
|
1618
|
+
# 始终使用 ours 策略合并 lock 文件(减少冲突)
|
|
1619
|
+
package-lock.json merge=ours
|
|
1620
|
+
yarn.lock merge=ours
|
|
1621
|
+
```
|
|
1622
|
+
|
|
1623
|
+
#### 5. Pull Request / Code Review 流程规范
|
|
1624
|
+
|
|
1625
|
+
**PR 模板设计**
|
|
1626
|
+
```markdown
|
|
1627
|
+
## 变更说明
|
|
1628
|
+
[简洁描述本次变更做了什么、为什么]
|
|
1629
|
+
|
|
1630
|
+
## 变更类型
|
|
1631
|
+
- [ ] 新功能 (feat)
|
|
1632
|
+
- [ ] Bug 修复 (fix)
|
|
1633
|
+
- [ ] 重构 (refactor)
|
|
1634
|
+
- [ ] 性能优化 (perf)
|
|
1635
|
+
|
|
1636
|
+
## 测试验证
|
|
1637
|
+
- [ ] 单元测试通过
|
|
1638
|
+
- [ ] 手动测试场景: [描述]
|
|
1639
|
+
- [ ] 截图/录屏(UI 变更必填)
|
|
1640
|
+
|
|
1641
|
+
## 影响范围
|
|
1642
|
+
[描述可能影响的模块或依赖方]
|
|
1643
|
+
|
|
1644
|
+
## Checklist
|
|
1645
|
+
- [ ] 代码自查完毕
|
|
1646
|
+
- [ ] 无调试代码 (console.log/debugger)
|
|
1647
|
+
- [ ] 文档已更新(如需要)
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
**PR 规模控制**
|
|
1651
|
+
- 理想 PR 大小:< 400 行(不含测试)
|
|
1652
|
+
- 超过 800 行:强制拆分为多个 PR
|
|
1653
|
+
- 可用 `git diff --stat origin/main` 提前检查
|
|
1654
|
+
|
|
1655
|
+
**分支保护规则(GitHub/GitLab 配置)**
|
|
1656
|
+
```
|
|
1657
|
+
main 分支保护:
|
|
1658
|
+
✅ Require pull request reviews (min: 1)
|
|
1659
|
+
✅ Require status checks to pass (CI/lint/test)
|
|
1660
|
+
✅ Require branches to be up to date
|
|
1661
|
+
✅ Restrict push access (仅管理员)
|
|
1662
|
+
✅ Require signed commits(高安全场景)
|
|
1663
|
+
```
|
|
1664
|
+
|
|
1665
|
+
#### 6. 输出工作流规范文档
|
|
1666
|
+
|
|
1667
|
+
整理为团队可直接使用的规范文档,格式如下:
|
|
1668
|
+
|
|
1669
|
+
```markdown
|
|
1670
|
+
## Git 工作流规范
|
|
1671
|
+
|
|
1672
|
+
### 分支命名
|
|
1673
|
+
- feature/<ticket-id>-short-description (如: feature/PROJ-123-user-login)
|
|
1674
|
+
- fix/<ticket-id>-short-description
|
|
1675
|
+
- hotfix/<version>-short-description (如: hotfix/1.2.1-payment-crash)
|
|
1676
|
+
- release/<version> (如: release/1.3.0)
|
|
1677
|
+
|
|
1678
|
+
### 提交规范
|
|
1679
|
+
格式: <type>(<scope>): <subject>
|
|
1680
|
+
示例: feat(auth): add JWT refresh token support
|
|
1681
|
+
|
|
1682
|
+
### 禁止行为
|
|
1683
|
+
❌ 直接推送到 main/master
|
|
1684
|
+
❌ force push 到共享分支
|
|
1685
|
+
❌ rebase 已推送的公共分支
|
|
1686
|
+
❌ 超过 1000 行的单次 PR(紧急 hotfix 除外)
|
|
1687
|
+
|
|
1688
|
+
### 分支生命周期
|
|
1689
|
+
- feature 分支: ≤ 5 个工作日
|
|
1690
|
+
- release 分支: ≤ 2 周
|
|
1691
|
+
- hotfix 分支: ≤ 24 小时
|
|
1692
|
+
```
|
|
1693
|
+
|
|
1694
|
+
**输出格式**: Markdown 工作流规范文档,含分支策略选型建议、提交规范示例、rebase/merge 决策指南、冲突解决 SOP 和 PR 规范模板
|
|
1695
|
+
|
|
1696
|
+
**注意事项**:
|
|
1697
|
+
- 分支策略没有银弹,根据团队规模和发版频率选择最适合的
|
|
1698
|
+
- force push 操作必须在团队内公告,避免其他成员本地分支混乱
|
|
1699
|
+
- 建议在 CI 中自动校验 commit message 格式,而非依赖人工审查
|
|
1700
|
+
- 冲突解决后务必运行测试,确保合并结果功能正常
|
|
1701
|
+
|
|
1702
|
+
---
|
|
1703
|
+
|
|
1704
|
+
### 16. 单元测试 (`unit-testing`)
|
|
1705
|
+
|
|
1706
|
+
**描述**: 运用 AAA 模式和 TDD 工作流编写高质量单元测试,建立覆盖率目标和 Mock 策略
|
|
1707
|
+
|
|
1708
|
+
**触发词**: `单元测试`, `unit test`, `写测试`, `write tests`, `TDD`, `测试设计`, `test design`, `mock 策略`, `mocking`, `测试覆盖率`, `coverage`, `@ethan test`, `@ethan unit-testing`
|
|
1709
|
+
|
|
1710
|
+
**执行步骤**:
|
|
1711
|
+
|
|
1712
|
+
#### 1. 明确测试目标与范围
|
|
1713
|
+
|
|
1714
|
+
在编写测试前,先明确测什么:
|
|
1715
|
+
|
|
1716
|
+
**测试金字塔**
|
|
1717
|
+
```
|
|
1718
|
+
┌───────────┐
|
|
1719
|
+
│ E2E 测试 │ (少量,慢,高置信)
|
|
1720
|
+
┌┴───────────┴┐
|
|
1721
|
+
│ 集成测试 │ (适量,中速)
|
|
1722
|
+
┌┴─────────────┴┐
|
|
1723
|
+
│ 单元测试 │ (大量,快,低成本)
|
|
1724
|
+
└───────────────┘
|
|
1725
|
+
```
|
|
1726
|
+
|
|
1727
|
+
**单元测试应该覆盖**
|
|
1728
|
+
- ✅ 纯函数的各种输入输出(含边界)
|
|
1729
|
+
- ✅ 类/模块的公共方法逻辑
|
|
1730
|
+
- ✅ 条件分支(if/switch/三元)
|
|
1731
|
+
- ✅ 错误处理路径(throw/catch)
|
|
1732
|
+
- ✅ 异步操作(Promise/async-await)
|
|
1733
|
+
|
|
1734
|
+
**不应该单元测试**
|
|
1735
|
+
- ❌ 简单的 getter/setter(无逻辑)
|
|
1736
|
+
- ❌ 第三方库内部实现
|
|
1737
|
+
- ❌ 框架本身(如 React 渲染机制)
|
|
1738
|
+
- ❌ 私有方法(通过公共方法间接测试)
|
|
1739
|
+
|
|
1740
|
+
#### 2. AAA 模式编写测试用例
|
|
1741
|
+
|
|
1742
|
+
每个测试用例遵循 **Arrange → Act → Assert** 三段式结构:
|
|
1743
|
+
|
|
1744
|
+
**基础示例(JavaScript/TypeScript with Vitest/Jest)**
|
|
1745
|
+
```typescript
|
|
1746
|
+
describe('calculateDiscount', () => {
|
|
1747
|
+
it('should apply 20% discount for premium users', () => {
|
|
1748
|
+
// Arrange(准备:设置测试数据和依赖)
|
|
1749
|
+
const user = { type: 'premium', cart: [{ price: 100 }, { price: 50 }] };
|
|
1750
|
+
const expectedTotal = 120; // 150 * 0.8
|
|
1751
|
+
|
|
1752
|
+
// Act(执行:调用被测函数)
|
|
1753
|
+
const result = calculateDiscount(user);
|
|
1754
|
+
|
|
1755
|
+
// Assert(断言:验证结果)
|
|
1756
|
+
expect(result.total).toBe(expectedTotal);
|
|
1757
|
+
expect(result.discountRate).toBe(0.2);
|
|
1758
|
+
});
|
|
1759
|
+
});
|
|
1760
|
+
```
|
|
1761
|
+
|
|
1762
|
+
**测试命名规范(Given-When-Then)**
|
|
1763
|
+
```typescript
|
|
1764
|
+
// 格式: should <expected behavior> when <condition>
|
|
1765
|
+
it('should return null when user is not found')
|
|
1766
|
+
it('should throw AuthError when token is expired')
|
|
1767
|
+
it('should apply 20% discount when user has premium status')
|
|
1768
|
+
|
|
1769
|
+
// 或使用 Given-When-Then 风格
|
|
1770
|
+
it('given empty cart, when checkout, then throws EmptyCartError')
|
|
1771
|
+
```
|
|
1772
|
+
|
|
1773
|
+
**边界条件测试清单**
|
|
1774
|
+
```typescript
|
|
1775
|
+
describe('parseAge', () => {
|
|
1776
|
+
// 正常值
|
|
1777
|
+
it('should parse valid age 25')
|
|
1778
|
+
// 边界值
|
|
1779
|
+
it('should accept minimum age 0')
|
|
1780
|
+
it('should accept maximum age 150')
|
|
1781
|
+
// 非法值
|
|
1782
|
+
it('should throw when age is negative')
|
|
1783
|
+
it('should throw when age exceeds 150')
|
|
1784
|
+
// 类型边界
|
|
1785
|
+
it('should throw when age is not a number')
|
|
1786
|
+
it('should throw when age is null or undefined')
|
|
1787
|
+
it('should handle decimal by flooring to integer')
|
|
1788
|
+
});
|
|
1789
|
+
```
|
|
1790
|
+
|
|
1791
|
+
#### 3. TDD 工作流(红-绿-重构)
|
|
1792
|
+
|
|
1793
|
+
**TDD 循环步骤**
|
|
1794
|
+
|
|
1795
|
+
```
|
|
1796
|
+
🔴 Red → 写一个失败的测试(先设计接口)
|
|
1797
|
+
🟢 Green → 写最少代码让测试通过(不过度设计)
|
|
1798
|
+
🔵 Refactor → 在测试保护下重构代码
|
|
1799
|
+
```
|
|
1800
|
+
|
|
1801
|
+
**实践示例:用 TDD 实现邮箱验证**
|
|
1802
|
+
|
|
1803
|
+
```typescript
|
|
1804
|
+
// Step 1 🔴 先写测试(此时 validateEmail 还不存在)
|
|
1805
|
+
describe('validateEmail', () => {
|
|
1806
|
+
it('should return true for valid email', () => {
|
|
1807
|
+
expect(validateEmail('user@example.com')).toBe(true);
|
|
1808
|
+
});
|
|
1809
|
+
it('should return false for missing @', () => {
|
|
1810
|
+
expect(validateEmail('userexample.com')).toBe(false);
|
|
1811
|
+
});
|
|
1812
|
+
it('should return false for empty string', () => {
|
|
1813
|
+
expect(validateEmail('')).toBe(false);
|
|
1814
|
+
});
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
// Step 2 🟢 写最简实现让测试通过
|
|
1818
|
+
export function validateEmail(email: string): boolean {
|
|
1819
|
+
return /^[^s@]+@[^s@]+.[^s@]+$/.test(email);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// Step 3 🔵 重构:提取正则为常量,添加类型注释
|
|
1823
|
+
const EMAIL_REGEX = /^[^s@]+@[^s@]+.[^s@]+$/;
|
|
1824
|
+
export function validateEmail(email: string): boolean {
|
|
1825
|
+
if (!email) return false;
|
|
1826
|
+
return EMAIL_REGEX.test(email);
|
|
1827
|
+
}
|
|
1828
|
+
```
|
|
1829
|
+
|
|
1830
|
+
**TDD 适用场景**
|
|
1831
|
+
- 明确需求的业务逻辑函数
|
|
1832
|
+
- 工具库/SDK 开发
|
|
1833
|
+
- Bug 修复(先写复现测试再修复)
|
|
1834
|
+
|
|
1835
|
+
**不强制 TDD 的场景**
|
|
1836
|
+
- 探索性开发阶段
|
|
1837
|
+
- UI 组件(先实现再补测试)
|
|
1838
|
+
|
|
1839
|
+
#### 4. Mock / Stub / Spy 策略
|
|
1840
|
+
|
|
1841
|
+
**三种测试替身的区别**
|
|
1842
|
+
|
|
1843
|
+
| 类型 | 用途 | 验证方式 |
|
|
1844
|
+
|------|------|---------|
|
|
1845
|
+
| **Stub** | 替换外部依赖,控制返回值 | 只验证输出 |
|
|
1846
|
+
| **Mock** | 验证函数是否被正确调用 | 验证调用行为 |
|
|
1847
|
+
| **Spy** | 监听真实函数的调用情况 | 包装真实实现 |
|
|
1848
|
+
|
|
1849
|
+
**Vitest/Jest 实践**
|
|
1850
|
+
```typescript
|
|
1851
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
1852
|
+
|
|
1853
|
+
// Stub: 控制外部 API 返回值
|
|
1854
|
+
vi.mock('../api/user', () => ({
|
|
1855
|
+
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
|
|
1856
|
+
}));
|
|
1857
|
+
|
|
1858
|
+
// Mock: 验证函数被调用
|
|
1859
|
+
it('should call sendEmail when user registers', async () => {
|
|
1860
|
+
const sendEmail = vi.fn();
|
|
1861
|
+
await registerUser({ email: 'test@test.com' }, { sendEmail });
|
|
1862
|
+
expect(sendEmail).toHaveBeenCalledOnce();
|
|
1863
|
+
expect(sendEmail).toHaveBeenCalledWith('test@test.com', expect.objectContaining({ subject: 'Welcome' }));
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
// Spy: 包装真实函数监听
|
|
1867
|
+
it('should log error when fetch fails', async () => {
|
|
1868
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1869
|
+
vi.mocked(fetchUser).mockRejectedValue(new Error('Network Error'));
|
|
1870
|
+
await loadUserProfile(1);
|
|
1871
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Network Error'));
|
|
1872
|
+
consoleSpy.mockRestore();
|
|
1873
|
+
});
|
|
1874
|
+
```
|
|
1875
|
+
|
|
1876
|
+
**Mock 黄金法则**
|
|
1877
|
+
- 只 Mock 跨边界的依赖(网络、数据库、文件系统、时间)
|
|
1878
|
+
- 不要 Mock 被测单元的内部实现
|
|
1879
|
+
- 每次测试后还原 Mock(使用 `beforeEach(() => vi.clearAllMocks())`)
|
|
1880
|
+
|
|
1881
|
+
#### 5. 覆盖率目标与质量保障
|
|
1882
|
+
|
|
1883
|
+
**覆盖率类型与目标**
|
|
1884
|
+
|
|
1885
|
+
| 覆盖率类型 | 说明 | 建议目标 |
|
|
1886
|
+
|----------|------|---------|
|
|
1887
|
+
| 语句覆盖(Statements) | 执行的语句比例 | ≥ 80% |
|
|
1888
|
+
| 分支覆盖(Branches) | if/else 分支比例 | ≥ 75% |
|
|
1889
|
+
| 函数覆盖(Functions) | 调用的函数比例 | ≥ 80% |
|
|
1890
|
+
| 行覆盖(Lines) | 执行的代码行比例 | ≥ 80% |
|
|
1891
|
+
|
|
1892
|
+
**Vitest 覆盖率配置**
|
|
1893
|
+
```typescript
|
|
1894
|
+
// vitest.config.ts
|
|
1895
|
+
export default defineConfig({
|
|
1896
|
+
test: {
|
|
1897
|
+
coverage: {
|
|
1898
|
+
provider: 'v8', // 或 'istanbul'
|
|
1899
|
+
reporter: ['text', 'html', 'lcov'],
|
|
1900
|
+
thresholds: {
|
|
1901
|
+
statements: 80,
|
|
1902
|
+
branches: 75,
|
|
1903
|
+
functions: 80,
|
|
1904
|
+
lines: 80,
|
|
1905
|
+
},
|
|
1906
|
+
exclude: [
|
|
1907
|
+
'node_modules/',
|
|
1908
|
+
'src/types/',
|
|
1909
|
+
'**/*.config.*',
|
|
1910
|
+
'**/*.d.ts',
|
|
1911
|
+
],
|
|
1912
|
+
},
|
|
1913
|
+
},
|
|
1914
|
+
});
|
|
1915
|
+
```
|
|
1916
|
+
|
|
1917
|
+
**覆盖率反模式(要避免)**
|
|
1918
|
+
```typescript
|
|
1919
|
+
// ❌ 为了覆盖率写无意义断言
|
|
1920
|
+
it('does something', () => {
|
|
1921
|
+
expect(doSomething()).toBeDefined(); // 没有验证具体行为
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
// ✅ 验证真实业务逻辑
|
|
1925
|
+
it('should return correct discounted price', () => {
|
|
1926
|
+
expect(calculatePrice(100, 0.1)).toBe(90);
|
|
1927
|
+
});
|
|
1928
|
+
```
|
|
1929
|
+
|
|
1930
|
+
**CI 集成**
|
|
1931
|
+
```yaml
|
|
1932
|
+
# .github/workflows/test.yml
|
|
1933
|
+
- name: Run tests with coverage
|
|
1934
|
+
run: npm run test -- --coverage
|
|
1935
|
+
|
|
1936
|
+
- name: Comment coverage on PR
|
|
1937
|
+
uses: MishaKav/jest-coverage-comment@main
|
|
1938
|
+
with:
|
|
1939
|
+
coverage-summary-path: ./coverage/coverage-summary.json
|
|
1940
|
+
```
|
|
1941
|
+
|
|
1942
|
+
**输出格式**: Markdown 测试方案文档,含测试用例设计(AAA 格式)、Mock 策略说明、覆盖率目标和 CI 配置示例
|
|
1943
|
+
|
|
1944
|
+
**注意事项**:
|
|
1945
|
+
- 测试应该是自文档化的,好的测试名称比注释更有价值
|
|
1946
|
+
- 避免测试实现细节,测试行为而非内部结构,有助于重构时测试不频繁失败
|
|
1947
|
+
- 不要追求 100% 覆盖率,关注核心业务逻辑的质量覆盖
|
|
1948
|
+
- 测试代码同样需要维护,避免过度复杂的测试辅助函数
|
|
1949
|
+
|
|
1950
|
+
---
|
|
1951
|
+
|
|
1952
|
+
### 17. 系统设计 (`system-design`)
|
|
1953
|
+
|
|
1954
|
+
**描述**: 从需求澄清到架构设计全流程,完成高并发分布式系统的方案设计与权衡分析
|
|
1955
|
+
|
|
1956
|
+
**触发词**: `系统设计`, `system design`, `架构设计`, `architecture design`, `高并发系统`, `分布式系统`, `distributed system`, `容量估算`, `capacity estimation`, `扩展性设计`, `scalability`, `@ethan design`, `@ethan system-design`
|
|
1957
|
+
|
|
1958
|
+
**执行步骤**:
|
|
1959
|
+
|
|
1960
|
+
#### 1. 需求澄清与范围界定
|
|
1961
|
+
|
|
1962
|
+
在动手设计前,花 5 分钟澄清需求:
|
|
1963
|
+
|
|
1964
|
+
**功能需求(Functional Requirements)**
|
|
1965
|
+
- 系统的核心用例是什么?(写出 3-5 个最关键的)
|
|
1966
|
+
- 哪些功能在 scope 内,哪些明确 out of scope?
|
|
1967
|
+
- 用户角色有哪些?各自的主要操作是什么?
|
|
1968
|
+
|
|
1969
|
+
**非功能需求(Non-Functional Requirements)**
|
|
1970
|
+
|
|
1971
|
+
| 维度 | 问题 | 示例指标 |
|
|
1972
|
+
|------|------|---------|
|
|
1973
|
+
| 规模 | 用户量 / DAU / QPS 是多少? | 1亿用户,1000万 DAU |
|
|
1974
|
+
| 性能 | 读写延迟要求?P99 是多少? | P99 < 100ms |
|
|
1975
|
+
| 可用性 | 允许多少停机时间? | 99.9%(每年 8.7h) |
|
|
1976
|
+
| 一致性 | 强一致 or 最终一致? | 最终一致(可接受) |
|
|
1977
|
+
| 持久性 | 数据丢失容忍度? | RPO = 0(不允许丢失) |
|
|
1978
|
+
|
|
1979
|
+
**明确边界的示例问题**
|
|
1980
|
+
```
|
|
1981
|
+
Q: 设计一个 Twitter
|
|
1982
|
+
A(先澄清):
|
|
1983
|
+
- 只需要发推/关注/Feed 功能吗?(排除私信、广告)
|
|
1984
|
+
- 用户规模:3亿用户,1亿 DAU?
|
|
1985
|
+
- 读写比例:推文读多写少,100:1?
|
|
1986
|
+
- 媒体文件:支持图片/视频吗?
|
|
1987
|
+
- 全球分发还是单地区?
|
|
1988
|
+
```
|
|
1989
|
+
|
|
1990
|
+
#### 2. 容量估算(Back-of-Envelope)
|
|
1991
|
+
|
|
1992
|
+
快速估算系统规模,为架构决策提供数据依据:
|
|
1993
|
+
|
|
1994
|
+
**常用基准数字**
|
|
1995
|
+
```
|
|
1996
|
+
内存访问: ~100ns
|
|
1997
|
+
SSD 访问: ~100μs
|
|
1998
|
+
HDD 访问: ~10ms
|
|
1999
|
+
网络往返(同数据中心):~0.5ms
|
|
2000
|
+
网络往返(跨地区): ~100ms
|
|
2001
|
+
|
|
2002
|
+
1 MB = 10^6 bytes
|
|
2003
|
+
1 GB = 10^9 bytes
|
|
2004
|
+
1 TB = 10^12 bytes
|
|
2005
|
+
```
|
|
2006
|
+
|
|
2007
|
+
**估算示例:设计微博(Twitter-like)**
|
|
2008
|
+
```
|
|
2009
|
+
用户数据:
|
|
2010
|
+
- DAU: 1亿
|
|
2011
|
+
- 每用户每天发1条推文 → 写 QPS = 100M / 86400 ≈ 1160 QPS
|
|
2012
|
+
- 每用户每天读100条 → 读 QPS = 100 × 1160 = 116,000 QPS
|
|
2013
|
+
|
|
2014
|
+
存储估算:
|
|
2015
|
+
- 单条推文: 140字 × 2字节(UTF-16) = 280字节 ≈ 300字节
|
|
2016
|
+
- 元数据(user_id, timestamp等): 100字节
|
|
2017
|
+
- 每条推文总计: ~400字节
|
|
2018
|
+
- 每日新增: 1.16K QPS × 400字节 × 86400 = ~40 GB/天
|
|
2019
|
+
- 5年存储: 40GB × 365 × 5 ≈ 73 TB
|
|
2020
|
+
|
|
2021
|
+
带宽估算:
|
|
2022
|
+
- 写带宽: 1160 × 400字节 = ~450 KB/s
|
|
2023
|
+
- 读带宽: 116K × 400字节 = ~45 MB/s
|
|
2024
|
+
```
|
|
2025
|
+
|
|
2026
|
+
**结论:** 读多写少(100:1),需要读缓存;存储量大需分库分表;单机无法支撑读 QPS 需多副本。
|
|
2027
|
+
|
|
2028
|
+
#### 3. 高层架构设计
|
|
2029
|
+
|
|
2030
|
+
从整体入手,画出系统的核心模块和数据流:
|
|
2031
|
+
|
|
2032
|
+
**通用分层架构**
|
|
2033
|
+
```
|
|
2034
|
+
客户端 (Web/Mobile/API Consumer)
|
|
2035
|
+
│
|
|
2036
|
+
▼
|
|
2037
|
+
DNS + CDN (静态资源 / 地理路由)
|
|
2038
|
+
│
|
|
2039
|
+
▼
|
|
2040
|
+
Load Balancer (L4/L7, 负载均衡 + SSL 终止)
|
|
2041
|
+
┌────┴────┐
|
|
2042
|
+
▼ ▼
|
|
2043
|
+
API Srv API Srv (无状态,水平扩展)
|
|
2044
|
+
│
|
|
2045
|
+
├──→ Cache (Redis: 热数据)
|
|
2046
|
+
├──→ Message Queue (Kafka: 异步解耦)
|
|
2047
|
+
├──→ Primary DB (写操作)
|
|
2048
|
+
└──→ Read Replica (读操作)
|
|
2049
|
+
│
|
|
2050
|
+
▼
|
|
2051
|
+
Object Storage (S3: 文件/媒体)
|
|
2052
|
+
Search Engine (Elasticsearch)
|
|
2053
|
+
```
|
|
2054
|
+
|
|
2055
|
+
**架构选型决策点**
|
|
2056
|
+
|
|
2057
|
+
| 场景 | 选型建议 |
|
|
2058
|
+
|------|---------|
|
|
2059
|
+
| 读多写少 | 读写分离 + 缓存层 |
|
|
2060
|
+
| 高写入吞吐 | 异步消息队列削峰 |
|
|
2061
|
+
| 数据量超百亿行 | 分库分表 / NoSQL |
|
|
2062
|
+
| 强一致性 | 单主 / Paxos / Raft |
|
|
2063
|
+
| 最终一致性 | 多主 / CRDT |
|
|
2064
|
+
| 低延迟全球访问 | CDN + 多地域部署 |
|
|
2065
|
+
| 复杂查询 | 专用搜索引擎 |
|
|
2066
|
+
|
|
2067
|
+
**微服务 vs 单体 决策**
|
|
2068
|
+
- 团队 < 10人,初创期:单体优先(避免过度工程)
|
|
2069
|
+
- 明确的服务边界、独立扩展需求:拆分微服务
|
|
2070
|
+
- 拆分原则:按业务边界(DDD 限界上下文),而非技术层
|
|
2071
|
+
|
|
2072
|
+
#### 4. 核心组件深度设计
|
|
2073
|
+
|
|
2074
|
+
针对最关键的 2-3 个组件进行深入设计:
|
|
2075
|
+
|
|
2076
|
+
**数据库 Schema 设计**
|
|
2077
|
+
```sql
|
|
2078
|
+
-- 示例:推文表设计
|
|
2079
|
+
CREATE TABLE tweets (
|
|
2080
|
+
id BIGINT PRIMARY KEY, -- Snowflake ID(分布式唯一ID)
|
|
2081
|
+
user_id BIGINT NOT NULL,
|
|
2082
|
+
content VARCHAR(280) NOT NULL,
|
|
2083
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
2084
|
+
like_count INT DEFAULT 0,
|
|
2085
|
+
retweet_count INT DEFAULT 0,
|
|
2086
|
+
INDEX idx_user_created (user_id, created_at DESC) -- 用户时间线查询
|
|
2087
|
+
);
|
|
2088
|
+
|
|
2089
|
+
-- Fan-out 策略:预写 vs 拉取
|
|
2090
|
+
-- 方案A: Push(写扩散): 发推时写入所有粉丝的 Feed 表
|
|
2091
|
+
-- 方案B: Pull(读扩散): 读取时聚合关注者的推文
|
|
2092
|
+
-- 混合方案: 普通用户 Push,大V(粉丝>100万)Pull
|
|
2093
|
+
```
|
|
2094
|
+
|
|
2095
|
+
**缓存策略**
|
|
2096
|
+
```
|
|
2097
|
+
Cache-Aside(旁路缓存)- 最通用
|
|
2098
|
+
读: 查缓存 → miss → 查DB → 写缓存 → 返回
|
|
2099
|
+
写: 更新DB → 删除缓存(避免双写不一致)
|
|
2100
|
+
|
|
2101
|
+
Write-Through(写穿)- 一致性高
|
|
2102
|
+
写: 同时写DB和缓存
|
|
2103
|
+
|
|
2104
|
+
Write-Behind(写回)- 高性能
|
|
2105
|
+
写: 先写缓存,异步批量写DB(风险:缓存宕机丢数据)
|
|
2106
|
+
|
|
2107
|
+
缓存 Key 设计示例:
|
|
2108
|
+
user:{userId}:profile → 用户资料
|
|
2109
|
+
user:{userId}:feed:page:{n} → 用户 Feed 分页
|
|
2110
|
+
tweet:{tweetId} → 单条推文
|
|
2111
|
+
```
|
|
2112
|
+
|
|
2113
|
+
**API 接口设计**
|
|
2114
|
+
```
|
|
2115
|
+
POST /tweets 发布推文
|
|
2116
|
+
GET /users/{id}/feed 获取 Feed (cursor分页)
|
|
2117
|
+
POST /tweets/{id}/like 点赞
|
|
2118
|
+
GET /tweets/{id} 获取单条推文
|
|
2119
|
+
|
|
2120
|
+
分页策略: cursor-based > offset-based(大数据量场景)
|
|
2121
|
+
cursor: base64(created_at + tweet_id)
|
|
2122
|
+
```
|
|
2123
|
+
|
|
2124
|
+
#### 5. 可扩展性与可用性权衡
|
|
2125
|
+
|
|
2126
|
+
**CAP 定理实践**
|
|
2127
|
+
```
|
|
2128
|
+
C(一致性)+ A(可用性)+ P(分区容错)三选二
|
|
2129
|
+
网络分区不可避免 → 通常是 CP 或 AP 的选择
|
|
2130
|
+
|
|
2131
|
+
CP 系统: ZooKeeper, HBase(金融交易、库存扣减)
|
|
2132
|
+
AP 系统: Cassandra, DynamoDB(社交Feed、购物车)
|
|
2133
|
+
```
|
|
2134
|
+
|
|
2135
|
+
**水平扩展策略**
|
|
2136
|
+
|
|
2137
|
+
| 层次 | 策略 |
|
|
2138
|
+
|------|------|
|
|
2139
|
+
| 无状态应用层 | 直接水平扩展 + 负载均衡 |
|
|
2140
|
+
| 有状态缓存 | 一致性哈希分片(Redis Cluster) |
|
|
2141
|
+
| 数据库水平 | 分库分表(按 user_id % N) |
|
|
2142
|
+
| 数据库垂直 | 主从复制,读写分离 |
|
|
2143
|
+
|
|
2144
|
+
**单点故障(SPOF)消除清单**
|
|
2145
|
+
- [ ] Load Balancer 双活/主备
|
|
2146
|
+
- [ ] 数据库主从 + 自动故障转移(MHA/Orchestrator)
|
|
2147
|
+
- [ ] 缓存集群(Redis Sentinel / Cluster)
|
|
2148
|
+
- [ ] 消息队列多副本(Kafka Replication Factor ≥ 3)
|
|
2149
|
+
- [ ] 跨可用区部署(Multi-AZ)
|
|
2150
|
+
|
|
2151
|
+
**限流与熔断**
|
|
2152
|
+
```
|
|
2153
|
+
限流: Token Bucket(突发流量友好)
|
|
2154
|
+
Sliding Window(精准限流)
|
|
2155
|
+
分级限流: 用户级 → 接口级 → 全局
|
|
2156
|
+
|
|
2157
|
+
熔断: Closed → Open(失败率>50%)→ Half-Open(探测恢复)
|
|
2158
|
+
工具: Resilience4j(Java)/ hystrix-go / Polly(.NET)
|
|
2159
|
+
```
|
|
2160
|
+
|
|
2161
|
+
#### 6. 输出系统设计文档
|
|
2162
|
+
|
|
2163
|
+
整理为结构化设计文档:
|
|
2164
|
+
|
|
2165
|
+
```markdown
|
|
2166
|
+
## 系统设计方案:[系统名称]
|
|
2167
|
+
|
|
2168
|
+
### 1. 需求概述
|
|
2169
|
+
**功能需求**(核心功能列表)
|
|
2170
|
+
**非功能需求**(QPS / 延迟 / 可用性 / 存储)
|
|
2171
|
+
|
|
2172
|
+
### 2. 容量估算
|
|
2173
|
+
| 指标 | 估算值 |
|
|
2174
|
+
|------|-------|
|
|
2175
|
+
| DAU | X 万 |
|
|
2176
|
+
| 写 QPS | X |
|
|
2177
|
+
| 读 QPS | X |
|
|
2178
|
+
| 存储(5年) | X TB |
|
|
2179
|
+
|
|
2180
|
+
### 3. 系统架构图
|
|
2181
|
+
[ASCII 图或 Mermaid 图]
|
|
2182
|
+
|
|
2183
|
+
### 4. 核心组件设计
|
|
2184
|
+
- **数据库 Schema**:[关键表设计]
|
|
2185
|
+
- **缓存策略**:[策略选择与理由]
|
|
2186
|
+
- **API 设计**:[关键接口]
|
|
2187
|
+
|
|
2188
|
+
### 5. 扩展性方案
|
|
2189
|
+
- **瓶颈点**:[识别的瓶颈]
|
|
2190
|
+
- **解决方案**:[具体方案]
|
|
2191
|
+
|
|
2192
|
+
### 6. 权衡与风险
|
|
2193
|
+
[已知权衡和设计风险]
|
|
2194
|
+
```
|
|
2195
|
+
|
|
2196
|
+
**输出格式**: Markdown 系统设计文档,含需求澄清结果、容量估算数据、架构图、核心组件设计方案和扩展性权衡分析
|
|
2197
|
+
|
|
2198
|
+
**注意事项**:
|
|
2199
|
+
- 系统设计没有标准答案,重点展示思考过程和权衡意识
|
|
2200
|
+
- 先画出高层架构,再逐步深入细节,避免一开始陷入细节
|
|
2201
|
+
- 主动提出设计中的权衡和不足,展示对复杂度的认知
|
|
2202
|
+
- 数量级估算误差在 10x 以内即可,重要的是数量级概念
|
|
2203
|
+
|
|
2204
|
+
---
|
|
2205
|
+
|
|
2206
|
+
### 18. 数据库优化 (`database-optimize`)
|
|
2207
|
+
|
|
2208
|
+
**描述**: 系统诊断数据库性能问题,涵盖 Schema 审查、索引设计、慢查询分析和 N+1 修复
|
|
2209
|
+
|
|
2210
|
+
**触发词**: `数据库优化`, `database optimize`, `慢查询`, `slow query`, `SQL 优化`, `SQL optimization`, `索引优化`, `index optimization`, `N+1 问题`, `N+1 query`, `查询性能`, `query performance`, `@ethan db`, `@ethan database-optimize`
|
|
2211
|
+
|
|
2212
|
+
**执行步骤**:
|
|
2213
|
+
|
|
2214
|
+
#### 1. Schema 设计审查
|
|
2215
|
+
|
|
2216
|
+
检查数据库表结构是否存在设计问题:
|
|
2217
|
+
|
|
2218
|
+
**规范化检查(防止冗余)**
|
|
2219
|
+
```sql
|
|
2220
|
+
-- ❌ 反模式:在用户表存储地址字符串
|
|
2221
|
+
CREATE TABLE users (
|
|
2222
|
+
id INT PRIMARY KEY,
|
|
2223
|
+
name VARCHAR(100),
|
|
2224
|
+
address VARCHAR(500) -- 难以精准查询城市/省份
|
|
2225
|
+
);
|
|
2226
|
+
|
|
2227
|
+
-- ✅ 正确:拆分为 addresses 表
|
|
2228
|
+
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100));
|
|
2229
|
+
CREATE TABLE addresses (
|
|
2230
|
+
id INT PRIMARY KEY,
|
|
2231
|
+
user_id INT REFERENCES users(id),
|
|
2232
|
+
province VARCHAR(50),
|
|
2233
|
+
city VARCHAR(50),
|
|
2234
|
+
detail VARCHAR(200)
|
|
2235
|
+
);
|
|
2236
|
+
```
|
|
2237
|
+
|
|
2238
|
+
**数据类型选择**
|
|
2239
|
+
|
|
2240
|
+
| 场景 | 推荐类型 | 避免 |
|
|
2241
|
+
|------|---------|------|
|
|
2242
|
+
| 主键 | BIGINT / UUID | INT(可能溢出) |
|
|
2243
|
+
| 状态枚举 | TINYINT / ENUM | VARCHAR |
|
|
2244
|
+
| 金额 | DECIMAL(10,2) | FLOAT(精度丢失)|
|
|
2245
|
+
| 时间 | TIMESTAMP / DATETIME | VARCHAR |
|
|
2246
|
+
| 短字符串(≤255) | VARCHAR(N) | TEXT |
|
|
2247
|
+
| 布尔值 | TINYINT(1) | VARCHAR('true') |
|
|
2248
|
+
|
|
2249
|
+
**常见 Schema 问题清单**
|
|
2250
|
+
- [ ] 是否有未使用的列?
|
|
2251
|
+
- [ ] VARCHAR 长度是否合理(不要都 VARCHAR(255))?
|
|
2252
|
+
- [ ] 外键是否有索引?
|
|
2253
|
+
- [ ] 是否有重复的字段(非规范化导致)?
|
|
2254
|
+
- [ ] 是否用了 TEXT/BLOB 存储应该单独存储的大文件?
|
|
2255
|
+
|
|
2256
|
+
#### 2. 索引设计策略
|
|
2257
|
+
|
|
2258
|
+
**索引类型选择**
|
|
2259
|
+
|
|
2260
|
+
```sql
|
|
2261
|
+
-- 单列索引:高选择性字段(如 email、手机号)
|
|
2262
|
+
CREATE INDEX idx_users_email ON users(email);
|
|
2263
|
+
|
|
2264
|
+
-- 联合索引:遵循最左前缀原则
|
|
2265
|
+
-- 适合查询: WHERE status = ? AND created_at > ?
|
|
2266
|
+
-- 适合查询: WHERE status = ?
|
|
2267
|
+
-- 不适合: WHERE created_at > ? (无法命中)
|
|
2268
|
+
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
|
|
2269
|
+
|
|
2270
|
+
-- 覆盖索引:索引包含查询所有字段,避免回表
|
|
2271
|
+
-- 查询: SELECT user_id, status FROM orders WHERE order_no = ?
|
|
2272
|
+
CREATE INDEX idx_orders_covering ON orders(order_no, user_id, status);
|
|
2273
|
+
|
|
2274
|
+
-- 前缀索引:长字符串节省空间
|
|
2275
|
+
CREATE INDEX idx_url_prefix ON pages(url(50));
|
|
2276
|
+
|
|
2277
|
+
-- 函数索引(MySQL 8.0+):对表达式建索引
|
|
2278
|
+
CREATE INDEX idx_lower_email ON users((LOWER(email)));
|
|
2279
|
+
```
|
|
2280
|
+
|
|
2281
|
+
**EXPLAIN 分析索引使用**
|
|
2282
|
+
```sql
|
|
2283
|
+
EXPLAIN SELECT * FROM orders
|
|
2284
|
+
WHERE user_id = 1001 AND status = 'PAID'
|
|
2285
|
+
ORDER BY created_at DESC LIMIT 10;
|
|
2286
|
+
|
|
2287
|
+
-- 关注字段:
|
|
2288
|
+
-- type: ref > range > index > ALL(ALL 最差)
|
|
2289
|
+
-- key: 使用的索引名(NULL 表示未使用索引)
|
|
2290
|
+
-- rows: 预估扫描行数(越小越好)
|
|
2291
|
+
-- Extra: Using filesort / Using temporary(需优化的信号)
|
|
2292
|
+
```
|
|
2293
|
+
|
|
2294
|
+
**索引原则**
|
|
2295
|
+
- 高频查询的 WHERE / JOIN / ORDER BY 字段建索引
|
|
2296
|
+
- 选择性低的字段慎建索引(如 status 只有3个值)
|
|
2297
|
+
- 避免在频繁更新的列上建过多索引(写性能代价)
|
|
2298
|
+
- 复合索引字段顺序:等值条件在前,范围条件在后
|
|
2299
|
+
|
|
2300
|
+
#### 3. 慢查询分析与优化
|
|
2301
|
+
|
|
2302
|
+
**开启慢查询日志**
|
|
2303
|
+
```sql
|
|
2304
|
+
-- MySQL 配置
|
|
2305
|
+
SET GLOBAL slow_query_log = 'ON';
|
|
2306
|
+
SET GLOBAL long_query_time = 1; -- 超过1秒记录
|
|
2307
|
+
SET GLOBAL log_queries_not_using_indexes = 'ON';
|
|
2308
|
+
|
|
2309
|
+
-- 查看慢查询日志文件位置
|
|
2310
|
+
SHOW VARIABLES LIKE 'slow_query_log_file';
|
|
2311
|
+
|
|
2312
|
+
-- 使用 pt-query-digest 分析日志
|
|
2313
|
+
pt-query-digest /var/log/mysql/slow.log | head -100
|
|
2314
|
+
```
|
|
2315
|
+
|
|
2316
|
+
**常见慢查询模式与修复**
|
|
2317
|
+
```sql
|
|
2318
|
+
-- ❌ 问题1: SELECT * 全列查询
|
|
2319
|
+
SELECT * FROM orders WHERE user_id = 1001;
|
|
2320
|
+
-- ✅ 修复: 只查需要的列
|
|
2321
|
+
SELECT id, order_no, status, total FROM orders WHERE user_id = 1001;
|
|
2322
|
+
|
|
2323
|
+
-- ❌ 问题2: 对索引列使用函数,导致索引失效
|
|
2324
|
+
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';
|
|
2325
|
+
-- ✅ 修复: 使用范围查询
|
|
2326
|
+
SELECT * FROM orders
|
|
2327
|
+
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';
|
|
2328
|
+
|
|
2329
|
+
-- ❌ 问题3: OR 导致索引失效(某些情况)
|
|
2330
|
+
SELECT * FROM users WHERE email = ? OR phone = ?;
|
|
2331
|
+
-- ✅ 修复: UNION ALL
|
|
2332
|
+
SELECT * FROM users WHERE email = ?
|
|
2333
|
+
UNION ALL
|
|
2334
|
+
SELECT * FROM users WHERE phone = ?;
|
|
2335
|
+
|
|
2336
|
+
-- ❌ 问题4: LIKE 前缀通配符
|
|
2337
|
+
SELECT * FROM products WHERE name LIKE '%iPhone%';
|
|
2338
|
+
-- ✅ 修复: 使用全文索引或 Elasticsearch
|
|
2339
|
+
SELECT * FROM products WHERE MATCH(name) AGAINST('iPhone' IN BOOLEAN MODE);
|
|
2340
|
+
|
|
2341
|
+
-- ❌ 问题5: 隐式类型转换
|
|
2342
|
+
SELECT * FROM users WHERE user_id = '1001'; -- user_id 是 INT
|
|
2343
|
+
-- ✅ 修复: 类型匹配
|
|
2344
|
+
SELECT * FROM users WHERE user_id = 1001;
|
|
2345
|
+
```
|
|
2346
|
+
|
|
2347
|
+
#### 4. N+1 查询识别与修复
|
|
2348
|
+
|
|
2349
|
+
**N+1 问题定义**:查询1次获取N条记录,再针对每条记录查询1次,共 N+1 次数据库访问。
|
|
2350
|
+
|
|
2351
|
+
**ORM 场景中的 N+1**
|
|
2352
|
+
```typescript
|
|
2353
|
+
// ❌ TypeORM N+1 示例:查100个用户 → 执行101次SQL
|
|
2354
|
+
const users = await userRepository.find(); // Query 1: SELECT * FROM users
|
|
2355
|
+
for (const user of users) {
|
|
2356
|
+
const orders = await user.orders; // Query 2-101: 每个用户各查一次
|
|
2357
|
+
console.log(orders.length);
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// ✅ 修复:使用 eager loading(JOIN)
|
|
2361
|
+
const users = await userRepository.find({
|
|
2362
|
+
relations: ['orders'], // 一次 JOIN 查询搞定
|
|
2363
|
+
});
|
|
2364
|
+
|
|
2365
|
+
// ✅ 或使用 QueryBuilder(更精确控制)
|
|
2366
|
+
const users = await userRepository
|
|
2367
|
+
.createQueryBuilder('user')
|
|
2368
|
+
.leftJoinAndSelect('user.orders', 'order')
|
|
2369
|
+
.where('order.status = :status', { status: 'PAID' })
|
|
2370
|
+
.getMany();
|
|
2371
|
+
```
|
|
2372
|
+
|
|
2373
|
+
**原生 SQL 批量查询模式**
|
|
2374
|
+
```sql
|
|
2375
|
+
-- ❌ N+1: 循环查询
|
|
2376
|
+
-- for user_id in user_ids: SELECT * FROM orders WHERE user_id = ?
|
|
2377
|
+
|
|
2378
|
+
-- ✅ 批量查询 + 应用层 Map 聚合
|
|
2379
|
+
SELECT user_id, COUNT(*) as order_count, SUM(total) as total_amount
|
|
2380
|
+
FROM orders
|
|
2381
|
+
WHERE user_id IN (1,2,3,...,100) -- 一次查询
|
|
2382
|
+
GROUP BY user_id;
|
|
2383
|
+
-- 在应用层用 Map 按 user_id 聚合
|
|
2384
|
+
```
|
|
2385
|
+
|
|
2386
|
+
**检测 N+1 工具**
|
|
2387
|
+
```
|
|
2388
|
+
- Laravel Debugbar(PHP)
|
|
2389
|
+
- Django Debug Toolbar(Python)
|
|
2390
|
+
- Bullet gem(Rails)
|
|
2391
|
+
- TypeORM logging: { logging: true } 观察 SQL 数量
|
|
2392
|
+
- DataLoader(GraphQL 场景批量加载)
|
|
2393
|
+
```
|
|
2394
|
+
|
|
2395
|
+
#### 5. 分区与分表策略
|
|
2396
|
+
|
|
2397
|
+
**表分区(Partitioning)— 单机方案**
|
|
2398
|
+
```sql
|
|
2399
|
+
-- 按时间范围分区(适合日志、订单历史)
|
|
2400
|
+
CREATE TABLE orders (
|
|
2401
|
+
id BIGINT,
|
|
2402
|
+
user_id INT,
|
|
2403
|
+
created_at DATETIME,
|
|
2404
|
+
total DECIMAL(10,2)
|
|
2405
|
+
) PARTITION BY RANGE (YEAR(created_at)) (
|
|
2406
|
+
PARTITION p2022 VALUES LESS THAN (2023),
|
|
2407
|
+
PARTITION p2023 VALUES LESS THAN (2024),
|
|
2408
|
+
PARTITION p2024 VALUES LESS THAN (2025),
|
|
2409
|
+
PARTITION pmax VALUES LESS THAN MAXVALUE
|
|
2410
|
+
);
|
|
2411
|
+
|
|
2412
|
+
-- 分区裁剪:查询自动只扫描相关分区
|
|
2413
|
+
SELECT * FROM orders WHERE created_at >= '2024-01-01';
|
|
2414
|
+
-- 只扫描 p2024 分区,跳过历史分区
|
|
2415
|
+
```
|
|
2416
|
+
|
|
2417
|
+
**分库分表策略(超千万行后考虑)**
|
|
2418
|
+
|
|
2419
|
+
| 方案 | 分片键选择 | 适用场景 |
|
|
2420
|
+
|------|----------|---------|
|
|
2421
|
+
| 水平分表(同库) | user_id % N | 单库容量瓶颈 |
|
|
2422
|
+
| 水平分库 | user_id % N | 读写 QPS 瓶颈 |
|
|
2423
|
+
| 按地区分库 | region | 合规/延迟要求 |
|
|
2424
|
+
|
|
2425
|
+
```
|
|
2426
|
+
分片键选择原则:
|
|
2427
|
+
- 选择查询中高频使用的字段(避免跨分片查询)
|
|
2428
|
+
- 选择数据分布均匀的字段(避免热点)
|
|
2429
|
+
- 一旦确定不能轻易更改
|
|
2430
|
+
|
|
2431
|
+
常见工具:
|
|
2432
|
+
- ShardingSphere(Java)
|
|
2433
|
+
- Vitess(MySQL 集群,YouTube 方案)
|
|
2434
|
+
- Citus(PostgreSQL 分布式扩展)
|
|
2435
|
+
```
|
|
2436
|
+
|
|
2437
|
+
**读写分离配置**
|
|
2438
|
+
```
|
|
2439
|
+
主库(Primary): 处理写操作 + 强一致读
|
|
2440
|
+
从库(Replica): 处理读操作(注意主从延迟,通常 <1s)
|
|
2441
|
+
|
|
2442
|
+
适用于读写比 > 4:1 的场景
|
|
2443
|
+
注意: 写后立即读可能读到旧数据(主从同步延迟)
|
|
2444
|
+
解决: 重要读操作路由到主库;或用 Redis 缓存最新写入
|
|
2445
|
+
```
|
|
2446
|
+
|
|
2447
|
+
**输出格式**: Markdown 优化报告,含 Schema 问题列表、索引设计方案、慢查询 EXPLAIN 分析、N+1 修复代码示例和分区建议
|
|
2448
|
+
|
|
2449
|
+
**注意事项**:
|
|
2450
|
+
- 优化前先用 EXPLAIN 分析,避免盲目加索引
|
|
2451
|
+
- 索引不是越多越好,每个索引都会降低写入性能,控制在 5-8 个以内
|
|
2452
|
+
- 分库分表是最后手段,优先考虑索引优化、缓存、读写分离
|
|
2453
|
+
- 生产环境加索引使用 gh-ost 或 pt-online-schema-change,避免锁表
|
|
2454
|
+
|
|
2455
|
+
---
|
|
2456
|
+
|
|
2457
|
+
### 19. Docker 容器化 (`docker`)
|
|
2458
|
+
|
|
2459
|
+
**描述**: 编写生产级 Dockerfile,实现多阶段构建、镜像优化和 docker-compose 编排
|
|
2460
|
+
|
|
2461
|
+
**触发词**: `Docker`, `docker`, `容器化`, `containerization`, `Dockerfile`, `dockerfile`, `docker-compose`, `镜像优化`, `image optimization`, `多阶段构建`, `multi-stage build`, `容器安全`, `@ethan docker`
|
|
2462
|
+
|
|
2463
|
+
**执行步骤**:
|
|
2464
|
+
|
|
2465
|
+
#### 1. Dockerfile 基础最佳实践
|
|
2466
|
+
|
|
2467
|
+
**基础规则清单**
|
|
2468
|
+
|
|
2469
|
+
```dockerfile
|
|
2470
|
+
# ✅ 使用具体版本标签,避免 latest(不可复现)
|
|
2471
|
+
FROM node:20.11-alpine3.19
|
|
2472
|
+
|
|
2473
|
+
# ✅ 设置工作目录(避免在根目录操作)
|
|
2474
|
+
WORKDIR /app
|
|
2475
|
+
|
|
2476
|
+
# ✅ 先复制依赖文件,利用层缓存
|
|
2477
|
+
# 依赖文件不变时,npm install 层直接复用缓存
|
|
2478
|
+
COPY package*.json ./
|
|
2479
|
+
RUN npm ci --only=production
|
|
2480
|
+
|
|
2481
|
+
# ✅ 再复制源码(源码改变不影响依赖缓存)
|
|
2482
|
+
COPY . .
|
|
2483
|
+
|
|
2484
|
+
# ✅ 使用非 root 用户运行(安全最佳实践)
|
|
2485
|
+
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
|
2486
|
+
USER appuser
|
|
2487
|
+
|
|
2488
|
+
# ✅ 仅暴露必要端口
|
|
2489
|
+
EXPOSE 3000
|
|
2490
|
+
|
|
2491
|
+
# ✅ 使用 ENTRYPOINT + CMD 组合(更灵活)
|
|
2492
|
+
ENTRYPOINT ["node"]
|
|
2493
|
+
CMD ["dist/index.js"]
|
|
2494
|
+
```
|
|
2495
|
+
|
|
2496
|
+
**层缓存优化原则**
|
|
2497
|
+
```
|
|
2498
|
+
构建缓存命中规则:指令 + 参数 + 上下文文件 都相同才命中缓存
|
|
2499
|
+
|
|
2500
|
+
优化策略:
|
|
2501
|
+
1. 变化频率低的指令放前面(基础镜像、系统依赖)
|
|
2502
|
+
2. 变化频率高的指令放后面(应用代码)
|
|
2503
|
+
3. 合并 RUN 指令减少层数
|
|
2504
|
+
|
|
2505
|
+
# ❌ 多个 RUN 产生多个层
|
|
2506
|
+
RUN apt-get update
|
|
2507
|
+
RUN apt-get install -y curl
|
|
2508
|
+
RUN apt-get clean
|
|
2509
|
+
|
|
2510
|
+
# ✅ 合并为一个 RUN,减少层数 + 及时清理缓存
|
|
2511
|
+
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
|
2512
|
+
```
|
|
2513
|
+
|
|
2514
|
+
#### 2. 多阶段构建(Multi-Stage Build)
|
|
2515
|
+
|
|
2516
|
+
多阶段构建将构建环境与运行环境分离,显著减小生产镜像体积:
|
|
2517
|
+
|
|
2518
|
+
**Node.js 应用示例**
|
|
2519
|
+
```dockerfile
|
|
2520
|
+
# ===== Stage 1: Build =====
|
|
2521
|
+
FROM node:20.11-alpine3.19 AS builder
|
|
2522
|
+
WORKDIR /app
|
|
2523
|
+
|
|
2524
|
+
# 安装所有依赖(含 devDependencies)
|
|
2525
|
+
COPY package*.json ./
|
|
2526
|
+
RUN npm ci
|
|
2527
|
+
|
|
2528
|
+
# 编译 TypeScript
|
|
2529
|
+
COPY . .
|
|
2530
|
+
RUN npm run build
|
|
2531
|
+
|
|
2532
|
+
# ===== Stage 2: Dependencies =====
|
|
2533
|
+
FROM node:20.11-alpine3.19 AS deps
|
|
2534
|
+
WORKDIR /app
|
|
2535
|
+
COPY package*.json ./
|
|
2536
|
+
# 只安装生产依赖
|
|
2537
|
+
RUN npm ci --only=production
|
|
2538
|
+
|
|
2539
|
+
# ===== Stage 3: Production =====
|
|
2540
|
+
FROM node:20.11-alpine3.19 AS production
|
|
2541
|
+
WORKDIR /app
|
|
2542
|
+
|
|
2543
|
+
# 只从前两个阶段复制必要文件
|
|
2544
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
2545
|
+
COPY --from=builder /app/dist ./dist
|
|
2546
|
+
|
|
2547
|
+
# 非 root 用户
|
|
2548
|
+
RUN addgroup -S app && adduser -S app -G app
|
|
2549
|
+
USER app
|
|
2550
|
+
|
|
2551
|
+
EXPOSE 3000
|
|
2552
|
+
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
|
|
2553
|
+
CMD ["node", "dist/index.js"]
|
|
2554
|
+
```
|
|
2555
|
+
|
|
2556
|
+
**效果对比**
|
|
2557
|
+
```
|
|
2558
|
+
单阶段构建(含 devDeps + 源码): ~800 MB
|
|
2559
|
+
多阶段构建(只含运行时): ~120 MB
|
|
2560
|
+
体积减少约 85%
|
|
2561
|
+
```
|
|
2562
|
+
|
|
2563
|
+
**Go 应用(静态二进制最小镜像)**
|
|
2564
|
+
```dockerfile
|
|
2565
|
+
FROM golang:1.22-alpine AS builder
|
|
2566
|
+
WORKDIR /app
|
|
2567
|
+
COPY go.mod go.sum ./
|
|
2568
|
+
RUN go mod download
|
|
2569
|
+
COPY . .
|
|
2570
|
+
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
|
|
2571
|
+
|
|
2572
|
+
# 使用 scratch(空镜像)或 distroless
|
|
2573
|
+
FROM gcr.io/distroless/static-debian12
|
|
2574
|
+
COPY --from=builder /app/server /server
|
|
2575
|
+
EXPOSE 8080
|
|
2576
|
+
ENTRYPOINT ["/server"]
|
|
2577
|
+
# 最终镜像仅 ~10MB
|
|
2578
|
+
```
|
|
2579
|
+
|
|
2580
|
+
#### 3. .dockerignore 与镜像安全
|
|
2581
|
+
|
|
2582
|
+
**配置 .dockerignore**
|
|
2583
|
+
```dockerignore
|
|
2584
|
+
# 排除不需要的文件,减小构建上下文
|
|
2585
|
+
node_modules
|
|
2586
|
+
npm-debug.log
|
|
2587
|
+
.git
|
|
2588
|
+
.gitignore
|
|
2589
|
+
.env
|
|
2590
|
+
.env.*
|
|
2591
|
+
*.md
|
|
2592
|
+
.DS_Store
|
|
2593
|
+
coverage/
|
|
2594
|
+
dist/
|
|
2595
|
+
.nyc_output
|
|
2596
|
+
__tests__
|
|
2597
|
+
*.test.ts
|
|
2598
|
+
Dockerfile*
|
|
2599
|
+
docker-compose*
|
|
2600
|
+
```
|
|
2601
|
+
|
|
2602
|
+
**镜像安全扫描**
|
|
2603
|
+
```bash
|
|
2604
|
+
# Trivy(推荐,免费开源)
|
|
2605
|
+
docker pull aquasec/trivy
|
|
2606
|
+
trivy image --severity HIGH,CRITICAL myapp:latest
|
|
2607
|
+
|
|
2608
|
+
# 输出示例:
|
|
2609
|
+
# CRITICAL: CVE-2024-xxxx in openssl 3.0.0 → 升级到 3.0.13
|
|
2610
|
+
|
|
2611
|
+
# 集成到 CI(GitHub Actions)
|
|
2612
|
+
- name: Scan Docker image
|
|
2613
|
+
uses: aquasecurity/trivy-action@master
|
|
2614
|
+
with:
|
|
2615
|
+
image-ref: 'myapp:${{ github.sha }}'
|
|
2616
|
+
severity: 'CRITICAL,HIGH'
|
|
2617
|
+
exit-code: '1' # 发现高危漏洞时 CI 失败
|
|
2618
|
+
```
|
|
2619
|
+
|
|
2620
|
+
**容器运行时安全配置**
|
|
2621
|
+
```bash
|
|
2622
|
+
# 禁止 root 运行(Dockerfile 中已设置 USER,运行时再确认)
|
|
2623
|
+
docker run --user 1001:1001 myapp:latest
|
|
2624
|
+
|
|
2625
|
+
# 只读文件系统(防止容器内写文件)
|
|
2626
|
+
docker run --read-only --tmpfs /tmp myapp:latest
|
|
2627
|
+
|
|
2628
|
+
# 限制资源
|
|
2629
|
+
docker run --memory="256m" --cpus="0.5" myapp:latest
|
|
2630
|
+
|
|
2631
|
+
# 丢弃不需要的 Linux Capabilities
|
|
2632
|
+
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp:latest
|
|
2633
|
+
|
|
2634
|
+
# 禁止权限提升
|
|
2635
|
+
docker run --security-opt no-new-privileges myapp:latest
|
|
2636
|
+
```
|
|
2637
|
+
|
|
2638
|
+
#### 4. Docker Compose 服务编排
|
|
2639
|
+
|
|
2640
|
+
**生产级 docker-compose.yml 示例**
|
|
2641
|
+
```yaml
|
|
2642
|
+
version: '3.9'
|
|
2643
|
+
|
|
2644
|
+
services:
|
|
2645
|
+
app:
|
|
2646
|
+
build:
|
|
2647
|
+
context: .
|
|
2648
|
+
dockerfile: Dockerfile
|
|
2649
|
+
target: production # 指定多阶段构建的目标阶段
|
|
2650
|
+
image: myapp:${APP_VERSION:-latest}
|
|
2651
|
+
restart: unless-stopped
|
|
2652
|
+
ports:
|
|
2653
|
+
- "3000:3000"
|
|
2654
|
+
environment:
|
|
2655
|
+
NODE_ENV: production
|
|
2656
|
+
DATABASE_URL: ${DATABASE_URL} # 从 .env 文件读取,不硬编码
|
|
2657
|
+
env_file:
|
|
2658
|
+
- .env.production
|
|
2659
|
+
depends_on:
|
|
2660
|
+
db:
|
|
2661
|
+
condition: service_healthy # 等待健康检查通过
|
|
2662
|
+
redis:
|
|
2663
|
+
condition: service_healthy
|
|
2664
|
+
healthcheck:
|
|
2665
|
+
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
|
|
2666
|
+
interval: 30s
|
|
2667
|
+
timeout: 5s
|
|
2668
|
+
retries: 3
|
|
2669
|
+
start_period: 40s
|
|
2670
|
+
deploy:
|
|
2671
|
+
resources:
|
|
2672
|
+
limits:
|
|
2673
|
+
cpus: '1.0'
|
|
2674
|
+
memory: 512M
|
|
2675
|
+
networks:
|
|
2676
|
+
- app-network
|
|
2677
|
+
|
|
2678
|
+
db:
|
|
2679
|
+
image: postgres:16-alpine
|
|
2680
|
+
restart: unless-stopped
|
|
2681
|
+
environment:
|
|
2682
|
+
POSTGRES_DB: ${DB_NAME}
|
|
2683
|
+
POSTGRES_USER: ${DB_USER}
|
|
2684
|
+
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
2685
|
+
volumes:
|
|
2686
|
+
- postgres-data:/var/lib/postgresql/data
|
|
2687
|
+
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
|
2688
|
+
healthcheck:
|
|
2689
|
+
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
|
|
2690
|
+
interval: 10s
|
|
2691
|
+
timeout: 5s
|
|
2692
|
+
retries: 5
|
|
2693
|
+
networks:
|
|
2694
|
+
- app-network
|
|
2695
|
+
|
|
2696
|
+
redis:
|
|
2697
|
+
image: redis:7-alpine
|
|
2698
|
+
restart: unless-stopped
|
|
2699
|
+
command: redis-server --requirepass ${REDIS_PASSWORD}
|
|
2700
|
+
volumes:
|
|
2701
|
+
- redis-data:/data
|
|
2702
|
+
healthcheck:
|
|
2703
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
2704
|
+
interval: 10s
|
|
2705
|
+
networks:
|
|
2706
|
+
- app-network
|
|
2707
|
+
|
|
2708
|
+
networks:
|
|
2709
|
+
app-network:
|
|
2710
|
+
driver: bridge
|
|
2711
|
+
|
|
2712
|
+
volumes:
|
|
2713
|
+
postgres-data:
|
|
2714
|
+
redis-data:
|
|
2715
|
+
```
|
|
2716
|
+
|
|
2717
|
+
**常用 Compose 命令**
|
|
2718
|
+
```bash
|
|
2719
|
+
docker compose up -d # 后台启动
|
|
2720
|
+
docker compose up -d --build # 重新构建并启动
|
|
2721
|
+
docker compose logs -f app # 实时查看日志
|
|
2722
|
+
docker compose exec app sh # 进入容器 shell
|
|
2723
|
+
docker compose ps # 查看服务状态
|
|
2724
|
+
docker compose down -v # 停止并删除 volume
|
|
2725
|
+
```
|
|
2726
|
+
|
|
2727
|
+
#### 5. 镜像优化与发布
|
|
2728
|
+
|
|
2729
|
+
**镜像大小优化总结**
|
|
2730
|
+
|
|
2731
|
+
| 优化手段 | 效果 |
|
|
2732
|
+
|---------|------|
|
|
2733
|
+
| 使用 Alpine 基础镜像 | node:20 → node:20-alpine,1.1GB → 150MB |
|
|
2734
|
+
| 多阶段构建 | 去除构建工具 & devDependencies |
|
|
2735
|
+
| .dockerignore | 减小构建上下文 |
|
|
2736
|
+
| 合并 RUN 清理缓存 | 减少层数和大小 |
|
|
2737
|
+
| distroless/scratch | Go/Rust 应用极小镜像 |
|
|
2738
|
+
|
|
2739
|
+
**镜像打标签规范**
|
|
2740
|
+
```bash
|
|
2741
|
+
# 语义化版本 + git commit hash
|
|
2742
|
+
docker build -t myapp:1.2.3 -t myapp:1.2.3-abc1234 .
|
|
2743
|
+
|
|
2744
|
+
# CI 中自动打标签
|
|
2745
|
+
docker build -t myregistry/myapp:${VERSION} -t myregistry/myapp:latest --label "git.commit=${GIT_SHA}" --label "build.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" .
|
|
2746
|
+
```
|
|
2747
|
+
|
|
2748
|
+
**镜像推送到 Registry**
|
|
2749
|
+
```bash
|
|
2750
|
+
# 登录到 GitHub Container Registry
|
|
2751
|
+
echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin
|
|
2752
|
+
|
|
2753
|
+
# 推送
|
|
2754
|
+
docker push ghcr.io/org/myapp:1.2.3
|
|
2755
|
+
|
|
2756
|
+
# 使用 Docker BuildKit(并行构建,更快)
|
|
2757
|
+
DOCKER_BUILDKIT=1 docker build .
|
|
2758
|
+
|
|
2759
|
+
# 多平台构建(兼容 ARM Mac 和 x86 服务器)
|
|
2760
|
+
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
|
|
2761
|
+
```
|
|
2762
|
+
|
|
2763
|
+
**输出格式**: Markdown 容器化方案,含优化后的 Dockerfile、.dockerignore、docker-compose.yml 配置和安全加固建议
|
|
2764
|
+
|
|
2765
|
+
**注意事项**:
|
|
2766
|
+
- 生产镜像绝不使用 :latest 标签,始终用具体版本号确保可复现
|
|
2767
|
+
- 绝不在 Dockerfile 中写入密钥或密码,使用环境变量或 Docker Secrets
|
|
2768
|
+
- 每次发版前用 Trivy 扫描镜像漏洞,CRITICAL 漏洞不上线
|
|
2769
|
+
- docker-compose 仅用于本地开发和小规模部署,生产大规模编排推荐 Kubernetes
|
|
2770
|
+
|
|
2771
|
+
---
|
|
2772
|
+
|
|
2773
|
+
### 20. CI/CD 流水线 (`cicd`)
|
|
2774
|
+
|
|
2775
|
+
**描述**: 设计完整 CI/CD 流水线,涵盖流水线阶段设计、测试自动化、部署门控和回滚策略
|
|
2776
|
+
|
|
2777
|
+
**触发词**: `CI/CD`, `cicd`, `流水线`, `pipeline`, `持续集成`, `continuous integration`, `持续部署`, `continuous deployment`, `自动化部署`, `automated deployment`, `GitHub Actions`, `构建优化`, `@ethan cicd`, `@ethan ci`
|
|
2778
|
+
|
|
2779
|
+
**执行步骤**:
|
|
2780
|
+
|
|
2781
|
+
#### 1. 流水线阶段设计
|
|
2782
|
+
|
|
2783
|
+
**标准 CI/CD 流水线结构**
|
|
2784
|
+
|
|
2785
|
+
```
|
|
2786
|
+
Push/PR → [CI 阶段] → [镜像构建] → [部署到 Staging] → [部署到 Production]
|
|
2787
|
+
|
|
2788
|
+
CI 阶段(每次 Push/PR 触发):
|
|
2789
|
+
├── 代码检查: Lint + Type Check
|
|
2790
|
+
├── 单元测试: Unit Tests + Coverage
|
|
2791
|
+
├── 安全扫描: SAST + Dependency Audit
|
|
2792
|
+
└── 构建验证: Build Success Check
|
|
2793
|
+
|
|
2794
|
+
镜像构建(CI 通过后):
|
|
2795
|
+
├── Docker Build(多平台)
|
|
2796
|
+
├── 镜像安全扫描(Trivy)
|
|
2797
|
+
└── 推送到 Registry(打 tag)
|
|
2798
|
+
|
|
2799
|
+
部署流程:
|
|
2800
|
+
├── Staging(自动,合并到 main 后)
|
|
2801
|
+
│ ├── 集成测试
|
|
2802
|
+
│ └── E2E 测试(冒烟)
|
|
2803
|
+
└── Production(需审批 or 手动触发)
|
|
2804
|
+
├── 部署策略(蓝绿/金丝雀)
|
|
2805
|
+
└── 部署后验证(健康检查)
|
|
2806
|
+
```
|
|
2807
|
+
|
|
2808
|
+
**快速反馈原则**
|
|
2809
|
+
- CI 总时长目标:< 10 分钟(开发者等待阈值)
|
|
2810
|
+
- 测试并行化:单元测试 → 集成测试 → E2E(分层执行)
|
|
2811
|
+
- Fail Fast:代码格式错误最先检查,最快发现
|
|
2812
|
+
|
|
2813
|
+
#### 2. GitHub Actions 流水线配置
|
|
2814
|
+
|
|
2815
|
+
**完整 CI 工作流示例**
|
|
2816
|
+
```yaml
|
|
2817
|
+
# .github/workflows/ci.yml
|
|
2818
|
+
name: CI
|
|
2819
|
+
|
|
2820
|
+
on:
|
|
2821
|
+
push:
|
|
2822
|
+
branches: [main, develop]
|
|
2823
|
+
pull_request:
|
|
2824
|
+
branches: [main]
|
|
2825
|
+
|
|
2826
|
+
env:
|
|
2827
|
+
NODE_VERSION: '20'
|
|
2828
|
+
REGISTRY: ghcr.io
|
|
2829
|
+
IMAGE_NAME: ${{ github.repository }}
|
|
2830
|
+
|
|
2831
|
+
jobs:
|
|
2832
|
+
# ─── 代码质量检查 ───────────────────────────────
|
|
2833
|
+
lint:
|
|
2834
|
+
name: Lint & Type Check
|
|
2835
|
+
runs-on: ubuntu-latest
|
|
2836
|
+
steps:
|
|
2837
|
+
- uses: actions/checkout@v4
|
|
2838
|
+
- uses: actions/setup-node@v4
|
|
2839
|
+
with:
|
|
2840
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
2841
|
+
cache: 'npm'
|
|
2842
|
+
- run: npm ci
|
|
2843
|
+
- run: npm run lint
|
|
2844
|
+
- run: npm run typecheck
|
|
2845
|
+
|
|
2846
|
+
# ─── 测试 ────────────────────────────────────────
|
|
2847
|
+
test:
|
|
2848
|
+
name: Unit Tests
|
|
2849
|
+
runs-on: ubuntu-latest
|
|
2850
|
+
steps:
|
|
2851
|
+
- uses: actions/checkout@v4
|
|
2852
|
+
- uses: actions/setup-node@v4
|
|
2853
|
+
with:
|
|
2854
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
2855
|
+
cache: 'npm'
|
|
2856
|
+
- run: npm ci
|
|
2857
|
+
- run: npm run test -- --coverage
|
|
2858
|
+
- name: Upload coverage to Codecov
|
|
2859
|
+
uses: codecov/codecov-action@v4
|
|
2860
|
+
with:
|
|
2861
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
2862
|
+
|
|
2863
|
+
# ─── 安全扫描 ───────────────────────────────────
|
|
2864
|
+
security:
|
|
2865
|
+
name: Security Audit
|
|
2866
|
+
runs-on: ubuntu-latest
|
|
2867
|
+
steps:
|
|
2868
|
+
- uses: actions/checkout@v4
|
|
2869
|
+
- run: npm audit --audit-level=high
|
|
2870
|
+
- uses: github/codeql-action/init@v3
|
|
2871
|
+
with:
|
|
2872
|
+
languages: javascript
|
|
2873
|
+
- uses: github/codeql-action/analyze@v3
|
|
2874
|
+
|
|
2875
|
+
# ─── 构建镜像 ───────────────────────────────────
|
|
2876
|
+
build:
|
|
2877
|
+
name: Build & Push Image
|
|
2878
|
+
needs: [lint, test, security]
|
|
2879
|
+
runs-on: ubuntu-latest
|
|
2880
|
+
if: github.ref == 'refs/heads/main'
|
|
2881
|
+
permissions:
|
|
2882
|
+
contents: read
|
|
2883
|
+
packages: write
|
|
2884
|
+
outputs:
|
|
2885
|
+
image-tag: ${{ steps.meta.outputs.tags }}
|
|
2886
|
+
steps:
|
|
2887
|
+
- uses: actions/checkout@v4
|
|
2888
|
+
- uses: docker/setup-buildx-action@v3
|
|
2889
|
+
- uses: docker/login-action@v3
|
|
2890
|
+
with:
|
|
2891
|
+
registry: ${{ env.REGISTRY }}
|
|
2892
|
+
username: ${{ github.actor }}
|
|
2893
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
2894
|
+
- uses: docker/metadata-action@v5
|
|
2895
|
+
id: meta
|
|
2896
|
+
with:
|
|
2897
|
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
2898
|
+
tags: |
|
|
2899
|
+
type=sha,prefix={{branch}}-
|
|
2900
|
+
type=semver,pattern={{version}}
|
|
2901
|
+
- uses: docker/build-push-action@v5
|
|
2902
|
+
with:
|
|
2903
|
+
push: true
|
|
2904
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
2905
|
+
cache-from: type=gha
|
|
2906
|
+
cache-to: type=gha,mode=max
|
|
2907
|
+
```
|
|
2908
|
+
|
|
2909
|
+
#### 3. 构建速度优化
|
|
2910
|
+
|
|
2911
|
+
**缓存策略**
|
|
2912
|
+
```yaml
|
|
2913
|
+
# npm/yarn 依赖缓存
|
|
2914
|
+
- uses: actions/cache@v4
|
|
2915
|
+
with:
|
|
2916
|
+
path: ~/.npm
|
|
2917
|
+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
2918
|
+
restore-keys: |
|
|
2919
|
+
${{ runner.os }}-node-
|
|
2920
|
+
|
|
2921
|
+
# Docker layer 缓存(使用 GitHub Actions Cache)
|
|
2922
|
+
- uses: docker/build-push-action@v5
|
|
2923
|
+
with:
|
|
2924
|
+
cache-from: type=gha
|
|
2925
|
+
cache-to: type=gha,mode=max
|
|
2926
|
+
```
|
|
2927
|
+
|
|
2928
|
+
**并行执行策略**
|
|
2929
|
+
```yaml
|
|
2930
|
+
# 使用 matrix 并行运行测试
|
|
2931
|
+
jobs:
|
|
2932
|
+
test:
|
|
2933
|
+
strategy:
|
|
2934
|
+
matrix:
|
|
2935
|
+
shard: [1, 2, 3, 4] # 4个并行 runner
|
|
2936
|
+
steps:
|
|
2937
|
+
- run: npm test -- --shard=${{ matrix.shard }}/4
|
|
2938
|
+
```
|
|
2939
|
+
|
|
2940
|
+
**跳过不必要的 CI**
|
|
2941
|
+
```yaml
|
|
2942
|
+
# 路径过滤:文档变更不触发完整 CI
|
|
2943
|
+
on:
|
|
2944
|
+
push:
|
|
2945
|
+
paths-ignore:
|
|
2946
|
+
- 'docs/**'
|
|
2947
|
+
- '*.md'
|
|
2948
|
+
- '.github/ISSUE_TEMPLATE/**'
|
|
2949
|
+
|
|
2950
|
+
# 或者使用 paths 只触发相关路径
|
|
2951
|
+
on:
|
|
2952
|
+
push:
|
|
2953
|
+
paths:
|
|
2954
|
+
- 'src/**'
|
|
2955
|
+
- 'tests/**'
|
|
2956
|
+
- 'package*.json'
|
|
2957
|
+
```
|
|
2958
|
+
|
|
2959
|
+
**Self-hosted Runner(节省 CI 费用)**
|
|
2960
|
+
```
|
|
2961
|
+
适用场景: 大型项目、私有依赖、特殊硬件需求
|
|
2962
|
+
注意事项:
|
|
2963
|
+
- 安全隔离(不要在 public repo 使用 self-hosted runner)
|
|
2964
|
+
- 定期更新 runner 软件
|
|
2965
|
+
- 隔离不同项目的 runner(避免环境污染)
|
|
2966
|
+
```
|
|
2967
|
+
|
|
2968
|
+
#### 4. 部署策略与门控
|
|
2969
|
+
|
|
2970
|
+
**三种主要部署策略**
|
|
2971
|
+
|
|
2972
|
+
**蓝绿部署(Blue-Green)**
|
|
2973
|
+
```
|
|
2974
|
+
适用: 需要零停机、可快速回滚的场景
|
|
2975
|
+
成本: 双倍资源(同时运行两套环境)
|
|
2976
|
+
|
|
2977
|
+
Blue(当前生产): v1.0 → 接收所有流量
|
|
2978
|
+
Green(新版本): v1.1 → 部署验证中
|
|
2979
|
+
切换: 负载均衡器流量从 Blue → Green(瞬间完成)
|
|
2980
|
+
回滚: 流量切回 Blue(秒级)
|
|
2981
|
+
```
|
|
2982
|
+
|
|
2983
|
+
**金丝雀部署(Canary Release)**
|
|
2984
|
+
```
|
|
2985
|
+
适用: 高风险变更、需要渐进式验证
|
|
2986
|
+
流程:
|
|
2987
|
+
1%流量 → 新版本(观察5min)
|
|
2988
|
+
→ 10%(观察15min)
|
|
2989
|
+
→ 50%(观察30min)
|
|
2990
|
+
→ 100%(全量)
|
|
2991
|
+
|
|
2992
|
+
Kubernetes 实现:
|
|
2993
|
+
kubectl scale deployment app-v2 --replicas=1 # 1/10 = 10%
|
|
2994
|
+
kubectl scale deployment app-v1 --replicas=9
|
|
2995
|
+
```
|
|
2996
|
+
|
|
2997
|
+
**部署门控(Deployment Gates)配置**
|
|
2998
|
+
```yaml
|
|
2999
|
+
# GitHub Environments 配置审批
|
|
3000
|
+
deploy-production:
|
|
3001
|
+
environment:
|
|
3002
|
+
name: production
|
|
3003
|
+
url: https://app.example.com
|
|
3004
|
+
# 需要人工审批
|
|
3005
|
+
steps:
|
|
3006
|
+
- name: Request approval
|
|
3007
|
+
uses: trstringer/manual-approval@v1
|
|
3008
|
+
with:
|
|
3009
|
+
approvers: team-lead,cto
|
|
3010
|
+
minimum-approvals: 1
|
|
3011
|
+
|
|
3012
|
+
# 自动门控:基于健康检查
|
|
3013
|
+
deploy-production:
|
|
3014
|
+
steps:
|
|
3015
|
+
- name: Deploy
|
|
3016
|
+
run: kubectl apply -f k8s/
|
|
3017
|
+
- name: Wait for rollout
|
|
3018
|
+
run: kubectl rollout status deployment/app --timeout=5m
|
|
3019
|
+
- name: Smoke test
|
|
3020
|
+
run: |
|
|
3021
|
+
sleep 10
|
|
3022
|
+
curl -f https://api.example.com/health || exit 1
|
|
3023
|
+
```
|
|
3024
|
+
|
|
3025
|
+
#### 5. 回滚策略与监控告警
|
|
3026
|
+
|
|
3027
|
+
**自动回滚触发条件**
|
|
3028
|
+
```yaml
|
|
3029
|
+
# 部署后自动验证,失败则回滚
|
|
3030
|
+
steps:
|
|
3031
|
+
- name: Deploy to production
|
|
3032
|
+
id: deploy
|
|
3033
|
+
run: kubectl set image deployment/app app=${{ env.NEW_IMAGE }}
|
|
3034
|
+
|
|
3035
|
+
- name: Monitor deployment health
|
|
3036
|
+
run: |
|
|
3037
|
+
# 等待10分钟,监控错误率
|
|
3038
|
+
for i in {1..20}; do
|
|
3039
|
+
ERROR_RATE=$(curl -s https://metrics.example.com/api/error-rate)
|
|
3040
|
+
if (( $(echo "$ERROR_RATE > 5" | bc -l) )); then
|
|
3041
|
+
echo "Error rate $ERROR_RATE% exceeds threshold, rolling back!"
|
|
3042
|
+
kubectl rollout undo deployment/app
|
|
3043
|
+
exit 1
|
|
3044
|
+
fi
|
|
3045
|
+
sleep 30
|
|
3046
|
+
done
|
|
3047
|
+
|
|
3048
|
+
- name: Rollback on failure
|
|
3049
|
+
if: failure() && steps.deploy.outcome == 'success'
|
|
3050
|
+
run: kubectl rollout undo deployment/app
|
|
3051
|
+
```
|
|
3052
|
+
|
|
3053
|
+
**Kubernetes 滚动更新配置**
|
|
3054
|
+
```yaml
|
|
3055
|
+
# deployment.yaml
|
|
3056
|
+
spec:
|
|
3057
|
+
strategy:
|
|
3058
|
+
type: RollingUpdate
|
|
3059
|
+
rollingUpdate:
|
|
3060
|
+
maxSurge: 1 # 最多多启动1个 Pod
|
|
3061
|
+
maxUnavailable: 0 # 始终保持满负载(零停机)
|
|
3062
|
+
minReadySeconds: 30 # Pod 就绪后等待30s再继续
|
|
3063
|
+
```
|
|
3064
|
+
|
|
3065
|
+
**部署通知**
|
|
3066
|
+
```yaml
|
|
3067
|
+
# 部署成功/失败通知到 Slack
|
|
3068
|
+
- name: Notify deployment status
|
|
3069
|
+
uses: slackapi/slack-github-action@v1
|
|
3070
|
+
with:
|
|
3071
|
+
channel-id: 'deployments'
|
|
3072
|
+
slack-message: |
|
|
3073
|
+
${{ job.status == 'success' && '✅' || '❌' }} Deployment to Production
|
|
3074
|
+
Version: ${{ github.sha }}
|
|
3075
|
+
Actor: ${{ github.actor }}
|
|
3076
|
+
Status: ${{ job.status }}
|
|
3077
|
+
env:
|
|
3078
|
+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
|
3079
|
+
```
|
|
3080
|
+
|
|
3081
|
+
**关键 CI/CD 指标**
|
|
3082
|
+
|
|
3083
|
+
| 指标 | 目标 | 说明 |
|
|
3084
|
+
|------|------|------|
|
|
3085
|
+
| Lead Time | < 1天 | 代码到生产的时间 |
|
|
3086
|
+
| Deploy Frequency | 每日1次+ | 部署频率 |
|
|
3087
|
+
| MTTR | < 1小时 | 故障恢复时间 |
|
|
3088
|
+
| Change Failure Rate | < 15% | 部署导致故障比例 |
|
|
3089
|
+
|
|
3090
|
+
**输出格式**: Markdown CI/CD 方案文档,含流水线阶段图、GitHub Actions YAML 配置、部署策略对比和回滚方案
|
|
3091
|
+
|
|
3092
|
+
**注意事项**:
|
|
3093
|
+
- 流水线应该是可靠的,不稳定的 CI 比没有 CI 更糟糕(影响信任度)
|
|
3094
|
+
- 保护 main 分支,禁止直接推送,所有变更必须经过 PR + CI 验证
|
|
3095
|
+
- 密钥统一用 GitHub Secrets / Vault 管理,严禁硬编码在配置文件中
|
|
3096
|
+
- 定期检查并更新 CI Actions 版本,避免使用废弃的 Action 版本
|
|
3097
|
+
|
|
3098
|
+
---
|
|
3099
|
+
|
|
3100
|
+
### 21. 性能优化 (`performance`)
|
|
3101
|
+
|
|
3102
|
+
**描述**: 系统化分析和优化前后端性能瓶颈,涵盖分析工具使用、优化策略和量化指标
|
|
3103
|
+
|
|
3104
|
+
**触发词**: `性能优化`, `performance`, `页面慢`, `接口慢`, `性能分析`, `profiling`, `Core Web Vitals`, `@ethan 性能`, `/性能优化`
|
|
3105
|
+
|
|
3106
|
+
**执行步骤**:
|
|
3107
|
+
|
|
3108
|
+
#### 1. 建立性能基线与目标
|
|
3109
|
+
|
|
3110
|
+
优化前先量化,避免盲目优化。
|
|
3111
|
+
|
|
3112
|
+
**前端核心指标(Core Web Vitals)**
|
|
3113
|
+
| 指标 | 含义 | 优秀 | 需改进 | 差 |
|
|
3114
|
+
|------|------|------|--------|-----|
|
|
3115
|
+
| LCP | 最大内容绘制 | ≤ 2.5s | ≤ 4s | > 4s |
|
|
3116
|
+
| INP | 交互响应延迟 | ≤ 200ms | ≤ 500ms | > 500ms |
|
|
3117
|
+
| CLS | 累积布局偏移 | ≤ 0.1 | ≤ 0.25 | > 0.25 |
|
|
3118
|
+
| TTFB | 首字节时间 | ≤ 800ms | ≤ 1.8s | > 1.8s |
|
|
3119
|
+
|
|
3120
|
+
**采集工具**
|
|
3121
|
+
```bash
|
|
3122
|
+
npm install -g @lhci/cli
|
|
3123
|
+
lhci autorun --collect.url=https://yoursite.com
|
|
3124
|
+
|
|
3125
|
+
npx autocannon -c 100 -d 30 http://localhost:3000/api/users
|
|
3126
|
+
```
|
|
3127
|
+
|
|
3128
|
+
#### 2. 前端性能优化
|
|
3129
|
+
|
|
3130
|
+
**资源加载优化**
|
|
3131
|
+
```html
|
|
3132
|
+
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
|
|
3133
|
+
<link rel="preconnect" href="https://api.example.com">
|
|
3134
|
+
<img src="hero.jpg" loading="eager" fetchpriority="high" />
|
|
3135
|
+
<img src="below-fold.jpg" loading="lazy" />
|
|
3136
|
+
```
|
|
3137
|
+
|
|
3138
|
+
**代码拆分(React)**
|
|
3139
|
+
```typescript
|
|
3140
|
+
const UserProfile = lazy(() => import('./pages/UserProfile'));
|
|
3141
|
+
|
|
3142
|
+
// 虚拟列表(大数据量)
|
|
3143
|
+
import { FixedSizeList } from 'react-window';
|
|
3144
|
+
<FixedSizeList height={600} itemCount={10000} itemSize={50}>
|
|
3145
|
+
{({ index, style }) => <div style={style}>Row {index}</div>}
|
|
3146
|
+
</FixedSizeList>
|
|
3147
|
+
```
|
|
3148
|
+
|
|
3149
|
+
**打包体积优化**
|
|
3150
|
+
```bash
|
|
3151
|
+
npx vite-bundle-visualizer
|
|
3152
|
+
# Tree-shaking: 按需引入
|
|
3153
|
+
import { debounce } from 'lodash-es'; // ✅ 非 import _ from 'lodash'
|
|
3154
|
+
```
|
|
3155
|
+
|
|
3156
|
+
#### 3. 后端与数据库性能优化
|
|
3157
|
+
|
|
3158
|
+
**数据库查询优化**
|
|
3159
|
+
```sql
|
|
3160
|
+
EXPLAIN ANALYZE SELECT u.*, COUNT(o.id)
|
|
3161
|
+
FROM users u LEFT JOIN orders o ON u.id = o.user_id
|
|
3162
|
+
WHERE u.status = 'active' GROUP BY u.id;
|
|
3163
|
+
|
|
3164
|
+
-- 复合索引
|
|
3165
|
+
CREATE INDEX idx_user_status_created ON users(status, created_at);
|
|
3166
|
+
```
|
|
3167
|
+
|
|
3168
|
+
**缓存策略(Redis)**
|
|
3169
|
+
```typescript
|
|
3170
|
+
async function getUserProfile(userId: string) {
|
|
3171
|
+
const cacheKey = `user:profile:${userId}`;
|
|
3172
|
+
const cached = await redis.get(cacheKey);
|
|
3173
|
+
if (cached) return JSON.parse(cached);
|
|
3174
|
+
const user = await db.users.findUnique({ where: { id: userId } });
|
|
3175
|
+
const ttl = 300 + Math.floor(Math.random() * 60); // 随机TTL防雪崩
|
|
3176
|
+
await redis.setex(cacheKey, ttl, JSON.stringify(user));
|
|
3177
|
+
return user;
|
|
3178
|
+
}
|
|
3179
|
+
```
|
|
3180
|
+
|
|
3181
|
+
**并行化异步操作**
|
|
3182
|
+
```typescript
|
|
3183
|
+
// ✅ 并行(快)
|
|
3184
|
+
const [user, orders] = await Promise.all([getUser(id), getOrders(id)]);
|
|
3185
|
+
```
|
|
3186
|
+
|
|
3187
|
+
#### 4. 性能优化 Checklist 与持续监控
|
|
3188
|
+
|
|
3189
|
+
**优化优先级矩阵**
|
|
3190
|
+
| 优化项 | 影响 | 成本 | 优先级 |
|
|
3191
|
+
|--------|------|------|--------|
|
|
3192
|
+
| 图片压缩/WebP | 高 | 低 | 🔴 立即 |
|
|
3193
|
+
| 关键资源预加载 | 高 | 低 | 🔴 立即 |
|
|
3194
|
+
| 数据库慢查询修复 | 高 | 中 | 🔴 立即 |
|
|
3195
|
+
| 代码拆分/懒加载 | 高 | 中 | 🟡 近期 |
|
|
3196
|
+
| Redis 缓存层 | 高 | 高 | 🟡 规划 |
|
|
3197
|
+
|
|
3198
|
+
**Lighthouse CI 集成**
|
|
3199
|
+
```yaml
|
|
3200
|
+
- name: Lighthouse CI
|
|
3201
|
+
uses: treosh/lighthouse-ci-action@v10
|
|
3202
|
+
with:
|
|
3203
|
+
urls: https://yoursite.com
|
|
3204
|
+
uploadArtifacts: true
|
|
3205
|
+
```
|
|
3206
|
+
|
|
3207
|
+
**性能优化报告模板**
|
|
3208
|
+
```
|
|
3209
|
+
优化前:LCP 4.8s | FCP 3.2s | P99 API 1200ms
|
|
3210
|
+
已实施:图片WebP → LCP -1.8s;加索引 → P99 -600ms
|
|
3211
|
+
优化后:LCP 2.3s ✅ | FCP 1.4s ✅ | P99 380ms ✅
|
|
3212
|
+
```
|
|
3213
|
+
|
|
3214
|
+
**输出格式**: Markdown 性能分析报告,含当前指标基线、瓶颈列表、优化方案和预期收益
|
|
3215
|
+
|
|
3216
|
+
**注意事项**:
|
|
3217
|
+
- 先测量再优化,不要猜测瓶颈,用数据说话
|
|
3218
|
+
- Core Web Vitals 直接影响 Google SEO 排名
|
|
3219
|
+
- 缓存是最有效的优化,但要仔细设计失效策略
|
|
3220
|
+
|
|
3221
|
+
---
|
|
3222
|
+
|
|
3223
|
+
### 22. 代码重构 (`refactoring`)
|
|
3224
|
+
|
|
3225
|
+
**描述**: 系统化识别代码坏味道,运用重构手法安全改善代码结构,不改变外部行为
|
|
3226
|
+
|
|
3227
|
+
**触发词**: `代码重构`, `refactoring`, `refactor`, `重构`, `坏味道`, `bad smell`, `技术债`, `technical debt`, `代码质量改善`, `@ethan refactor`, `@ethan 重构`
|
|
3228
|
+
|
|
3229
|
+
**执行步骤**:
|
|
3230
|
+
|
|
3231
|
+
#### 1. 识别代码坏味道(Bad Smells)
|
|
3232
|
+
|
|
3233
|
+
重构前先诊断,明确改善目标:
|
|
3234
|
+
|
|
3235
|
+
**最常见的 12 种坏味道**
|
|
3236
|
+
|
|
3237
|
+
| 坏味道 | 症状 | 危害 |
|
|
3238
|
+
|--------|------|------|
|
|
3239
|
+
| **重复代码** | 相同逻辑出现 ≥2 处 | 修改需同步多处,极易遗漏 |
|
|
3240
|
+
| **过长函数** | 函数 > 20 行 | 难以理解、测试、复用 |
|
|
3241
|
+
| **过大的类** | 类承担过多职责 | 违反 SRP,耦合严重 |
|
|
3242
|
+
| **过长参数列表** | 参数 > 4 个 | 调用复杂,难以记忆 |
|
|
3243
|
+
| **发散式变化** | 一个类因不同原因被修改 | 违反 SRP |
|
|
3244
|
+
| **散弹式修改** | 一个变化需改多处 | 高耦合,遗漏风险高 |
|
|
3245
|
+
| **依恋情结** | 方法频繁访问其他类数据 | 逻辑放错了地方 |
|
|
3246
|
+
| **数据泥团** | 多处总是成组出现的数据 | 缺少封装 |
|
|
3247
|
+
| **基本类型偏执** | 用原始类型代替小对象 | 缺少领域建模 |
|
|
3248
|
+
| **注释过多** | 用注释弥补代码的不清晰 | 注释是坏味道的遮羞布 |
|
|
3249
|
+
| **过深嵌套** | 条件/循环嵌套 > 3 层 | 圈复杂度高,难以追踪 |
|
|
3250
|
+
| **僵尸代码** | 死代码、被注释的代码块 | 干扰阅读,增加维护负担 |
|
|
3251
|
+
|
|
3252
|
+
```bash
|
|
3253
|
+
# 快速扫描工具
|
|
3254
|
+
npx eslint src --rule '{"complexity": ["warn", 10]}' # 圈复杂度
|
|
3255
|
+
npx jscpd src --threshold 5 # 重复代码检测
|
|
3256
|
+
ethan scan --todo # TODO/FIXME 清单
|
|
3257
|
+
```
|
|
3258
|
+
|
|
3259
|
+
#### 2. 核心重构手法
|
|
3260
|
+
|
|
3261
|
+
**提炼函数(Extract Function)** — 最常用
|
|
3262
|
+
|
|
3263
|
+
```typescript
|
|
3264
|
+
// Before: 过长函数,注释掩盖意图
|
|
3265
|
+
function processOrder(order: Order) {
|
|
3266
|
+
// 计算折扣
|
|
3267
|
+
let discount = 0;
|
|
3268
|
+
if (order.user.isPremium) discount = 0.1;
|
|
3269
|
+
if (order.total > 1000) discount += 0.05;
|
|
3270
|
+
const finalPrice = order.total * (1 - discount);
|
|
3271
|
+
|
|
3272
|
+
// 发送确认邮件
|
|
3273
|
+
const subject = `订单 ${order.id} 确认`;
|
|
3274
|
+
sendEmail(order.user.email, subject, finalPrice);
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
// After: 每个函数做一件事
|
|
3278
|
+
function calculateDiscount(order: Order): number {
|
|
3279
|
+
let discount = 0;
|
|
3280
|
+
if (order.user.isPremium) discount = 0.1;
|
|
3281
|
+
if (order.total > 1000) discount += 0.05;
|
|
3282
|
+
return discount;
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
function sendOrderConfirmation(order: Order, finalPrice: number): void {
|
|
3286
|
+
const subject = `订单 ${order.id} 确认`;
|
|
3287
|
+
sendEmail(order.user.email, subject, finalPrice);
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
function processOrder(order: Order) {
|
|
3291
|
+
const discount = calculateDiscount(order);
|
|
3292
|
+
const finalPrice = order.total * (1 - discount);
|
|
3293
|
+
sendOrderConfirmation(order, finalPrice);
|
|
3294
|
+
}
|
|
3295
|
+
```
|
|
3296
|
+
|
|
3297
|
+
**以多态取代条件(Replace Conditional with Polymorphism)**
|
|
3298
|
+
|
|
3299
|
+
```typescript
|
|
3300
|
+
// Before: switch 散弹式修改
|
|
3301
|
+
function getShippingCost(order: Order): number {
|
|
3302
|
+
switch (order.type) {
|
|
3303
|
+
case 'standard': return order.weight * 10;
|
|
3304
|
+
case 'express': return order.weight * 20 + 50;
|
|
3305
|
+
case 'overnight': return order.weight * 30 + 100;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
// After: 策略模式/多态
|
|
3310
|
+
abstract class ShippingStrategy {
|
|
3311
|
+
abstract calculate(order: Order): number;
|
|
3312
|
+
}
|
|
3313
|
+
class StandardShipping extends ShippingStrategy {
|
|
3314
|
+
calculate(order: Order) { return order.weight * 10; }
|
|
3315
|
+
}
|
|
3316
|
+
class ExpressShipping extends ShippingStrategy {
|
|
3317
|
+
calculate(order: Order) { return order.weight * 20 + 50; }
|
|
3318
|
+
}
|
|
3319
|
+
```
|
|
3320
|
+
|
|
3321
|
+
**引入参数对象(Introduce Parameter Object)**
|
|
3322
|
+
|
|
3323
|
+
```typescript
|
|
3324
|
+
// Before: 过长参数列表
|
|
3325
|
+
function createReport(startDate: Date, endDate: Date, userId: string, format: string) {}
|
|
3326
|
+
|
|
3327
|
+
// After: 封装为值对象
|
|
3328
|
+
interface ReportParams { dateRange: DateRange; userId: string; format: string; }
|
|
3329
|
+
function createReport(params: ReportParams) {}
|
|
3330
|
+
```
|
|
3331
|
+
|
|
3332
|
+
**其他常用手法速查**
|
|
3333
|
+
|
|
3334
|
+
| 手法 | 适用场景 |
|
|
3335
|
+
|------|---------|
|
|
3336
|
+
| 提炼类(Extract Class) | 一个类承担过多职责 |
|
|
3337
|
+
| 移动函数(Move Function) | 方法与数据不在一处 |
|
|
3338
|
+
| 内联函数(Inline Function) | 函数体比名字更清晰 |
|
|
3339
|
+
| 分解条件(Decompose Conditional) | 复杂 if-else 逻辑 |
|
|
3340
|
+
| 卫语句(Guard Clauses) | 深层嵌套 → 提前返回 |
|
|
3341
|
+
| 以查询取代临时变量 | 中间临时变量过多 |
|
|
3342
|
+
|
|
3343
|
+
#### 3. 重构安全网:测试先行
|
|
3344
|
+
|
|
3345
|
+
**重构铁律:没有测试,不要重构**
|
|
3346
|
+
|
|
3347
|
+
```bash
|
|
3348
|
+
# Step 1: 确保现有测试覆盖率充足
|
|
3349
|
+
npm run test:coverage
|
|
3350
|
+
# 目标:被重构的模块覆盖率 > 80%
|
|
3351
|
+
|
|
3352
|
+
# Step 2: 若无测试,先补特征测试(Characterization Test)
|
|
3353
|
+
# 不是测试"应该如何",而是记录"当前如何"
|
|
3354
|
+
it('characterization: processOrder returns expected price', () => {
|
|
3355
|
+
const result = processOrder(mockOrder);
|
|
3356
|
+
expect(result).toMatchSnapshot(); // 先快照,重构后验证不变
|
|
3357
|
+
});
|
|
3358
|
+
|
|
3359
|
+
# Step 3: 小步前进 — 每次重构后立即运行测试
|
|
3360
|
+
npm test -- --watch
|
|
3361
|
+
```
|
|
3362
|
+
|
|
3363
|
+
**重构工作流**
|
|
3364
|
+
|
|
3365
|
+
```
|
|
3366
|
+
识别目标 → 写/补测试 → 最小重构 → 运行测试 → 提交
|
|
3367
|
+
↑____________________________|
|
|
3368
|
+
循环,每次改动 < 30min
|
|
3369
|
+
```
|
|
3370
|
+
|
|
3371
|
+
**IDE 辅助重构(减少手工失误)**
|
|
3372
|
+
|
|
3373
|
+
| 操作 | VS Code / WebStorm |
|
|
3374
|
+
|------|-------------------|
|
|
3375
|
+
| 提炼函数 | Ctrl+Shift+R → Extract Method |
|
|
3376
|
+
| 重命名 | F2 → 自动更新所有引用 |
|
|
3377
|
+
| 移动文件 | 拖拽 → 自动更新 import |
|
|
3378
|
+
| 提炼变量 | Ctrl+Shift+R → Extract Variable |
|
|
3379
|
+
|
|
3380
|
+
#### 4. 重构策略与输出
|
|
3381
|
+
|
|
3382
|
+
**Boy Scout Rule(童子军规则)**
|
|
3383
|
+
> 让代码比你来时更干净一点,每次 PR 顺手重构接触到的代码。
|
|
3384
|
+
|
|
3385
|
+
**大规模重构策略:Strangler Fig Pattern(绞杀榕模式)**
|
|
3386
|
+
|
|
3387
|
+
```
|
|
3388
|
+
旧系统 ──[façade]──→ 新模块(逐步替换)
|
|
3389
|
+
|
|
|
3390
|
+
└──→ 旧模块(逐步废弃)
|
|
3391
|
+
```
|
|
3392
|
+
|
|
3393
|
+
1. 在旧代码外包一层 Façade/Adapter
|
|
3394
|
+
2. 新功能全部写在新结构中
|
|
3395
|
+
3. 旧调用方逐步迁移到新结构
|
|
3396
|
+
4. 旧代码最终归零删除
|
|
3397
|
+
|
|
3398
|
+
**何时停止重构**
|
|
3399
|
+
|
|
3400
|
+
| 信号 | 建议 |
|
|
3401
|
+
|------|------|
|
|
3402
|
+
| 测试全绿,代码可读性提升 | 提交,结束本轮 |
|
|
3403
|
+
| 发现需要改外部接口 | 创建新 Issue,本次不做 |
|
|
3404
|
+
| 重构范围不断扩大 | 停止,重新评估范围 |
|
|
3405
|
+
|
|
3406
|
+
**重构输出清单**
|
|
3407
|
+
- [ ] 坏味道清单(标注优先级 P1/P2/P3)
|
|
3408
|
+
- [ ] 本次重构的 Diff 说明(what changed & why)
|
|
3409
|
+
- [ ] 测试覆盖率前后对比
|
|
3410
|
+
- [ ] 技术债记录到 Issue/Backlog
|
|
3411
|
+
|
|
3412
|
+
**输出格式**: Markdown 重构报告:坏味道清单 + 重构手法说明 + 测试覆盖率变化 + 技术债 Backlog
|
|
3413
|
+
|
|
3414
|
+
**注意事项**:
|
|
3415
|
+
- 重构前必须有测试覆盖,否则是在盲目改动——叫重写不叫重构
|
|
3416
|
+
- 每次重构只做一件事,不要同时修改功能
|
|
3417
|
+
- 利用 IDE 的自动重构功能,减少手工失误
|
|
3418
|
+
- 技术债需要持续还,但不要以重构为名无限延期需求
|
|
3419
|
+
|
|
3420
|
+
---
|
|
3421
|
+
|
|
3422
|
+
### 23. 可观测性 (`observability`)
|
|
3423
|
+
|
|
3424
|
+
**描述**: 建立日志、指标、链路追踪三支柱体系,实现系统状态完全可观测,快速定位生产问题
|
|
3425
|
+
|
|
3426
|
+
**触发词**: `可观测性`, `observability`, `监控`, `monitoring`, `日志`, `logging`, `链路追踪`, `tracing`, `指标`, `metrics`, `SLO`, `SLA`, `告警`, `alerting`, `@ethan 监控`, `@ethan observability`
|
|
3427
|
+
|
|
3428
|
+
**执行步骤**:
|
|
3429
|
+
|
|
3430
|
+
#### 1. 三支柱体系设计
|
|
3431
|
+
|
|
3432
|
+
**可观测性三支柱(Three Pillars of Observability)**
|
|
3433
|
+
|
|
3434
|
+
| 支柱 | 回答的问题 | 工具栈 |
|
|
3435
|
+
|------|-----------|--------|
|
|
3436
|
+
| **Logs(日志)** | 发生了什么? | Winston/Pino + ELK/Loki |
|
|
3437
|
+
| **Metrics(指标)** | 系统状况如何? | Prometheus + Grafana |
|
|
3438
|
+
| **Traces(链路)** | 请求经过了哪里? | OpenTelemetry + Jaeger/Tempo |
|
|
3439
|
+
|
|
3440
|
+
**选型建议**
|
|
3441
|
+
|
|
3442
|
+
```
|
|
3443
|
+
轻量级单体: Pino + Prometheus + Grafana
|
|
3444
|
+
微服务标准: OpenTelemetry SDK → Collector → Jaeger + Prometheus + Loki
|
|
3445
|
+
云原生托管: Datadog / New Relic / AWS CloudWatch (开箱即用)
|
|
3446
|
+
```
|
|
3447
|
+
|
|
3448
|
+
**黄金信号(Golden Signals)— 4个必监控指标**
|
|
3449
|
+
|
|
3450
|
+
| 信号 | 说明 | 告警阈值示例 |
|
|
3451
|
+
|------|------|-------------|
|
|
3452
|
+
| **Latency(延迟)** | P50/P99/P999 响应时间 | P99 > 500ms |
|
|
3453
|
+
| **Traffic(流量)** | RPS / 并发连接数 | 环比突增 50% |
|
|
3454
|
+
| **Errors(错误率)** | 5xx / 业务错误比例 | > 0.1% |
|
|
3455
|
+
| **Saturation(饱和度)** | CPU/内存/队列深度 | CPU > 80% |
|
|
3456
|
+
|
|
3457
|
+
#### 2. 结构化日志规范
|
|
3458
|
+
|
|
3459
|
+
**日志必须是结构化 JSON,不要用 console.log**
|
|
3460
|
+
|
|
3461
|
+
```typescript
|
|
3462
|
+
// ❌ Bad: 非结构化,无法机器解析
|
|
3463
|
+
console.log(`用户 ${userId} 下单失败: ${error.message}`);
|
|
3464
|
+
|
|
3465
|
+
// ✅ Good: 结构化 JSON 日志(使用 Pino)
|
|
3466
|
+
import pino from 'pino';
|
|
3467
|
+
const logger = pino({ level: 'info' });
|
|
3468
|
+
|
|
3469
|
+
logger.error({
|
|
3470
|
+
event: 'order.create.failed',
|
|
3471
|
+
userId,
|
|
3472
|
+
orderId,
|
|
3473
|
+
errorCode: error.code,
|
|
3474
|
+
msg: error.message,
|
|
3475
|
+
durationMs: Date.now() - startTime,
|
|
3476
|
+
});
|
|
3477
|
+
```
|
|
3478
|
+
|
|
3479
|
+
**日志级别规范**
|
|
3480
|
+
|
|
3481
|
+
| 级别 | 使用场景 | 生产建议 |
|
|
3482
|
+
|------|---------|---------|
|
|
3483
|
+
| ERROR | 需要立即处理的错误 | 触发告警 |
|
|
3484
|
+
| WARN | 不影响功能但需关注 | 记录 + 汇总 |
|
|
3485
|
+
| INFO | 关键业务事件(下单/登录) | 默认级别 |
|
|
3486
|
+
| DEBUG | 调试信息,技术细节 | 生产关闭 |
|
|
3487
|
+
|
|
3488
|
+
**必带字段(Mandatory Fields)**
|
|
3489
|
+
|
|
3490
|
+
```typescript
|
|
3491
|
+
interface LogContext {
|
|
3492
|
+
traceId: string; // 链路追踪 ID
|
|
3493
|
+
spanId: string; // 当前 Span ID
|
|
3494
|
+
userId?: string; // 用户 ID(有则带)
|
|
3495
|
+
requestId: string; // 请求唯一 ID
|
|
3496
|
+
service: string; // 服务名
|
|
3497
|
+
version: string; // 服务版本
|
|
3498
|
+
env: string; // prod / staging
|
|
3499
|
+
}
|
|
3500
|
+
```
|
|
3501
|
+
|
|
3502
|
+
**日志采样策略**
|
|
3503
|
+
|
|
3504
|
+
```typescript
|
|
3505
|
+
// 高流量场景:ERROR 全量,INFO 10% 采样
|
|
3506
|
+
const shouldLog = (level: string) =>
|
|
3507
|
+
level === 'error' || Math.random() < 0.1;
|
|
3508
|
+
```
|
|
3509
|
+
|
|
3510
|
+
#### 3. 指标采集与告警(Prometheus + Grafana)
|
|
3511
|
+
|
|
3512
|
+
**RED 方法论(微服务推荐)**
|
|
3513
|
+
- **R**ate — 每秒请求数
|
|
3514
|
+
- **E**rrors — 错误率
|
|
3515
|
+
- **D**uration — 请求时延分布
|
|
3516
|
+
|
|
3517
|
+
```typescript
|
|
3518
|
+
// Node.js 指标暴露(prom-client)
|
|
3519
|
+
import { Counter, Histogram, register } from 'prom-client';
|
|
3520
|
+
|
|
3521
|
+
const httpRequests = new Counter({
|
|
3522
|
+
name: 'http_requests_total',
|
|
3523
|
+
help: 'Total HTTP requests',
|
|
3524
|
+
labelNames: ['method', 'route', 'status'],
|
|
3525
|
+
});
|
|
3526
|
+
|
|
3527
|
+
const httpDuration = new Histogram({
|
|
3528
|
+
name: 'http_request_duration_seconds',
|
|
3529
|
+
help: 'HTTP request duration in seconds',
|
|
3530
|
+
labelNames: ['method', 'route'],
|
|
3531
|
+
buckets: [0.005, 0.01, 0.05, 0.1, 0.5, 1, 5],
|
|
3532
|
+
});
|
|
3533
|
+
|
|
3534
|
+
// Express 中间件
|
|
3535
|
+
app.use((req, res, next) => {
|
|
3536
|
+
const end = httpDuration.startTimer({ method: req.method, route: req.path });
|
|
3537
|
+
res.on('finish', () => {
|
|
3538
|
+
httpRequests.inc({ method: req.method, route: req.path, status: res.statusCode });
|
|
3539
|
+
end();
|
|
3540
|
+
});
|
|
3541
|
+
next();
|
|
3542
|
+
});
|
|
3543
|
+
|
|
3544
|
+
// 暴露 /metrics 端点
|
|
3545
|
+
app.get('/metrics', async (_, res) => {
|
|
3546
|
+
res.set('Content-Type', register.contentType);
|
|
3547
|
+
res.end(await register.metrics());
|
|
3548
|
+
});
|
|
3549
|
+
```
|
|
3550
|
+
|
|
3551
|
+
**Grafana 告警规则示例(Alertmanager)**
|
|
3552
|
+
|
|
3553
|
+
```yaml
|
|
3554
|
+
groups:
|
|
3555
|
+
- name: api-alerts
|
|
3556
|
+
rules:
|
|
3557
|
+
- alert: HighErrorRate
|
|
3558
|
+
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.01
|
|
3559
|
+
for: 2m
|
|
3560
|
+
labels:
|
|
3561
|
+
severity: critical
|
|
3562
|
+
annotations:
|
|
3563
|
+
summary: "错误率超过 1%,当前: {{ $value | humanizePercentage }}"
|
|
3564
|
+
|
|
3565
|
+
- alert: SlowP99
|
|
3566
|
+
expr: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 1
|
|
3567
|
+
for: 5m
|
|
3568
|
+
labels:
|
|
3569
|
+
severity: warning
|
|
3570
|
+
annotations:
|
|
3571
|
+
summary: "P99 延迟超过 1s"
|
|
3572
|
+
```
|
|
3573
|
+
|
|
3574
|
+
#### 4. 分布式链路追踪(OpenTelemetry)
|
|
3575
|
+
|
|
3576
|
+
**OpenTelemetry 是行业标准 —— 一次接入,多后端支持**
|
|
3577
|
+
|
|
3578
|
+
```typescript
|
|
3579
|
+
// 初始化 OTel(Node.js)
|
|
3580
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
3581
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
3582
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
|
|
3583
|
+
|
|
3584
|
+
const sdk = new NodeSDK({
|
|
3585
|
+
traceExporter: new OTLPTraceExporter({
|
|
3586
|
+
url: 'http://otel-collector:4318/v1/traces',
|
|
3587
|
+
}),
|
|
3588
|
+
instrumentations: [
|
|
3589
|
+
getNodeAutoInstrumentations(), // 自动追踪 HTTP/Express/DB
|
|
3590
|
+
],
|
|
3591
|
+
serviceName: 'order-service',
|
|
3592
|
+
});
|
|
3593
|
+
sdk.start();
|
|
3594
|
+
```
|
|
3595
|
+
|
|
3596
|
+
**手动创建 Span(业务关键路径)**
|
|
3597
|
+
|
|
3598
|
+
```typescript
|
|
3599
|
+
import { trace } from '@opentelemetry/api';
|
|
3600
|
+
const tracer = trace.getTracer('order-service');
|
|
3601
|
+
|
|
3602
|
+
async function createOrder(data: OrderData) {
|
|
3603
|
+
return tracer.startActiveSpan('order.create', async (span) => {
|
|
3604
|
+
try {
|
|
3605
|
+
span.setAttributes({
|
|
3606
|
+
'order.user_id': data.userId,
|
|
3607
|
+
'order.item_count': data.items.length,
|
|
3608
|
+
'order.total': data.total,
|
|
3609
|
+
});
|
|
3610
|
+
|
|
3611
|
+
const order = await db.orders.create(data);
|
|
3612
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
3613
|
+
return order;
|
|
3614
|
+
} catch (err) {
|
|
3615
|
+
span.recordException(err as Error);
|
|
3616
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
3617
|
+
throw err;
|
|
3618
|
+
} finally {
|
|
3619
|
+
span.end();
|
|
3620
|
+
}
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
```
|
|
3624
|
+
|
|
3625
|
+
**SLO 定义模板**
|
|
3626
|
+
|
|
3627
|
+
```yaml
|
|
3628
|
+
SLO: API 可用性
|
|
3629
|
+
SLI: (成功请求数 / 总请求数) * 100%
|
|
3630
|
+
目标: ≥ 99.9% (月度 = 允许 43.8 min 故障)
|
|
3631
|
+
告警: 1h 内错误预算消耗 > 5% 时 PagerDuty 通知
|
|
3632
|
+
```
|
|
3633
|
+
|
|
3634
|
+
**输出格式**: Markdown 可观测性方案:技术栈选型 + 日志/指标/链路配置代码 + 告警规则 + SLO 定义
|
|
3635
|
+
|
|
3636
|
+
**注意事项**:
|
|
3637
|
+
- 可观测性要从项目初期建立,生产出了问题再加往往太晚
|
|
3638
|
+
- 日志一定要带 traceId,否则微服务间无法串联请求链路
|
|
3639
|
+
- SLO 要与产品/业务方共同制定,不能只是技术侧自说自话
|
|
3640
|
+
- 告警要有"降噪"机制(for: 2m),避免毛刺误报打扰团队
|
|
3641
|
+
|
|
3642
|
+
---
|
|
3643
|
+
|
|
3644
|
+
### 24. 设计模式 (`design-patterns`)
|
|
3645
|
+
|
|
3646
|
+
**描述**: 识别适用场景,选择合适的 GoF 设计模式,提升代码可扩展性与可维护性
|
|
3647
|
+
|
|
3648
|
+
**触发词**: `设计模式`, `design pattern`, `design patterns`, `模式`, `GoF`, `工厂模式`, `单例模式`, `观察者模式`, `策略模式`, `装饰器模式`, `依赖注入`, `代理模式`, `@ethan 设计模式`, `@ethan design-patterns`
|
|
3649
|
+
|
|
3650
|
+
**执行步骤**:
|
|
3651
|
+
|
|
3652
|
+
#### 1. 三大类模式全景
|
|
3653
|
+
|
|
3654
|
+
**23 种 GoF 模式分类速查**
|
|
3655
|
+
|
|
3656
|
+
| 类型 | 模式 | 解决的核心问题 |
|
|
3657
|
+
|------|------|--------------|
|
|
3658
|
+
| **创建型** | Factory Method | 子类决定创建哪种对象 |
|
|
3659
|
+
| | Abstract Factory | 创建一族相关对象 |
|
|
3660
|
+
| | Builder | 分步骤构建复杂对象 |
|
|
3661
|
+
| | Singleton | 全局唯一实例 |
|
|
3662
|
+
| | Prototype | 克隆已有对象 |
|
|
3663
|
+
| **结构型** | Adapter | 接口转换,兼容不兼容的接口 |
|
|
3664
|
+
| | Decorator | 动态添加行为(不继承) |
|
|
3665
|
+
| | Facade | 简化复杂子系统的接口 |
|
|
3666
|
+
| | Proxy | 控制对象访问(缓存/权限/懒加载)|
|
|
3667
|
+
| | Composite | 树形结构,统一处理单个和组合 |
|
|
3668
|
+
| **行为型** | Observer | 一对多事件通知 |
|
|
3669
|
+
| | Strategy | 运行时切换算法 |
|
|
3670
|
+
| | Command | 将请求封装为对象(支持撤销)|
|
|
3671
|
+
| | Iterator | 统一遍历集合的方式 |
|
|
3672
|
+
| | State | 状态机,行为随状态变化 |
|
|
3673
|
+
| | Chain of Responsibility | 请求沿链传递,直到被处理 |
|
|
3674
|
+
| | Template Method | 算法骨架固定,子类填充步骤 |
|
|
3675
|
+
|
|
3676
|
+
**最常用的 5 个(优先掌握)**:Strategy, Observer, Factory, Decorator, Proxy
|
|
3677
|
+
|
|
3678
|
+
#### 2. 高频模式 TypeScript 实现
|
|
3679
|
+
|
|
3680
|
+
**策略模式(Strategy)— 取代 if/switch 的最佳武器**
|
|
3681
|
+
|
|
3682
|
+
```typescript
|
|
3683
|
+
// 场景:支付方式可扩展
|
|
3684
|
+
interface PaymentStrategy {
|
|
3685
|
+
pay(amount: number): Promise<void>;
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
class WechatPay implements PaymentStrategy {
|
|
3689
|
+
async pay(amount: number) { /* 微信支付逻辑 */ }
|
|
3690
|
+
}
|
|
3691
|
+
class AlipayStrategy implements PaymentStrategy {
|
|
3692
|
+
async pay(amount: number) { /* 支付宝逻辑 */ }
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
class PaymentService {
|
|
3696
|
+
constructor(private strategy: PaymentStrategy) {}
|
|
3697
|
+
async checkout(amount: number) {
|
|
3698
|
+
await this.strategy.pay(amount);
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
// 运行时切换,新增支付方式不改原有代码
|
|
3703
|
+
const service = new PaymentService(new WechatPay());
|
|
3704
|
+
```
|
|
3705
|
+
|
|
3706
|
+
**观察者模式(Observer / EventEmitter)**
|
|
3707
|
+
|
|
3708
|
+
```typescript
|
|
3709
|
+
// 场景:订单状态变更通知多个系统
|
|
3710
|
+
class OrderEventEmitter extends EventEmitter {
|
|
3711
|
+
emitOrderCreated(order: Order) {
|
|
3712
|
+
this.emit('order:created', order);
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
const emitter = new OrderEventEmitter();
|
|
3717
|
+
emitter.on('order:created', sendConfirmationEmail);
|
|
3718
|
+
emitter.on('order:created', updateInventory);
|
|
3719
|
+
emitter.on('order:created', triggerRecommendation);
|
|
3720
|
+
```
|
|
3721
|
+
|
|
3722
|
+
**装饰器模式(Decorator)— 不改原类,添加横切关注点**
|
|
3723
|
+
|
|
3724
|
+
```typescript
|
|
3725
|
+
// 场景:为任意服务添加缓存
|
|
3726
|
+
function withCache<T extends object>(service: T, ttlMs = 60_000): T {
|
|
3727
|
+
return new Proxy(service, {
|
|
3728
|
+
get(target, prop) {
|
|
3729
|
+
const original = (target as Record<string, unknown>)[prop as string];
|
|
3730
|
+
if (typeof original !== 'function') return original;
|
|
3731
|
+
const cache = new Map<string, { value: unknown; expiry: number }>();
|
|
3732
|
+
return async (...args: unknown[]) => {
|
|
3733
|
+
const key = JSON.stringify(args);
|
|
3734
|
+
const cached = cache.get(key);
|
|
3735
|
+
if (cached && Date.now() < cached.expiry) return cached.value;
|
|
3736
|
+
const value = await (original as Function).apply(target, args);
|
|
3737
|
+
cache.set(key, { value, expiry: Date.now() + ttlMs });
|
|
3738
|
+
return value;
|
|
3739
|
+
};
|
|
3740
|
+
},
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
const cachedUserService = withCache(userService, 30_000);
|
|
3745
|
+
```
|
|
3746
|
+
|
|
3747
|
+
#### 3. 创建型模式实践
|
|
3748
|
+
|
|
3749
|
+
**工厂模式(Factory)— 解耦对象创建与使用**
|
|
3750
|
+
|
|
3751
|
+
```typescript
|
|
3752
|
+
// 场景:根据配置创建不同日志处理器
|
|
3753
|
+
interface Logger {
|
|
3754
|
+
log(message: string): void;
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
class ConsoleLogger implements Logger {
|
|
3758
|
+
log(message: string) { console.log(message); }
|
|
3759
|
+
}
|
|
3760
|
+
class FileLogger implements Logger {
|
|
3761
|
+
log(message: string) { fs.appendFile('app.log', message); }
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
// 工厂函数(简单场景推荐函数而非类)
|
|
3765
|
+
function createLogger(type: 'console' | 'file'): Logger {
|
|
3766
|
+
if (type === 'file') return new FileLogger();
|
|
3767
|
+
return new ConsoleLogger();
|
|
3768
|
+
}
|
|
3769
|
+
```
|
|
3770
|
+
|
|
3771
|
+
**建造者模式(Builder)— 处理复杂对象构造**
|
|
3772
|
+
|
|
3773
|
+
```typescript
|
|
3774
|
+
// 场景:SQL 查询构建,参数组合多变
|
|
3775
|
+
class QueryBuilder {
|
|
3776
|
+
private query = { table: '', conditions: [] as string[], limit: 100 };
|
|
3777
|
+
|
|
3778
|
+
from(table: string) { this.query.table = table; return this; }
|
|
3779
|
+
where(condition: string) { this.query.conditions.push(condition); return this; }
|
|
3780
|
+
limit(n: number) { this.query.limit = n; return this; }
|
|
3781
|
+
build() {
|
|
3782
|
+
const where = this.query.conditions.length
|
|
3783
|
+
? `WHERE ${this.query.conditions.join(' AND ')}`
|
|
3784
|
+
: '';
|
|
3785
|
+
return `SELECT * FROM ${this.query.table} ${where} LIMIT ${this.query.limit}`;
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
// 链式调用,可读性极强
|
|
3790
|
+
const sql = new QueryBuilder()
|
|
3791
|
+
.from('orders')
|
|
3792
|
+
.where('status = "pending"')
|
|
3793
|
+
.where('created_at > NOW() - INTERVAL 7 DAY')
|
|
3794
|
+
.limit(50)
|
|
3795
|
+
.build();
|
|
3796
|
+
```
|
|
3797
|
+
|
|
3798
|
+
**单例注意事项**
|
|
3799
|
+
|
|
3800
|
+
```typescript
|
|
3801
|
+
// ⚠️ 单例慎用:全局状态 = 隐式耦合
|
|
3802
|
+
// 适用:DB连接池、全局配置、Logger
|
|
3803
|
+
// 不适用:有状态的业务逻辑
|
|
3804
|
+
|
|
3805
|
+
// Node.js 模块缓存天然单例
|
|
3806
|
+
export const db = createDbPool(); // 模块级单例,足够用
|
|
3807
|
+
```
|
|
3808
|
+
|
|
3809
|
+
#### 4. 模式选型指南与反模式
|
|
3810
|
+
|
|
3811
|
+
**场景 → 模式 速查表**
|
|
3812
|
+
|
|
3813
|
+
| 遇到这种问题 | 考虑使用 |
|
|
3814
|
+
|------------|---------|
|
|
3815
|
+
| if/switch 随需求不断增长 | Strategy / Command |
|
|
3816
|
+
| 一个变化需要修改多处 | Observer / Mediator |
|
|
3817
|
+
| 需要在运行时给对象加功能 | Decorator / Proxy |
|
|
3818
|
+
| 创建逻辑复杂,参数多 | Factory / Builder |
|
|
3819
|
+
| 需要统一处理树形结构 | Composite |
|
|
3820
|
+
| 需要兼容旧接口/第三方库 | Adapter / Facade |
|
|
3821
|
+
| 请求需要多步验证/处理 | Chain of Responsibility |
|
|
3822
|
+
| 对象行为随状态显著变化 | State |
|
|
3823
|
+
|
|
3824
|
+
**反模式:过度设计的信号**
|
|
3825
|
+
|
|
3826
|
+
```
|
|
3827
|
+
❌ 为了用设计模式而用设计模式
|
|
3828
|
+
❌ 一个功能引入 3 层抽象(不超过 2 个接口)
|
|
3829
|
+
❌ 到处都是 Manager / Handler / Processor 命名
|
|
3830
|
+
❌ 接口只有一个实现类(YAGNI 原则)
|
|
3831
|
+
|
|
3832
|
+
✅ 正确姿势:先写简单代码,当变化来临时再重构引入模式
|
|
3833
|
+
```
|
|
3834
|
+
|
|
3835
|
+
**SOLID 原则与模式的关系**
|
|
3836
|
+
|
|
3837
|
+
| 原则 | 对应常用模式 |
|
|
3838
|
+
|------|------------|
|
|
3839
|
+
| S 单一职责 | Facade, Extract Class |
|
|
3840
|
+
| O 开闭原则 | Strategy, Decorator |
|
|
3841
|
+
| L 里氏替换 | Template Method |
|
|
3842
|
+
| I 接口隔离 | Adapter |
|
|
3843
|
+
| D 依赖倒置 | Factory, DI Container |
|
|
3844
|
+
|
|
3845
|
+
**输出清单**
|
|
3846
|
+
- [ ] 识别到的模式应用场景(带代码位置)
|
|
3847
|
+
- [ ] 选型理由(为什么选这个模式而不是另一个)
|
|
3848
|
+
- [ ] 实现示例(TypeScript,保持 < 50 行)
|
|
3849
|
+
- [ ] 过度设计风险说明
|
|
3850
|
+
|
|
3851
|
+
**输出格式**: Markdown 设计模式方案:场景分析 + 模式选型理由 + TypeScript 实现示例 + 反模式警示
|
|
3852
|
+
|
|
3853
|
+
**注意事项**:
|
|
3854
|
+
- 设计模式是工具,不是目标——代码能跑、能改才是目标
|
|
3855
|
+
- TypeScript 的类型系统让很多模式更安全,善用 interface + generic
|
|
3856
|
+
- 函数式替代方案通常比类更简洁:Strategy → 高阶函数,Observer → EventEmitter
|
|
3857
|
+
- 重构引入模式时,必须有测试覆盖,见 refactoring Skill
|
|
3858
|
+
|
|
3859
|
+
---
|
|
3860
|
+
|
|
1454
3861
|
*Ethan - Your AI Workflow Assistant | 让每一步都有据可依*
|