创建仓库
This commit is contained in:
159
Go项目实战/00_顶层设计/00_软件产品全生命周期管理规范.md
Normal file
159
Go项目实战/00_顶层设计/00_软件产品全生命周期管理规范.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 📘 软件产品全生命周期管理规范 (PDLC Guidelines)
|
||||
date created: 星期日, 十二月 7日 2025, 12:49:19 下午
|
||||
date modified: 星期日, 十二月 7日 2025, 12:49:54 下午
|
||||
---
|
||||
这是一个通用的、标准化的《互联网软件产品全生命周期(PDLC)管理规范》。此文档旨在为从灵感到交付的全过程提供顶层指导,适用于中大型项目或追求工程卓越的小型团队。
|
||||
|
||||
---
|
||||
|
||||
# 📘 软件产品全生命周期管理规范 (PDLC Guidelines)
|
||||
|
||||
版本: 2.0 (通用标准版)
|
||||
|
||||
适用范围: 全栈开发、SaaS 产品、企业级应用系统
|
||||
|
||||
核心目标: 降低不确定性,确保交付质量,实现可预测的工程化产出。Shutterstock
|
||||
|
||||
---
|
||||
|
||||
## 阶段概览 (Phase Overview)
|
||||
|
||||
我们将产品落地过程划分为 7 个核心阶段(P0 - P6)。每个阶段都有明确的准入(Entry)和准出(Exit)标准。
|
||||
|
||||
|**阶段代号**|**阶段名称**|**核心角色**|**关键产出物**|
|
||||
|---|---|---|---|
|
||||
|**P0**|**立项与价值验证 (Inception)**|PM, Tech Lead, Stakeholder|BRD, 可行性分析报告|
|
||||
|**P1**|**需求定义与原型 (Definition)**|PM, UI/UX|PRD, 原型图 (Figma)|
|
||||
|**P2**|**技术方案设计 (Technical Design)**|Architect, Backend, Frontend|TDD, API 契约, ER 图|
|
||||
|**P3**|**开发与实现 (Development)**|Developers|源代码, 单元测试|
|
||||
|**P4**|**质量保障与验证 (Verification)**|QA, Developers|测试报告, Bug 清单|
|
||||
|**P5**|**发布与部署 (Release)**|DevOps, Tech Lead|镜像, Release Note|
|
||||
|**P6**|**运维与迭代 (Operations)**|SRE, Ops, PM|监控面板, 运营数据报告|
|
||||
|
||||
---
|
||||
|
||||
## 📅 详细阶段拆解
|
||||
|
||||
### P0: 立项与价值验证 (Inception & Strategy)
|
||||
|
||||
**目的:** 明确“为什么要做”。防止团队在伪需求或技术不可行的方向上浪费资源。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **商业需求分析:** 确定业务痛点、目标用户及商业价值。
|
||||
2. **技术可行性预研 (PoC):** 针对关键技术难点(如 AI 模型效果、高并发瓶颈)进行快速验证。
|
||||
3. **资源评估:** 粗略估算所需人力、时间及服务器成本。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `BRD (Business Requirement Document)`:商业需求文档。
|
||||
- `PoC Demo`:概念验证原型(如有必要)。
|
||||
- **决策门 (Gate):** **Go / No-Go**。如果 ROI(投入产出比)过低,在此阶段终止。
|
||||
|
||||
### P1: 需求定义与产品设计 (Product Definition)
|
||||
|
||||
**目的:** 明确“要做成什么样”。将模糊的想法转化为具象的功能逻辑和视觉形态。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **需求细化:** 编写详细的功能列表、用户故事 (User Stories) 和验收标准 (AC)。
|
||||
2. **交互设计 (UX):** 绘制用户流程图 (User Flow)、低保真线框图。
|
||||
3. **视觉设计 (UI):** 输出高保真设计稿、UI 切图、设计规范 (Design System)。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `PRD (Product Requirement Document)`:产品需求规格说明书(唯一真理来源)。
|
||||
- `Figma/Sketch Files`:高保真设计稿。
|
||||
- **决策门 (Gate):** **需求评审 (PRD Review)**。开发团队确认需求逻辑闭环,无歧义。
|
||||
|
||||
### P2: 技术方案设计 (Technical Design)
|
||||
|
||||
**目的:** 明确“怎么实现”。**这是程序员最重要的规划阶段,严禁跳过此阶段直接编码。**
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **架构设计:** 确定微服务拆分、技术选型、中间件依赖(Redis/MQ/DB)。
|
||||
2. **数据建模 (Schema Design):** 绘制 ER 图,编写 DDL (SQL 建表语句),确定索引策略。
|
||||
3. **接口定义 (API Contract):** 定义 URL、Method、Request/Response JSON 结构、错误码。
|
||||
4. **详细设计 (TDD):** 核心算法逻辑、状态机流转图、时序图、缓存策略设计。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `TDD (Technical Design Document)`:技术设计文档。
|
||||
- `ER Diagram & SQL Scripts`:数据库模型与迁移脚本。
|
||||
- `OpenAPI/Swagger Spec`:API 接口定义文档。
|
||||
- **决策门 (Gate):** **技术评审 (Design Review)**。架构师或 Tech Lead 确认方案具备扩展性、安全性及性能达标。
|
||||
|
||||
### P3: 开发与实现 (Implementation)
|
||||
|
||||
**目的:** 将设计转化为代码。注重代码质量与规范。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **环境准备:** 本地开发环境搭建、Mock 数据生成。
|
||||
2. **编码 (Coding):** 后端 API 开发、前端组件开发、业务逻辑实现。
|
||||
3. **单元测试 (Unit Test):** 编写核心逻辑的单元测试,确保覆盖率。
|
||||
4. **代码审查 (Code Review):** 提交 Merge Request,进行同行评审。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `Source Code`:符合规范的源码。
|
||||
- `Unit Test Report`:单元测试通过报告。
|
||||
- **决策门 (Gate):** **代码合并 (Merge)**。CI 流水线检查通过(Lint, Test, Build)。
|
||||
|
||||
### P4: 质量保障与验证 (Quality Assurance)
|
||||
|
||||
**目的:** 确保交付物符合需求且无重大缺陷。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **集成测试:** 前后端联调,确保接口数据交互正常。
|
||||
2. **系统测试:** QA 团队根据测试用例进行全量测试。
|
||||
3. **非功能测试:** 性能测试 (Load Test)、安全扫描 (Security Scan)。
|
||||
4. **Bug 修复:** 开发修复 QA 发现的问题并回归。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `Test Cases`:测试用例。
|
||||
- `Bug List`:缺陷清单及修复记录。
|
||||
- `Performance Report`:压测报告(可选)。
|
||||
- **决策门 (Gate):** **验收评审 (UAT)**。Bug 清零或无 P0/P1 级 Bug,PM 验收通过。
|
||||
|
||||
### P5: 发布与部署 (Release & Deployment)
|
||||
|
||||
**目的:** 安全、平滑地将产品推向生产环境。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **构建交付:** 编译二进制文件、构建 Docker 镜像。
|
||||
2. **预发布验证 (Staging):** 在仿真环境中进行最后一次冒烟测试。
|
||||
3. **正式部署 (Production):** 灰度发布 (Canary) 或 蓝绿部署,执行数据库迁移。
|
||||
4. **回滚预案:** 准备好一旦失败的一键回滚脚本。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `Release Note`:发布说明(变更日志)。
|
||||
- `Docker Image / Binaries`:制品。
|
||||
- **决策门 (Gate):** **上线检查清单 (Checklist)**。确认配置、密钥、数据库备份均已就绪。
|
||||
|
||||
### P6: 运维与持续迭代 (Operations & Maintenance)
|
||||
|
||||
**目的:** 保障系统稳定性,根据反馈进行优化。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **监控告警:** 配置 CPU/内存、QPS、错误率监控,设置 PagerDuty 告警。
|
||||
2. **日志审计:** 收集与分析运行日志 (ELK/Loki)。
|
||||
3. **数据复盘:** 分析用户行为数据,验证 P0 阶段的商业假设。
|
||||
4. **事故复盘 (Post-mortem):** 若发生故障,撰写复盘报告,制定改进措施。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `SLA Report`:服务可用性报告。
|
||||
- `User Analytics`:用户数据分析报表。
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 关键支撑体系 (Supporting Pillars)
|
||||
|
||||
除了上述流程,以下三个支撑体系贯穿始终:
|
||||
|
||||
1. **项目管理 (Project Management):** 使用 Jira/Trello 管理任务看板,每日站会同步进度,识别风险。
|
||||
2. **配置管理 (Configuration Management):** 代码版本控制 (Git Flow),环境配置隔离 (Env Vars)。
|
||||
3. **文档工程 (Documentation):** 保持 BRD, PRD, API 文档与代码的同步更新,避免“文档腐烂”。
|
||||
34
Go项目实战/00_顶层设计/一个Go项目的基本骨架.md
Normal file
34
Go项目实战/00_顶层设计/一个Go项目的基本骨架.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
tags: []
|
||||
date created: 星期日, 十二月 7日 2025, 11:44:41 中午
|
||||
date modified: 星期日, 十二月 7日 2025, 11:57:43 中午
|
||||
---
|
||||
|
||||
```plaintext
|
||||
your-api-project/
|
||||
├── cmd/
|
||||
│ └── server/
|
||||
│ ├── main.go # 调用 wire 注入,获取 app 实例,执行 app.Run()
|
||||
│ └── wire.go # Wire 依赖注入
|
||||
├── config/ # Viper 配置结构体
|
||||
├── internal/
|
||||
│ ├── api/ # (DTO层) 纯数据传输对象,无逻辑
|
||||
│ │ ├── request/
|
||||
│ │ └── response/
|
||||
│ ├── controller/ # (接口层) 解析 request -> 调 service -> 组装 response
|
||||
│ ├── service/ # (应用服务层) 编排业务逻辑,操作 Entity
|
||||
│ ├── repository/ # (资源层) 负责 CRUD,屏蔽数据库差异
|
||||
│ ├── entity/ # (领域层) 核心业务实体 (User, Article),带 GORM tag
|
||||
│ ├── router/ # (路由层) NewRouter() *gin.Engine
|
||||
│ └── middleware/ # Gin 中间件
|
||||
├── pkg/ # (基础设施层) 通用工具
|
||||
│ ├── app/ # 统一响应封装 (Gin Result)
|
||||
│ ├── auth/ # JWT 签发与解析
|
||||
│ ├── hasher/ # 密码加密 (Argon2 / Bcrypt)
|
||||
│ ├── logger/ # Zap 配置
|
||||
│ └── timeutil/ # 时间处理工具
|
||||
├── migrations/ # 数据库变更 SQL
|
||||
├── docs/ # Swagger
|
||||
├── go.mod
|
||||
└── Makefile
|
||||
```
|
||||
109
Go项目实战/00_顶层设计/关于个人开发者的开发模式.md
Normal file
109
Go项目实战/00_顶层设计/关于个人开发者的开发模式.md
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 渐进式开发最佳实践
|
||||
date created: 星期一, 十二月 8日 2025, 12:04:31 凌晨
|
||||
date modified: 星期一, 十二月 8日 2025, 12:05:12 凌晨
|
||||
---
|
||||
|
||||
# 渐进式开发最佳实践
|
||||
|
||||
## 1. 必须在写代码前锁定的“硬约束” (The Non-Negotiables)
|
||||
|
||||
即使是后规划细节,但这 **4 样东西** 是一旦开工就很难改的,必须在骨架阶段定死:
|
||||
|
||||
1. **目录结构 (Directory Layout):** `cmd`, `internal`, `pkg` 怎么分。这决定了你能不能顺畅地加代码。
|
||||
|
||||
- _现状:_ 我们已经定好了 (Modular Clean Architecture)。
|
||||
|
||||
2. **核心技术栈与基础设施:** 选 Gin 还是 Echo,用 GORM 还是 SQLX,依赖注入用 Wire 还是手写。
|
||||
|
||||
- _现状:_ 我们已经定好了 (Gin+GORM+Wire+Viper)。
|
||||
|
||||
3. **统一的交互规范:** API 怎么返回错误?数据库怎么管理变更?日志打在哪里?
|
||||
|
||||
- _现状:_ 我们已经定好了 (JSON Envelope, Golang-Migrate, Zap)。
|
||||
|
||||
4. **核心领域模型 (Core Schema):** 最关键的表(User, Role)。
|
||||
|
||||
- _原因:_ 它们是系统的地基,地基不稳,后面写 Service 逻辑会反复推倒重来。
|
||||
|
||||
---
|
||||
|
||||
## 2. 可以(且应该)推迟设计的“软逻辑” (The Deferrables)
|
||||
|
||||
这些内容不要现在想,想了也是白想,等写到那个函数时再具体的“具体问题具体分析”:
|
||||
|
||||
1. **复杂的业务算法:** 比如“文章的热度排名算法”、“复杂的权限递归校验逻辑”。
|
||||
|
||||
- _策略:_ 先写个 `return true` 或简单的逻辑占位,跑通流程再说。
|
||||
|
||||
2. **极致的性能优化:** 比如“这里要不要加 Redis 缓存?”、“这里 SQL 要不要分表?”。
|
||||
|
||||
- _策略:_ 先跑通功能 (Make it work),再优化性能 (Make it fast)。
|
||||
|
||||
3. **非核心字段的定义:** 比如文章表里要不要加 `seo_keywords`,用户表要不要加 `wechat_id`。
|
||||
|
||||
- _策略:_ 用到了再加 migration,不要为了“未来可能用到”而过度设计。
|
||||
|
||||
4. **具体的 API 参数细节:** 比如“更新文章是传 ID 还是传 UUID”。
|
||||
|
||||
- _策略:_ 写 Handler 的时候,顺手定义 DTO 就行。
|
||||
|
||||
---
|
||||
|
||||
## 3. 个人开发者的“曳光弹”开发流 (The Tracer Bullet Workflow)
|
||||
|
||||
不要试图“横向”开发(先把所有 50 张表设计完,再把所有 API 定义完)。
|
||||
|
||||
要进行 “纵向”切片开发(Vertical Slice)。
|
||||
|
||||
**推荐的实操步骤:**
|
||||
|
||||
### 第一阶段:搭建“行走的骨架” (Day 1 - 也就是你现在该做的)
|
||||
|
||||
**目标:** 一个能跑起来、能连数据库、有日志、有 Swagger 文档,但没有任何业务功能的空壳。
|
||||
|
||||
1. **初始化项目:** 建立文件夹,`go mod init`。
|
||||
2. **配置基础设施:** 放入 `Makefile`, `docker-compose.yml`, `config.yaml`。
|
||||
3. **连通性测试:** 写一个简单的 `GET /ping` 接口,确保能读到配置,能连上 DB,能打出日志。
|
||||
4. **依赖注入跑通:** 确保 `wire` 能自动生成代码并启动 Server。
|
||||
5. **结果:** 你拥有了一个**“生产级”的 Hello World**。
|
||||
|
||||
### 第二阶段:第一个“核心切片” (Day 2-3)
|
||||
|
||||
**目标:** 打通 User 模块的完整闭环。
|
||||
|
||||
1. **DB:** 设计 `users` 表,执行 Migrate。
|
||||
2. **Entity:** 生成 User Struct。
|
||||
3. **Repo:** 写 `CreateUser` 和 `FindUser` 的 CRUD。
|
||||
4. **Service:** 写 `Register` 逻辑(这里涉及到密码加密,此时再去查 Argon2 怎么用)。
|
||||
5. **API:** 定义 `RegisterReq` DTO,写 Handler。
|
||||
6. **测试:** 用 Postman/Swagger 调通注册接口。
|
||||
|
||||
### 第三阶段:具体问题具体分析 (Day 4+)
|
||||
|
||||
**目标:** 遇到什么写什么。
|
||||
|
||||
- _场景:_ 比如你做到了“文章发布”,突然发现需要“敏感词过滤”。
|
||||
- _动作:_ 停下来,在 `pkg/util` 里设计一个敏感词过滤函数(或者找个库),然后在 Service 层调用它。
|
||||
- _这就是你说的:_ “进行到具体的业务逻辑的时候进行设计”。
|
||||
|
||||
---
|
||||
|
||||
## 4. 总结给你的建议
|
||||
|
||||
你现在的焦虑在于试图**预见未来**。
|
||||
|
||||
**作为个人开发者,你的代码就是你的设计文档。**
|
||||
|
||||
**你的下一步行动:**
|
||||
|
||||
1. **停止**思考复杂的业务场景。
|
||||
2. **立即**按照我们之前确定的目录结构,新建文件夹。
|
||||
3. **立即**把 `docker-compose` 跑起来,把 `main.go` 跑起来。
|
||||
4. **立即**实现“用户注册”这一个功能。
|
||||
|
||||
只要“骨架”(架构分层、依赖注入、数据库管理方式)是对的,后面你往里面填什么肉(业务逻辑),怎么填,都不会把楼盖歪。
|
||||
|
||||
**准备好开始初始化项目文件夹了吗?**
|
||||
130
Go项目实战/00_顶层设计/关于项目的顶层设计模式和风格.md
Normal file
130
Go项目实战/00_顶层设计/关于项目的顶层设计模式和风格.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🏗️ Project Architecture & Design Guidelines (v1.0)
|
||||
date created: 星期日, 十二月 7日 2025, 11:57:43 中午
|
||||
date modified: 星期二, 十二月 9日 2025, 11:00:14 晚上
|
||||
---
|
||||
|
||||
# 🏗️ Project Architecture & Design Guidelines (v1.0)
|
||||
|
||||
项目代号: Enterprise-CMS-Core
|
||||
|
||||
架构风格: 模块化整洁架构 (Modular Clean Architecture)
|
||||
|
||||
核心原则: 实用主义 (Pragmatic)、Go 原生思维 (Idiomatic)、领域驱动 (DDD-Lite)
|
||||
|
||||
## 1. 技术栈约束 (Tech Stack Constraints)
|
||||
|
||||
- **Language:** Go 1.21+
|
||||
- **Web Framework:** Gin
|
||||
- **Database:** PostgreSQL (Primary), Redis (Cache)
|
||||
- **ORM:** GORM (With Migration Tools)
|
||||
- **Dependency Injection:** Google Wire
|
||||
- **Configuration:** Viper (YAML)
|
||||
- **Observability:** Zap (Log), Prometheus (Metrics), Jaeger (Trace)
|
||||
- **Documentation:** Swagger / OpenAPI 3.0
|
||||
|
||||
---
|
||||
|
||||
## 2. 目录结构规范 (Directory Structure)
|
||||
|
||||
采用 **“按领域分包 (Package by Domain)”** 的扁平化结构,而非传统的按层分包。
|
||||
|
||||
```Plaintext
|
||||
root/
|
||||
├── cmd/server/
|
||||
│ ├── main.go # 仅包含 wire 初始化与 app.Run()
|
||||
│ └── wire.go # 顶层依赖注入定义
|
||||
├── config/ # 配置文件模板 (config.yaml)
|
||||
├── internal/
|
||||
│ ├── api/ # [API层] 全局通用的 HTTP DTO (Request/Response)
|
||||
│ ├── middleware/ # [中间件] Gin 中间件 (Auth, CORS, Logger)
|
||||
│ ├── pkg/ # [基础设施] 内部通用组件 (AppResult, ErrorCode)
|
||||
│ │
|
||||
│ │ # --- 核心业务领域 (Domain Modules) ---
|
||||
│ │ # 每个领域包内部扁平化,自包含所有逻辑
|
||||
│ ├── user/ # [示例] 用户领域
|
||||
│ │ ├── entity.go # 核心实体 (GORM Model)
|
||||
│ │ ├── repository.go # 仓储接口定义 + GORM 实现
|
||||
│ │ ├── service.go # 业务逻辑 (Service Struct)
|
||||
│ │ ├── handler.go # HTTP 控制器 (Controller)
|
||||
│ │ └── provider.go # Wire ProviderSet
|
||||
│ │
|
||||
│ └── article/ # [示例] 文章领域 (结构同上)
|
||||
│
|
||||
├── pkg/ # [外部库] 可抽离的通用工具 (Hash, JWT, Logger封装)
|
||||
├── migrations/ # 数据库迁移 SQL 文件 (up/down)
|
||||
├── go.mod
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心架构设计规则 (Architectural Rules)
|
||||
|
||||
### 3.1. 依赖倒置与注入 (IoC & DI)
|
||||
|
||||
- **规则:** 严禁在业务代码中手动 `New()` 依赖对象。
|
||||
- **实现:** 所有依赖关系必须通过 `NewStruct(dep Interface)` 构造函数声明,并由 `Google Wire` 在编译期自动组装。
|
||||
- **模块化注入:** 每个领域包(如 `internal/user`)必须包含一个 `provider.go`,导出 `var ProviderSet = wire.NewSet(…)`,供顶层 `cmd/server/wire.go` 聚合。
|
||||
|
||||
### 3.2. 接口策略 (Interface Strategy)
|
||||
|
||||
- **Repository (必须):** 仓储层**必须**定义接口(例如 `UserRepository`),以支持 Mock 测试和数据库切换。
|
||||
- **Service (按需):** 默认**不需要**定义 Service 接口,直接使用 Struct。仅在以下情况提取接口:
|
||||
|
||||
1. 出现循环依赖。
|
||||
2. 需要对 Service 进行 Mock 测试。
|
||||
3. 该 Service 存在多种策略实现(如 `PaymentService` 有支付宝/微信两种实现)。
|
||||
|
||||
### 3.3. 领域包扁平化 (Flat Domain Package)
|
||||
|
||||
- **规则:** 在 `internal/user/` 等领域包内,**不再**建立 `service/`, `repo/` 子目录。
|
||||
- **原因:** 利用 Go 的 `package` 级私有可见性,隐藏领域内部细节(如辅助函数、内部 DTO),仅暴露必要的 Handler 和 Service 方法。
|
||||
|
||||
### 3.4. 数据模型 (Model Vs Entity)
|
||||
|
||||
- **策略:** 采用 **"Pragmatic Entity"** 模式。
|
||||
- **定义:** `entity.go` 中的结构体既是业务实体,也是 GORM 模型(带 `gorm:"…"` 标签)。
|
||||
- **例外:** 只有当数据库存储结构与业务逻辑结构差异巨大时,才在 Repository 内部引入独立的 PO (Persistent Object) 并进行转换。
|
||||
|
||||
---
|
||||
|
||||
## 4. 编码实施标准 (Implementation Standards)
|
||||
|
||||
### 4.1. 错误处理 (Error Handling)
|
||||
|
||||
- **禁止:** 严禁直接返回 `error` 字符串给前端。
|
||||
- **必须:** Service 层返回标准 `error`,Controller 层通过 `pkg/app` 将其转换为统一响应格式。
|
||||
- **格式:**
|
||||
|
||||
```Go
|
||||
// Response JSON
|
||||
{
|
||||
"code": 20001,
|
||||
"msg": "User already exists",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2. 数据库交互 (Database Interaction)
|
||||
|
||||
- **禁止:** Controller 层严禁导入 `gorm` 包,严禁执行 SQL。
|
||||
- **迁移:** 生产环境严禁使用 `AutoMigrate`。必须使用 `migrations/` 目录下的版本化 SQL 脚本进行变更。
|
||||
|
||||
### 4.3. 路由注册 (Router Registration)
|
||||
|
||||
- **规则:** 路由不再集中管理。
|
||||
- **实现:** 每个领域包暴露一个 `RegisterRoutes(r *gin.RouterGroup)` 方法。在 `main.go` 启动时,统一调用各模块的注册方法。
|
||||
|
||||
---
|
||||
|
||||
## 5. AI 编程指令 (Instruction for AI Agent)
|
||||
|
||||
> **当作为 AI 助手编写代码时,请严格遵守以下指令:**
|
||||
|
||||
1. **Context Check:** 在生成代码前,检查当前目录结构是否符合 `Section 2`。如果不符,请优先建议重构或遵循现有结构。
|
||||
2. **No Logic Leak:** 确保 HTTP 处理逻辑(解析参数、校验参数)留在 `handler.go`,业务规则(判断权限、计算)留在 `service.go`,SQL 操作留在 `repository.go`。
|
||||
3. **Wire Awareness:** 每当新增 Service 或 Repository,必须自动更新同目录下的 `provider.go`,并在 `cmd/server/wire.go` 中检查是否需要重新生成。
|
||||
4. **Testability:** 编写 Repository 代码时,优先考虑“如何 Mock”。
|
||||
8
Go项目实战/00_顶层设计/架构设计.md
Normal file
8
Go项目实战/00_顶层设计/架构设计.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
tags: []
|
||||
date created: 星期日, 十二月 7日 2025, 1:14:57 下午
|
||||
date modified: 星期日, 十二月 7日 2025, 1:22:34 下午
|
||||
---
|
||||
- **部署架构:** 采用 **Modular Monolith (模块化单体)**。严禁跨模块直连数据库表。
|
||||
- **异步通信:** 引入 **Asynq (Redis)** 处理非核心路径业务(邮件、日志),拒绝 Kafka。
|
||||
- **缓存一致性:** 强制执行 **Cache-Aside + Delete on Write** 策略。
|
||||
169
Go项目实战/01_数据模型建立/AI 辅助数据建模通用 SOP (v1.0).md
Normal file
169
Go项目实战/01_数据模型建立/AI 辅助数据建模通用 SOP (v1.0).md
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🛡️ AI 辅助数据建模通用 SOP (v1.0)
|
||||
date created: 星期日, 十二月 7日 2025, 9:16:59 晚上
|
||||
date modified: 星期二, 十二月 9日 2025, 11:27:28 晚上
|
||||
---
|
||||
|
||||
# 🛡️ AI 辅助数据建模通用 SOP (v1.0)
|
||||
|
||||
**核心理念:**
|
||||
|
||||
1. **DBA 思维优先:** 永远先设计 SQL (Source of Truth),再生成代码 (ORM)。
|
||||
2. **可视逻辑验证:** 在写代码前,必须通过 ER 图确认业务逻辑闭环。
|
||||
3. **对抗性评审:** 利用 AI 的多重人格(架构师/攻击者)自我找茬。
|
||||
|
||||
---
|
||||
|
||||
## 📋 准备工作:定义变量
|
||||
|
||||
在使用以下 Prompt 前,请先在脑海或记事本中替换以下占位符:
|
||||
|
||||
- `{技术栈}`: 例如 PostgreSQL 15, MySQL 8.0, TiDB
|
||||
- `{ORM框架}`: 例如 GORM (Go), TypeORM (Node), Hibernate (Java)
|
||||
- `{业务模块}`: 例如 用户中心, 订单交易, 库存管理
|
||||
- `{具体需求}`: 粘贴你的 PRD 片段或业务规则描述
|
||||
|
||||
---
|
||||
|
||||
## 阶段一:上下文注入与规范确立 (Context & Standards)
|
||||
|
||||
**目的:** 确立“宪法”。防止 AI 自由发挥导致命名风格混乱或忽略关键字段。
|
||||
|
||||
### 🤖 通用 Prompt (复制使用)
|
||||
|
||||
```markdown
|
||||
你现在是我的 **Senior DBA (首席数据库管理员)** 和 **后端架构师**。
|
||||
我们将基于 `{技术栈}` 和 `{ORM框架}` 进行 `{业务模块}` 的数据库设计。
|
||||
|
||||
在开始具体设计前,请牢记并遵守以下 **[设计宪法]**:
|
||||
|
||||
1. **命名规范:**
|
||||
- 表名: 复数形式,snake_case (如 `user_orders`).
|
||||
- 字段: snake_case (如 `is_verified`).
|
||||
- 索引: `idx_表名_字段` (普通), `uniq_表名_字段` (唯一).
|
||||
- 外键: `fk_本表_关联表`.
|
||||
|
||||
2. **基础字段 (Base Model):**
|
||||
- 所有业务表必须包含: `id` (主键), `created_at`, `updated_at`.
|
||||
- 需要软删除的表必须包含: `deleted_at`.
|
||||
- 乐观锁(如有需要): `version`.
|
||||
|
||||
3. **类型约束:**
|
||||
- 金额: 严禁使用 Float/Double,必须使用 `DECIMAL` 或 `BigInt` (存分).
|
||||
- 枚举: 尽量在应用层处理,数据库存 `SmallInt` 或 `String`,避免使用 DB 级 ENUM.
|
||||
- 时间: 统一使用带时区的 `TIMESTAMPTZ` (PostgreSQL) 或 `DATETIME`.
|
||||
|
||||
4. **安全与性能:**
|
||||
- 必填字段显式标记 `NOT NULL`。
|
||||
- 外键必须加索引。
|
||||
- 物理外键约束建议使用 `ON DELETE RESTRICT` 防止误删,除非明确需要级联。
|
||||
|
||||
收到请回复:“DBA 模式已就绪,请提供具体业务需求。”
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段二:概念验证 (Conceptual Modeling - ER Diagram)
|
||||
|
||||
**目的:** 宏观排雷。通过可视化图表快速识别逻辑错误(如:1 对多搞成了多对多,或者环状依赖)。
|
||||
|
||||
### 🤖 通用 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
请根据以下 `{具体需求}`,绘制 **Mermaid 格式** 的 ER 关系图 (Entity Relationship Diagram)。
|
||||
|
||||
**需求输入:**
|
||||
"""
|
||||
(在此处粘贴你的业务逻辑,例如:一个用户可以有多个角色,文章必须属于一个分类…)
|
||||
"""
|
||||
|
||||
**绘图要求:**
|
||||
1. 展示实体(Entity)及其核心属性。
|
||||
2. 精准标注关系基数 (Cardinality):
|
||||
- `||--o{` (1 对多)
|
||||
- `}|--|{` (多 对 多,需画出中间表)
|
||||
- `||--||` (1 对 1)
|
||||
3. 在图表下方简要说明关键关系的业务含义。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段三:物理建模 (Physical Schema - SQL DDL)
|
||||
|
||||
**目的:** 产出真理。这是最关键的一步,SQL DDL 定义了数据的最终形态。
|
||||
|
||||
### 🤖 通用 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
ER 图确认无误。请生成 **生产级 (Production-Ready) 的 SQL DDL 建表脚本**。
|
||||
|
||||
**执行要求:**
|
||||
1. **完整性:** 包含 `CREATE TABLE`, `CREATE INDEX`, 以及必要的 `COMMENT ON` 语句。
|
||||
2. **字段细节:**
|
||||
- 针对 JSON 数据使用数据库原生类型 (如 PG 的 `JSONB`)。
|
||||
- 针对长文本使用 `TEXT`。
|
||||
- 默认值 `DEFAULT` 处理到位 (如 `DEFAULT 0`, `DEFAULT FALSE`, `DEFAULT NOW()`).
|
||||
3. **约束定义:**
|
||||
- 明确定义 `PRIMARY KEY`。
|
||||
- 显式定义 `CONSTRAINT` 名称 (便于排错)。
|
||||
4. **索引策略:**
|
||||
- 除了主键,请根据业务查询场景(如“按状态查询”、“按时间范围排序”)主动添加辅助索引。
|
||||
- 解释每个索引添加的理由。
|
||||
|
||||
请直接输出 SQL 代码块。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段四:代码映射 (Code Generation - ORM Struct)
|
||||
|
||||
**目的:** 翻译。将 SQL 完美映射为代码,利用 AI 自动处理繁琐的 Tag。
|
||||
|
||||
### 🤖 通用 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
基于上述生成的 SQL 脚本,请编写对应的 `GORM (Go)` 模型代码 (Entity/Model)。
|
||||
|
||||
**代码要求:**
|
||||
1. **Tag 映射:** 完整包含 DB 列名映射、主键定义、默认值定义。
|
||||
- (若为 GORM): 使用 `gorm:"column:xyz;type:…"`.
|
||||
2. **JSON 序列化:**
|
||||
- 所有字段添加 `json:"camelCaseName"`.
|
||||
- **敏感字段** (如密码、盐值) 必须设为 `json:"-"` 以防接口泄露。
|
||||
3. **类型安全:**
|
||||
- 数据库允许 NULL 的字段,在代码中请使用 指针类型 (如 `*string`) 或 专用 Null 类型 (如 `sql.NullString`)。
|
||||
4. **文件结构:** 不需要 `gorm.Model` 继承,请显式写出字段,以保证对 JSON Tag 的控制权。
|
||||
|
||||
请输出 Go/Java/TS 代码块。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段五:红队测试与评审 (Critique & Optimization)
|
||||
|
||||
**目的:** 找茬。让 AI 模拟极端的架构师,攻击当前设计,发现隐患。
|
||||
|
||||
### 🤖 通用 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
现在,请切换角色为 **Google 首席架构师 (Principal Architect)**。
|
||||
请对上述 SQL 设计进行 **“红队测试” (Red Teaming)** 评审。
|
||||
|
||||
**评审维度:**
|
||||
1. **扩展性瓶颈:** 如果单表数据量达到 5000 万行,目前的索引设计是否会失效?哪个查询会最慢?
|
||||
2. **数据一致性:** 是否存在业务逻辑上需要事务保证,但当前 Schema 难以支持的场景?
|
||||
3. **反范式建议:** 是否有过度规范化导致查询需要 Join 太多表?是否建议增加冗余字段?
|
||||
4. **边缘情况:** `NULL` 值的处理是否会在聚合查询时导致 Bug?
|
||||
|
||||
请列出 top 3 风险点,并给出具体的 **优化建议** (如:修改索引、增加冗余字段、修改类型)。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 💡 使用小贴士
|
||||
|
||||
1. **不要一次性发完:** 强烈建议**分步执行**。AI 的上下文窗口虽然大,但分步确认能极大提高准确率。
|
||||
2. **迭代修改:** 在“阶段三”生成 SQL 后,如果你发现不满意,手动修改 SQL,然后把修改后的 SQL 发给 AI 进入“阶段四”。**永远以 SQL 为准**。
|
||||
3. **保留对话:** 把这个对话保留为一个独立的 Session,后续增加字段时,回到这个 Session 继续操作,保持上下文连贯。
|
||||
65
Go项目实战/01_数据模型建立/Mermaid ER 图.md
Normal file
65
Go项目实战/01_数据模型建立/Mermaid ER 图.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
tags: []
|
||||
date created: 星期日, 十二月 7日 2025, 1:31:36 下午
|
||||
date modified: 星期日, 十二月 7日 2025, 1:32:46 下午
|
||||
---
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
users ||--o{ user_roles : "assigns"
|
||||
roles ||--o{ user_roles : "assigned to"
|
||||
roles ||--o{ role_permissions : "grants"
|
||||
permissions ||--o{ role_permissions : "granted to"
|
||||
|
||||
users {
|
||||
bigint id PK "主键 (BigSerial)"
|
||||
string username "用户名 (唯一)"
|
||||
string password_hash "哈希密码 (Argon2/Bcrypt)"
|
||||
string email "邮箱 (可选,唯一)"
|
||||
string nickname "昵称"
|
||||
string avatar_url "头像URL"
|
||||
text bio "简介"
|
||||
string status "状态 (active/inactive/banned)"
|
||||
timestamptz created_at "创建时间"
|
||||
timestamptz updated_at "更新时间"
|
||||
timestamptz deleted_at "软删除时间"
|
||||
}
|
||||
|
||||
roles {
|
||||
bigint id PK "主键 (BigSerial)"
|
||||
string code "角色代码 (admin/editor/user)"
|
||||
string name "角色名称"
|
||||
text description "角色描述"
|
||||
boolean is_system "系统角色(不可删除)"
|
||||
timestamptz created_at "创建时间"
|
||||
timestamptz updated_at "更新时间"
|
||||
timestamptz deleted_at "软删除时间"
|
||||
}
|
||||
|
||||
permissions {
|
||||
bigint id PK "主键 (BigSerial)"
|
||||
string code "权限代码 (module:action:scope)"
|
||||
string name "权限名称"
|
||||
text description "权限描述"
|
||||
string category "权限分类"
|
||||
timestamptz created_at "创建时间"
|
||||
timestamptz updated_at "更新时间"
|
||||
timestamptz deleted_at "软删除时间"
|
||||
}
|
||||
|
||||
user_roles {
|
||||
bigint id PK "主键 (BigSerial)"
|
||||
bigint user_id FK "用户ID"
|
||||
bigint role_id FK "角色ID"
|
||||
timestamptz created_at "关联时间"
|
||||
timestamptz updated_at "更新时间"
|
||||
}
|
||||
|
||||
role_permissions {
|
||||
bigint id PK "主键 (BigSerial)"
|
||||
bigint role_id FK "角色ID"
|
||||
bigint permission_id FK "权限ID"
|
||||
timestamptz created_at "关联时间"
|
||||
timestamptz updated_at "更新时间"
|
||||
}
|
||||
```
|
||||
183
Go项目实战/01_数据模型建立/规范数据库设计 & 变更管理及工程流操作.md
Normal file
183
Go项目实战/01_数据模型建立/规范数据库设计 & 变更管理及工程流操作.md
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🛠️ Database Engineering & Migration Standard (v1.0)
|
||||
date created: 星期日, 十二月 7日 2025, 10:31:59 晚上
|
||||
date modified: 星期二, 十二月 9日 2025, 10:14:44 晚上
|
||||
---
|
||||
|
||||
# 🛠️ Database Engineering & Migration Standard (v1.0)
|
||||
|
||||
文档用途: 规范数据库设计、变更管理及工程流操作。
|
||||
|
||||
适用范围: 所有涉及 Schema 变更的后端开发任务。
|
||||
|
||||
核心原则: Code First (Logic) but SQL First (Schema). 严禁生产环境使用 ORM 自动建表。
|
||||
|
||||
---
|
||||
|
||||
## 1. 基础设施与工具链 (Infrastructure & Tools)
|
||||
|
||||
本项目采用 **“容器化数据库 + 版本化迁移工具”** 的架构。
|
||||
|
||||
| **组件** | **选型** | **说明** |
|
||||
| --------------- | ------------------ | ----------------------------------------- |
|
||||
| **Database** | **PostgreSQL 15+** | 运行于 Docker 容器中,保证开发/生产环境一致。 |
|
||||
| **Schema Mgmt** | **Golang-Migrate** | CLI 工具,用于生成和执行版本化 SQL 脚本。 |
|
||||
| **GUI Client** | **Navicat** | 推荐 Navicat / DataGrip / DBeaver,仅用于设计和验证。 |
|
||||
| **Automation** | **Make** | 封装常用命令,屏蔽底层复杂参数。 |
|
||||
|
||||
### 1.1 目录结构规范
|
||||
|
||||
Plaintext
|
||||
|
||||
```bash
|
||||
project-root/
|
||||
├── migrations/ # [Source of Truth] 存放所有 SQL 变更文件
|
||||
│ ├── 000001_init_users.up.sql
|
||||
│ └── 000001_init_users.down.sql
|
||||
├── internal/
|
||||
│ └── {domain}/ # 领域包
|
||||
│ └── entity.go # [Code Mapping] GORM 结构体定义
|
||||
├── docker-compose.yml # 定义本地 DB 容器
|
||||
└── Makefile # 集成迁移命令
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据库设计规范 (Design Standards)
|
||||
|
||||
### 2.1 命名约定
|
||||
|
||||
- **表名:** 必须使用**复数**形式,`snake_case` (e.g., `users`, `order_items`).
|
||||
- **字段名:** 全小写,`snake_case` (e.g., `created_at`, `user_id`).
|
||||
- **索引名:**
|
||||
- 普通索引: `idx_tablename_column`
|
||||
- 唯一索引: `uniq_tablename_column`
|
||||
- **外键名:** `fk_tablename_ref_tablename`
|
||||
|
||||
### 2.2 关键字段约束
|
||||
|
||||
所有业务表**必须**包含以下基础字段:
|
||||
|
||||
```SQL
|
||||
id BIGSERIAL PRIMARY KEY, -- 或 UUID
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ -- 仅在需要软删除时添加
|
||||
```
|
||||
|
||||
### 2.3 设计禁忌
|
||||
|
||||
1. **严禁** 使用物理外键的级联删除 (`ON DELETE CASCADE`),除非是关联性极强的子表(如文章标签关联)。核心业务数据必须使用 `ON DELETE RESTRICT`。
|
||||
2. **严禁** 在涉及金额的字段使用 `FLOAT` 或 `DOUBLE`,必须使用 `DECIMAL` 或 `BIGINT` (分)。
|
||||
3. **严禁** 将 `NULL` 作为布尔值的第三种状态。布尔字段必须设置 `NOT NULL DEFAULT FALSE`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 标准作业流程 (SOP)
|
||||
|
||||
开发人员需严格遵循以下 **5 步闭环** 进行数据库变更:
|
||||
|
||||
### Step 1: 启动环境
|
||||
|
||||
确保本地 Docker 数据库正在运行。
|
||||
|
||||
```Bash
|
||||
make network # 对应 docker-compose up -d
|
||||
```
|
||||
|
||||
### Step 2: 创建迁移文件 (Create)
|
||||
|
||||
使用 Makefile 生成成对的 `.sql` 文件(up/down)。
|
||||
|
||||
- `name` 参数应简短描述变更内容(如 `add_avatar_to_users`)。
|
||||
|
||||
```Bash
|
||||
make new_migration name=init_schema
|
||||
# 输出:
|
||||
# Created migrations/000001_init_schema.up.sql
|
||||
# Created migrations/000001_init_schema.down.sql
|
||||
```
|
||||
|
||||
### Step 3: 编写 SQL (Edit)
|
||||
|
||||
- **UP 文件:** 填入 `CREATE TABLE`, `ALTER TABLE`, `CREATE INDEX` 等正向操作。
|
||||
- _技巧:_ 可在 GUI 工具中设计好表结构,复制生成的 DDL 语句粘贴至此。
|
||||
- **DOWN 文件:** 填入对应的回滚操作(如 `DROP TABLE`, `DROP INDEX`)。
|
||||
|
||||
### Step 4: 执行变更 (Apply)
|
||||
|
||||
将 SQL 应用到本地数据库。
|
||||
|
||||
```Bash
|
||||
make migrate_up
|
||||
```
|
||||
|
||||
_验证:_ 使用 GUI 工具连接数据库,确认表结构已更新。
|
||||
|
||||
### Step 5: 代码映射 (Mapping)
|
||||
|
||||
在 `internal/{domain}/entity.go` 中编写对应的 Go Struct。
|
||||
|
||||
- 确保 `gorm` tag 与数据库定义一致。
|
||||
- 确保 `json` tag 符合 API 契约。
|
||||
|
||||
---
|
||||
|
||||
## 4. 自动化配置 (Automation)
|
||||
|
||||
将以下内容固化到项目根目录的 `Makefile` 中。
|
||||
|
||||
> **注意:** 确保 `DB_DSN` 与 `docker-compose.yml` 中的配置完全一致。
|
||||
|
||||
```Makefile
|
||||
# ==============================================================================
|
||||
# Database & Migration Logic
|
||||
# ==============================================================================
|
||||
|
||||
# Database Connection String
|
||||
# 格式: postgres://user:password@host:port/dbname?sslmode=disable
|
||||
DB_DSN := postgres://postgres:secret@localhost:5432/cms_core?sslmode=disable
|
||||
|
||||
.PHONY: network new_migration migrate_up migrate_down migrate_force
|
||||
|
||||
# 1. 启动本地环境
|
||||
network:
|
||||
docker-compose up -d
|
||||
|
||||
# 2. 创建新的迁移文件 (Usage: make new_migration name=create_users)
|
||||
new_migration:
|
||||
@if [ -z "$(name)" ]; then echo "Error: name is required"; exit 1; fi
|
||||
migrate create -ext sql -dir migrations -seq $(name)
|
||||
|
||||
# 3. 执行所有未执行的迁移 (Up)
|
||||
migrate_up:
|
||||
migrate -path migrations -database "$(DB_DSN)" up
|
||||
|
||||
# 4. 回滚上一次迁移 (Down 1 step)
|
||||
migrate_down:
|
||||
migrate -path migrations -database "$(DB_DSN)" down 1
|
||||
|
||||
# 5. 强制修复版本 (当 dirty database 时使用, version 为具体的版本号)
|
||||
migrate_force:
|
||||
migrate -path migrations -database "$(DB_DSN)" force $(version)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 故障排查 (Troubleshooting)
|
||||
|
||||
**Q: 执行 migrate_up 时报错 "Dirty database version x".**
|
||||
|
||||
- **原因:** 上一次迁移执行到一半失败了(可能是 SQL 语法错误),导致版本锁死。
|
||||
- **解决:**
|
||||
|
||||
1. 手动修复 SQL 文件中的语法错误。
|
||||
2. 执行 `make migrate_force version=x` (x 是失败前的那个版本号)。
|
||||
3. 再次执行 `make migrate_up`。
|
||||
|
||||
**Q: 多人协作时产生版本冲突。**
|
||||
|
||||
- **现象:** 你有一个 `0003_add_xx.up.sql`,同事提交代码后也有一个 `0003_add_yy.up.sql`。
|
||||
- **解决:** 重命名你的迁移文件编号为 `0004`,确保序列号在时间轴上是递增且唯一的。
|
||||
183
Go项目实战/02_接口设计/AI 辅助 API 定义方法论 (v1.0).md
Normal file
183
Go项目实战/02_接口设计/AI 辅助 API 定义方法论 (v1.0).md
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🔌 AI 辅助 API 定义方法论 (v1.0)
|
||||
date created: 星期日, 十二月 7日 2025, 11:43:04 晚上
|
||||
date modified: 星期日, 十二月 7日 2025, 11:44:30 晚上
|
||||
---
|
||||
|
||||
# 🔌 AI 辅助 API 定义方法论 (v1.0)
|
||||
|
||||
**核心理念:**
|
||||
|
||||
1. **DTO 先行:** 先定义输入 (Request) 和输出 (Response) 的数据结构,再写业务逻辑。
|
||||
2. **注释即文档:** 利用 AI 自动生成繁琐的 Swagger 注释 (`@Summary`, `@Param`…)。
|
||||
3. **契约可视化:** 在写第一行逻辑代码前,先能在 Swagger UI 上看到接口定义。
|
||||
|
||||
---
|
||||
|
||||
## 阶段一:API 资源设计 (Design)
|
||||
|
||||
**目的:** 确定 URL 路径、HTTP 方法和 JSON 数据结构,确保符合 RESTful 规范。
|
||||
|
||||
### 🤖 通用 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
你现在是我的 **API 架构师**。
|
||||
我们已经完成了数据库设计,现在需要设计 `{业务模块}` (例如: User) 的 API 接口。
|
||||
|
||||
**输入上下文:**
|
||||
1. **业务实体:** `{粘贴 User 的 Entity 代码或 SQL}`
|
||||
2. **功能需求:** 注册、登录、获取个人资料、更新资料。
|
||||
|
||||
**请输出 API 设计方案 (表格形式):**
|
||||
1. **Method:** GET/POST/PUT/PATCH/DELETE
|
||||
2. **Path:** URL 路径 (使用 RESTful 风格, 如 `/api/v1/users/:id`)
|
||||
3. **Request Body:** 关键字段 (JSON 示例)
|
||||
4. **Response:** 成功返回的数据结构 (JSON 示例)
|
||||
|
||||
**设计原则:**
|
||||
- 使用统一的响应信封: `{ "code": 200, "msg": "success", "data": ... }`
|
||||
- 更新操作区分 PUT (全量) 和 PATCH (局部)。
|
||||
- 敏感字段 (密码) 绝对不能出现在 Response 中。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段二:生成 DTO 结构体 (Contract Definition)
|
||||
|
||||
**目的:** 将 JSON 设计转化为 Go 结构体。这是前后端交互的**法律条文**。
|
||||
|
||||
**工程位置:** `internal/api/request/` (入参) 和 `internal/api/response/` (出参)。
|
||||
|
||||
### 🤖 通用 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
设计确认通过。请基于上述设计,生成 Go 语言的 **DTO (Data Transfer Object) 结构体**。
|
||||
|
||||
**技术约束:**
|
||||
1. 使用 `gin` 的 binding 标签进行参数校验 (如 `binding:"required,email"`).
|
||||
2. 使用 `json` 标签定义字段名 (camelCase).
|
||||
3. **分离 Request 和 Response:** 不要直接复用数据库 Entity,必须定义独立的 DTO。
|
||||
|
||||
**输出代码要求:**
|
||||
- `UserRegisterReq` (包含 Email, Password, ConfirmPassword)
|
||||
- `UserLoginReq`
|
||||
- `UserProfileResp` (不含密码,转换时间格式)
|
||||
|
||||
请直接输出 Go 代码,放在 package `user_dto` 下。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段三:生成 Handler 骨架与 Swagger 注释 (Implementation Skeleton)
|
||||
|
||||
**目的:** 这是一个“体力活”。AI 最擅长帮我们要写几十行的 Swagger 注释。
|
||||
|
||||
**工程位置:** `internal/user/handler.go`
|
||||
|
||||
### 🤖 通用 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
现在请生成 Gin Handler 的**代码骨架**,并附带完整的 **Swagger 注释**。
|
||||
|
||||
**输入:**
|
||||
DTO 结构体已定义: `UserRegisterReq`, `UserProfileResp`...
|
||||
|
||||
**输出要求:**
|
||||
1. **Swagger 注释:** 必须包含 `@Summary`, `@Tags`, `@Accept json`, `@Produce json`, `@Param`, `@Success`, `@Router`。
|
||||
2. **Handler 签名:** 接收 `*gin.Context`。
|
||||
3. **参数绑定:** 在 Handler 内部生成 `ShouldBindJSON` 代码块。
|
||||
4. **占位返回:** 暂时直接返回 Mock 数据或 `http.StatusOK`,**不要写具体的 Service 业务逻辑**。
|
||||
|
||||
**示例注释格式:**
|
||||
// Register
|
||||
// @Summary 用户注册
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body user_dto.UserRegisterReq true "注册信息"
|
||||
// @Success 200 {object} app.Result{data=user_dto.UserProfileResp}
|
||||
// @Router /api/v1/auth/register [post]
|
||||
func (h *UserHandler) Register(c *gin.Context) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 工程落地操作指南 (How to Execute)
|
||||
|
||||
### 1. 文件安放位置
|
||||
|
||||
不要乱放,严格遵守目录结构:
|
||||
|
||||
```Plaintext
|
||||
internal/
|
||||
├── api/ # [Contract Layer] 存放 DTO
|
||||
│ ├── request/ # 入参结构体
|
||||
│ │ └── user_req.go
|
||||
│ └── response/ # 出参结构体
|
||||
│ └── user_resp.go
|
||||
└── user/ # [Domain Layer]
|
||||
└── handler.go # 控制器 (含 Swagger 注释)
|
||||
```
|
||||
|
||||
### 2. 实操步骤 (SOP)
|
||||
|
||||
#### Step 1: 定义 DTO (The Contract)
|
||||
|
||||
- 运行阶段二的 Prompt。
|
||||
- 将代码复制到 `internal/api/request/user_req.go`。
|
||||
- **这一步完成了,就代表你和前端的接口契约签好了。**
|
||||
|
||||
#### Step 2: 编写 Handler 骨架 (The Skeleton)
|
||||
|
||||
- 运行阶段三的 Prompt。
|
||||
- 将代码复制到 `internal/user/handler.go`。
|
||||
- 确保此时代码能编译通过(缺少 Service 调用没关系,先留空)。
|
||||
|
||||
#### Step 3: 生成 Swagger 文档 (Generate)
|
||||
|
||||
这是验证的关键一步。我们需要使用 `swag` 工具扫描你的注释并生成 JSON 文档。
|
||||
|
||||
**在终端执行:**
|
||||
|
||||
```Bash
|
||||
swag init -g cmd/server/main.go -o docs
|
||||
```
|
||||
|
||||
_(注意: `-g` 指向你的 main 函数入口,swag 会从那里开始递归扫描)_
|
||||
|
||||
#### Step 4: 启动服务并验证 (Verify)
|
||||
|
||||
- 运行 `go run cmd/server/main.go`。
|
||||
- 打开浏览器访问 `http://localhost:8080/swagger/index.html`。
|
||||
- **你看到的界面,就是你刚刚定义的“接口合同”。**
|
||||
|
||||
---
|
||||
|
||||
## 💡 常见问题与技巧
|
||||
|
||||
**Q: 为什么不直接用 Entity 作为 Response?**
|
||||
|
||||
- **A:** **千万别这么做。** Entity 包含 `password_hash`,包含 `deleted_at`,这些都不该给前端。DTO 让你有精准控制返回字段的权利。
|
||||
|
||||
**Q: Swagger 注释太难写了,容易写错格式。**
|
||||
|
||||
- **A:** 这就是为什么要用 AI 的原因。**永远不要手写 Swagger 注释**。把 Handler 代码发给 AI,对它说:“_请帮我补全 Swagger 注释,参数是 X,返回值是 Y_”。
|
||||
|
||||
**Q: 接口变了怎么办?**
|
||||
|
||||
- **A:**
|
||||
|
||||
1. 修改 DTO (Go Struct)。
|
||||
2. 让 AI 更新 Handler 里的 Swagger 注释。
|
||||
3. 运行 `swag init`。
|
||||
4. 文档自动更新。
|
||||
|
||||
---
|
||||
|
||||
**总结你的下一步行动:**
|
||||
|
||||
1. **DTO 设计:** 使用 Prompt 生成 `User` 相关的 Request/Response 结构体。
|
||||
2. **骨架生成:** 使用 Prompt 生成带有 Swagger 注释的 `UserHandler`。
|
||||
3. **文档验证:** 运行 `swag init` 并在浏览器中确认接口文档无误。
|
||||
174
Go项目实战/02_接口设计/七七八八的接口设计相关问题.md
Normal file
174
Go项目实战/02_接口设计/七七八八的接口设计相关问题.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 接口版本控制策略 (API Versioning Strategy)
|
||||
date created: 星期日, 十二月 7日 2025, 11:38:52 晚上
|
||||
date modified: 星期日, 十二月 7日 2025, 11:42:18 晚上
|
||||
---
|
||||
|
||||
# 七七八八的接口设计相关问题
|
||||
|
||||
## 1. 接口版本控制策略 (API Versioning Strategy)
|
||||
|
||||
**核心问题:** 当你发布了 V1 版本后,某天需要修改接口字段(比如把 `name` 拆分为 `first_name` 和 `last_name`),如何保证老版本的 App 不会崩溃?
|
||||
|
||||
**三种主流流派:**
|
||||
|
||||
1. **URI Path Versioning (推荐):**
|
||||
|
||||
- **格式:** `https://api.example.com/v1/users`
|
||||
- **优点:** 直观、易于调试、缓存友好。这也是 GitHub, Twitter, Google API 采用的主流方案。
|
||||
- **落地:** 我们在 Gin 的 Router Group 中直接体现:
|
||||
|
||||
Go
|
||||
|
||||
```bash
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
v1.GET("/users", ...)
|
||||
}
|
||||
```
|
||||
|
||||
2. **Header Versioning:**
|
||||
|
||||
- **格式:** Header 中添加 `Accept: application/vnd.myapi.v1+json`
|
||||
- **优点:** URL 干净。
|
||||
- **缺点:** 调试麻烦(浏览器直接访问 URL 看不到结果),CDN 缓存配置复杂。**不推荐 MVP 阶段使用。**
|
||||
|
||||
3. **Query Parameter:**
|
||||
|
||||
- **格式:** `/users?version=1`
|
||||
- **评价:** 看起来很土,通常不用于 RESTful API。
|
||||
|
||||
**👉 你的策略:** 坚定选择 **URI Path Versioning (`/api/v1`)**。只在发生**破坏性变更 (Breaking Change)** 时才升级到 v2。新增字段不算破坏性变更,不需要升级版本。
|
||||
|
||||
---
|
||||
|
||||
## 2. HTTP 方法的精准语义 (Verbs Semantics)
|
||||
|
||||
很多新手只会用 `GET` 和 `POST`。企业级 API 必须精准区分以下方法的含义:
|
||||
|
||||
|**方法**|**语义**|**幂等性 (Idempotency)**|**典型场景**|
|
||||
|---|---|---|---|
|
||||
|**GET**|获取资源|✅ 是|获取文章列表、详情|
|
||||
|**POST**|新建资源|❌ 否|发布新文章、提交评论|
|
||||
|**PUT**|**全量替换**资源|✅ 是|修改文章(客户端发送文章的完整 JSON,没传的字段会被置空)|
|
||||
|**PATCH**|**局部更新**资源|❌ 否 (理论上)|修改文章状态(只传 `{"status": "published"}`,其他字段不变)|
|
||||
|**DELETE**|删除资源|✅ 是|删除文章|
|
||||
|
||||
⚠️ 重点关注 PUT vs PATCH:
|
||||
|
||||
在 Go 语言中实现 PATCH 有点麻烦(因为 Go 的结构体默认值问题,你很难区分用户是传了 0 还是没传这个字段)。
|
||||
|
||||
- **最佳实践:** 对于 CMS 这种表单复杂的系统,**修改接口首选 `PUT` (全量)**,或者针对特定状态修改提供独立接口(如 `POST /articles/:id/publish`)。如果必须做 `PATCH`,DTO 需使用指针类型 `*string` 来判断是否为 `nil`。
|
||||
|
||||
---
|
||||
|
||||
## 3. RESTful URL 设计模式 (Resource Naming)
|
||||
|
||||
**原则:URL 中只出现名词,不出现动词。**
|
||||
|
||||
- ❌ **反例 (RPC 风格 - 不要这么做):**
|
||||
- `/api/getUsers`
|
||||
- `/api/createUser`
|
||||
- `/api/deleteArticle?id=1`
|
||||
- ✅ **正例 (REST 风格):**
|
||||
- `GET /api/v1/users` (获取列表)
|
||||
- `POST /api/v1/users` (创建)
|
||||
- `DELETE /api/v1/articles/1` (删除 ID 为 1 的文章)
|
||||
|
||||
**复杂关系的嵌套设计:**
|
||||
|
||||
- _场景:_ 获取某篇文章下的评论。
|
||||
- _设计:_ `GET /api/v1/articles/{article_id}/comments`
|
||||
- _场景:_ 获取某个作者的所有文章。
|
||||
- _设计:_ `GET /api/v1/users/{user_id}/articles`
|
||||
|
||||
---
|
||||
|
||||
## 4. 列表接口三剑客:分页、排序、筛选 (Pagination, Sorting, Filtering)
|
||||
|
||||
你的 CMS 一定会有“文章列表”页面,这个接口是最复杂的。不要为每种查询都写一个新接口,要设计一个**通用的查询接口**。
|
||||
|
||||
**最佳实践标准:**
|
||||
|
||||
1. **分页 (Pagination):**
|
||||
|
||||
- 使用 `page` (页码) 和 `page_size` (每页条数)。
|
||||
- URL 示例: `/articles?page=2&page_size=20`
|
||||
- **注意:** 要限制 `page_size` 的最大值(如 100),防止恶意用户一次请求 100 万条数据把数据库打挂。
|
||||
|
||||
2. **排序 (Sorting):**
|
||||
|
||||
- 使用 `sort` 参数。`-` 代表降序,无符号代表升序。
|
||||
- URL 示例: `/articles?sort=-created_at` (按创建时间倒序)
|
||||
- URL 示例: `/articles?sort=view_count,-created_at` (先按浏览量升序,再按时间倒序)
|
||||
|
||||
3. **筛选 (Filtering):**
|
||||
|
||||
- 直接使用字段名作为参数。
|
||||
- URL 示例: `/articles?category_id=1&status=published`
|
||||
|
||||
---
|
||||
|
||||
## 5. 状态码与错误处理 (Status Codes & Error Handling)
|
||||
|
||||
**不要永远只返回 200 OK!**
|
||||
|
||||
前端开发最恨的就是:HTTP 状态码是 200,结果 Body 里写着 `{"code": 500, "msg": "Error"}`。这会让监控系统失效。
|
||||
|
||||
**你需要遵守的“HTTP 状态码地图”:**
|
||||
|
||||
- **2xx (成功):**
|
||||
- `200 OK`: 通用成功。
|
||||
- `201 Created`: 创建成功 (POST 返回)。
|
||||
- `204 No Content`: 删除成功 (DELETE 返回,不带 Body)。
|
||||
- **4xx (客户端错误 - 前端背锅):**
|
||||
- `400 Bad Request`: 参数校验失败(如邮箱格式不对)。
|
||||
- `401 Unauthorized`: 未登录/Token 过期。
|
||||
- `403 Forbidden`: 登录了,但没权限(如普通用户想删文章)。
|
||||
- `404 Not Found`: 资源不存在。
|
||||
- **5xx (服务端错误 - 你背锅):**
|
||||
- `500 Internal Server Error`: 代码崩了/数据库挂了。
|
||||
|
||||
统一错误响应格式 (JSON Envelope):
|
||||
|
||||
无论发生什么错误,Body 必须保持结构一致,方便前端拦截:
|
||||
|
||||
JSON
|
||||
|
||||
```bash
|
||||
{
|
||||
"code": 40001, // 业务错误码 (你在 pkg/e 定义的)
|
||||
"message": "标题已存在", // 给用户看的提示
|
||||
"request_id": "abc-123" // 方便查日志的 Trace ID
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 进阶知识:幂等性 (Idempotency)
|
||||
|
||||
**概念:** 用户因为网络卡顿,连续点击了 3 次“发布文章”按钮,你的系统会创建 1 篇文章还是 3 篇?
|
||||
|
||||
- **非幂等:** `POST`。点 3 次创建 3 篇。
|
||||
- **幂等:** `PUT` / `DELETE`。改 3 次和改 1 次结果一样;删 3 次和删 1 次结果一样(都是没了)。
|
||||
|
||||
如何处理 POST 的幂等性?
|
||||
|
||||
对于关键业务(如支付、核心数据创建),通常要求前端在 Header 带一个 Idempotency-Key (UUID)。后端 Redis 记录这个 Key,如果短时间内收到相同的 Key,直接返回上次的结果,不执行数据库插入。
|
||||
|
||||
(注:MVP 阶段可以先不做,但面试或者做高可靠系统时必须知道)
|
||||
|
||||
---
|
||||
|
||||
## 总结:在你开始定义 API 之前
|
||||
|
||||
请在心中建立这套标准:
|
||||
|
||||
1. **URL:** `/api/v1/名词`
|
||||
2. **Method:** 查询用 GET,新建用 POST,修改用 PUT,删除用 DELETE。
|
||||
3. **List:** 永远记得预留 `page`, `page_size`。
|
||||
4. **Response:** 永远返回统一的 JSON 结构,报错必须用 4xx/5xx 状态码。
|
||||
|
||||
**准备好了吗?我们可以开始基于这些原则,设计 User 模块的 API 了。**
|
||||
259
Go项目实战/03_基础设施/01_错误处理/01_基础设施详细设计规格说明书.md
Normal file
259
Go项目实战/03_基础设施/01_错误处理/01_基础设施详细设计规格说明书.md
Normal file
@@ -0,0 +1,259 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🏗️ Infrastructure Design Specifications (v1.2)
|
||||
- 🏗️ Infrastructure Design Specifications (v1.1)
|
||||
- 🏗️ Infrastructure Design Specifications (v1.0)
|
||||
date created: 星期三, 十二月 10日 2025, 9:41:53 上午
|
||||
date modified: 星期三, 十二月 10日 2025, 10:37:49 上午
|
||||
---
|
||||
|
||||
# 🏗️ Infrastructure Design Specifications (v1.2)
|
||||
|
||||
项目名称: Enterprise-CMS-Core
|
||||
|
||||
模块: Infrastructure (Error Handling & Response)
|
||||
|
||||
版本: 1.2.0 (Refined)
|
||||
|
||||
状态: [✅ 已锁定]
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计目标 (Design Objectives)
|
||||
|
||||
本模块旨在构建一套**统一的、类型安全的、框架解耦**的 HTTP 响应与错误处理机制。
|
||||
|
||||
- **统一性:** 无论成功与否,API 必须返回结构一致的 JSON Envelope。
|
||||
- **可观测性:** 错误必须携带业务语义(ErrorCode),而非仅返回 HTTP 500。
|
||||
- **解耦性:** 业务逻辑层 (Service) 不感知 HTTP 框架 (Gin),仅通过 Go 原生 `error` 接口交互。
|
||||
- **高内聚:** 错误码定义与错误实体封装在同一包内,减少调用摩擦。
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术选型基线 (Tech Stack Baseline)
|
||||
|
||||
|**组件**|**选型**|**约束说明**|
|
||||
|---|---|---|
|
||||
|**HTTP Context**|`github.com/gin-gonic/gin`|仅在 `internal/pkg/app` (Level 1) 和 `handler` 层使用。**严禁**在 `service` 层引入。|
|
||||
|**Error Handling**|Go Standard Library|使用 Go 1.13+ `errors` (`Is`, `As`, `New`) 和 `fmt.Errorf`。**严禁**引入第三方 error 库 (如 `pkg/errors`)。|
|
||||
|**Serialization**|`encoding/json`|使用标准库。MVP 阶段暂不引入 `json-iterator`。|
|
||||
|**Concurrency**|`sync.RWMutex`|用于保护错误码 Map 的并发读取(读多写少场景)。|
|
||||
| **Tracing** | Gin Keys | Trace ID 必须由前置中间件(或网关)注入。Gin Context Key 约定为 `"X-Trace-ID"`。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心设计模式 (Design Patterns)
|
||||
|
||||
### 3.1 响应封装:Context Object & Factory
|
||||
|
||||
采用 **“上下文对象”** 模式对 `gin.Context` 进行封装,提供链式调用的体验。
|
||||
|
||||
- **模式:** `app.New(c).Success(data)`
|
||||
- **优势:** 屏蔽底层框架差异,统一入口。
|
||||
|
||||
### 3.2 错误处理:安全与动态机制 (Security & Dynamics)
|
||||
|
||||
- **双层信息架构:**
|
||||
- **User Msg (Safe):** JSON Body 中的 `msg` 字段。**仅**允许返回 `ecode` 中定义的静态文案,或经过白名单过滤的动态参数(如参数名)。
|
||||
- **Log Detail (Unsafe):** 服务端日志。必须记录完整的 `err.Error()`(包含堆栈、SQL 错误、`fmt.Errorf` 包装的底层原因)。
|
||||
- **动态文案支持:**
|
||||
- `ecode` 包需提供 `WithMsg(msg string)` 或 `WithDetails(args …any)` 方法,用于**安全地**覆盖默认文案。
|
||||
- **示例:** `return ecode.InvalidParams.WithMsg("Email 格式错误")`。
|
||||
|
||||
### 3.3 状态码管理:Centralized Registry
|
||||
|
||||
采用 **“集中式注册表”** 模式。
|
||||
|
||||
- **约束:** 所有业务错误码 (Business Code) 必须在 `internal/pkg/ecode` 包中定义为 `const`。
|
||||
- **禁止:** 严禁在业务代码中硬编码数字(Magic Number)。
|
||||
|
||||
### 3.4 错误码号段分配:Error Code Allocation
|
||||
|
||||
结构定义:
|
||||
|
||||
错误码采用 5 位数字结构:A BB NN
|
||||
|
||||
- **A (万位):** 模块/领域 (1=Infra, 2=User, 3=Content…)
|
||||
- **BB (千百位):** 组件/子模块分类
|
||||
- **NN (十个位):** 具体错误流水号
|
||||
|
||||
#### 1. 基础设施层 (System / Infra) - `10000 - 19999`
|
||||
|
||||
针对基础设施,**必须**严格遵守以下二级分类,严禁混用:
|
||||
|
||||
|**二级区间 (Sub-Range)**|**组件归属 (Component)**|**典型示例 (Examples)**|
|
||||
|---|---|---|
|
||||
|**10000 - 10099**|**Server General**|`10000` (Success), `10001` (Unknown Error), `10002` (Panic Recovered)|
|
||||
|**10100 - 10199**|**Database (Internal)**|`10100` (DB Connection Lost), `10101` (SQL Syntax Error) - _注意:业务查空属业务码,不在此列_|
|
||||
|**10200 - 10299**|**Cache (Redis)**|`10200` (Redis Timeout), `10201` (Key Evicted Unexpectedly)|
|
||||
|**10300 - 10399**|**Serialization**|`10300` (JSON Marshal Failed), `10301` (Invalid Request Body)|
|
||||
|**10400 - 10499**|**Middleware/Gateway**|`10400` (Too Many Requests/Rate Limit), `10401` (Route Not Found)|
|
||||
|**10500 - 10599**|**3rd Party API**|`10500` (External Service Unavailable), `10501` (SMS Send Failed)|
|
||||
|
||||
#### 2. 业务模块层 (Business Modules) - `20000+`
|
||||
|
||||
业务模块建议参考同等逻辑进行二级划分(由各模块负责人定义,但建议遵循以下范式):
|
||||
|
||||
|**一级区间**|**模块**|**二级区间示例**|
|
||||
|---|---|---|
|
||||
|**20000 - 29999**|**User / Auth**|`200xx` (基础账户), `201xx` (登录/Token), `202xx` (RBAC 权限), `203xx` (KYC 认证)|
|
||||
|**30000 - 39999**|**Content (CMS)**|`300xx` (文章), `301xx` (分类/标签), `302xx` (评论), `303xx` (审核流)|
|
||||
|
||||
---
|
||||
|
||||
## 4. 交互协议与数据流 (Interaction Protocol)
|
||||
|
||||
### 4.1 JSON 响应契约 (The Contract)
|
||||
|
||||
所有 HTTP 接口返回的 Body 必须符合以下结构:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"code": 20001, // 业务状态码 (0=成功, 非0=错误)
|
||||
"msg": "用户已存在", // 开发者提示/用户提示
|
||||
"data": { … }, // 业务数据 (成功时为 Object/Array, 失败时为 null)
|
||||
"trace_id": "abc-123" // 必填。取值优先级: c.GetHeader("X-Trace-ID") -> c.GetString("X-Trace-ID") -> UUID生成
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 HTTP 状态码策略 (Status Code Policy)
|
||||
|
||||
本项目采用 **"Hybrid 策略 "**:
|
||||
|
||||
- **HTTP 200 OK:**
|
||||
- 所有 **业务逻辑错误** (Code `2xxxx` - `4xxxx`)。
|
||||
- 前端通过 Body 中的 `code != 0` 判断业务异常。
|
||||
- _理由:_ 避免网关(如 Nginx)拦截 4xx 响应并替换为默认错误页,导致前端拿不到 JSON 数据。
|
||||
- **HTTP 500 Internal Server Error:**
|
||||
- 所有 **基础设施错误** (Code `1xxxx`),包括 Panic、数据库断连、Redis 超时。
|
||||
- _理由:_ 触发云厂商负载均衡器 (LB) 的熔断机制,将流量切出故障节点。
|
||||
- **HTTP 401/403:**
|
||||
- 仅用于网关层面的拦截(如 JWT 格式错误),业务层鉴权失败建议走 HTTP 200 + Code `20101`。
|
||||
|
||||
### 4.3 跨层交互时序 (Cross-Layer Flow)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Controller (Handler)
|
||||
participant S as Service (Domain)
|
||||
participant I as Infra (pkg/app)
|
||||
participant E as Ecode (pkg/ecode)
|
||||
|
||||
C->>I: app.New(c) 初始化
|
||||
C->>S: Call Business Logic
|
||||
alt 成功
|
||||
S-->>C: return (data, nil)
|
||||
C->>I: app.Success(data)
|
||||
I-->>Client: JSON {code:0, data:…}
|
||||
else 失败 (业务错误)
|
||||
S-->>C: return (nil, ecode.New(20001))
|
||||
C->>I: app.Error(err)
|
||||
I->>I: errors.As(err) -> 提取 Code 20001
|
||||
I-->>Client: JSON {code:20001, msg:"…"}
|
||||
else 失败 (系统错误)
|
||||
S-->>C: return (nil, errors.New("DB error"))
|
||||
C->>I: app.Error(err)
|
||||
I->>I: errors.As(err) -> 失败 (Fallback)
|
||||
I-->>Client: JSON {code:50000, msg:"Internal Error"}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 目录结构与职责 (Directory & Responsibilities)
|
||||
|
||||
```Plaintext
|
||||
internal/
|
||||
├── middleware/ # [New] 全局中间件
|
||||
│ ├── recovery.go # Panic 捕获 -> 转换为 ecode.ServerError (50000)
|
||||
│ └── not_found.go # 404 捕获 -> 转换为 ecode.NotFound (40400)
|
||||
│
|
||||
└── pkg/
|
||||
├── ecode/ # [Level 0] 错误核心包 (无内部依赖)
|
||||
│ ├── code.go # const 常量定义 (UserNotFound = 20001)
|
||||
│ ├── msg.go # 错误码文案映射 (Map & GetMsg)
|
||||
│ └── error.go # Error 结构体定义 (New, Parse 方法)
|
||||
│
|
||||
└── app/ # [Level 1] HTTP 响应封装 (依赖 gin, ecode)
|
||||
└── response.go # NewResponse, Success, Error 方法
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 开发规范与 Linter 规则 (Linting Rules)
|
||||
|
||||
1. **包引用原则:**
|
||||
|
||||
- `ecode` 包必须保持零依赖(只依赖标准库)。
|
||||
- `app` 包依赖 `ecode`。
|
||||
|
||||
2. **Service 层纯净性:**
|
||||
|
||||
- `internal/domain/service` 代码中**严禁出现** `import "github.com/gin-gonic/gin"`。
|
||||
- `internal/domain/service` 代码中**严禁出现** `import "enterprise-cms-core/internal/pkg/app"`。
|
||||
- 只允许引入 `internal/pkg/ecode`。
|
||||
|
||||
3. **错误包装与响应清洗:**
|
||||
|
||||
- **Log:** `app.Error(err)` 内部必须将 `err` 的完整堆栈打印到 Zap 日志中。
|
||||
- **Response:**
|
||||
- 若 `err` 可被断言为 `*ecode.Error`,则取其 `Msg` 字段返回。
|
||||
- 若 `err` 仅为普通 `error` (如 DB error),**严禁**直接将其内容返回给前端,必须统一兜底返回 `ecode.ServerError` 的文案("Internal Server Error")。
|
||||
|
||||
4. **全局兜底机制 (Global Safety Net):**
|
||||
- 项目必须在 `internal/middleware` 中实现 `Recovery` 中间件。
|
||||
- **严禁**让 Gin 默认的 Panic 堆栈直接输出到 HTTP Body。
|
||||
- **必须**捕获所有 Panic,并调用 `app.Error(ecode.ServerError)` 统一输出为符合 JSON 契约的格式 (`{"code": 50000, "msg": "Internal Server Error", …}`)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 工程化实施标准 (Engineering Standards)
|
||||
|
||||
### 7.1 代码风格契约 (Code Style Contract)
|
||||
|
||||
为确保代码长期可维护,生成的代码必须严格遵守以下 Go 惯用语 (Idioms):
|
||||
|
||||
1. **命名规范:**
|
||||
- **缩写:** 使用全大写缩写 (如 `ServeHTTP`, `ID`, `URL`),严禁 `Url`, `Id`。
|
||||
- **局部变量:** 保持短小 (如 `ctx`, `err`, `req`),避免 Java 式的长命名 (如 `requestContext`, `errorObject`)。
|
||||
- **工厂方法:** `ecode` 包内使用 `New()`, `app` 包内使用 `NewResponse()`。
|
||||
|
||||
2. **代码组织:**
|
||||
- **Import 分组:** 标准库 -> 第三方库 -> 内部库 (enterprise-cms-core/…)。
|
||||
- **Guard Clauses:** 优先使用“卫语句”提前返回,减少 `else` 嵌套层级。
|
||||
|
||||
### 7.2 注释与文档 (Documentation)
|
||||
|
||||
为了提升团队协作效率,所有 Exported (首字母大写) 的类型、函数、常量必须包含符合 GoDoc 规范的**中文注释**。
|
||||
|
||||
- **格式规范:** `// FunctionName 中文描述…`
|
||||
- **关键:** 注释**必须**以函数/变量名开头,且与中文描述之间**保留一个空格**。这是 Go 官方工具链解析文档的标准要求。
|
||||
- **内容重心:**
|
||||
- **摘要:** 第一行简明扼要地说明“它是做什么的”。
|
||||
- **详情 (可选):** 解释 **"Why" (设计意图)** 和 **"Caveats" (副作用/注意事项)**,而非翻译代码逻辑。
|
||||
- **示例:**
|
||||
|
||||
```Go
|
||||
// Success 向客户端写入标准的 JSON 成功响应。
|
||||
//
|
||||
// 注意:
|
||||
// 1. 无论业务逻辑如何,此方法会将 HTTP 状态码强制设置为 200。
|
||||
// 2. data 字段若为 nil,将序列化为 JSON 的 null。
|
||||
func (r *Response) Success(data any) { … }
|
||||
|
||||
// UserNotFound 表示用户不存在的业务错误码 (20001)。
|
||||
const UserNotFound = 20001
|
||||
```
|
||||
|
||||
### 7.3 可扩展性设计 (Extensibility Patterns)
|
||||
|
||||
为了应对未来需求变更,本模块需采用以下模式:
|
||||
|
||||
1. **Functional Options (针对 `app` 包):**
|
||||
- 构造 `Response` 对象时,应支持 Option 模式,以便未来无需破坏函数签名即可添加新字段(如 TraceID, DebugInfo)。
|
||||
- *定义:* `type Option func(*Response)`
|
||||
- *签名:* `func New(c *gin.Context, opts …Option) *Response`
|
||||
|
||||
2. **Interface Segregation (接口隔离):**
|
||||
- 虽然 `ecode` 是基础值对象,但 `app` 层若涉及复杂逻辑,应定义 `Responder` 接口,方便 Mock 测试。
|
||||
247
Go项目实战/03_基础设施/01_错误处理/02_AI 辅助基础设施构建 SOP (v2.1) -错误处理与响应篇.md
Normal file
247
Go项目实战/03_基础设施/01_错误处理/02_AI 辅助基础设施构建 SOP (v2.1) -错误处理与响应篇.md
Normal file
@@ -0,0 +1,247 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🏗️ AI 辅助基础设施构建 SOP (v2.1) - [错误处理与响应篇]
|
||||
- 🏗️ AI 辅助基础设施构建 SOP (v2.0) - [错误处理与响应篇]
|
||||
- 🏗️ AI 辅助基础设施构建 SOP (v1.1) - [错误处理与响应篇]
|
||||
- 🏗️ AI 辅助基础设施构建 SOP (v1.0) - [错误处理与响应篇]
|
||||
date created: 星期三, 十二月 10日 2025, 12:34:57 凌晨
|
||||
date modified: 星期三, 十二月 10日 2025, 11:55:08 中午
|
||||
---
|
||||
|
||||
# 🏗️ AI 辅助基础设施构建 SOP (v2.1) - [错误处理与响应篇]
|
||||
|
||||
**核心理念:**
|
||||
|
||||
1. **Contract First (契约优先):** 永远先定义对外暴露的 JSON 结构,再写内部 Go 结构体。
|
||||
2. **DX Driven (体验驱动):** 在实现逻辑前,先写“伪代码”验证调用是否顺手。
|
||||
3. **Atomic Delivery (原子交付):** 单次交互只生成一个文件,利用“上下文锚点”串联上下文。
|
||||
|
||||
---
|
||||
|
||||
## 📋 准备工作:变量与架构确认
|
||||
|
||||
在使用以下 Prompt 前,请确认上下文:
|
||||
|
||||
- `{语言/框架}`: Go 1.24+ / Gin
|
||||
- `{模块路径}`:
|
||||
- `internal/pkg/ecode` (Level 0: 错误码 + 错误实体 + 映射逻辑)
|
||||
- `internal/pkg/app` (Level 1: HTTP 响应封装,依赖 `ecode`)
|
||||
- `{架构约束}`: `ecode` 包零依赖;`app` 包依赖 `ecode`。
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: 原子化任务拆解 (The MECE Protocol)
|
||||
|
||||
**目的:** 将大需求拆解为一组符合 MECE 原则的微任务清单。
|
||||
|
||||
### 🤖 拆解者 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
你现在是我的 **Tech Lead (技术负责人)**。
|
||||
我们要实现 `{模块名称}` 模块。为了防止代码生成中断和逻辑混乱,请不要直接开始写代码。
|
||||
|
||||
请先执行 **“MECE 任务拆解”**:
|
||||
|
||||
**1. 架构约束分析:**
|
||||
- 本模块遵循 Modular Clean Architecture。
|
||||
- `internal/pkg/ecode`: 包含错误码常量、错误实体结构体、错误文案映射。**严禁依赖上层包**。
|
||||
- `internal/pkg/app`: 包含 Gin 的 Response 封装。依赖 `ecode`。
|
||||
|
||||
**2. 原子化切分:**
|
||||
请将开发工作拆解为 3-5 个“原子任务步”。
|
||||
- 每个步骤必须针对**单个物理文件**。
|
||||
- 步骤必须遵循依赖顺序(底层先于上层)。
|
||||
|
||||
**3. 输出格式:**
|
||||
请输出一个 **Markdown Checklist (执行清单)**。
|
||||
格式示例:
|
||||
- [ ] **Step 1: {文件名}** - {核心职责} (依赖: 无)
|
||||
- [ ] **Step 2: {文件名}** - {核心职责} (依赖: Step 1)
|
||||
…
|
||||
|
||||
**模块需求:**
|
||||
我们需要一套统一的 HTTP 错误处理机制,支持自定义业务错误码,统一返回 JSON 格式。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 0.5: API 签名锁定 (API Surface Lock)
|
||||
|
||||
**目的:** 在实现具体逻辑前,强制锁定所有 Public 方法的签名,防止实现阶段出现参数不一致。
|
||||
|
||||
### 🤖 Prompt 0.5: 生成接口定义
|
||||
|
||||
**[发送给 AI]:**
|
||||
|
||||
````markdown
|
||||
在开始写代码前,请先为 `internal/pkg/app` 包定义 **Public API 签名 (Exported Functions)**。
|
||||
请直接提供 `Responder` 接口定义或核心函数的函数头(无需函数体)。
|
||||
|
||||
**要求:**
|
||||
1. **一致性:** 确认 `context` 参数的位置(建议统一作为第一个参数)。
|
||||
2. **完整性:** 必须包含 `New`, `Success`, `Error` 以及我们刚才讨论的 `ErrorCtx` (处理 trace_id)。
|
||||
3. **Go Doc:** 为每个方法写出符合 Go 标准的注释。
|
||||
|
||||
**期望输出示例:**
|
||||
|
||||
```go
|
||||
// Response wraps the gin.Context for unified JSON response.
|
||||
type Response struct { … }
|
||||
|
||||
// New creates a new Response wrapper.
|
||||
func New(c *gin.Context) *Response { … }
|
||||
|
||||
// Success sends a successful response with data.
|
||||
func (r *Response) Success(data any) { … }
|
||||
```
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 契约定义 (Contract Definition)
|
||||
|
||||
**目的:** 确立“对外口径”。
|
||||
|
||||
### 🤖 Prompt 1: 定义 JSON 结构 (复制使用)
|
||||
|
||||
```Markdown
|
||||
你现在是我的 **API 治理专家**。
|
||||
请设计一套统一的 **HTTP 响应结构 (JSON Envelope)**。
|
||||
|
||||
**设计原则:**
|
||||
1. **统一性:** 无论成功还是失败,Body 结构一致。
|
||||
2. **字段要求:** 必须包含 `code` (int), `msg` (string), `data` (any), `trace_id` (string)。
|
||||
|
||||
**任务:**
|
||||
请给出以下 3 种场景的 JSON 响应示例,并解释设计理由:
|
||||
- 场景 A: 成功返回对象。
|
||||
- 场景 B: 成功返回空列表 (明确 `data` 是 `null` 还是 `[]`)。
|
||||
- 场景 C: 业务错误 (如 Code 20001)。
|
||||
|
||||
**[关键补充约束]**
|
||||
1. **安全性优先:** `app.Error(err)` 处理逻辑中,必须区分**用户可见文案**和**底层调试信息**。若 `err` 包含底层堆栈(如 SQL 错误),JSON 中的 `msg` 必须降级显示为 `ecode` 定义的通用文案(如 "Internal Error"),严禁透传底层 Error String。
|
||||
2. **HTTP 状态码:** 本项目强制执行 **"HTTP 200 OK + Business Code"** 策略。除非 Gin 框架层崩溃,否则 HTTP Status 永远为 200。
|
||||
3. **Trace ID:** 假设 `c.GetString("trace_id")` 可以获取 ID,请在 `app.New(c)` 时将其注入 Response 结构体。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 体验验证 (DX Verification)
|
||||
|
||||
**目的:** 模拟业务层调用,防止基础设施“反人类”。
|
||||
|
||||
### 🤖 Prompt 2: 伪代码验证 (复制使用)
|
||||
|
||||
```Markdown
|
||||
JSON 结构已确认。
|
||||
假设我们已经有了 `internal/pkg/ecode` 和 `internal/pkg/app`。
|
||||
|
||||
请写一段 Gin Handler 的 **伪代码 (Pseudo-code)**,展示开发者该如何使用它们。
|
||||
|
||||
**验证重点:**
|
||||
1. **业务错误:** 如何返回 `ecode.New(20001, "…")`?
|
||||
2. **响应封装:** 如何调用 `app.New(c).Success(data)`?
|
||||
3. **代码简洁性:** 避免大量的 `if err != nil` 重复代码。
|
||||
|
||||
请展示最优雅的写法。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 迭代式核心实现 (Iterative Implementation)
|
||||
|
||||
**核心机制:** 这是一个**循环步骤**。请查看 Phase 0 生成的 Checklist,**逐个文件**执行。
|
||||
|
||||
### 🔄 循环动作 A: 生成代码
|
||||
|
||||
**[用户动作]:** 复制 Checklist 中当前未完成的步骤(例如 "Step 1: 生成 ecode/code.go")。
|
||||
|
||||
**[发送 Prompt]:**
|
||||
|
||||
```Markdown
|
||||
我们现在执行 **Step {N}**。
|
||||
|
||||
**任务目标:**
|
||||
{粘贴 Phase 0 Checklist 中的当前步骤描述}
|
||||
|
||||
**上下文约束 (严禁修改):**
|
||||
1. **JSON 契约:** `{粘贴 Phase 1 确认的 JSON}`
|
||||
2. **DX 规范:** `{粘贴 Phase 2 确认的伪代码}`
|
||||
3. **依赖控制:** 如果是 `ecode` 包,严禁引用 `app` 或 `gin`。
|
||||
|
||||
**输出要求:**
|
||||
请仅生成该步骤对应的 `{文件名}` 源代码。不要生成测试代码。
|
||||
|
||||
**通用代码质量约束 (Linter Rules):**
|
||||
1. **注释规范:** 所有 Exported (首字母大写) 的结构体、函数、常量必须包含符合 Go Doc 规范的注释。
|
||||
2. **复杂度控制:** 确保 `gocyclo` (圈复杂度) 低于 10。如果逻辑复杂,请拆分为私有函数。
|
||||
3. **错误检查:** 严禁忽略 error 返回值(如 `json.Marshal`),必须处理或 Log。
|
||||
4. **Lint 检查:** 生成的代码必须能通过 `errcheck` 和 `staticcheck`。
|
||||
```
|
||||
|
||||
### 🔄 循环动作 B: 上下文锚点 (Context Anchoring)
|
||||
|
||||
**[用户动作]:** 代码生成并确认无误后,发送此 Prompt 以建立记忆锚点。
|
||||
|
||||
**[发送 Prompt]:**
|
||||
|
||||
```Markdown
|
||||
已确认 `{文件名}` 代码无误。
|
||||
请将该代码存入你的**短期记忆**,作为后续步骤的上下文依赖。
|
||||
**不要重复输出它**。我们准备进入下一步。
|
||||
```
|
||||
|
||||
_(重复 A -> B,直到所有源码文件生成完毕)_
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 极限防御测试 (Extreme Defensive Testing)
|
||||
|
||||
**目的:** 模拟“最糟糕”的业务代码调用,确保基础设施不崩。
|
||||
|
||||
### 🤖 Prompt 4: 生成红队测试用例
|
||||
|
||||
```markdown
|
||||
所有核心代码已生成。现在请为 `internal/pkg/app/response.go` 编写单元测试 `response_test.go`。
|
||||
|
||||
**请覆盖以下 4 个极端场景 (Test Cases):**
|
||||
|
||||
1. **Raw Error 降级:**
|
||||
- **场景:** 传入 `errors.New("db connection broken")` (非 ecode 类型)。
|
||||
- **断言:** HTTP 状态码为 500 (或 200+Code 50000),Msg 为 "Internal Server Error" (严禁泄漏原始错误信息)。
|
||||
|
||||
2. **Double Response 防护:**
|
||||
- **场景:** 在同一个 Handler 中连续调用 `app.Success()` 两次。
|
||||
- **断言:** 第二次调用应被忽略或记录 Warning 日志,且不应导致 Panic。
|
||||
|
||||
3. **Nil Data 安全:**
|
||||
- **场景:** 调用 `app.Success(nil)`。
|
||||
- **断言:** JSON 中的 `data` 字段应为 `null` (或 `{}`,取决于契约),不应 Panic。
|
||||
|
||||
4. **并发 Map 读写:**
|
||||
- **场景:** 启动 100 个 Goroutine 并发调用 `ecode.GetMsg(code)`。
|
||||
- **断言:** `test -race` 必须通过,无数据竞争。
|
||||
|
||||
请输出完整的 Test 代码。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 最终验收 (SRE Review)
|
||||
|
||||
**目的:** 模拟运维视角审查。
|
||||
|
||||
### 🤖 Prompt 5: 找茬模式 (复制使用)
|
||||
|
||||
```Markdown
|
||||
切换角色为 **SRE (站点可靠性工程师)**。
|
||||
请审查上述所有代码(ecode + app)。
|
||||
|
||||
**风险排查:**
|
||||
1. **Panic 风险:** 是否有未捕获的 Panic 点?
|
||||
2. **监控盲区:** 当前的 Error Log 是否包含了足够的上下文(如 StackTrace)供排查?
|
||||
3. **状态码混淆:** 我们采用了“HTTP 200 + 业务码”模式,请确认这是否会影响网关层的 5xx 告警配置?
|
||||
|
||||
请简要列出 2-3 个优化建议。
|
||||
```
|
||||
161
Go项目实战/03_基础设施/01_错误处理/03_README_错误处理.md
Normal file
161
Go项目实战/03_基础设施/01_错误处理/03_README_错误处理.md
Normal file
@@ -0,0 +1,161 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- "🛡️ 基础设施模块:错误处理与响应系统 (Infra: Error Handling & Response)"
|
||||
date created: 星期三, 十二月 10日 2025, 12:10:32 中午
|
||||
date modified: 星期三, 十二月 10日 2025, 12:12:02 中午
|
||||
---
|
||||
|
||||
# 🛡️ 基础设施模块:错误处理与响应系统 (Infra: Error Handling & Response)
|
||||
|
||||
## 1\. 模块概述
|
||||
|
||||
本模块实现了 **Modular Clean Architecture** 中的基础设施层 (`Level 0` & `Level 1`),提供了一套统一的、安全的、可观测的 HTTP 响应机制。
|
||||
|
||||
**核心能力:**
|
||||
|
||||
- **统一契约:** 所有 API 响应(成功、失败、Panic、404)严格遵循 `{code, msg, data, trace_id}` 结构。
|
||||
- **安全降级:** 自动识别业务错误与系统错误。对系统级错误(如 SQL 失败)进行“掩码”处理,防止敏感信息泄露。
|
||||
- **可观测性:** 集成 Prometheus 埋点,通过 `X-Biz-Code` 实现业务级监控;全链路 TraceID 自动注入。
|
||||
- **开发体验:** 提供 `Responder` 接口与工厂模式,支持 Handler 层的依赖注入与 Mock 测试。
|
||||
|
||||
-----
|
||||
|
||||
## 2\. 文件清单 (File Manifest)
|
||||
|
||||
以下代码位于项目根目录 `gitea-aliyun/Klein/enterprise-cms-core/` 下:
|
||||
|
||||
### Level 0: 基础领域层 (`internal/pkg/ecode`)
|
||||
|
||||
> **依赖:** 零依赖 (仅标准库)
|
||||
|
||||
| 文件名 | 类型 | 核心职责 |
|
||||
| :--- | :--- | :--- |
|
||||
| `code.go` | Const | **错误码注册表**。定义 `1xxxx` (系统) 和 `2xxxx` (业务) 常量。 |
|
||||
| `msg.go` | Data | **文案映射**。维护全局 `map[int]string`,提供并发安全的 `GetMsg`。 |
|
||||
| `error.go` | Struct | **错误实体**。实现 `error` 接口,支持 `WithMsg`/`WithDetails` 扩展。 |
|
||||
| `ecode_test.go` | Test | 验证并发安全性及不可变性。 |
|
||||
|
||||
### Level 1: 应用工具层 (`internal/pkg/app`)
|
||||
|
||||
> **依赖:** `gin`, `ecode`
|
||||
|
||||
| 文件名 | 类型 | 核心职责 |
|
||||
| :--- | :--- | :--- |
|
||||
| `responder.go` | Interface | **接口定义**。定义 `Responder` 接口与 `Factory` 函数类型,用于解耦。 |
|
||||
| `response.go` | Impl | **核心实现**。封装 Gin Context,实现 JSON 序列化、错误清洗、监控埋点。 |
|
||||
| `options.go` | Pattern | **功能选项**。提供 `WithTraceID` 等扩展配置。 |
|
||||
| `response_test.go` | Test | 验证 JSON 契约、空指针防御及错误降级逻辑。 |
|
||||
|
||||
### Global: 全局中间件 (`internal/middleware`)
|
||||
|
||||
> **依赖:** `gin`, `pkg/app`, `pkg/ecode`, `prometheus`
|
||||
|
||||
| 文件名 | 类型 | 核心职责 |
|
||||
| :--- | :--- | :--- |
|
||||
| `recovery.go` | Safety | **Panic 兜底**。捕获 Panic 并转换为标准 JSON 500 响应。 |
|
||||
| `not_found.go` | Route | **404 兜底**。将无路由请求转换为标准 JSON 404 响应。 |
|
||||
| `metrics.go` | Monitor | **业务监控**。采集 `http_requests_total` 指标,包含 `biz_code` 标签。 |
|
||||
|
||||
-----
|
||||
|
||||
## 3\. 快速上手 (Quick Start)
|
||||
|
||||
### 3.1 定义新错误
|
||||
|
||||
在 `internal/pkg/ecode/code.go` 添加常量,并在 `msg.go` 添加文案。
|
||||
|
||||
```go
|
||||
// code.go
|
||||
const UserBalanceInsufficient = 20005
|
||||
|
||||
// msg.go
|
||||
msg = map[int]string{
|
||||
// ...
|
||||
UserBalanceInsufficient: "User Balance Insufficient",
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 在 Handler 中使用 (推荐写法)
|
||||
|
||||
使用依赖注入的 `app.Factory` 创建响应器,而非直接调用 `app.New`。
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gitea-aliyun/Klein/enterprise-cms-core/internal/pkg/app"
|
||||
"gitea-aliyun/Klein/enterprise-cms-core/internal/pkg/ecode"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
// 注入 Responder 工厂,便于测试 Mock
|
||||
RespFactory app.Factory
|
||||
}
|
||||
|
||||
func (h *UserHandler) Create(c *gin.Context) {
|
||||
// 1. 创建响应器
|
||||
resp := h.RespFactory(c)
|
||||
|
||||
// 2. 模拟业务逻辑
|
||||
if err := h.Service.Create(); err != nil {
|
||||
// 自动处理错误:如果是业务错误直接返回;如果是系统错误则降级并记录日志
|
||||
resp.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 成功响应
|
||||
resp.Success(gin.H{"status": "created"})
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 系统接入 (Main.go)
|
||||
|
||||
在 HTTP Server 启动时注册全局中间件。
|
||||
|
||||
```go
|
||||
r := gin.New()
|
||||
|
||||
// 1. Recovery (必须最先注册)
|
||||
r.Use(middleware.Recovery())
|
||||
|
||||
// 2. Metrics (监控业务码)
|
||||
r.Use(middleware.BusinessMetrics())
|
||||
|
||||
// ... 注册业务路由 ...
|
||||
|
||||
// 3. 404 处理 (最后注册)
|
||||
r.NoRoute(middleware.NotFound())
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
## 4\. 设计决策说明 (Architecture Decisions)
|
||||
|
||||
### A. HTTP 200 Always 策略
|
||||
|
||||
- **规则:** 除非网络层崩溃,所有接口(包括业务错误和系统错误)均返回 `HTTP 200 OK`。
|
||||
- **原因:** 防止网关(Nginx/ALB)拦截非 200 响应并替换 Body,确保前端始终能解析 JSON 中的 `code`。
|
||||
|
||||
### B. 安全掩码 (Security Masking)
|
||||
|
||||
- **输入:** `db.Query` 失败返回 `sql: connection refused`。
|
||||
- **输出:** 前端收到 `{ "code": 10000, "msg": "Internal Server Error" }`。
|
||||
- **日志:** 服务端 Error Log 记录原始堆栈。
|
||||
- **目的:** 杜绝数据库结构、IP 等敏感信息通过报错接口泄露。
|
||||
|
||||
### C. 监控指标 (Metrics)
|
||||
|
||||
- **指标名:** `http_requests_total`
|
||||
- **关键标签:** `biz_code` (业务状态码)。
|
||||
- **SRE 告警:** 请针对 `biz_code >= 10000` (系统错误) 配置告警,而非 HTTP Status Code。
|
||||
|
||||
-----
|
||||
|
||||
## 5\. 测试指南
|
||||
|
||||
本模块已包含完整的单元测试与竞态检测 (Race Detection)。
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
go test -v -race ./internal/pkg/...
|
||||
```
|
||||
27
Go项目实战/03_基础设施/01_错误处理/04_错误处理模块文件夹骨架.md
Normal file
27
Go项目实战/03_基础设施/01_错误处理/04_错误处理模块文件夹骨架.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
tags: []
|
||||
date created: 星期三, 十二月 10日 2025, 11:00:25 上午
|
||||
date modified: 星期三, 十二月 10日 2025, 11:56:10 中午
|
||||
---
|
||||
|
||||
```plaintext
|
||||
enterprise-cms-core/
|
||||
├── internal/
|
||||
│ ├── pkg/
|
||||
│ │ ├── ecode/ # [Level 0] 基础领域层
|
||||
│ │ │ ├── code.go # [Const] 纯常量定义 (ErrorCode Registry)
|
||||
│ │ │ ├── error.go # [Type] 核心结构体定义 (struct Error)
|
||||
│ │ │ ├── msg.go # [Data] 错误码文案映射 (var msg map[int]string)
|
||||
│ │ │ └── ecode_test.go # [Test] 单元测试
|
||||
│ │ │
|
||||
│ │ └── app/ # [Level 1] 应用工具层
|
||||
│ │ ├── responder.go # [Interface] 👈 修正点: 定义 type Responder interface
|
||||
│ │ ├── response.go # [Impl] 定义 type Response struct (实现逻辑)
|
||||
│ │ ├── options.go # [Pattern] 定义 Functional Options (配置扩展)
|
||||
│ │ └── response_test.go # [Test] 单元测试
|
||||
│ │
|
||||
│ └── middleware/ # [Global]
|
||||
│ ├── recovery.go # Panic 捕获
|
||||
│ ├── not_found.go # 404 处理
|
||||
│ └── metrics.go
|
||||
```
|
||||
548
Go项目实战/03_基础设施/01_错误处理/99_错误处理上下文.md
Normal file
548
Go项目实战/03_基础设施/01_错误处理/99_错误处理上下文.md
Normal file
@@ -0,0 +1,548 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- Project Context Aggregation
|
||||
date created: 星期三, 十二月 10日 2025, 11:10:48 上午
|
||||
date modified: 星期三, 十二月 10日 2025, 11:12:52 上午
|
||||
---
|
||||
|
||||
# Project Context Aggregation
|
||||
|
||||
> Source Items: 3
|
||||
|
||||
==== AI 辅助基础设施构建 SOP (v2.1) - 错误处理与响应篇.md ====
|
||||
|
||||
```markdown
|
||||
# 🏗️ AI 辅助基础设施构建 SOP (v2.1) - [错误处理与响应篇]
|
||||
|
||||
**核心理念:**
|
||||
|
||||
1. **Contract First (契约优先):** 永远先定义对外暴露的 JSON 结构,再写内部 Go 结构体。
|
||||
2. **DX Driven (体验驱动):** 在实现逻辑前,先写“伪代码”验证调用是否顺手。
|
||||
3. **Atomic Delivery (原子交付):** 单次交互只生成一个文件,利用“上下文锚点”串联上下文。
|
||||
|
||||
---
|
||||
|
||||
## 📋 准备工作:变量与架构确认
|
||||
|
||||
在使用以下 Prompt 前,请确认上下文:
|
||||
|
||||
- `{语言/框架}`: Go 1.24+ / Gin
|
||||
- `{模块路径}`:
|
||||
- `internal/pkg/ecode` (Level 0: 错误码 + 错误实体 + 映射逻辑)
|
||||
- `internal/pkg/app` (Level 1: HTTP 响应封装,依赖 `ecode`)
|
||||
- `{架构约束}`: `ecode` 包零依赖;`app` 包依赖 `ecode`。
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: 原子化任务拆解 (The MECE Protocol)
|
||||
|
||||
**目的:** 将大需求拆解为一组符合 MECE 原则的微任务清单。
|
||||
|
||||
### 🤖 拆解者 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
你现在是我的 **Tech Lead (技术负责人)**。
|
||||
我们要实现 `{模块名称}` 模块。为了防止代码生成中断和逻辑混乱,请不要直接开始写代码。
|
||||
|
||||
请先执行 **“MECE 任务拆解”**:
|
||||
|
||||
**1. 架构约束分析:**
|
||||
- 本模块遵循 Modular Clean Architecture。
|
||||
- `internal/pkg/ecode`: 包含错误码常量、错误实体结构体、错误文案映射。**严禁依赖上层包**。
|
||||
- `internal/pkg/app`: 包含 Gin 的 Response 封装。依赖 `ecode`。
|
||||
|
||||
**2. 原子化切分:**
|
||||
请将开发工作拆解为 3-5 个“原子任务步”。
|
||||
- 每个步骤必须针对**单个物理文件**。
|
||||
- 步骤必须遵循依赖顺序(底层先于上层)。
|
||||
|
||||
**3. 输出格式:**
|
||||
请输出一个 **Markdown Checklist (执行清单)**。
|
||||
格式示例:
|
||||
- [ ] **Step 1: {文件名}** - {核心职责} (依赖: 无)
|
||||
- [ ] **Step 2: {文件名}** - {核心职责} (依赖: Step 1)
|
||||
…
|
||||
|
||||
**模块需求:**
|
||||
我们需要一套统一的 HTTP 错误处理机制,支持自定义业务错误码,统一返回 JSON 格式。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 0.5: API 签名锁定 (API Surface Lock)
|
||||
|
||||
**目的:** 在实现具体逻辑前,强制锁定所有 Public 方法的签名,防止实现阶段出现参数不一致。
|
||||
|
||||
### 🤖 Prompt 0.5: 生成接口定义
|
||||
|
||||
**[发送给 AI]:**
|
||||
|
||||
````markdown
|
||||
在开始写代码前,请先为 `internal/pkg/app` 包定义 **Public API 签名 (Exported Functions)**。
|
||||
请直接提供 `Responder` 接口定义或核心函数的函数头(无需函数体)。
|
||||
|
||||
**要求:**
|
||||
1. **一致性:** 确认 `context` 参数的位置(建议统一作为第一个参数)。
|
||||
2. **完整性:** 必须包含 `New`, `Success`, `Error` 以及我们刚才讨论的 `ErrorCtx` (处理 trace_id)。
|
||||
3. **Go Doc:** 为每个方法写出符合 Go 标准的注释。
|
||||
|
||||
**期望输出示例:**
|
||||
|
||||
```go
|
||||
// Response wraps the gin.Context for unified JSON response.
|
||||
type Response struct { … }
|
||||
|
||||
// New creates a new Response wrapper.
|
||||
func New(c *gin.Context) *Response { … }
|
||||
|
||||
// Success sends a successful response with data.
|
||||
func (r *Response) Success(data any) { … }
|
||||
```
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 契约定义 (Contract Definition)
|
||||
|
||||
**目的:** 确立“对外口径”。
|
||||
|
||||
### 🤖 Prompt 1: 定义 JSON 结构 (复制使用)
|
||||
|
||||
```Markdown
|
||||
你现在是我的 **API 治理专家**。
|
||||
请设计一套统一的 **HTTP 响应结构 (JSON Envelope)**。
|
||||
|
||||
**设计原则:**
|
||||
1. **统一性:** 无论成功还是失败,Body 结构一致。
|
||||
2. **字段要求:** 必须包含 `code` (int), `msg` (string), `data` (any), `trace_id` (string)。
|
||||
|
||||
**任务:**
|
||||
请给出以下 3 种场景的 JSON 响应示例,并解释设计理由:
|
||||
- 场景 A: 成功返回对象。
|
||||
- 场景 B: 成功返回空列表 (明确 `data` 是 `null` 还是 `[]`)。
|
||||
- 场景 C: 业务错误 (如 Code 20001)。
|
||||
|
||||
**[关键补充约束]**
|
||||
1. **安全性优先:** `app.Error(err)` 处理逻辑中,必须区分**用户可见文案**和**底层调试信息**。若 `err` 包含底层堆栈(如 SQL 错误),JSON 中的 `msg` 必须降级显示为 `ecode` 定义的通用文案(如 "Internal Error"),严禁透传底层 Error String。
|
||||
2. **HTTP 状态码:** 本项目强制执行 **"HTTP 200 OK + Business Code"** 策略。除非 Gin 框架层崩溃,否则 HTTP Status 永远为 200。
|
||||
3. **Trace ID:** 假设 `c.GetString("trace_id")` 可以获取 ID,请在 `app.New(c)` 时将其注入 Response 结构体。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 体验验证 (DX Verification)
|
||||
|
||||
**目的:** 模拟业务层调用,防止基础设施“反人类”。
|
||||
|
||||
### 🤖 Prompt 2: 伪代码验证 (复制使用)
|
||||
|
||||
```Markdown
|
||||
JSON 结构已确认。
|
||||
假设我们已经有了 `internal/pkg/ecode` 和 `internal/pkg/app`。
|
||||
|
||||
请写一段 Gin Handler 的 **伪代码 (Pseudo-code)**,展示开发者该如何使用它们。
|
||||
|
||||
**验证重点:**
|
||||
1. **业务错误:** 如何返回 `ecode.New(20001, "…")`?
|
||||
2. **响应封装:** 如何调用 `app.New(c).Success(data)`?
|
||||
3. **代码简洁性:** 避免大量的 `if err != nil` 重复代码。
|
||||
|
||||
请展示最优雅的写法。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 迭代式核心实现 (Iterative Implementation)
|
||||
|
||||
**核心机制:** 这是一个**循环步骤**。请查看 Phase 0 生成的 Checklist,**逐个文件**执行。
|
||||
|
||||
### 🔄 循环动作 A: 生成代码
|
||||
|
||||
**[用户动作]:** 复制 Checklist 中当前未完成的步骤(例如 "Step 1: 生成 ecode/code.go")。
|
||||
|
||||
**[发送 Prompt]:**
|
||||
|
||||
```Markdown
|
||||
我们现在执行 **Step {N}**。
|
||||
|
||||
**任务目标:**
|
||||
{粘贴 Phase 0 Checklist 中的当前步骤描述}
|
||||
|
||||
**上下文约束 (严禁修改):**
|
||||
1. **JSON 契约:** `{粘贴 Phase 1 确认的 JSON}`
|
||||
2. **DX 规范:** `{粘贴 Phase 2 确认的伪代码}`
|
||||
3. **依赖控制:** 如果是 `ecode` 包,严禁引用 `app` 或 `gin`。
|
||||
|
||||
**输出要求:**
|
||||
请仅生成该步骤对应的 `{文件名}` 源代码。不要生成测试代码。
|
||||
|
||||
**通用代码质量约束 (Linter Rules):**
|
||||
1. **注释规范:** 所有 Exported (首字母大写) 的结构体、函数、常量必须包含符合 Go Doc 规范的注释。
|
||||
2. **复杂度控制:** 确保 `gocyclo` (圈复杂度) 低于 10。如果逻辑复杂,请拆分为私有函数。
|
||||
3. **错误检查:** 严禁忽略 error 返回值(如 `json.Marshal`),必须处理或 Log。
|
||||
4. **Lint 检查:** 生成的代码必须能通过 `errcheck` 和 `staticcheck`。
|
||||
```
|
||||
|
||||
### 🔄 循环动作 B: 上下文锚点 (Context Anchoring)
|
||||
|
||||
**[用户动作]:** 代码生成并确认无误后,发送此 Prompt 以建立记忆锚点。
|
||||
|
||||
**[发送 Prompt]:**
|
||||
|
||||
```Markdown
|
||||
已确认 `{文件名}` 代码无误。
|
||||
请将该代码存入你的**短期记忆**,作为后续步骤的上下文依赖。
|
||||
**不要重复输出它**。我们准备进入下一步。
|
||||
```
|
||||
|
||||
_(重复 A -> B,直到所有源码文件生成完毕)_
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 极限防御测试 (Extreme Defensive Testing)
|
||||
|
||||
**目的:** 模拟“最糟糕”的业务代码调用,确保基础设施不崩。
|
||||
|
||||
### 🤖 Prompt 4: 生成红队测试用例
|
||||
|
||||
```markdown
|
||||
所有核心代码已生成。现在请为 `internal/pkg/app/response.go` 编写单元测试 `response_test.go`。
|
||||
|
||||
**请覆盖以下 4 个极端场景 (Test Cases):**
|
||||
|
||||
1. **Raw Error 降级:**
|
||||
- **场景:** 传入 `errors.New("db connection broken")` (非 ecode 类型)。
|
||||
- **断言:** HTTP 状态码为 500 (或 200+Code 50000),Msg 为 "Internal Server Error" (严禁泄漏原始错误信息)。
|
||||
|
||||
2. **Double Response 防护:**
|
||||
- **场景:** 在同一个 Handler 中连续调用 `app.Success()` 两次。
|
||||
- **断言:** 第二次调用应被忽略或记录 Warning 日志,且不应导致 Panic。
|
||||
|
||||
3. **Nil Data 安全:**
|
||||
- **场景:** 调用 `app.Success(nil)`。
|
||||
- **断言:** JSON 中的 `data` 字段应为 `null` (或 `{}`,取决于契约),不应 Panic。
|
||||
|
||||
4. **并发 Map 读写:**
|
||||
- **场景:** 启动 100 个 Goroutine 并发调用 `ecode.GetMsg(code)`。
|
||||
- **断言:** `test -race` 必须通过,无数据竞争。
|
||||
|
||||
请输出完整的 Test 代码。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 最终验收 (SRE Review)
|
||||
|
||||
**目的:** 模拟运维视角审查。
|
||||
|
||||
### 🤖 Prompt 5: 找茬模式 (复制使用)
|
||||
|
||||
```Markdown
|
||||
切换角色为 **SRE (站点可靠性工程师)**。
|
||||
请审查上述所有代码(ecode + app)。
|
||||
|
||||
**风险排查:**
|
||||
1. **Panic 风险:** 是否有未捕获的 Panic 点?
|
||||
2. **监控盲区:** 当前的 Error Log 是否包含了足够的上下文(如 StackTrace)供排查?
|
||||
3. **状态码混淆:** 我们采用了“HTTP 200 + 业务码”模式,请确认这是否会影响网关层的 5xx 告警配置?
|
||||
|
||||
请简要列出 1-2 个优化建议。
|
||||
```
|
||||
|
||||
```bash
|
||||
|
||||
==== 基础设施详细设计规格说明书.md ====
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🏗️ Infrastructure Design Specifications (v1.2)
|
||||
- 🏗️ Infrastructure Design Specifications (v1.1)
|
||||
- 🏗️ Infrastructure Design Specifications (v1.0)
|
||||
date created: 星期三, 十二月 10日 2025, 9:41:53 上午
|
||||
date modified: 星期三, 十二月 10日 2025, 10:37:49 上午
|
||||
---
|
||||
|
||||
# 🏗️ Infrastructure Design Specifications (v1.2)
|
||||
|
||||
项目名称: Enterprise-CMS-Core
|
||||
|
||||
模块: Infrastructure (Error Handling & Response)
|
||||
|
||||
版本: 1.2.0 (Refined)
|
||||
|
||||
状态: [✅ 已锁定]
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计目标 (Design Objectives)
|
||||
|
||||
本模块旨在构建一套**统一的、类型安全的、框架解耦**的 HTTP 响应与错误处理机制。
|
||||
|
||||
- **统一性:** 无论成功与否,API 必须返回结构一致的 JSON Envelope。
|
||||
- **可观测性:** 错误必须携带业务语义(ErrorCode),而非仅返回 HTTP 500。
|
||||
- **解耦性:** 业务逻辑层 (Service) 不感知 HTTP 框架 (Gin),仅通过 Go 原生 `error` 接口交互。
|
||||
- **高内聚:** 错误码定义与错误实体封装在同一包内,减少调用摩擦。
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术选型基线 (Tech Stack Baseline)
|
||||
|
||||
|**组件**|**选型**|**约束说明**|
|
||||
|---|---|---|
|
||||
|**HTTP Context**|`github.com/gin-gonic/gin`|仅在 `internal/pkg/app` (Level 1) 和 `handler` 层使用。**严禁**在 `service` 层引入。|
|
||||
|**Error Handling**|Go Standard Library|使用 Go 1.13+ `errors` (`Is`, `As`, `New`) 和 `fmt.Errorf`。**严禁**引入第三方 error 库 (如 `pkg/errors`)。|
|
||||
|**Serialization**|`encoding/json`|使用标准库。MVP 阶段暂不引入 `json-iterator`。|
|
||||
|**Concurrency**|`sync.RWMutex`|用于保护错误码 Map 的并发读取(读多写少场景)。|
|
||||
| **Tracing** | Gin Keys | Trace ID 必须由前置中间件(或网关)注入。Gin Context Key 约定为 `"X-Trace-ID"`。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心设计模式 (Design Patterns)
|
||||
|
||||
### 3.1 响应封装:Context Object & Factory
|
||||
|
||||
采用 **“上下文对象”** 模式对 `gin.Context` 进行封装,提供链式调用的体验。
|
||||
|
||||
- **模式:** `app.New(c).Success(data)`
|
||||
- **优势:** 屏蔽底层框架差异,统一入口。
|
||||
|
||||
### 3.2 错误处理:安全与动态机制 (Security & Dynamics)
|
||||
|
||||
- **双层信息架构:**
|
||||
- **User Msg (Safe):** JSON Body 中的 `msg` 字段。**仅**允许返回 `ecode` 中定义的静态文案,或经过白名单过滤的动态参数(如参数名)。
|
||||
- **Log Detail (Unsafe):** 服务端日志。必须记录完整的 `err.Error()`(包含堆栈、SQL 错误、`fmt.Errorf` 包装的底层原因)。
|
||||
- **动态文案支持:**
|
||||
- `ecode` 包需提供 `WithMsg(msg string)` 或 `WithDetails(args …any)` 方法,用于**安全地**覆盖默认文案。
|
||||
- **示例:** `return ecode.InvalidParams.WithMsg("Email 格式错误")`。
|
||||
|
||||
### 3.3 状态码管理:Centralized Registry
|
||||
|
||||
采用 **“集中式注册表”** 模式。
|
||||
|
||||
- **约束:** 所有业务错误码 (Business Code) 必须在 `internal/pkg/ecode` 包中定义为 `const`。
|
||||
- **禁止:** 严禁在业务代码中硬编码数字(Magic Number)。
|
||||
|
||||
### 3.4 错误码号段分配:Error Code Allocation
|
||||
|
||||
结构定义:
|
||||
|
||||
错误码采用 5 位数字结构:A BB NN
|
||||
|
||||
- **A (万位):** 模块/领域 (1=Infra, 2=User, 3=Content…)
|
||||
- **BB (千百位):** 组件/子模块分类
|
||||
- **NN (十个位):** 具体错误流水号
|
||||
|
||||
#### 1. 基础设施层 (System / Infra) - `10000 - 19999`
|
||||
|
||||
针对基础设施,**必须**严格遵守以下二级分类,严禁混用:
|
||||
|
||||
|**二级区间 (Sub-Range)**|**组件归属 (Component)**|**典型示例 (Examples)**|
|
||||
|---|---|---|
|
||||
|**10000 - 10099**|**Server General**|`10000` (Success), `10001` (Unknown Error), `10002` (Panic Recovered)|
|
||||
|**10100 - 10199**|**Database (Internal)**|`10100` (DB Connection Lost), `10101` (SQL Syntax Error) - _注意:业务查空属业务码,不在此列_|
|
||||
|**10200 - 10299**|**Cache (Redis)**|`10200` (Redis Timeout), `10201` (Key Evicted Unexpectedly)|
|
||||
|**10300 - 10399**|**Serialization**|`10300` (JSON Marshal Failed), `10301` (Invalid Request Body)|
|
||||
|**10400 - 10499**|**Middleware/Gateway**|`10400` (Too Many Requests/Rate Limit), `10401` (Route Not Found)|
|
||||
|**10500 - 10599**|**3rd Party API**|`10500` (External Service Unavailable), `10501` (SMS Send Failed)|
|
||||
|
||||
#### 2. 业务模块层 (Business Modules) - `20000+`
|
||||
|
||||
业务模块建议参考同等逻辑进行二级划分(由各模块负责人定义,但建议遵循以下范式):
|
||||
|
||||
|**一级区间**|**模块**|**二级区间示例**|
|
||||
|---|---|---|
|
||||
|**20000 - 29999**|**User / Auth**|`200xx` (基础账户), `201xx` (登录/Token), `202xx` (RBAC 权限), `203xx` (KYC 认证)|
|
||||
|**30000 - 39999**|**Content (CMS)**|`300xx` (文章), `301xx` (分类/标签), `302xx` (评论), `303xx` (审核流)|
|
||||
|
||||
---
|
||||
|
||||
## 4. 交互协议与数据流 (Interaction Protocol)
|
||||
|
||||
### 4.1 JSON 响应契约 (The Contract)
|
||||
|
||||
所有 HTTP 接口返回的 Body 必须符合以下结构:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"code": 20001, // 业务状态码 (0=成功, 非0=错误)
|
||||
"msg": "用户已存在", // 开发者提示/用户提示
|
||||
"data": { … }, // 业务数据 (成功时为 Object/Array, 失败时为 null)
|
||||
"trace_id": "abc-123" // 必填。取值优先级: c.GetHeader("X-Trace-ID") -> c.GetString("X-Trace-ID") -> UUID生成
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 HTTP 状态码策略 (Status Code Policy)
|
||||
|
||||
本项目采用 **"Hybrid 策略 "**:
|
||||
|
||||
- **HTTP 200 OK:**
|
||||
- 所有 **业务逻辑错误** (Code `2xxxx` - `4xxxx`)。
|
||||
- 前端通过 Body 中的 `code != 0` 判断业务异常。
|
||||
- _理由:_ 避免网关(如 Nginx)拦截 4xx 响应并替换为默认错误页,导致前端拿不到 JSON 数据。
|
||||
- **HTTP 500 Internal Server Error:**
|
||||
- 所有 **基础设施错误** (Code `1xxxx`),包括 Panic、数据库断连、Redis 超时。
|
||||
- _理由:_ 触发云厂商负载均衡器 (LB) 的熔断机制,将流量切出故障节点。
|
||||
- **HTTP 401/403:**
|
||||
- 仅用于网关层面的拦截(如 JWT 格式错误),业务层鉴权失败建议走 HTTP 200 + Code `20101`。
|
||||
|
||||
### 4.3 跨层交互时序 (Cross-Layer Flow)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Controller (Handler)
|
||||
participant S as Service (Domain)
|
||||
participant I as Infra (pkg/app)
|
||||
participant E as Ecode (pkg/ecode)
|
||||
|
||||
C->>I: app.New(c) 初始化
|
||||
C->>S: Call Business Logic
|
||||
alt 成功
|
||||
S-->>C: return (data, nil)
|
||||
C->>I: app.Success(data)
|
||||
I-->>Client: JSON {code:0, data:…}
|
||||
else 失败 (业务错误)
|
||||
S-->>C: return (nil, ecode.New(20001))
|
||||
C->>I: app.Error(err)
|
||||
I->>I: errors.As(err) -> 提取 Code 20001
|
||||
I-->>Client: JSON {code:20001, msg:"…"}
|
||||
else 失败 (系统错误)
|
||||
S-->>C: return (nil, errors.New("DB error"))
|
||||
C->>I: app.Error(err)
|
||||
I->>I: errors.As(err) -> 失败 (Fallback)
|
||||
I-->>Client: JSON {code:50000, msg:"Internal Error"}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 目录结构与职责 (Directory & Responsibilities)
|
||||
|
||||
```Plaintext
|
||||
internal/
|
||||
├── middleware/ # [New] 全局中间件
|
||||
│ ├── recovery.go # Panic 捕获 -> 转换为 ecode.ServerError (50000)
|
||||
│ └── not_found.go # 404 捕获 -> 转换为 ecode.NotFound (40400)
|
||||
│
|
||||
└── pkg/
|
||||
├── ecode/ # [Level 0] 错误核心包 (无内部依赖)
|
||||
│ ├── code.go # const 常量定义 (UserNotFound = 20001)
|
||||
│ ├── msg.go # 错误码文案映射 (Map & GetMsg)
|
||||
│ └── error.go # Error 结构体定义 (New, Parse 方法)
|
||||
│
|
||||
└── app/ # [Level 1] HTTP 响应封装 (依赖 gin, ecode)
|
||||
└── response.go # NewResponse, Success, Error 方法
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 开发规范与 Linter 规则 (Linting Rules)
|
||||
|
||||
1. **包引用原则:**
|
||||
|
||||
- `ecode` 包必须保持零依赖(只依赖标准库)。
|
||||
- `app` 包依赖 `ecode`。
|
||||
|
||||
2. **Service 层纯净性:**
|
||||
|
||||
- `internal/domain/service` 代码中**严禁出现** `import "github.com/gin-gonic/gin"`。
|
||||
- `internal/domain/service` 代码中**严禁出现** `import "enterprise-cms-core/internal/pkg/app"`。
|
||||
- 只允许引入 `internal/pkg/ecode`。
|
||||
|
||||
3. **错误包装与响应清洗:**
|
||||
|
||||
- **Log:** `app.Error(err)` 内部必须将 `err` 的完整堆栈打印到 Zap 日志中。
|
||||
- **Response:**
|
||||
- 若 `err` 可被断言为 `*ecode.Error`,则取其 `Msg` 字段返回。
|
||||
- 若 `err` 仅为普通 `error` (如 DB error),**严禁**直接将其内容返回给前端,必须统一兜底返回 `ecode.ServerError` 的文案("Internal Server Error")。
|
||||
|
||||
4. **全局兜底机制 (Global Safety Net):**
|
||||
- 项目必须在 `internal/middleware` 中实现 `Recovery` 中间件。
|
||||
- **严禁**让 Gin 默认的 Panic 堆栈直接输出到 HTTP Body。
|
||||
- **必须**捕获所有 Panic,并调用 `app.Error(ecode.ServerError)` 统一输出为符合 JSON 契约的格式 (`{"code": 50000, "msg": "Internal Server Error", …}`)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 工程化实施标准 (Engineering Standards)
|
||||
|
||||
### 7.1 代码风格契约 (Code Style Contract)
|
||||
|
||||
为确保代码长期可维护,生成的代码必须严格遵守以下 Go 惯用语 (Idioms):
|
||||
|
||||
1. **命名规范:**
|
||||
- **缩写:** 使用全大写缩写 (如 `ServeHTTP`, `ID`, `URL`),严禁 `Url`, `Id`。
|
||||
- **局部变量:** 保持短小 (如 `ctx`, `err`, `req`),避免 Java 式的长命名 (如 `requestContext`, `errorObject`)。
|
||||
- **工厂方法:** `ecode` 包内使用 `New()`, `app` 包内使用 `NewResponse()`。
|
||||
|
||||
2. **代码组织:**
|
||||
- **Import 分组:** 标准库 -> 第三方库 -> 内部库 (enterprise-cms-core/…)。
|
||||
- **Guard Clauses:** 优先使用“卫语句”提前返回,减少 `else` 嵌套层级。
|
||||
|
||||
### 7.2 注释与文档 (Documentation)
|
||||
|
||||
为了提升团队协作效率,所有 Exported (首字母大写) 的类型、函数、常量必须包含符合 GoDoc 规范的**中文注释**。
|
||||
|
||||
- **格式规范:** `// FunctionName 中文描述…`
|
||||
- **关键:** 注释**必须**以函数/变量名开头,且与中文描述之间**保留一个空格**。这是 Go 官方工具链解析文档的标准要求。
|
||||
- **内容重心:**
|
||||
- **摘要:** 第一行简明扼要地说明“它是做什么的”。
|
||||
- **详情 (可选):** 解释 **"Why" (设计意图)** 和 **"Caveats" (副作用/注意事项)**,而非翻译代码逻辑。
|
||||
- **示例:**
|
||||
|
||||
```Go
|
||||
// Success 向客户端写入标准的 JSON 成功响应。
|
||||
//
|
||||
// 注意:
|
||||
// 1. 无论业务逻辑如何,此方法会将 HTTP 状态码强制设置为 200。
|
||||
// 2. data 字段若为 nil,将序列化为 JSON 的 null。
|
||||
func (r *Response) Success(data any) { … }
|
||||
|
||||
// UserNotFound 表示用户不存在的业务错误码 (20001)。
|
||||
const UserNotFound = 20001
|
||||
```
|
||||
|
||||
### 7.3 可扩展性设计 (Extensibility Patterns)
|
||||
|
||||
为了应对未来需求变更,本模块需采用以下模式:
|
||||
|
||||
1. **Functional Options (针对 `app` 包):**
|
||||
- 构造 `Response` 对象时,应支持 Option 模式,以便未来无需破坏函数签名即可添加新字段(如 TraceID, DebugInfo)。
|
||||
- *定义:* `type Option func(*Response)`
|
||||
- *签名:* `func New(c *gin.Context, opts …Option) *Response`
|
||||
|
||||
2. **Interface Segregation (接口隔离):**
|
||||
- 虽然 `ecode` 是基础值对象,但 `app` 层若涉及复杂逻辑,应定义 `Responder` 接口,方便 Mock 测试。
|
||||
|
||||
```bash
|
||||
|
||||
==== 错误处理模块文件夹骨架.md ====
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
date created: 星期三, 十二月 10日 2025, 11:00:25 上午
|
||||
date modified: 星期三, 十二月 10日 2025, 11:04:26 上午
|
||||
---
|
||||
|
||||
```plaintext
|
||||
enterprise-cms-core/
|
||||
├── internal/
|
||||
│ ├── pkg/
|
||||
│ │ ├── ecode/ # [Level 0] 基础领域层
|
||||
│ │ │ ├── code.go # [Const] 纯常量定义 (ErrorCode Registry)
|
||||
│ │ │ ├── error.go # [Type] 核心结构体定义 (struct Error)
|
||||
│ │ │ ├── msg.go # [Data] 错误码文案映射 (var msg map[int]string)
|
||||
│ │ │ └── ecode_test.go # [Test] 单元测试
|
||||
│ │ │
|
||||
│ │ └── app/ # [Level 1] 应用工具层
|
||||
│ │ ├── responder.go # [Interface] 👈 修正点: 定义 type Responder interface
|
||||
│ │ ├── response.go # [Impl] 定义 type Response struct (实现逻辑)
|
||||
│ │ ├── options.go # [Pattern] 定义 Functional Options (配置扩展)
|
||||
│ │ └── response_test.go # [Test] 单元测试
|
||||
│ │
|
||||
│ └── middleware/ # [Global]
|
||||
│ ├── recovery.go # Panic 捕获
|
||||
│ └── not_found.go # 404 处理
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- "📋 Phase 0: 基础设施构建执行清单 (Execution Checklist)"
|
||||
date created: 星期三, 十二月 10日 2025, 11:15:25 上午
|
||||
date modified: 星期三, 十二月 10日 2025, 11:41:27 中午
|
||||
---
|
||||
|
||||
# 📋 Phase 0: 基础设施构建执行清单 (Execution Checklist)
|
||||
|
||||
- [ ] **Step 1: `internal/pkg/ecode/code.go`** - **定义错误码注册表**
|
||||
- **核心职责:** 仅定义 `int` 类型的 `const` 常量。包括基础设施类(10000+)和业务类(20000+)错误码。
|
||||
- **依赖:** 无 (Root Node)。
|
||||
- **注意:** 需严格遵循文档中的“五位数字”分段规则。
|
||||
|
||||
- [ ] **Step 2: `internal/pkg/ecode/msg.go`** - **定义错误文案映射**
|
||||
- **核心职责:** 初始化全局 `map[int]string`,提供 `GetMsg(code)` 方法。
|
||||
- **依赖:** Step 1 (`code.go` 中的常量)。
|
||||
- **注意:** 使用 `sync.RWMutex` 保护并发读写(虽然主要是读),文案必须是“用户安全”的。
|
||||
|
||||
- [ ] **Step 3: `internal/pkg/ecode/error.go`** - **实现核心错误实体**
|
||||
- **核心职责:** 定义 `struct Error`,实现 `error` 接口,提供 `New()`, `Error()`, `Code()` 等方法。支持 `WithDetails` 等动态扩展。
|
||||
- **依赖:** Step 1 & Step 2。
|
||||
- **注意:** 这是 Service 层唯一允许引用的错误对象。
|
||||
|
||||
- [ ] **Step 4: `internal/pkg/app/options.go`** - **定义响应配置模式**
|
||||
- **核心职责:** 定义 `type Option func(*Response)` 及常用的 Option 实现(如 `WithTraceID`)。
|
||||
- **依赖:** 无(或仅依赖标准库)。
|
||||
- **注意:** 先于 `response.go` 实现,以便主逻辑直接使用配置项,符合“开闭原则”。
|
||||
|
||||
- [ ] **Step 5: `internal/pkg/app/response.go`** - **实现 HTTP 响应封装**
|
||||
- **核心职责:** 定义 `Response` 结构体,封装 `New`, `Success`, `Error` 方法。处理 JSON 序列化、TraceID 注入、以及将 `error` 接口清洗为 `ecode` 的逻辑。
|
||||
- **依赖:** Step 3 (`ecode`), Step 4 (`options`), `Gin Context`。
|
||||
- **注意:** 需实现“双层信息架构”:日志记录原始错误,HTTP Body 返回安全文案。
|
||||
|
||||
- [ ] **Step 6: internal/pkg/app/responder.go** - **定义 Responder 接口**
|
||||
- **核心职责:** 定义 `Responder` 接口,解耦具体实现,方便未来 Mock 测试。
|
||||
- **依赖:** `app/response.go` (实现关系)。
|
||||
145
Go项目实战/03_基础设施/01_错误处理/Phase 1 统一响应结构定义 (The Contract).md
Normal file
145
Go项目实战/03_基础设施/01_错误处理/Phase 1 统一响应结构定义 (The Contract).md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 📦 统一响应结构定义 (The Contract)
|
||||
date created: 星期三, 十二月 10日 2025, 11:23:15 上午
|
||||
date modified: 星期三, 十二月 10日 2025, 12:12:46 中午
|
||||
---
|
||||
|
||||
# Phase 1 统一响应结构定义 (The Contract)
|
||||
|
||||
## 📦 统一响应结构定义 (The Contract)
|
||||
|
||||
所有 HTTP 接口(无论成功与否)必须严格返回以下 JSON 结构:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"code": 20001, // 业务状态码 (0=成功, 非0=错误)
|
||||
"msg": "用户已存在", // 用户可见的提示文案 (Safe Message)
|
||||
"data": { ... }, // 业务数据 payload (成功时返回,失败时通常为 null)
|
||||
"trace_id": "a1b2-c3d4" // 全链路追踪 ID (必填,用于 SRE 排查)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 场景示例与设计理由
|
||||
|
||||
### 🟢 场景 A: 成功返回对象 (Single Object)
|
||||
|
||||
请求: GET /api/v1/users/1001
|
||||
|
||||
HTTP Status: 200 OK
|
||||
|
||||
```JSON
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "OK",
|
||||
"data": {
|
||||
"user_id": 1001,
|
||||
"nickname": "TechLead_01",
|
||||
"avatar": "https://cdn.example.com/u/1001.jpg"
|
||||
},
|
||||
"trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef"
|
||||
}
|
||||
```
|
||||
|
||||
**📌 设计理由:**
|
||||
|
||||
- **Code 0:** 符合业界惯例(如 Google/Tencent API),`0` 明确表示逻辑执行成功。
|
||||
- **Data 类型:** 返回具体的 Object。
|
||||
|
||||
---
|
||||
|
||||
### 🟡 场景 B: 成功返回空列表 (Empty List)
|
||||
|
||||
请求: GET /api/v1/articles?category=golang (假设该分类下无文章)
|
||||
|
||||
HTTP Status: 200 OK
|
||||
|
||||
```JSON
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "OK",
|
||||
"data": {
|
||||
"list": [],
|
||||
"total": 0
|
||||
},
|
||||
"trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef"
|
||||
}
|
||||
```
|
||||
|
||||
**📌 设计理由:**
|
||||
|
||||
- **Data 不为 `null`:** 对于列表型接口,`data` 内部的 `list` 字段必须返回空数组 `[]`,而不是 `null`。
|
||||
- _原因:_ 前端可以直接调用 `.map()` 或 `.forEach()` 而无需判空,极大降低前端出现 `Cannot read property 'map' of null` 的崩溃风险。
|
||||
- **结构一致性:** 即使是列表,建议包裹在 Object 中(如 `{list: [], total: 0}`),方便未来扩展分页字段。
|
||||
|
||||
---
|
||||
|
||||
### 🔴 场景 C: 业务/系统错误 (Error Handling)
|
||||
|
||||
这里我们需要区分 **“预期内的业务错误”** 和 **“预期外的系统错误”**,但在 JSON 表现上它们必须是一致的。
|
||||
|
||||
Case C-1: 预期内的业务错误
|
||||
|
||||
场景: 用户尝试注册已存在的邮箱。
|
||||
|
||||
Service 层返回: ecode.UserAlreadyExist (Code: 20001)
|
||||
|
||||
```JSON
|
||||
{
|
||||
"code": 20001,
|
||||
"msg": "用户已存在",
|
||||
"data": null,
|
||||
"trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef"
|
||||
}
|
||||
```
|
||||
|
||||
Case C-2: 预期外的系统错误 (触发安全防御)
|
||||
|
||||
场景: 数据库突然断连,GORM 返回 dial tcp 127.0.0.1:5432: connect: connection refused。
|
||||
|
||||
Service 层返回: 原生 error 对象。
|
||||
|
||||
```JSON
|
||||
{
|
||||
"code": 50000,
|
||||
"msg": "Internal Server Error", // <--- 严禁显示 "dial tcp ..."
|
||||
"data": null,
|
||||
"trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef"
|
||||
}
|
||||
```
|
||||
|
||||
**📌 设计理由:**
|
||||
|
||||
- **Security (安全降级):**
|
||||
- `app.Error(err)` 的内部逻辑必须执行 **Type Assertion (类型断言)**。
|
||||
- 若 `err` 是 `*ecode.Error`,则透传其 `msg`(Case C-1)。
|
||||
- 若 `err` 是普通 `error`(Case C-2),视为系统级异常。**必须**将 JSON 中的 `msg` 强制重写为 `"Internal Server Error"` 或通用文案,防止数据库表结构、IP 地址等敏感信息泄露给攻击者。
|
||||
- **HTTP 200:** 即使是 Code 50000,HTTP Status 依然保持 200。这确保了网关层(Nginx/Gateway)不会拦截 Body,前端始终能解析 JSON 拿到 `code` 和 `trace_id` 用于展示和报错。
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ 关键实现逻辑预告
|
||||
|
||||
为了实现上述契约,在接下来的 **Step 5: `internal/pkg/app/response.go`** 中,我们将实现如下核心逻辑:
|
||||
|
||||
1. **Trace ID 注入:** 在 `New(c)` 时执行 `r.traceID = c.GetString("trace_id")`。
|
||||
2. **错误清洗:**
|
||||
|
||||
```Go
|
||||
// 伪代码逻辑预览
|
||||
func (r *Response) Error(err error) {
|
||||
if e, ok := err.(*ecode.Error); ok {
|
||||
// 业务错误:直接使用
|
||||
r.json(e.Code(), e.Msg())
|
||||
} else {
|
||||
// 系统错误:记录原始日志,但在 JSON 中降级
|
||||
log.Error("System Error", zap.Error(err), zap.String("trace_id", r.traceID))
|
||||
r.json(ecode.ServerErr, "Internal Server Error") // 🔒 安全替换
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这套设计已经满足了 **Phase 1** 的所有契约要求。
|
||||
111
Go项目实战/03_基础设施/01_错误处理/SOP 增强补丁:长代码&多文件分步生成策略.md
Normal file
111
Go项目实战/03_基础设施/01_错误处理/SOP 增强补丁:长代码&多文件分步生成策略.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🛠️ SOP 增强补丁:长代码/多文件分步生成策略
|
||||
date created: 星期三, 十二月 10日 2025, 9:21:58 上午
|
||||
date modified: 星期三, 十二月 10日 2025, 9:34:54 上午
|
||||
---
|
||||
|
||||
# 🛠️ SOP 增强补丁:长代码/多文件分步生成策略
|
||||
|
||||
核心原则: 原子化交付 (Atomic Delivery)。
|
||||
|
||||
不要命令 AI “写完这个模块”。要命令 AI “写完这个文件” 或者 “写完这个结构体的具体方法”。
|
||||
|
||||
## 策略一:按物理文件拆分 (File-Level Sharding)
|
||||
|
||||
对于基础设施模块,通常可以自然拆分为多个文件。
|
||||
|
||||
操作动作:
|
||||
|
||||
修改 SOP 的 阶段三,不再一次性要求生成所有文件,而是分轮次请求。
|
||||
|
||||
### 🤖 优化后的 Prompt 序列
|
||||
|
||||
**第一轮:仅生成错误码定义**
|
||||
|
||||
```Markdown
|
||||
我们先处理 `internal/pkg/code` 包。
|
||||
请仅生成 `code.go` 文件。
|
||||
内容包含:
|
||||
1. package 声明。
|
||||
2. const 常量定义(错误码)。
|
||||
3. 暂时不要包含 `GetMsg` 的具体 map 映射逻辑,只定义常量。
|
||||
```
|
||||
|
||||
**第二轮:生成错误码映射**
|
||||
|
||||
```Markdown
|
||||
很好。现在请生成同目录下的 `msg.go` 文件。
|
||||
内容包含:
|
||||
1. `var msgFlags = map[int]string{…}` 映射表。
|
||||
2. `func Text(code int) string` 方法的实现。
|
||||
注意:请确保引用了 `code.go` 中定义的常量。
|
||||
```
|
||||
|
||||
**第三轮:生成响应结构体**
|
||||
|
||||
```Markdown
|
||||
现在进入 `internal/pkg/app` 包。
|
||||
请生成 `response.go`。
|
||||
实现 `Response` 结构体定义、`NewResponse` 工厂函数,以及 `Success` 方法。
|
||||
暂时**不要**实现 `Error` 相关方法,我们下一步单独写。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 策略二:骨架先行,血肉填充 (Skeleton First, Flesh Later)
|
||||
|
||||
如果单文件(如 `service.go`)依然很长(超过 500 行),使用此策略。先生成接口和空方法,再逐个填充逻辑。
|
||||
|
||||
### 🤖 优化后的 Prompt 序列
|
||||
|
||||
**第一轮:生成骨架 (The Skeleton)**
|
||||
|
||||
```Markdown
|
||||
我们要实现 `UserService`。由于逻辑复杂,请先生成**骨架代码**。
|
||||
要求:
|
||||
1. 定义 Struct 和所有 Method 的签名 (Signature)。
|
||||
2. Method 内部留空,或仅写 `panic("implement me")`。
|
||||
3. 包含完整的各种 import 和结构体字段注入。
|
||||
```
|
||||
|
||||
**第二轮:填充核心方法 (The Flesh)**
|
||||
|
||||
```Markdown
|
||||
现在,请给出 `Register` 和 `Login` 这两个方法的完整实现代码。
|
||||
请直接输出这两个函数的完整内容,无需重复输出 Struct 定义。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 策略三:上下文锚点 (Context Anchoring)
|
||||
|
||||
当你完成了第一部分代码(比如 `code.go`),在开始下一部分之前,需要让 AI **“记住但不输出”**,以节省 Token 并防止幻觉。
|
||||
|
||||
### 🤖 关键 Prompt (用于连接两个步骤)
|
||||
|
||||
```Markdown
|
||||
[用户动作]:
|
||||
(将 AI 生成的 code.go 代码复制到本地 IDE,确认无误)
|
||||
|
||||
[发送给 AI 的指令]:
|
||||
已确认 `code.go` 代码无误。
|
||||
现在,请将以下代码作为**“已知上下文”**存入你的短期记忆,**不要重复输出它**,但在后续生成 `msg.go` 时必须严格遵守其中的常量定义。
|
||||
|
||||
[粘贴 code.go 的完整代码]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 你的工作流修正建议
|
||||
|
||||
回到你当前的 **错误处理模块**,我建议你采用 **“策略一:按物理文件拆分”**。
|
||||
|
||||
因为你的 `code` 包和 `app` 包本身就应该物理隔离,避免循环依赖。
|
||||
|
||||
**推荐的执行顺序:**
|
||||
|
||||
1. **Prompt:** 生成 `internal/pkg/code/err_code.go` (仅包含 const)。
|
||||
2. **Prompt:** 生成 `internal/pkg/code/err_msg.go` (包含 map 和 Text 方法)。
|
||||
3. **Prompt:** 生成 `internal/pkg/app/response.go` (包含 Response 结构体和 Success/Error 方法)。
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🧩 SOP 补丁:MECE 任务拆解协议 (The MECE Protocol)
|
||||
date created: 星期三, 十二月 10日 2025, 9:26:43 上午
|
||||
date modified: 星期三, 十二月 10日 2025, 9:30:27 上午
|
||||
---
|
||||
|
||||
# 🧩 SOP 补丁:MECE 任务拆解协议 (The MECE Protocol)
|
||||
|
||||
**适用场景:** 任何代码行数预估 > 200 行,或涉及多个文件交互的大型模块(如:错误处理、RBAC 权限系统、订单状态机)。
|
||||
|
||||
**插入位置:** 在原有 SOP 的 **[阶段一:契约定义]** 之前执行。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 0: 原子化任务拆解 (Atomic Decomposition)
|
||||
|
||||
**目的:** 将大需求拆解为一组符合 **MECE 原则 (相互独立,完全穷尽)** 的微任务。确保每个微任务的上下文长度都在 AI 的“舒适区”内,且具备清晰的依赖顺序。
|
||||
|
||||
### 🤖 拆解者 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
你现在是我的 **Tech Lead (技术负责人)**。
|
||||
我们要实现 `{模块名称}` 模块。为了防止代码生成中断和逻辑混乱,请不要直接开始写代码。
|
||||
|
||||
请先执行 **“MECE 任务拆解”**:
|
||||
|
||||
**1. 依赖分析:**
|
||||
分析该模块涉及哪些物理文件?它们之间的依赖关系是什么?(例如:B 依赖 A,则 A 必须先完成)。
|
||||
|
||||
**2. 原子化切分:**
|
||||
将开发工作拆解为 3-5 个“原子任务步”。
|
||||
- 每个步骤必须针对**单个物理文件**或**一组紧密相关的函数**。
|
||||
- 每个步骤必须是独立的,可执行的。
|
||||
|
||||
**3. 输出格式:**
|
||||
请输出一个 **Markdown Checklist (执行清单)**。
|
||||
格式示例:
|
||||
- [ ] **Step 1: {文件名}** - {核心职责} (依赖: 无)
|
||||
- [ ] **Step 2: {文件名}** - {核心职责} (依赖: Step 1)
|
||||
…
|
||||
|
||||
**模块上下文:**
|
||||
{此处粘贴你的需求或 PRD 片段}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 你的工作流变更 (Workflow Update)
|
||||
|
||||
引入此补丁后,你的新工作流变成了:
|
||||
|
||||
1. **Phase 0 (New):** 发送拆解 Prompt -> **获得清单**。
|
||||
2. **Phase 1 (User Action):** 选中清单中的 **Step 1** -> 发送 Prompt:“请执行 Step 1,生成 `code.go`…”。
|
||||
3. **Phase 2 (User Action):** 拿到代码 -> 存入本地 -> **锚点确认** ("Step 1 已完成,代码如下…")。
|
||||
4. **Phase 3 (User Action):** 选中清单中的 **Step 2** -> 发送 Prompt:“基于 Step 1,请执行 Step 2…”。
|
||||
|
||||
### 为什么这样做有效?
|
||||
|
||||
1. **Token 节省:** AI 在生成 Step 2 时,不需要你在 Prompt 里重新描述 Step 1 的需求,只需要把 Step 1 已经生成的代码贴给它作为 Context 即可。
|
||||
2. **避免幻觉:** 因为每个 Step 只有一个目标,AI 不会“顾头不顾尾”。
|
||||
3. **断点续传:** 如果 Step 2 生成错了,你只需要重新生成 Step 2,而不需要推倒重来。
|
||||
113
Go项目实战/03_基础设施/02_日志/01_设计目标.md
Normal file
113
Go项目实战/03_基础设施/02_日志/01_设计目标.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 核心设计目标 (Core Design Goals)
|
||||
date created: 星期三, 十二月 10日 2025, 10:27:39 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 10:28:15 晚上
|
||||
---
|
||||
|
||||
# 1. 核心设计目标 (Core Design Goals)
|
||||
|
||||
## 目标一:全链路上下文关联 (Contextual Traceability)
|
||||
|
||||
这是最核心的差异点。传统的 `log.Println("Database error")` 在并发环境下毫无价值,因为你不知道这条错误属于哪个请求。
|
||||
|
||||
- **设计要求**:
|
||||
- **自动注入 TraceID**: 必须能够从 `context.Context` 中提取 `TraceID`(目前 `internal/pkg/app` 已经生成了 TraceID),并自动将其附加到每一条日志中。
|
||||
- **请求元数据绑定**: 除了 TraceID,还应支持自动绑定 `UserID`、`IP`、`Method`、`Path` 等元数据,形成请求的完整快照。
|
||||
- **跨组件穿透**: 日志对象必须能够在 Layer 之间传递(例如 Controller -> Service -> Repository),且保持上下文不丢失。
|
||||
|
||||
## 目标二:严格的结构化契约 (Strict Structured Schema)
|
||||
|
||||
日志是写给机器看的,不是写给通过 SSH 连上服务器的人看的。
|
||||
|
||||
- **设计要求**:
|
||||
- **JSON First**: 生产环境强制使用 JSON 格式。
|
||||
- **Schema 统一**: 字段命名必须统一。例如,不要混用 `uid`, `user_id`, `userId`,必须在设计阶段锁定为 snake_case (如 `user_id`)。
|
||||
- **类型安全**: 时间戳必须统一格式(推荐 ISO8601 或 Unix Nano),数字字段不能变成字符串(便于聚合计算)。
|
||||
|
||||
## 目标三:高性能与零侵入 (High Performance & Zero Allocation)
|
||||
|
||||
日志通常是系统中 IO 最密集的组件之一。
|
||||
|
||||
- **设计要求**:
|
||||
- **低 GC 压力**: 利用 Zap 的核心优势,避免大量的 `interface{}` 反射和字符串拼接,使用强类型的 Field(如 `zap.Int`, `zap.String`)。
|
||||
- **异步 IO (可选)**: 考虑是否引入 Buffer 机制(牺牲极端崩溃下的日志完整性换取吞吐量)。
|
||||
- **Level 级联过滤**: 在 Debug 级别关闭时,Debug 级别的日志构造逻辑(如复杂的对象序列化)不应被执行。
|
||||
|
||||
## 目标四:安全与合规 (Security & Compliance)
|
||||
|
||||
这往往是被忽视的一点,也是导致安全事故的频发区。
|
||||
|
||||
- **设计要求**:
|
||||
- **敏感数据脱敏**: 必须具备“黑名单”机制。任何包含 `password`, `token`, `mobile`, `credit_card` 的字段在输出前必须被自动掩盖(Masking)。
|
||||
- **安全截断**: 防止打印过大的 Body(如 Base64 图片上传)导致磁盘爆满或日志系统瘫痪,限制单条日志最大长度。
|
||||
|
||||
---
|
||||
|
||||
# 2. 场景化行为对比 (Dev Vs Prod)
|
||||
|
||||
为了兼顾开发体验和生产运维标准,我们需要在设计中明确区分两种环境的行为。
|
||||
|
||||
|**维度**|**开发环境 (Development)**|**生产环境 (Production)**|**设计意图**|
|
||||
|---|---|---|---|
|
||||
|**编码格式**|Console (彩色,人类易读)|JSON (机器易读)|开发追求直观;生产追求 ELK 解析效率。|
|
||||
|**输出目标**|Stdout (控制台)|File + Stdout (双写)|开发侧容器即焚;生产侧需持久化 + 容器采集。|
|
||||
|**日志级别**|Debug|Info / Warn|生产环境过滤掉大量 Debug 噪音,节省存储成本。|
|
||||
|**堆栈追踪**|Error 级别即打印|Panic 或 Fatal 才打印|减少生产环境日志体积,除非发生严重故障。|
|
||||
|**调用行号**|显示 (Caller)|显示 (Caller)|快速定位代码位置。|
|
||||
|
||||
---
|
||||
|
||||
# 3. 架构定位与边界 (Architecture Boundary)
|
||||
|
||||
我们需要明确日志模块在架构中的位置:
|
||||
|
||||
- **位置**: 属于 `Infrastructure Layer` (Level 0/1)。
|
||||
- **依赖关系**:
|
||||
- **被谁依赖**: 所有层(Handler, Service, Repository)都依赖 Log。
|
||||
- **依赖谁**: 仅依赖标准库和第三方 Log Driver (Zap),**不应依赖业务逻辑**。
|
||||
- **与其他模块的关系**:
|
||||
- **vs `ecode`**: `ecode` 定义错误的**类型**(Code),Log 记录错误的**现场**(Stack/Trace)。
|
||||
- **vs `app.Response`**: Response 负责**对用户说话**(经过清洗的、友好的信息),Log 负责**对开发者说话**(原始的、包含脏数据的真相)。
|
||||
|
||||
---
|
||||
|
||||
# 4. 深度反思与自我反驳 (Critical Thinking & Risk Analysis)
|
||||
|
||||
在敲定设计目标前,必须审视潜在的矛盾和风险:
|
||||
|
||||
**反驳点 1:全链路上下文(TraceID)的传递成本**
|
||||
|
||||
- **挑战**: 要想让 Repository 层的日志也打出 TraceID,必须修改所有方法的签名为 `func (ctx context.Context, …)`。这对现有代码(如果是非 Context 风格)是巨大的重构。
|
||||
- **回应**: 我们的 `Repository` 接口目前设计中已经包含了 `context.Context`。这是一个必须遵守的“硬约束”。如果缺少 Context,日志将断层。
|
||||
- **结论**: 必须在规范中强调:**所有层级的方法首个参数必须是 Context**。
|
||||
|
||||
**反驳点 2:脱敏机制的性能损耗**
|
||||
|
||||
- **挑战**: 如果每一条日志都要遍历字段去匹配“黑名单”进行正则替换,CPU 开销极大。
|
||||
- **回应**: 不能使用正则扫描全文。
|
||||
- **修正方案**: 利用 Zap 的 `Hook` 或 `Core` 包装,仅针对特定 Key(如 `password`)进行值替换,或者要求开发者在打印敏感结构体时显式调用 `.Reduct()` 方法,而非隐式全局扫描。隐式扫描在 Golang 中通常是性能杀手。
|
||||
|
||||
**反驳点 3:异步写入的丢数据风险**
|
||||
|
||||
- **挑战**: 为了性能使用 `Buffered Write`,如果进程被 `kill -9` 或 Panic 崩溃,缓冲区日志会丢失,而崩溃前的日志往往最重要。
|
||||
- **回应**: 对于交易类系统,**可靠性 > 吞吐量**。
|
||||
- **结论**: 默认采用 **同步写入 (Sync Write)**。Zap 本身性能已经足够强(纳秒级),除非达到数万 QPS,否则不需要引入 Buffer。对于 Panic,必须使用 `defer logger.Sync()` 确保刷盘。
|
||||
|
||||
---
|
||||
|
||||
# 5. 待确认问题 (Open Questions)
|
||||
|
||||
在进入下一步(编写详细规格说明书)之前,我需要确认你对以下几点的偏好:
|
||||
|
||||
1. **全局单例 vs 纯依赖注入**:
|
||||
|
||||
- _选项 A_: 提供 `log.Info()` 全局静态方法(方便,但有副作用)。
|
||||
- _选项 B_: 强制必须通过 `l.Info()` 实例方法调用(架构更洁癖,但调用繁琐)。
|
||||
- _推荐_: **选项 A + B**。提供全局方法作为快捷方式(底层代理到单例),同时支持 DI 注入。你倾向于哪种?
|
||||
|
||||
2. **日志轮转 (Rotation) 策略**:
|
||||
|
||||
- 你是倾向于按 **大小** 切割(如 100MB 一个文件),还是按 **时间** 切割(每天一个文件)?
|
||||
- _通常建议_: 按大小切割(防止单文件过大无法打开),配合最大文件保留数。
|
||||
99
Go项目实战/03_基础设施/02_日志/02_技术栈基线.md
Normal file
99
Go项目实战/03_基础设施/02_日志/02_技术栈基线.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 核心引擎 (The Engine):Uber Zap
|
||||
date created: 星期三, 十二月 10日 2025, 10:28:15 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 10:29:20 晚上
|
||||
---
|
||||
|
||||
# 1. 核心引擎 (The Engine):Uber Zap
|
||||
|
||||
行业共识 (Consensus):
|
||||
|
||||
在 Go 语言的高性能后端领域,go.uber.org/zap 是目前无可争议的事实标准(De Facto Standard)。
|
||||
|
||||
我的推荐:
|
||||
|
||||
坚定地使用 Zap,不要犹豫。
|
||||
|
||||
**老兵的经验谈 (Why & How):**
|
||||
|
||||
- **为何不是 Logrus?** Logrus 胜在 API 极其友好(兼容标准库),但它底层大量使用反射(Reflection)和锁,在高并发场景下是严重的性能瓶颈(GC 压力大)。
|
||||
- **为何不是 Slog (Go 1.21+)?** Slog 是 Go 官方推出的结构化日志接口。虽然它是未来,但目前的生态和性能优化(尤其是在 JSON 序列化的极致性能上)尚未完全超越 Zap。且 Zap 可以很方便地作为 Slog 的 Backend。但在本项目中,为了追求极致性能和成熟度,直接使用 Zap 原生 API 是最高效的。
|
||||
- **关键决策点**:
|
||||
- **Field 强类型**: 我们必须强制团队使用 `zap.String("key", "val")` 而非 `zap.Any("key", val)`。`Any` 会导致反射,破坏 Zap 的零内存分配(Zero Allocation)优势。这是代码审查(Code Review)的红线。
|
||||
- **Logger vs SugaredLogger**:
|
||||
- **核心业务链路 (Hot Path)**: 使用 `zap.Logger`(极致性能,但语法繁琐)。
|
||||
- **初始化/非热点代码**: 使用 `zap.SugaredLogger`(语法类似 `printf`,性能稍弱但开发快)。
|
||||
- **基线**: 我们的封装层默认暴露 `Logger` 能力,保留高性能入口。
|
||||
|
||||
# 2. 轮转插件 (Rotation): Lumberjack V2
|
||||
|
||||
行业共识 (Consensus):
|
||||
|
||||
日志切割看似简单,实则坑多(并发写冲突、文件重命名原子性、不同操作系统的文件锁差异)。
|
||||
|
||||
我的推荐:
|
||||
|
||||
使用 gopkg.in/natefinch/lumberjack.v2。
|
||||
|
||||
**老兵的经验谈:**
|
||||
|
||||
- **不要造轮子**: 我见过无数团队尝试自己写 `file.Write` 然后计数切割,最后都在“多进程并发写同一个日志文件”或者“日志压缩时导致 IO 飙升”这些问题上翻车。
|
||||
- **配置陷阱**:
|
||||
- `MaxSize`: 建议 **100MB**。太小导致文件碎片化,太大导致像 grep/vim 这种工具打开困难。
|
||||
- `MaxBackups`: 建议保留 **30-50 个**。
|
||||
- `MaxAge`: 建议 **7-14 天**。
|
||||
- **Compress**: 建议 **开启 (True)**。历史日志压缩存储(gzip)能节省 90% 以上的磁盘空间,这对于云盘成本控制非常重要。
|
||||
|
||||
# 3. 上下文管理 (Context Awareness): 自研封装层
|
||||
|
||||
这是我们作为“架构师”必须介入的地方。原生 Zap 不懂业务上下文,我们需要一个胶水层。
|
||||
|
||||
技术难点:
|
||||
|
||||
如何优雅地把 TraceID 塞进每一行日志?
|
||||
|
||||
设计路线:
|
||||
|
||||
我们需要定义一个轻量级的 Wrapper 或者 Helper 函数。
|
||||
|
||||
- **不要**:重写 `zap.Logger` 结构体的所有方法(那样维护成本太高)。
|
||||
- **要**:提供一个入口函数,例如 `log.WithContext(ctx)`。
|
||||
- **原理**:这个函数会从 `ctx` 取出 `TraceID`,然后调用 `zap.With(zap.String("trace_id", id))`,返回一个携带了该字段的子 Logger 实例。这是一次极低成本的指针操作。
|
||||
|
||||
# 4. 抽象策略与混合模式 (Hybrid Pattern)
|
||||
|
||||
结合你选择的 **Option A+B**,我们的技术实现路径如下:
|
||||
|
||||
1. **全局变量 (The Global)**:
|
||||
|
||||
- 在 `internal/pkg/log` 包内部维护一个私有的 `var globalLogger *zap.Logger`。
|
||||
- 利用 `sync.Once` 确保其并发安全的初始化。
|
||||
- **兜底策略**: 在 `init()` 函数中先给它一个默认的 `Console Logger`。这样即使开发者忘记调用 `InitLogger`,程序启动时的日志也不会 panic,只会打印到控制台。
|
||||
|
||||
2. **依赖注入 (The DI)**:
|
||||
|
||||
- 在 `internal/pkg/log` 暴露一个 `Provider` 函数,供 Wire 使用。
|
||||
- 这个 Provider 返回的必须是**同一个**底层 Zap 实例的引用(或者其派生),确保配置(如 Level、Output Path)是一致的。
|
||||
|
||||
---
|
||||
|
||||
# 总结:最终的技术栈清单
|
||||
|
||||
|**组件**|**选型**|**理由**|
|
||||
|---|---|---|
|
||||
|**Logger Core**|`go.uber.org/zap` (v1.27+)|高性能、类型安全、零内存分配。|
|
||||
|**Rotation**|`gopkg.in/natefinch/lumberjack.v2`|成熟稳定,处理并发写文件不仅是事实标准,更是避坑指南。|
|
||||
|**Config**|`spf13/viper` (已引入)|利用现有的 Viper 读取 yaml 配置,实现动态等级调整。|
|
||||
|**Trace Inject**|`Custom Wrapper` (Standard Lib)|基于 `context` 的轻量封装,连接 `gin.Context` 与 `zap.Fields`。|
|
||||
|
||||
老师的最后叮嘱 (The Moral of the story):
|
||||
|
||||
我们现在的设计,本质上是在 Zap 的高性能 和 业务开发的便利性 之间走钢丝。
|
||||
|
||||
最大的风险在于:封装层写得太重。
|
||||
|
||||
如果我们在 log.Info 里面加了太多的锁、反射或者字符串拼接,那么引入 Zap 的意义就没了。所以,接下来的详细设计文档中,我们要时刻警惕“过度封装”。
|
||||
|
||||
如果这个技术栈基线你没有异议,我们就以此为基础,开始生成《全局日志模块详细设计规格说明书》。
|
||||
80
Go项目实战/03_基础设施/02_日志/03_核心设计模式.md
Normal file
80
Go项目实战/03_基础设施/02_日志/03_核心设计模式.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 核心模式:装饰器模式的变体 (Context-Decorator Pattern)
|
||||
date created: 星期三, 十二月 10日 2025, 10:37:54 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 10:38:26 晚上
|
||||
---
|
||||
|
||||
# 1. 核心模式:装饰器模式的变体 (Context-Decorator Pattern)
|
||||
|
||||
这是我们处理 `TraceID` 和上下文的核心手段。
|
||||
|
||||
- 传统误区 (Over-Abstraction):
|
||||
|
||||
定义一个庞大的 MyLogger 结构体,把 zap.Logger 藏在里面,然后重写 Info, Error 等所有方法。
|
||||
|
||||
- _后果_:维护成本极高,每次 Zap 更新或增加新特性(如 `Panic` 或 `DPanic`),你都得跟着改代码。且容易在转发参数时产生逃逸分析(Escape Analysis)导致的内存分配。
|
||||
- 我们的决策 (The Thin Wrapper):
|
||||
|
||||
只封装“获取 Logger”的动作,不封装“Logger 本身”。
|
||||
|
||||
我们将定义一个函数 log.WithContext(ctx context.Context) *zap.Logger。
|
||||
|
||||
- _行为_:这个函数极其轻量。它从 `ctx` 中取出 `TraceID`,调用 `zap.With()` 生成一个新的 Zap 实例并返回。
|
||||
- _优势_:业务代码拿到的依然是原生的 `*zap.Logger`。这意味着开发者可以直接使用 Zap 强大的 `zap.String`, `zap.Int` 等强类型字段构建方法,享受极致性能,没有任何中间层损耗。
|
||||
|
||||
# 2. 接口策略:拒绝通用接口 (Concrete Type Dependency)
|
||||
|
||||
这是 Go 语言工程实践中关于日志的一个特殊共识,也是反直觉的地方。
|
||||
|
||||
- 传统误区 (The Java/Interface Way):
|
||||
|
||||
定义一个 type ILogger interface { Info(msg string, args …interface{}) }。
|
||||
|
||||
- _后果_:`args …interface{}` 会导致大量的反射(Reflection)和装箱(Boxing),这直接抹杀了 Zap 存在的意义。Zap 的核心设计哲学就是通过 `zap.Field` 避免使用 `interface{}`。
|
||||
- 我们的决策 (Concrete Type):
|
||||
|
||||
直接依赖 *zap.Logger 具体类型。
|
||||
|
||||
- _原则_:在 Handler、Service、Repository 层,注入的类型就是 `*zap.Logger`。
|
||||
- _测试怎么办_:不要 Mock 日志接口。在单元测试中,直接传入 `zap.NewNop()`(什么都不做)或者 `zap.NewExample()`(输出到测试控制台)。这比 Mock 一个接口要简单且真实得多。
|
||||
|
||||
# 3. 访问模式:混合单例与依赖注入 (The Hybrid Accessor)
|
||||
|
||||
结合之前讨论的 Option A+B,我们通过设计模式来解决“初始化顺序”和“热加载”的问题。
|
||||
|
||||
- 设计挑战:
|
||||
|
||||
如果 main.go 还没来得及读配置初始化 Logger,其他 init() 函数里就调用了日志,程序会 Panic。
|
||||
|
||||
- **我们的决策 (Thread-Safe Proxy)**:
|
||||
- **原子替换 (Atomic Swap)**:全局变量 `globalLogger` 不会直接暴露给外部修改。我们将使用 `unsafe.Pointer` 或 `atomic.Value` (配合 Zap 的 `ReplaceGlobals`) 来保证在运行时重新加载配置(如动态修改 Log Level)时,不会发生并发读写冲突。
|
||||
- **懒汉式兜底 (Lazy Fallback)**:在 `internal/pkg/log` 的 `init()` 中,我们会默认初始化一个 `Console Logger`。这样即使 `main` 函数一行代码都没跑,只要引用了包,日志功能就是可用的(虽然配置是默认的)。这极大提升了开发体验(DX)。
|
||||
|
||||
# 4. 字段构建模式:结构化优先 (Field-First API)
|
||||
|
||||
这关乎团队的编码规范,属于 API 设计模式。
|
||||
|
||||
- 传统误区 (Printf Style):
|
||||
|
||||
使用 SugaredLogger 的 Infof("User %s login failed, error: %v", user, err)。
|
||||
|
||||
- _后果_:日志分析系统(ELK)只能拿到一串文本,无法对 `user` 进行聚合统计。
|
||||
- 我们的决策 (Structured Style):
|
||||
|
||||
默认只暴露 Logger(强类型),在必要时才暴露 SugaredLogger。
|
||||
|
||||
- _强制规范_:代码中必须写成 `log.Info("user login failed", zap.String("user", user), zap.Error(err))`。
|
||||
- _设计意图_:通过 API 的设计,“强迫”开发者思考每一个字段的语义。这虽然写起来繁琐一点,但对于后期的运维和排查是无价的。
|
||||
|
||||
---
|
||||
|
||||
# 总结:设计规格书的基调
|
||||
|
||||
基于以上讨论,在接下来的规格说明书中,我们将确立以下基调:
|
||||
|
||||
1. **不造轮子**:核心逻辑全权委托给 `zap` 和 `lumberjack`。
|
||||
2. **薄封装**:`pkg/log` 代码行数应控制在 200 行以内,只做配置解析和 Context 桥接。
|
||||
3. **强类型**:严禁在核心路径使用 `interface{}`。
|
||||
4. **显式传递**:通过 `WithContext` 显式传递上下文,而不是依赖某些黑魔法(如 Goroutine Local Storage)。
|
||||
123
Go项目实战/03_基础设施/02_日志/04_架构逻辑.md
Normal file
123
Go项目实战/03_基础设施/02_日志/04_架构逻辑.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 代码组织方式 (Code Organization)
|
||||
date created: 星期三, 十二月 10日 2025, 10:42:21 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:38:44 晚上
|
||||
---
|
||||
|
||||
# 1. 代码组织方式 (Code Organization)
|
||||
|
||||
我们将遵循 **“高内聚、低耦合”** 的原则,将日志模块放置在 `internal/pkg/log` 下。这里是所有日志逻辑的物理家园。
|
||||
|
||||
建议的文件结构如下(逻辑分层):
|
||||
|
||||
- **`log.go` (Facade/Entry Point)**:
|
||||
- 这是对外暴露的统一入口。包含全局单例的定义、初始化函数 (`Init`)、以及最常用的静态方法代理(如 `Info`, `Error`, `WithContext`)。
|
||||
- **设计意图**: 让其他模块只 import 这一个包就能完成 90% 的工作。
|
||||
- **`options.go` (Configuration)**:
|
||||
- 定义配置结构体(Level, Filename, MaxSize, MaxAge 等)。
|
||||
- **设计意图**: 将配置解析逻辑与日志初始化逻辑分离,方便单元测试。
|
||||
- **`zap.go` (Core Implementation)**:
|
||||
- 负责 `zap.Logger` 的具体构建。包含 Encoder 配置(JSON vs Console)、Writer 配置(Lumberjack 集成)和 Level 动态调整逻辑。
|
||||
- 这是“脏活累活”集中的地方,屏蔽 Zap 的复杂构建细节。
|
||||
- **`context.go` (The Bridge)**:
|
||||
- **核心组件**。实现 `TraceID` 的提取逻辑。
|
||||
- 定义如何从 `context.Context` 中挖掘元数据,并将其转化为 `zap.Field`。
|
||||
|
||||
---
|
||||
|
||||
# 2. 调用方式与依赖注入 (Invocation & DI)
|
||||
|
||||
这里有一个经典的架构冲突:**Singleton(单例) vs Dependency Injection(依赖注入)**。我们的策略是 **“依赖注入为主,单例为辅”**,但在具体使用上有一个极其重要的**反直觉设计**。
|
||||
|
||||
## A. 为什么 Service 层不应保存 Request Logger?
|
||||
|
||||
你可能会想在 Service 初始化时注入一个带 Context 的 Logger。
|
||||
|
||||
- **错误做法**: `type UserService struct { logger *zap.Logger }`,然后在请求进来时试图把 request-scoped 的 logger 塞进去。
|
||||
- **架构事实**: 在 Wire 依赖注入中,`Service`、`Repository` 通常是 **单例 (Singleton)** 的(即整个应用生命周期只有一个实例)。
|
||||
- **结论**: 你**不能**把属于某一次 HTTP 请求的 `TraceID` 注入到单例的 Struct 成员变量中。
|
||||
|
||||
## B. 正确的调用范式 (The Best Practice)
|
||||
|
||||
Logger 作为**工具能力**被注入,Context 作为**请求参数**被传递。
|
||||
|
||||
1. **依赖注入 (Setup Phase)**:
|
||||
|
||||
- 在 `NewUserUsecase` 时,注入基础的 `*zap.Logger`(不带 TraceID)。
|
||||
- 这个 Logger 配置好了输出路径、Level 等全局属性。
|
||||
|
||||
2. **方法调用 (Runtime Phase)**:
|
||||
|
||||
- 在具体的方法(如 `Register`)中,使用 `log.WithContext(ctx)` 来“临时”生成一个带有 TraceID 的 Logger 实例。
|
||||
|
||||
**示例逻辑流**:
|
||||
|
||||
- **Struct 定义**: `struct { baseLogger *zap.Logger }`
|
||||
- **方法内部**: `l := log.WithContext(ctx, u.baseLogger)` -> `l.Info("user registered")`
|
||||
- **说明**: 这里的 `WithContext` 是一个纯内存操作(浅拷贝),开销极小,可以放心高频调用。
|
||||
|
||||
## C. 高性能场景:作用域复用 (Scoped Logger)
|
||||
|
||||
虽然 `log.WithContext` 是浅拷贝,但在循环或长链路中频繁调用仍会产生大量临时对象,增加 GC 压力。
|
||||
|
||||
- **反模式 (Anti-Pattern)**: 在 `for` 循环内部调用 `log.WithContext(ctx)`。
|
||||
- **最佳实践 (Best Practice)**: **作用域提升**。在函数或循环入口处调用一次 `WithContext`,生成局部变量 `l` (Logger),随后全程复用该变量。
|
||||
|
||||
---
|
||||
|
||||
# 3. 数据流与 TraceID 传递 (Data Flow)
|
||||
|
||||
这是实现“全链路可观测性”的生命线。数据流必须打通以下四个关卡:
|
||||
|
||||
## 关卡 1:入口 (Entry - Middleware)
|
||||
|
||||
- **位置**: `internal/middleware/trace.go` (需新建) 或集成在 `response` 包中。
|
||||
- **行为**: 当 HTTP 请求到达,生成一个 UUID。
|
||||
- **动作**: 使用 `c.Set("X-Trace-ID", uuid)` 将其放入 Gin 的上下文存储中。同时,将其放入 HTTP Response **动作**:
|
||||
1. 调用 `pkg/log.WithTraceID(ctx, uuid)` 将 `UUID` 注入标准 `Context`。
|
||||
2. 执行 `c.Request = c.Request.WithContext(newCtx)` 将其回写。
|
||||
3. (可选) 同时放入 Gin 上下文存储和 Response Header 供前端使用。
|
||||
|
||||
## 关卡 2:桥接 (Bridge - Context Adapter)
|
||||
|
||||
- **位置**: `internal/pkg/log/context.go`
|
||||
- **设计原则**: `pkg/log` **不依赖** `gin`,只识别标准库 `context.Context`。
|
||||
- **行为**: `log.WithContext(ctx) 调用内部帮助函数 GetTraceID(ctx) 获取 TraceID。`
|
||||
- **前置条件**: 必须依赖上游(Middleware)将 TraceID 提前注入到标准 Context 中。
|
||||
- **输出**: 返回一个预置了 `zap.String("trace_id", id)` 字段的 Logger。
|
||||
|
||||
## 关卡 3:穿透 (Propagation - Service/Repo)
|
||||
|
||||
- **行为**: 所有的业务方法签名必须包含 `ctx context.Context` 作为第一个参数。
|
||||
- **动作**: 严禁在层级调用中丢弃 Context(例如使用 `context.Background()` 替代传入的 ctx),这会导致链路断裂。
|
||||
|
||||
## 关卡 4:异步与后台边界 (Async & Background Boundary)
|
||||
|
||||
- **高危场景**: 在 Handler 中启动 Goroutine 处理耗时任务。
|
||||
- **陷阱**: `gin.Context` 是非线程安全的。如果 Goroutine 执行时 HTTP 请求已结束,Gin 会重置该 Context,导致数据竞争或脏读。
|
||||
- **解决方案**: 必须在主协程中执行 `ctx.Copy()`,将副本传递给 Goroutine。日志模块必须支持处理这种副本 Context。
|
||||
- **新增场景:后台任务 (Background Tasks)**
|
||||
- **场景**: 定时任务 (Cron)、消息队列消费者 (MQ Consumer)、系统初始化。
|
||||
- **问题**: 初始 `context.Background()` 不包含 TraceID。
|
||||
- **动作**: 必须调用 `log.StartBackgroundTrace(ctx)` 进行“播种”。该函数会检测 Context,若无 TraceID 则生成新 ID 并注入,确保链路可追踪。
|
||||
|
||||
---
|
||||
|
||||
# 4. 关键架构思考:防腐层 (Anti-Corruption Layer)
|
||||
|
||||
我们在设计时还需考虑一层“防腐”。
|
||||
|
||||
- **问题**: 如果未来我们想给所有的日志加一个字段,比如 `env=prod`,或者想把所有的 `trace_id` 改名为 `traceId`。
|
||||
- **对策**: 所有的业务代码**严禁**直接手动构建 `zap.String("trace_id", …)`。
|
||||
- **约束**: 这个字段的 Key 必须定义在 `pkg/log` 的常量中,且只能由 `WithContext` 内部逻辑自动附加。业务开发者只负责传 Context,不负责管 ID 怎么拼写。
|
||||
|
||||
---
|
||||
|
||||
# 总结
|
||||
|
||||
- **代码位置**: `internal/pkg/log`,包含 `log.go` (入口), `zap.go` (实现), `context.go` (桥接)。
|
||||
- **调用方式**: 注入 Base Logger -> 方法内 `WithContext(ctx)` -> 打印。
|
||||
- **数据流**: Middleware 生成 -> Gin Context 携带 -> Log Adapter 提取 -> Zap Field 输出。
|
||||
- **并发安全**: 警惕 Gin Context 在 Goroutine 中的误用,强调 `Copy()` 机制。
|
||||
76
Go项目实战/03_基础设施/02_日志/05_目录结构与职责.md
Normal file
76
Go项目实战/03_基础设施/02_日志/05_目录结构与职责.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 目录结构与职责
|
||||
date created: 星期三, 十二月 10日 2025, 10:45:40 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:40:48 晚上
|
||||
---
|
||||
|
||||
# 目录结构与职责
|
||||
|
||||
## 1. 目录结构设计 (Directory Structure)
|
||||
|
||||
该结构旨在实现 **“配置分离”**、**“核心隐藏”** 与 **“上下文桥接”**。
|
||||
|
||||
```Plaintext
|
||||
internal/
|
||||
├── middleware/ # [Global] 全局中间件层
|
||||
│ ├── access_log.go # [New] HTTP 请求访问日志 (请求入/出记录, 耗时统计)
|
||||
│ └── trace.go # [New] 链路追踪 (生成/透传 TraceID -> 注入 Context)
|
||||
│
|
||||
└── pkg/
|
||||
└── log/ # [Level 0] 全局日志核心包 (基于 Zap)
|
||||
├── log.go # [Facade] 对外入口 (Init, Global L(), Static Proxies)
|
||||
├── options.go # [Config] 配置定义 (Level, FilePath, MaxSize)
|
||||
├── zap.go # [Core] Zap 实例构建 (Encoder, Core, AtomicLevel)
|
||||
├── writer.go # [IO] 输出源管理 (Lumberjack 轮转, Console/File 双写)
|
||||
├── context.go # [Bridge] 上下文桥接 (WithContext, TraceID 提取)
|
||||
└── standard.go # [Schema] 标准字段定义 (Standardized Field Constructors)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 文件职责详解 (Responsibilities)
|
||||
|
||||
### A. `internal/pkg/log` (核心日志包)
|
||||
|
||||
这是一个基础设施包,不应依赖任何业务逻辑(User, Order 等)。
|
||||
|
||||
| **文件名** | **职责描述** | **关键设计点 (Design Decisions)** |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`log.go`** | **门面 (Facade) 与单例管理**。<br>1. 维护私有全局变量 `globalLogger`。<br>2. 提供 `Init(opts)` 初始化入口。<br>3. 提供 `L()` 获取底层 `*zap.Logger`。<br>4. 提供 `Info/Error` 等静态代理方法。 | **单例兜底**:在 `init()` 中初始化一个默认的 `Nop` 或 `Console` Logger,防止未初始化调用导致 Panic。<br>**Caller 修正**:<br>1. 底层 `globalLogger` 配置 `AddCallerSkip(0)`。<br>2. 静态代理方法 (`Info`, `Error`) 内部使用 `WithOptions(AddCallerSkip(1))`。<br>3. `L()` 和 `WithContext()` 返回原生 Logger (Skip 0),确保业务层直接调用时行号正确。 |
|
||||
| **`options.go`** | **配置对象 (DTO)**。<br>定义 `Options` 结构体,用于接收 Viper 的配置映射。 | **配置解耦**:只定义 struct,不包含逻辑。支持从 `config.yaml` 的 `log` 节点自动 Unmarshal。 |
|
||||
| **`zap.go`** | **核心构建工厂 (Factory)**。<br>负责组装 Encoder (JSON/Console)、Writer 和 Level。<br>实现 `New(opts)` 函数。 | **环境隔离**:<br>- Dev: ConsoleEncoder + StackTrace (Warn 级)<br>- Prod: JsonEncoder + StackTrace (Panic 级) |
|
||||
| **`writer.go`** | **IO 输出管理**。<br>封装 `lumberjack.Logger`。<br>实现 `zapcore.WriteSyncer` 接口。 | **可靠性**:配置 `Lumberjack` 的 `Compress: true` 和 `MaxSize: 100MB`。实现 Console + File 的 **Tee (双写)** 模式。 |
|
||||
| **`context.go`** | **上下文装饰器与播种器 (Decorator & Seeder)**。<br>1. `WithContext(ctx)`: 提取 TraceID。<br>2. **[New] `StartBackgroundTrace(ctx)`**: 为后台任务生成并注入根 TraceID。 | **零侵入**:仅通过 `zap.With()` 附加字段,返回 **派生 Logger**,不修改全局 Logger,线程安全。 |
|
||||
| **`standard.go`** | **标准化字段与存取器 (Schema & Accessor)**。<br>1. 定义**私有** Context Key 类型,防止碰撞。<br>2. 提供 `WithTraceID(ctx, id)` 和 `GetTraceID(ctx)` 公开方法。<br>3. 定义标准字段构造器 (如 `zap.String("trace_id", …)`)。 | **规范约束**:<br>- 统一使用 snake_case。<br>- 防止拼写错误 (如 `uid` vs `user_id`)。 |
|
||||
|
||||
### B. `internal/middleware` (中间件集成)
|
||||
|
||||
这是日志模块与 HTTP 框架 (Gin) 结合的触点。
|
||||
|
||||
| **文件名** | **职责描述** | **交互逻辑** |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| **`trace.go`** | **链路起点**。<br>链路追踪 (生成 TraceID -> **注入标准 Context** -> 挂载回 Gin Request) | **上下游打通**:保证 TraceID 在微服务或网关间的透传能力。 |
|
||||
| **`access_log.go`** | **流量审计**。<br>1. 记录 `Start Time`。<br>2. 执行 `c.Next()`。<br>3. 计算 `Latency`。<br>4. 打印结构化日志。 | **字段映射**:<br>`path`, `method`, `status`, `client_ip`, `latency`, `user_agent`。**必须使用 `log.WithContext(c)`**。 |
|
||||
| `recovery.go` | 结构化灾难恢复。<br>1. `defer recover()` 捕获 Panic。<br>2. 获取 Stack Trace。<br>3. **调用 `pkg/log` 记录 JSON 格式的 Error 日志** (包含 `stack` 字段)。<br>4. 返回 500 响应。 | **替代 Gin 默认组件**:必须使用 `gin.New()` 启动,手动注册此中间件,杜绝默认的控制台文本打印。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据流转图示 (Data Flow)
|
||||
|
||||
为了确保你理解“上下文”是如何流动的,以下是逻辑路径:
|
||||
|
||||
1. **Request In** -> `middleware/trace.go` -> 生成 `trace_id` -> **Wrap 进 `std.Context`**。 …
|
||||
2. **`pkg/log/context.go`** -> 从 **`std.Context`** 取出 `trace_id` -> …
|
||||
3. **`pkg/log/context.go`** -> 从 `gin.Context` 取出 `trace_id` -> 创建带字段的 `zap.Logger`。
|
||||
4. **`pkg/log/zap.go`** -> 序列化为 JSON `{…"trace_id":"xyz"…}`。
|
||||
5. **`pkg/log/writer.go`** -> 写入 `app.log` 文件 (由 Lumberjack 轮转)。
|
||||
|
||||
## 4. 依赖关系检查 (Dependency Check)
|
||||
|
||||
- `pkg/log` **不依赖** `middleware` (防止循环依赖)。
|
||||
- `middleware` **依赖** `pkg/log` (调用日志打印)。
|
||||
- `pkg/log` **仅依赖** `uber-go/zap`, `natefinch/lumberjack`。**严禁依赖** `gin` 或其他 Web 框架。所有 Context 操作均基于 Go 标准库接口。
|
||||
|
||||
这个结构完全穷尽了我们在前几轮讨论中确定的技术决策。如果确认无误,我们将在下一步生成具体的代码实现。
|
||||
181
Go项目实战/03_基础设施/02_日志/06_日志模块开发规范与质量保证手册.md
Normal file
181
Go项目实战/03_基础设施/02_日志/06_日志模块开发规范与质量保证手册.md
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 《日志模块开发规范与质量保证手册》
|
||||
- 一、 核心开发规范 (The Golden Rules)
|
||||
date created: 星期三, 十二月 10日 2025, 10:53:19 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:31:04 晚上
|
||||
---
|
||||
|
||||
# 《日志模块开发规范与质量保证手册》
|
||||
|
||||
---
|
||||
|
||||
## 一、 核心开发规范 (The Golden Rules)
|
||||
|
||||
这部分是“软约束”,属于团队共识,通过 Code Review 和 AI 辅助检查来执行。
|
||||
|
||||
### 1. 键名命名公约 (Key Naming Convention)
|
||||
|
||||
日志是给机器(ELK/Loki)读的,键名必须统一,方便建立索引。
|
||||
|
||||
- **规则**: 严禁使用 CamelCase (小驼峰) 或 PascalCase (大驼峰),**必须且只能使用 snake_case (下划线命名)**。
|
||||
- **反例**: `userId`, `IPAddress`, `httpStatus`
|
||||
- **正例**: `user_id`, `client_ip`, `http_status`
|
||||
- **理由**: 多数数据库和搜索引擎(如 Elasticsearch)的分词器对下划线更友好,且 SQL 查询习惯也是下划线。
|
||||
|
||||
### 2. 类型安全铁律 (Type Safety Strictness)
|
||||
|
||||
利用 Zap 的强类型优势,拒绝隐式转换。
|
||||
|
||||
- **规则**: 在业务热点路径(Hot Path)中,**严禁使用 `zap.Any`、`zap.Reflect` 或 `Sugar` 模式**。
|
||||
- **例外**: 仅在应用启动(Init)、Panic 恢复或非高频的配置加载阶段允许使用 `SugaredLogger`。
|
||||
- **理由**: `zap.Any` 会触发反射(Reflection),导致内存逃逸和 GC 压力。这是高性能系统的“隐形杀手”。
|
||||
|
||||
### 3. 上下文优先原则 (Context First)
|
||||
|
||||
日志不是孤岛,必须依附于请求上下文。
|
||||
|
||||
- **规则**: 所有 Controller、Service、Repository 层的方法,如果需要打印日志,**必须**使用 `log.WithContext(ctx).Info(…)` 及其变体。
|
||||
- **禁止**: 严禁在业务流程中直接调用全局的 `log.Info(…)`(除非是系统级事件,如定时任务启动)。
|
||||
- **理由**: 只有通过 `WithContext`,才能将 TraceID 串联起来。
|
||||
|
||||
### 4. 哨兵值与魔法字符串 (Sentinels & Magic Strings)
|
||||
|
||||
- **规则**: 核心日志字段的 Key 必须定义为常量(Constant)。
|
||||
- **实现**: 在 `pkg/log/standard.go` 中定义 `const TraceIDKey = "trace_id"`。
|
||||
- **禁止**: 代码中出现手写的 `zap.String("trace_id", …)`,防止拼写错误(如写成 `traceid`)。
|
||||
|
||||
### 5. 热点路径复用原则 (Hot Path Reuse)
|
||||
|
||||
针对循环(Loop)或复杂长流程函数,严禁重复构建 Context Logger。
|
||||
|
||||
- **规则**: 必须在作用域入口处初始化 Logger 实例,并在该作用域内复用。
|
||||
- **反例 (Bad)**:
|
||||
|
||||
```Go
|
||||
for _, item := range items {
|
||||
// ❌ 每次循环都分配内存
|
||||
log.WithContext(ctx).Info("processing", zap.String("id", item.ID))
|
||||
}
|
||||
```
|
||||
|
||||
- **正例 (Good)**:
|
||||
|
||||
```Go
|
||||
// ✅ 只分配一次,复用 l
|
||||
l := log.WithContext(ctx)
|
||||
for _, item := range items {
|
||||
l.Info("processing", zap.String("id", item.ID))
|
||||
}
|
||||
```
|
||||
|
||||
- **理由**: 减少大量临时的 `zap.Logger` 结构体分配,降低 GC 的 Scavenge 阶段耗时。
|
||||
|
||||
### 6. 后台任务播种原则 (Background Trace Seeding)
|
||||
|
||||
所有非 HTTP 触发的后台任务入口(Goroutine, Cron, MQ Handler),必须是“有状态”的。
|
||||
|
||||
- **规则**: 任务的第一行代码必须调用 `StartBackgroundTrace`。
|
||||
- **反例 (Bad)**:
|
||||
|
||||
```Go
|
||||
func ProcessOrder(msg []byte) {
|
||||
ctx := context.Background()
|
||||
// ❌ 此时 ctx 空空如也,日志将丢失 TraceID
|
||||
log.WithContext(ctx).Info("processing order")
|
||||
}
|
||||
```
|
||||
|
||||
- **正例 (Good)**:
|
||||
|
||||
```Go
|
||||
func ProcessOrder(msg []byte) {
|
||||
// ✅ 自动生成一个新的 TraceID 注入 ctx
|
||||
ctx := log.StartBackgroundTrace(context.Background())
|
||||
log.WithContext(ctx).Info("processing order")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、 Linter 规则配置 (Automated Enforcement)
|
||||
|
||||
这部分是“硬约束”,我们将在 `.golangci.yml` 中配置这些规则,强行阻断不合规代码的提交。
|
||||
|
||||
### 1. 禁用标准库日志 (`depguard`)
|
||||
|
||||
防止开发人员手滑使用了 Go 原生的 `log` 或 `fmt` 打印日志。
|
||||
|
||||
Linter: depguard
|
||||
|
||||
配置策略:
|
||||
|
||||
- **Deny**:
|
||||
- `log`: 标准库日志(无结构化,无法分级)。
|
||||
- `fmt.Print*`: 控制台打印(生产环境绝对禁止)。
|
||||
- `github.com/sirupsen/logrus`: 防止引入其他日志库。
|
||||
|
||||
### 2. 强制错误处理 (`errcheck`)
|
||||
|
||||
Zap 的 `Sync()` 方法可能会返回错误(特别是在 Linux 的 `/dev/stdout` 上),通常需要忽略,但写入文件的错误不能忽略。
|
||||
|
||||
Linter: errcheck / gosec
|
||||
|
||||
配置策略:
|
||||
|
||||
- 对 `logger.Sync()` 的错误处理进行豁免(Exclude),因为在某些 OS 下 stdout sync 必然报错,这是已知 issue。
|
||||
- 但对 `logger.Info` 等方法的 IO 错误,原则上 Zap 内部处理了,不需要业务层捕获。
|
||||
|
||||
### 3. 自定义规则 (`ruleguard` - 高级)
|
||||
|
||||
标准的 Linter 无法检测“键名必须是 snake_case”。如果需要极致的管控,我们可以引入 `ruleguard`。
|
||||
|
||||
AI 辅助检查逻辑:
|
||||
|
||||
由于配置 ruleguard 较复杂,我们约定在 AI 生成代码阶段 执行此逻辑:
|
||||
|
||||
- **Check 1**: 正则匹配所有 `zap.String("([a-z]+[A-Z][a-z]+)", …)` 模式,如果发现驼峰命名,立刻自我修正。
|
||||
- **Check 2**: 扫描代码中是否存在 `fmt.Print`,如有则报错。
|
||||
|
||||
---
|
||||
|
||||
## 三、 安全与脱敏规范 (Security & Masking)
|
||||
|
||||
这是日志系统的“红线”。
|
||||
|
||||
### 1. PII (个人敏感信息) 零容忍
|
||||
|
||||
- **黑名单字段**: `password`, `token`, `access_token`, `refresh_token`, `credit_card`, `id_card`.
|
||||
- **处理方式**:
|
||||
- **方案 A (拦截器)**: 在 `zapcore` 层加 Hook,但这会损耗性能。
|
||||
- **方案 B (显式脱敏)**: 要求 AI 在生成代码时,对于敏感字段,自动包裹脱敏函数。例如 `zap.String("mobile", mask.Mobile(u.Mobile))`。
|
||||
- **决策**: 采用 **方案 B**。依赖编码时的自觉和 AI 的辅助,性能最优。
|
||||
|
||||
### 2. 大字段截断
|
||||
|
||||
- **规则**: 禁止将 Base64 图片数据、巨大的 HTML 内容直接打入日志。
|
||||
- **限制**: 单个 Field 的 Value 长度建议限制在 2KB 以内。
|
||||
|
||||
---
|
||||
|
||||
## 四、 AI 辅助编码的“质量契约” (AI Quality Contract)
|
||||
|
||||
为了确保我(AI)生成的代码符合上述规范,请你(用户)在审查我的代码时,使用以下 **Checklist** 进行验证。这也是我对你的承诺:
|
||||
|
||||
1. **Imports 检查**: 确认没有引入 `log` 或 `fmt`。
|
||||
2. **Context 检查**: 确认 `log.WithContext(ctx)` 是日志调用的唯一起手式。
|
||||
3. **Keys 检查**: 确认所有 JSON Key 都是 `snake_case`。
|
||||
4. **Args 检查**: 确认使用的是 `zap.String/Int` 等强类型构造器,而非 `zap.Any`。
|
||||
5. **Config 检查**: 确认没有硬编码的路径(如 `/var/log`),必须来自 `options.go`。
|
||||
|
||||
---
|
||||
|
||||
## 五、 总结与下一步
|
||||
|
||||
我们确立了:
|
||||
|
||||
1. **命名**: 强制 snake_case。
|
||||
2. **类型**: 拒绝 `zap.Any`,拒绝 `fmt`。
|
||||
3. **上下文**: 强制 `WithContext`。
|
||||
4. **安全**: 显式脱敏。
|
||||
146
Go项目实战/03_基础设施/02_日志/07_日志模块工程化实施标准.md
Normal file
146
Go项目实战/03_基础设施/02_日志/07_日志模块工程化实施标准.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 日志模块工程化实施标准
|
||||
date created: 星期三, 十二月 10日 2025, 10:58:53 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:42:26 晚上
|
||||
---
|
||||
|
||||
# 日志模块工程化实施标准
|
||||
|
||||
---
|
||||
|
||||
## 一、 注释与文档规范 (Documentation Standards)
|
||||
|
||||
目标:“中文友好 (Chinese Friendly)” 且 “符合 GoDoc 标准”。
|
||||
|
||||
我们采用 混合语言策略:结构定义用英文(为了 IDE 兼容性),业务解释用中文(为了团队协作)。
|
||||
|
||||
### 1. 导出的包与函数 (Exported Symbols)
|
||||
|
||||
所有对外暴露的函数(首字母大写),必须编写文档注释。
|
||||
|
||||
- **格式要求**:
|
||||
- 第一行:`// FunctionName 简短的英文或中文摘要` (符合 Go Lint 检查)。
|
||||
- 空一行。
|
||||
- 详细说明:**必须使用中文**,解释函数的行为、副作用(Side Effects)和潜在风险。
|
||||
- 参数说明:如果有复杂参数,使用 `// - param: explanation` 格式。
|
||||
- **范例 (Style Guide)**:
|
||||
|
||||
> // WithContext returns a logger with the trace ID injected.
|
||||
>
|
||||
> //
|
||||
>
|
||||
> // [功能]: 从 context.Context 中提取 TraceID 并附加到 Logger 字段中。
|
||||
>
|
||||
> // [注意]: 这是一个轻量级操作,但如果 ctx 为 nil,将返回原始 Logger 的 fallback。
|
||||
>
|
||||
> // [场景]: 务必在 Controller 或 Service 的入口处优先调用。
|
||||
|
||||
### 2. 内部实现细节 (Internal Logic)
|
||||
|
||||
对于 `internal/pkg/log` 内部复杂的逻辑(如 `lumberjack` 的配置转换),必须在代码块上方添加中文注释。
|
||||
|
||||
- **原则**:解释 **“为什么这么做 (Why)”**,而不是“做了什么 (What)”。代码本身已经展示了做了什么。
|
||||
- **范例**:
|
||||
|
||||
> // [Why]: 这里不使用 zap.NewProduction 自带的 OutputPaths,
|
||||
>
|
||||
> // 因为我们需要同时输出到控制台 (为了 Docker 采集) 和文件 (为了本地容灾),
|
||||
>
|
||||
> // 且文件输出需要通过 Lumberjack 进行轮转控制。
|
||||
|
||||
### 3. README 维护
|
||||
|
||||
在 `internal/pkg/log/README.md` 中维护一份**“速查手册”**。
|
||||
|
||||
- **必填内容**:
|
||||
- 如何在 `config.yaml` 中配置(给出默认值)。
|
||||
- 如何动态调整日志级别(如通过信号或 API)。
|
||||
- 常见错误码(Code)与日志关键字的对应关系。
|
||||
|
||||
---
|
||||
|
||||
## 二、 可拓展性设计 (Extensibility Design)
|
||||
|
||||
虽然我们拒绝“过度封装”,但必须为未来的变化预留接口(Hook Points)。
|
||||
|
||||
### 1. 配置扩展:Functional Options 模式
|
||||
|
||||
我们在 `Init` 函数中,不应列出所有参数,而应使用 `Option` 模式。
|
||||
|
||||
- **设计**: `func Init(opts …Option) error`
|
||||
- **预留能力**: 未来如果需要添加“发送日志到 Kafka”或“开启 Sentry 报警”,只需新增一个 `WithKafka(addr)` 的 Option,而无需修改 `Init` 的函数签名,保证了对旧代码的兼容性。
|
||||
|
||||
### 2. 核心扩展:Zap Hooks
|
||||
|
||||
Zap 原生支持 `Hooks`。我们的封装必须暴露这一能力。
|
||||
|
||||
- **场景**: 当日志级别为 `Error` 或 `Fatal` 时,可能需要同步触发飞书/钉钉报警。
|
||||
- **实现标准**: 在 `zap.go` 的构建逻辑中,检查配置是否定义了 Hooks。这允许我们在不侵入日志核心代码的情况下,挂载报警逻辑。
|
||||
|
||||
### 3. 字段扩展:Context Key Registry
|
||||
|
||||
随着业务发展,需要记录的元数据会增加(如 `TenantID`, `RequestID`, `SpanID`)。
|
||||
|
||||
- **标准**: 不要在 `context.go` 里写死 key 的提取逻辑。
|
||||
- **设计**: 定义一个 `type ContextExtractor func(ctx) []Field` 类型。默认提供 `TraceIDExtractor`。允许在初始化时注册新的 Extractor。这使得业务线可以自定义需要提取的 Context 字段。
|
||||
|
||||
---
|
||||
|
||||
## 三、 查漏补缺 (Gap Analysis)
|
||||
|
||||
在之前的讨论中,有几个隐蔽但致命的工程细节尚未覆盖,这里作为最后防线进行补充。
|
||||
|
||||
### 1. 关于 `Logger.Fatal` 的使用禁令
|
||||
|
||||
- **风险**: `zap.Logger.Fatal` 会在打印日志后调用 `os.Exit(1)`。
|
||||
- **工程标准**: **在 Web 服务(HTTP Server)中,严禁在业务逻辑层调用 `Fatal`。**
|
||||
- _原因_: 这会直接杀死整个进程,导致所有正在处理的请求中断(没有 Graceful Shutdown)。
|
||||
- _替代_: 遇到不可恢复错误,使用 `Error` 级别日志,并返回 `500` 错误给客户端,由上层中间件处理。
|
||||
- _例外_: 仅在 `main.go` 启动阶段(如连不上数据库、读不到配置)可以使用 `Fatal`。
|
||||
|
||||
### 2. 时间格式的一致性
|
||||
|
||||
- **问题**: Zap 默认的时间格式可能是浮点数(Unix Epoch)或非标准字符串。
|
||||
- **标准**: 生产环境统一配置为 **`ISO8601` (2025-12-10T22:00:00.000Z)**。
|
||||
- _理由_: 这种格式跨时区友好,且能被几乎所有日志分析工具(ELK, Splunk, CloudWatch)自动识别并建立时间索引。
|
||||
|
||||
### 3. 动态日志级别 (Hot Reload)
|
||||
|
||||
- **需求**: 线上出 Bug 时,需要临时把 Level 调成 Debug,查完再调回 Info,且不能重启服务。
|
||||
- **实现标准**: 利用 `zap.AtomicLevel`。
|
||||
- 我们需要暴露一个 HTTP 接口(如 `PUT /admin/log/level`)或监听配置文件的 `fsnotify` 事件。
|
||||
- 收到变更信号后,直接调用 `atomicLevel.SetLevel(zap.DebugLevel)`。这是线程安全的,无需重启实例。
|
||||
|
||||
### 4. 测试支持 (Testing Support)
|
||||
|
||||
- **问题**: 单元测试时,不仅不想看到日志刷屏,有时还需要断言“是否打印了某条错误日志”。
|
||||
- **标准**:
|
||||
- 提供 `pkg/log/test_helper.go`。
|
||||
- 封装 `zaptest/observer`。
|
||||
- 允许测试代码通过 `log.NewTestLogger()` 获取一个观察者对象,从而断言 `logs.FilterMessage("error").Len() == 1`。
|
||||
|
||||
### 5. 链路完整性保障
|
||||
|
||||
- **风险**: 开发者容易遗忘在 `go func()` 中传递 Context。
|
||||
- **标准**: 在 Code Review 时,重点检查所有 `go` 关键字后是否跟随了 Context 的传递或播种操作。
|
||||
|
||||
### 6. 框架初始化与 Panic 处理
|
||||
|
||||
- **风险**: `gin.Default()` 会自动注册只打印文本日志的 Recovery 中间件,破坏 JSON 格式。
|
||||
- **标准**:
|
||||
1. 必须使用 `gin.New()` 初始化 Engine。
|
||||
2. 必须手动注册我们自定义的 `middleware.Recovery` 和 `middleware.AccessLog`。
|
||||
3. 确保 Panic 日志中包含 TraceID(从 `c.Request.Context` 中尝试恢复)。
|
||||
|
||||
---
|
||||
|
||||
## 四、 总结与就绪确认
|
||||
|
||||
至此,我们已经完成了日志模块的**全生命周期设计**:
|
||||
|
||||
1. **架构**: 基础设施层,无业务依赖。
|
||||
2. **技术栈**: Zap + Lumberjack + Context Adapter。
|
||||
3. **模式**: 单例兜底 + 依赖注入,强类型约束。
|
||||
4. **规范**: Snake_case 键名,中文友好文档,严禁 Fatal。
|
||||
233
Go项目实战/03_基础设施/02_日志/AI 辅助基础设施构建 SOP (v1.0)-全局日志篇.md
Normal file
233
Go项目实战/03_基础设施/02_日志/AI 辅助基础设施构建 SOP (v1.0)-全局日志篇.md
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🏗️ AI 辅助基础设施构建 SOP (v1.0) - [全局日志篇]
|
||||
date created: 星期三, 十二月 10日 2025, 11:50:40 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:52:08 晚上
|
||||
---
|
||||
|
||||
# 🏗️ AI 辅助基础设施构建 SOP (v1.0) - [全局日志篇]
|
||||
|
||||
**核心理念:**
|
||||
|
||||
1. **Configuration Driven (配置驱动):** 先定义配置结构与 Viper 映射,再实现逻辑。
|
||||
2. **Zero Allocation Constraint (零分配约束):** 在 Prompt 层面封杀 `zap.Any`,强制使用强类型字段。
|
||||
3. **Layered Delivery (分层交付):** 先交付 `pkg/log` (Level 0),再交付 `middleware` (Level 1)。
|
||||
|
||||
---
|
||||
|
||||
## 📋 准备工作:上下文注入
|
||||
|
||||
在使用以下 Prompt 前,请确保 AI 已理解《全局日志模块详细设计说明书》的全部内容。
|
||||
|
||||
- `{语言/框架}`: Go 1.24+ / Uber Zap / Lumberjack v2
|
||||
- `{模块路径}`: `internal/pkg/log` (核心) & `internal/middleware` (集成)
|
||||
- `{关键约束}`: `pkg/log` **严禁依赖** `gin` 或 `viper` (仅接收 Config struct)。
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: 依赖隔离与任务拆解 (The Dependency-Aware MECE)
|
||||
|
||||
**目的:** 防止 AI 在编写日志核心时引入业务层代码(如 Gin),导致循环依赖。
|
||||
|
||||
### 🤖 拆解者 Prompt (复制使用)
|
||||
|
||||
```Markdown
|
||||
你现在是我的 **System Architect (系统架构师)**。
|
||||
我们要实现 `Global Logging Infrastructure`。基于《详细设计说明书》,请执行 **“依赖隔离任务拆解”**。
|
||||
|
||||
**1. 架构红线 (Architecture Rules):**
|
||||
- **Level 0 (Core):** `internal/pkg/log`。只依赖 `zap`, `lumberjack`, standard `context`。**严禁依赖 `gin`**。
|
||||
- **Level 1 (Integration):** `internal/middleware`。依赖 `internal/pkg/log` 和 `gin`。
|
||||
|
||||
**2. 原子化切分:**
|
||||
请将工作拆解为两个独立的 Batch,每个 Batch 包含若干 Step。
|
||||
- **Batch A (Core)**: 必须按 `options.go` (配置) -> `zap.go` (构造) -> `context.go` (桥接) -> `log.go` (门面) 的顺序。
|
||||
- **Batch B (Middleware)**: 包含 `trace.go`, `access_log.go`, `recovery.go`。
|
||||
|
||||
**3. 输出格式:**
|
||||
请输出一个 **Markdown Checklist**。
|
||||
格式示例:
|
||||
- [ ] **Batch A - Step 1: {文件名}** - {核心职责} (关键设计点: …)
|
||||
…
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 0.5: API 签名锁定 (API Surface Lock)
|
||||
|
||||
**目的:** 在实现 `zap` 复杂构建逻辑前,先锁死对外暴露的“门面”方法,确保调用体验。
|
||||
|
||||
### 🤖 Prompt 0.5: 定义门面接口
|
||||
|
||||
```Markdown
|
||||
在实现具体逻辑前,让我们先锁定 `internal/pkg/log` 的 **Public API**。
|
||||
请只输出 `log.go` 和 `context.go` 中 **Exported Functions** 的签名(无需函数体)。
|
||||
|
||||
**关键要求:**
|
||||
1. **初始化:** `Init(opts …Option)` 设计为 Functional Options 模式还是直接传 Struct?(依据设计文档应为 Struct 传入,但保留 Option 扩展性)。
|
||||
2. **上下文注入:** `WithContext(ctx context.Context) *zap.Logger` 的签名确认。
|
||||
3. **静态代理:** `Info`, `Error` 等静态方法如何处理 `CallerSkip`?请在注释中说明。
|
||||
4. **后台任务:** 必须包含 `StartBackgroundTrace(ctx)` 的定义。
|
||||
|
||||
请输出带有完整 Go Doc 的接口定义代码块。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 配置契约定义 (Configuration Contract)
|
||||
|
||||
**目的:** 只有确定了“怎么配”,才能决定“怎么写”。
|
||||
|
||||
### 🤖 Prompt 1: 定义配置结构与 Schema
|
||||
|
||||
```Markdown
|
||||
你现在是 **DevOps 专家**。
|
||||
请定义日志模块的配置结构 (`options.go`) 以及对应的 YAML 写法。
|
||||
|
||||
**任务:**
|
||||
1. **Go Struct:** 定义 `Options` 结构体。
|
||||
- 包含 `Level`, `Format` (json/console), `Filename`, `MaxSize`, `MaxBackups`, `MaxAge`, `Compress`。
|
||||
- Tag 必须适配 `mapstructure` (Viper 使用)。
|
||||
2. **Default Value:** 提供一个 `NewOptions()` 函数返回生产环境推荐的默认值 (100MB, 30个文件, JSON 格式)。
|
||||
3. **YAML Example:** 给出一个 `config.yaml` 的片段示例。
|
||||
|
||||
**约束:**
|
||||
- 字段类型必须明确(如 `MaxSize` 是 int 还是 string? 建议 int 单位 MB)。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 体验验证 (DX Verification)
|
||||
|
||||
**目的:** 验证开发者在业务代码中打印日志是否顺手,防止过度封装导致 API 臃肿。
|
||||
|
||||
### 🤖 Prompt 2: 伪代码验证 (复制使用)
|
||||
|
||||
```Markdown
|
||||
配置和接口已锁定。请写一段 **Service 层** 的伪代码,展示如何使用该日志库。
|
||||
|
||||
**场景验证:**
|
||||
1. **标准调用:** 在 `UserRegister` 方法中,如何打日志并自动带上 TraceID?
|
||||
2. **强类型约束:** 展示使用 `zap.String`, `zap.Int` 的写法。**严禁出现 `zap.Any`**。
|
||||
3. **子 Context:** 在 `go func()` 中如何使用 `StartBackgroundTrace` 保证链路不断?
|
||||
4. **Error 处理:** 遇到 DB 错误时,如何记录 log 并返回 error?
|
||||
|
||||
请展示代码,并自我评价是否符合“低心智负担”原则。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 核心防御式实现 (Core Defensive Implementation)
|
||||
|
||||
**核心机制:** 这是一个**循环步骤**。针对 `internal/pkg/log` 的每个文件执行。
|
||||
|
||||
### 🔄 循环动作 A: 生成代码
|
||||
|
||||
**[发送 Prompt]:**
|
||||
|
||||
```Markdown
|
||||
我们现在执行 **Batch A - Step {N}**。
|
||||
|
||||
**任务目标:**
|
||||
生成 `{文件名}` (例如 `zap.go`)。
|
||||
|
||||
**设计文档引用:**
|
||||
- 引用《设计说明书》中关于 `{章节名}` 的要求。
|
||||
|
||||
**代码质量硬性约束 (Hard Constraints):**
|
||||
1. **Snake Case:** 所有的 JSON Key (包括 TraceID) 必须手动指定为 snake_case (如 `zap.String("trace_id", v)`)。
|
||||
2. **No Zap Any:** 严禁在核心逻辑中使用 `zap.Any`。如果是 map/struct,必须手动拆解或实现 `zapcore.ObjectMarshaler`。
|
||||
3. **Safety:**
|
||||
- `writer.go`: Lumberjack 的 `Compress` 必须默认为 true。
|
||||
- `log.go`: `globalLogger` 必须有 `sync.Once` 保护,且默认初始化为 Console (避免 nil pointer)。
|
||||
4. **Caller Skip:** 确保静态方法 (log.Info) 和实例方法 (logger.Info) 的 Caller 层级正确,都能定位到业务代码行号。
|
||||
|
||||
请生成完整代码。
|
||||
```
|
||||
|
||||
### 🔄 循环动作 B: 质量检查锚点
|
||||
|
||||
**[发送 Prompt]:**
|
||||
|
||||
```Markdown
|
||||
代码已生成。请进行 **Self-Correction (自我修正)**:
|
||||
1. 检查是否有 `fmt.Print` 残留?
|
||||
2. 检查 `log.go` 中的静态方法是否使用了 `WithOptions(zap.AddCallerSkip(1))`?如果没用,业务层行号会报错。
|
||||
3. 检查是否引入了 `gin` 或其他业务包?(Level 0 严禁依赖)。
|
||||
|
||||
确认无误后,存入记忆,继续下一步。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 中间件集成 (Middleware Integration)
|
||||
|
||||
**目的:** 只有当核心 Log 库稳定后,才实现 Gin 中间件。
|
||||
|
||||
### 🤖 Prompt 4: 实现链路追踪与访问日志
|
||||
|
||||
```Markdown
|
||||
现在进入 **Batch B**。我们需要实现 `internal/middleware/trace.go` 和 `access_log.go`。
|
||||
|
||||
**任务要求:**
|
||||
1. **Trace Middleware:**
|
||||
- 从 Request Header (`X-Trace-ID`) 读取,若无则生成 UUID。
|
||||
- **关键点:** 必须调用 `log.WithTraceID(ctx, id)` 将 ID 注入 Standard Context,再回写到 `c.Request`。
|
||||
2. **Access Log Middleware:**
|
||||
- 记录 Start Time, End Time, Latency。
|
||||
- 使用 `log.WithContext(c.Request.Context()).Info(…)` 打印。
|
||||
- **字段映射:** `method`, `path`, `ip`, `status`, `latency` (ms)。
|
||||
3. **Recovery Middleware:**
|
||||
- 捕获 Panic。
|
||||
- 打印包含 Stack Trace 的 JSON Error 日志 (非 Console 文本)。
|
||||
- 返回 500 响应。
|
||||
|
||||
请一次性生成这三个文件的核心逻辑。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 极限防御测试 (Extreme Defensive Testing)
|
||||
|
||||
**目的:** 验证并发安全、文件轮转和敏感数据脱敏。
|
||||
|
||||
### 🤖 Prompt 5: 生成红队测试用例
|
||||
|
||||
```Markdown
|
||||
核心代码已就绪。请为 `pkg/log` 编写单元测试 `log_test.go`。
|
||||
|
||||
**请覆盖以下 3 个高危场景 (Test Cases):**
|
||||
|
||||
1. **并发竞争 (Race Detection):**
|
||||
- 启动 100 个 Goroutine,同时调用 `log.WithContext(ctx).Info(…)`。
|
||||
- 断言:`go test -race` 不报错,且 TraceID 不串号。
|
||||
|
||||
2. **Caller 准确性验证:**
|
||||
- 编写一个测试辅助函数,解析输出的 JSON,断言 `caller` 字段指向的是测试代码行号,而不是 `log.go` 内部。
|
||||
|
||||
3. **敏感数据脱敏 (Mock):**
|
||||
- 模拟打印 `zap.String("password", "123456")`。
|
||||
- (注意:如果我们在 Zap Core 层没做拦截,这里需要验证是否通过 AI 辅助代码生成时的规范来避免。此处请测试:如果定义了 Masking Hook,密码是否被替换为 `******`)。
|
||||
- *注:根据设计文档,我们采用“编码规范+AI辅助”策略,故此处测试应侧重于“TraceID 是否在 Log 中正确出现”。*
|
||||
|
||||
请输出 Test 代码。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: SRE 验收 (SRE Review)
|
||||
|
||||
### 🤖 Prompt 6: 运维视角审查
|
||||
|
||||
```Markdown
|
||||
切换角色为 **SRE**。请审查生成的日志模块。
|
||||
|
||||
**检查清单:**
|
||||
1. **磁盘爆炸风险:** `Lumberjack` 配置是否真的生效了?(检查 WriteSyncer 的封装)。
|
||||
2. **性能损耗:** `WithContext` 是否每次都创建了过多的 Zap 对象?(确认是否只是 Shallow Copy)。
|
||||
3. **索引友好度:** 时间戳是否为 ISO8601 (`2025-12-10T…`)?如果是 Epoch float,ELK 处理会麻烦。
|
||||
4. **容灾:** 如果 `logs/` 目录不可写(权限问题),程序会 Panic 还是降级输出到 Stdout?
|
||||
|
||||
请给出 1-2 个具体的 `TODO` 优化项。
|
||||
```
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 📋 Global Logging Infrastructure - Task Checklist
|
||||
date created: 星期三, 十二月 10日 2025, 11:55:42 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:56:24 晚上
|
||||
---
|
||||
收到。作为系统架构师,我已根据《全局日志模块详细设计说明书》及 SOP 要求,完成了**依赖隔离任务拆解 (Dependency-Aware MECE Breakdown)**。
|
||||
|
||||
此拆解方案严格遵循 **Level 0 (Core)** 与 **Level 1 (Integration)** 的物理隔离,确保核心日志包零依赖于业务框架。
|
||||
|
||||
---
|
||||
|
||||
# 📋 Global Logging Infrastructure - Task Checklist
|
||||
|
||||
## Phase 1: Batch A - Core Implementation (Level 0)
|
||||
|
||||
> 目录: internal/pkg/log
|
||||
>
|
||||
> 约束: 仅依赖 zap, lumberjack, context。严禁 import gin, viper。
|
||||
|
||||
- [ ] **Batch A - Step 1: 配置与标准定义 (`options.go`, `standard.go`)**
|
||||
|
||||
- **核心职责:** 定义日志配置结构体 (Config Struct) 及全局统一的键名常量 (Standard Keys)。
|
||||
- **关键设计:**
|
||||
- `Options` 结构体需包含 `mapstructure` tag 以适配外部 Viper 解析。
|
||||
- 预定义 `trace_id`, `user_id` 等常量为 `snake_case`,杜绝魔法字符串。
|
||||
- 包含 `DefaultOptions()` 返回生产环境推荐配置 (100MB, Compress=true)。
|
||||
|
||||
- [ ] **Batch A - Step 2: 核心构建与 IO (`writer.go`, `zap.go`)**
|
||||
|
||||
- **核心职责:** 封装 Lumberjack 文件轮转逻辑,构建 `zap.Core` 与 `zap.Logger` 实例。
|
||||
- **关键设计:**
|
||||
- **IO 分离:** `writer.go` 实现 `zapcore.WriteSyncer`,强制开启 `Compress: true`。
|
||||
- **环境隔离:** `zap.go` 根据配置决定使用 `JSON Encoder` (Prod) 或 `Console Encoder` (Dev)。
|
||||
- **双写机制:** 实现 Tee 模式,同时输出到文件和控制台 (Stdout)。
|
||||
|
||||
- [ ] **Batch A - Step 3: 上下文桥接 (`context.go`)**
|
||||
|
||||
- **核心职责:** 实现标准 `context.Context` 到 `zap.Field` 的转换逻辑。
|
||||
- **关键设计:**
|
||||
- **TraceID 注入:** 实现 `WithContext(ctx)`,从 Context 提取 TraceID 并返回带有 `trace_id` 字段的 `*zap.Logger`。
|
||||
- **后台播种:** 实现 `StartBackgroundTrace(ctx)`,为 Cron/Goroutine 任务生成根 TraceID。
|
||||
- **零侵入:** 仅依赖标准库 Context,不依赖 Gin Context。
|
||||
|
||||
- [ ] **Batch A - Step 4: 全局门面 (`log.go`)**
|
||||
|
||||
- **核心职责:** 管理全局单例 (Singleton),提供静态代理方法 (Static Proxy)。
|
||||
- **关键设计:**
|
||||
- **懒汉兜底:** `globalLogger` 默认初始化为 Console Logger (Nop),防止未调用 `Init` 时 Panic。
|
||||
- **Caller 修正:** 静态方法 (`log.Info`) 必须使用 `AddCallerSkip(1)`,确保日志行号指向业务代码而非 `log.go`。
|
||||
- **并发安全:** `Init()` 必须使用 `sync.Once` 或互斥锁保护。
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Batch B - Middleware Integration (Level 1)
|
||||
|
||||
> 目录: internal/middleware
|
||||
>
|
||||
> 约束: 依赖 internal/pkg/log, gin, pkg/app。
|
||||
|
||||
- [ ] **Batch B - Step 1: 链路追踪中间件 (`trace.go`)**
|
||||
|
||||
- **核心职责:** 请求入口处的 TraceID 生成与注入。
|
||||
- **关键设计:**
|
||||
- 优先读取 Header `X-Trace-ID`,无则生成 UUID。
|
||||
- **关键动作:** 调用 `log.WithTraceID(ctx, id)` 将 ID 注入 **Standard Context**,再回写到 `c.Request`,打通后续所有层的日志链路。
|
||||
|
||||
- [ ] **Batch B - Step 2: 访问日志中间件 (`access_log.go`)**
|
||||
|
||||
- **核心职责:** 记录 HTTP 请求的黄金指标 (Golden Signals)。
|
||||
- **关键设计:**
|
||||
- 必须使用 `log.WithContext(c.Request.Context())` 打印,确保包含 TraceID。
|
||||
- 记录字段:`method`, `path`, `status`, `latency` (ms), `client_ip`。
|
||||
|
||||
- [ ] **Batch B - Step 3: 灾难恢复中间件 (`recovery.go`)**
|
||||
|
||||
- **核心职责:** 替换 Gin 默认 Recovery,提供结构化 Panic 日志。
|
||||
- **关键设计:**
|
||||
- 捕获 `panic` -> 获取 Stack Trace -> 构造 JSON Error 日志 (包含 `stack` 字段)。
|
||||
- 联动 `pkg/app` 返回标准 JSON 500 响应,通过 `pkg/log` 记录系统级错误。
|
||||
|
||||
---
|
||||
|
||||
# 🏁 Next Action
|
||||
|
||||
建议按照 Checklist 顺序,从 **Batch A - Step 1** 开始执行。是否现在开始 Phase 0.5 (API 签名锁定) 或直接生成 Step 1 代码?
|
||||
864
Go项目实战/03_基础设施/全局日志模块 (Global Logging Infrastructure) 详细设计说明书.md
Normal file
864
Go项目实战/03_基础设施/全局日志模块 (Global Logging Infrastructure) 详细设计说明书.md
Normal file
@@ -0,0 +1,864 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- Project Context Aggregation
|
||||
date created: 星期三, 十二月 10日 2025, 11:02:10 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:50:40 晚上
|
||||
---
|
||||
|
||||
# Project Context Aggregation
|
||||
|
||||
> Source Items: 1
|
||||
|
||||
==== 02_ 日志\01_ 设计目标.md ====
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 核心设计目标 (Core Design Goals)
|
||||
date created: 星期三, 十二月 10日 2025, 10:27:39 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 10:28:15 晚上
|
||||
---
|
||||
|
||||
# 1. 核心设计目标 (Core Design Goals)
|
||||
|
||||
## 目标一:全链路上下文关联 (Contextual Traceability)
|
||||
|
||||
这是最核心的差异点。传统的 `log.Println("Database error")` 在并发环境下毫无价值,因为你不知道这条错误属于哪个请求。
|
||||
|
||||
- **设计要求**:
|
||||
- **自动注入 TraceID**: 必须能够从 `context.Context` 中提取 `TraceID`(目前 `internal/pkg/app` 已经生成了 TraceID),并自动将其附加到每一条日志中。
|
||||
- **请求元数据绑定**: 除了 TraceID,还应支持自动绑定 `UserID`、`IP`、`Method`、`Path` 等元数据,形成请求的完整快照。
|
||||
- **跨组件穿透**: 日志对象必须能够在 Layer 之间传递(例如 Controller -> Service -> Repository),且保持上下文不丢失。
|
||||
|
||||
## 目标二:严格的结构化契约 (Strict Structured Schema)
|
||||
|
||||
日志是写给机器看的,不是写给通过 SSH 连上服务器的人看的。
|
||||
|
||||
- **设计要求**:
|
||||
- **JSON First**: 生产环境强制使用 JSON 格式。
|
||||
- **Schema 统一**: 字段命名必须统一。例如,不要混用 `uid`, `user_id`, `userId`,必须在设计阶段锁定为 snake_case (如 `user_id`)。
|
||||
- **类型安全**: 时间戳必须统一格式(推荐 ISO8601 或 Unix Nano),数字字段不能变成字符串(便于聚合计算)。
|
||||
|
||||
## 目标三:高性能与零侵入 (High Performance & Zero Allocation)
|
||||
|
||||
日志通常是系统中 IO 最密集的组件之一。
|
||||
|
||||
- **设计要求**:
|
||||
- **低 GC 压力**: 利用 Zap 的核心优势,避免大量的 `interface{}` 反射和字符串拼接,使用强类型的 Field(如 `zap.Int`, `zap.String`)。
|
||||
- **异步 IO (可选)**: 考虑是否引入 Buffer 机制(牺牲极端崩溃下的日志完整性换取吞吐量)。
|
||||
- **Level 级联过滤**: 在 Debug 级别关闭时,Debug 级别的日志构造逻辑(如复杂的对象序列化)不应被执行。
|
||||
|
||||
## 目标四:安全与合规 (Security & Compliance)
|
||||
|
||||
这往往是被忽视的一点,也是导致安全事故的频发区。
|
||||
|
||||
- **设计要求**:
|
||||
- **敏感数据脱敏**: 必须具备“黑名单”机制。任何包含 `password`, `token`, `mobile`, `credit_card` 的字段在输出前必须被自动掩盖(Masking)。
|
||||
- **安全截断**: 防止打印过大的 Body(如 Base64 图片上传)导致磁盘爆满或日志系统瘫痪,限制单条日志最大长度。
|
||||
|
||||
---
|
||||
|
||||
# 2. 场景化行为对比 (Dev Vs Prod)
|
||||
|
||||
为了兼顾开发体验和生产运维标准,我们需要在设计中明确区分两种环境的行为。
|
||||
|
||||
|**维度**|**开发环境 (Development)**|**生产环境 (Production)**|**设计意图**|
|
||||
|---|---|---|---|
|
||||
|**编码格式**|Console (彩色,人类易读)|JSON (机器易读)|开发追求直观;生产追求 ELK 解析效率。|
|
||||
|**输出目标**|Stdout (控制台)|File + Stdout (双写)|开发侧容器即焚;生产侧需持久化 + 容器采集。|
|
||||
|**日志级别**|Debug|Info / Warn|生产环境过滤掉大量 Debug 噪音,节省存储成本。|
|
||||
|**堆栈追踪**|Error 级别即打印|Panic 或 Fatal 才打印|减少生产环境日志体积,除非发生严重故障。|
|
||||
|**调用行号**|显示 (Caller)|显示 (Caller)|快速定位代码位置。|
|
||||
|
||||
---
|
||||
|
||||
# 3. 架构定位与边界 (Architecture Boundary)
|
||||
|
||||
我们需要明确日志模块在架构中的位置:
|
||||
|
||||
- **位置**: 属于 `Infrastructure Layer` (Level 0/1)。
|
||||
- **依赖关系**:
|
||||
- **被谁依赖**: 所有层(Handler, Service, Repository)都依赖 Log。
|
||||
- **依赖谁**: 仅依赖标准库和第三方 Log Driver (Zap),**不应依赖业务逻辑**。
|
||||
- **与其他模块的关系**:
|
||||
- **vs `ecode`**: `ecode` 定义错误的**类型**(Code),Log 记录错误的**现场**(Stack/Trace)。
|
||||
- **vs `app.Response`**: Response 负责**对用户说话**(经过清洗的、友好的信息),Log 负责**对开发者说话**(原始的、包含脏数据的真相)。
|
||||
|
||||
---
|
||||
|
||||
# 4. 深度反思与自我反驳 (Critical Thinking & Risk Analysis)
|
||||
|
||||
在敲定设计目标前,必须审视潜在的矛盾和风险:
|
||||
|
||||
**反驳点 1:全链路上下文(TraceID)的传递成本**
|
||||
|
||||
- **挑战**: 要想让 Repository 层的日志也打出 TraceID,必须修改所有方法的签名为 `func (ctx context.Context, …)`。这对现有代码(如果是非 Context 风格)是巨大的重构。
|
||||
- **回应**: 我们的 `Repository` 接口目前设计中已经包含了 `context.Context`。这是一个必须遵守的“硬约束”。如果缺少 Context,日志将断层。
|
||||
- **结论**: 必须在规范中强调:**所有层级的方法首个参数必须是 Context**。
|
||||
|
||||
**反驳点 2:脱敏机制的性能损耗**
|
||||
|
||||
- **挑战**: 如果每一条日志都要遍历字段去匹配“黑名单”进行正则替换,CPU 开销极大。
|
||||
- **回应**: 不能使用正则扫描全文。
|
||||
- **修正方案**: 利用 Zap 的 `Hook` 或 `Core` 包装,仅针对特定 Key(如 `password`)进行值替换,或者要求开发者在打印敏感结构体时显式调用 `.Reduct()` 方法,而非隐式全局扫描。隐式扫描在 Golang 中通常是性能杀手。
|
||||
|
||||
**反驳点 3:异步写入的丢数据风险**
|
||||
|
||||
- **挑战**: 为了性能使用 `Buffered Write`,如果进程被 `kill -9` 或 Panic 崩溃,缓冲区日志会丢失,而崩溃前的日志往往最重要。
|
||||
- **回应**: 对于交易类系统,**可靠性 > 吞吐量**。
|
||||
- **结论**: 默认采用 **同步写入 (Sync Write)**。Zap 本身性能已经足够强(纳秒级),除非达到数万 QPS,否则不需要引入 Buffer。对于 Panic,必须使用 `defer logger.Sync()` 确保刷盘。
|
||||
|
||||
---
|
||||
|
||||
# 5. 待确认问题 (Open Questions)
|
||||
|
||||
在进入下一步(编写详细规格说明书)之前,我需要确认你对以下几点的偏好:
|
||||
|
||||
1. **全局单例 vs 纯依赖注入**:
|
||||
|
||||
- _选项 A_: 提供 `log.Info()` 全局静态方法(方便,但有副作用)。
|
||||
- _选项 B_: 强制必须通过 `l.Info()` 实例方法调用(架构更洁癖,但调用繁琐)。
|
||||
- _推荐_: **选项 A + B**。提供全局方法作为快捷方式(底层代理到单例),同时支持 DI 注入。你倾向于哪种?
|
||||
|
||||
2. **日志轮转 (Rotation) 策略**:
|
||||
|
||||
- 你是倾向于按 **大小** 切割(如 100MB 一个文件),还是按 **时间** 切割(每天一个文件)?
|
||||
- _通常建议_: 按大小切割(防止单文件过大无法打开),配合最大文件保留数。
|
||||
```
|
||||
|
||||
==== 02_ 日志\02_ 技术栈基线.md ====
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 核心引擎 (The Engine):Uber Zap
|
||||
date created: 星期三, 十二月 10日 2025, 10:28:15 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 10:29:20 晚上
|
||||
---
|
||||
|
||||
# 1. 核心引擎 (The Engine):Uber Zap
|
||||
|
||||
行业共识 (Consensus):
|
||||
|
||||
在 Go 语言的高性能后端领域,go.uber.org/zap 是目前无可争议的事实标准(De Facto Standard)。
|
||||
|
||||
我的推荐:
|
||||
|
||||
坚定地使用 Zap,不要犹豫。
|
||||
|
||||
**老兵的经验谈 (Why & How):**
|
||||
|
||||
- **为何不是 Logrus?** Logrus 胜在 API 极其友好(兼容标准库),但它底层大量使用反射(Reflection)和锁,在高并发场景下是严重的性能瓶颈(GC 压力大)。
|
||||
- **为何不是 Slog (Go 1.21+)?** Slog 是 Go 官方推出的结构化日志接口。虽然它是未来,但目前的生态和性能优化(尤其是在 JSON 序列化的极致性能上)尚未完全超越 Zap。且 Zap 可以很方便地作为 Slog 的 Backend。但在本项目中,为了追求极致性能和成熟度,直接使用 Zap 原生 API 是最高效的。
|
||||
- **关键决策点**:
|
||||
- **Field 强类型**: 我们必须强制团队使用 `zap.String("key", "val")` 而非 `zap.Any("key", val)`。`Any` 会导致反射,破坏 Zap 的零内存分配(Zero Allocation)优势。这是代码审查(Code Review)的红线。
|
||||
- **Logger vs SugaredLogger**:
|
||||
- **核心业务链路 (Hot Path)**: 使用 `zap.Logger`(极致性能,但语法繁琐)。
|
||||
- **初始化/非热点代码**: 使用 `zap.SugaredLogger`(语法类似 `printf`,性能稍弱但开发快)。
|
||||
- **基线**: 我们的封装层默认暴露 `Logger` 能力,保留高性能入口。
|
||||
|
||||
# 2. 轮转插件 (Rotation): Lumberjack V2
|
||||
|
||||
行业共识 (Consensus):
|
||||
|
||||
日志切割看似简单,实则坑多(并发写冲突、文件重命名原子性、不同操作系统的文件锁差异)。
|
||||
|
||||
我的推荐:
|
||||
|
||||
使用 gopkg.in/natefinch/lumberjack.v2。
|
||||
|
||||
**老兵的经验谈:**
|
||||
|
||||
- **不要造轮子**: 我见过无数团队尝试自己写 `file.Write` 然后计数切割,最后都在“多进程并发写同一个日志文件”或者“日志压缩时导致 IO 飙升”这些问题上翻车。
|
||||
- **配置陷阱**:
|
||||
- `MaxSize`: 建议 **100MB**。太小导致文件碎片化,太大导致像 grep/vim 这种工具打开困难。
|
||||
- `MaxBackups`: 建议保留 **30-50 个**。
|
||||
- `MaxAge`: 建议 **7-14 天**。
|
||||
- **Compress**: 建议 **开启 (True)**。历史日志压缩存储(gzip)能节省 90% 以上的磁盘空间,这对于云盘成本控制非常重要。
|
||||
|
||||
# 3. 上下文管理 (Context Awareness): 自研封装层
|
||||
|
||||
这是我们作为“架构师”必须介入的地方。原生 Zap 不懂业务上下文,我们需要一个胶水层。
|
||||
|
||||
技术难点:
|
||||
|
||||
如何优雅地把 TraceID 塞进每一行日志?
|
||||
|
||||
设计路线:
|
||||
|
||||
我们需要定义一个轻量级的 Wrapper 或者 Helper 函数。
|
||||
|
||||
- **不要**:重写 `zap.Logger` 结构体的所有方法(那样维护成本太高)。
|
||||
- **要**:提供一个入口函数,例如 `log.WithContext(ctx)`。
|
||||
- **原理**:这个函数会从 `ctx` 取出 `TraceID`,然后调用 `zap.With(zap.String("trace_id", id))`,返回一个携带了该字段的子 Logger 实例。这是一次极低成本的指针操作。
|
||||
|
||||
# 4. 抽象策略与混合模式 (Hybrid Pattern)
|
||||
|
||||
结合你选择的 **Option A+B**,我们的技术实现路径如下:
|
||||
|
||||
1. **全局变量 (The Global)**:
|
||||
|
||||
- 在 `internal/pkg/log` 包内部维护一个私有的 `var globalLogger *zap.Logger`。
|
||||
- 利用 `sync.Once` 确保其并发安全的初始化。
|
||||
- **兜底策略**: 在 `init()` 函数中先给它一个默认的 `Console Logger`。这样即使开发者忘记调用 `InitLogger`,程序启动时的日志也不会 panic,只会打印到控制台。
|
||||
|
||||
2. **依赖注入 (The DI)**:
|
||||
|
||||
- 在 `internal/pkg/log` 暴露一个 `Provider` 函数,供 Wire 使用。
|
||||
- 这个 Provider 返回的必须是**同一个**底层 Zap 实例的引用(或者其派生),确保配置(如 Level、Output Path)是一致的。
|
||||
|
||||
---
|
||||
|
||||
# 总结:最终的技术栈清单
|
||||
|
||||
|**组件**|**选型**|**理由**|
|
||||
|---|---|---|
|
||||
|**Logger Core**|`go.uber.org/zap` (v1.27+)|高性能、类型安全、零内存分配。|
|
||||
|**Rotation**|`gopkg.in/natefinch/lumberjack.v2`|成熟稳定,处理并发写文件不仅是事实标准,更是避坑指南。|
|
||||
|**Config**|`spf13/viper` (已引入)|利用现有的 Viper 读取 yaml 配置,实现动态等级调整。|
|
||||
|**Trace Inject**|`Custom Wrapper` (Standard Lib)|基于 `context` 的轻量封装,连接 `gin.Context` 与 `zap.Fields`。|
|
||||
|
||||
老师的最后叮嘱 (The Moral of the story):
|
||||
|
||||
我们现在的设计,本质上是在 Zap 的高性能 和 业务开发的便利性 之间走钢丝。
|
||||
|
||||
最大的风险在于:封装层写得太重。
|
||||
|
||||
如果我们在 log.Info 里面加了太多的锁、反射或者字符串拼接,那么引入 Zap 的意义就没了。所以,接下来的详细设计文档中,我们要时刻警惕“过度封装”。
|
||||
|
||||
如果这个技术栈基线你没有异议,我们就以此为基础,开始生成《全局日志模块详细设计规格说明书》。
|
||||
```
|
||||
|
||||
==== 02_ 日志\03_ 核心设计模式.md ====
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 核心模式:装饰器模式的变体 (Context-Decorator Pattern)
|
||||
date created: 星期三, 十二月 10日 2025, 10:37:54 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 10:38:26 晚上
|
||||
---
|
||||
|
||||
# 1. 核心模式:装饰器模式的变体 (Context-Decorator Pattern)
|
||||
|
||||
这是我们处理 `TraceID` 和上下文的核心手段。
|
||||
|
||||
- 传统误区 (Over-Abstraction):
|
||||
|
||||
定义一个庞大的 MyLogger 结构体,把 zap.Logger 藏在里面,然后重写 Info, Error 等所有方法。
|
||||
|
||||
- _后果_:维护成本极高,每次 Zap 更新或增加新特性(如 `Panic` 或 `DPanic`),你都得跟着改代码。且容易在转发参数时产生逃逸分析(Escape Analysis)导致的内存分配。
|
||||
- 我们的决策 (The Thin Wrapper):
|
||||
|
||||
只封装“获取 Logger”的动作,不封装“Logger 本身”。
|
||||
|
||||
我们将定义一个函数 log.WithContext(ctx context.Context) *zap.Logger。
|
||||
|
||||
- _行为_:这个函数极其轻量。它从 `ctx` 中取出 `TraceID`,调用 `zap.With()` 生成一个新的 Zap 实例并返回。
|
||||
- _优势_:业务代码拿到的依然是原生的 `*zap.Logger`。这意味着开发者可以直接使用 Zap 强大的 `zap.String`, `zap.Int` 等强类型字段构建方法,享受极致性能,没有任何中间层损耗。
|
||||
|
||||
# 2. 接口策略:拒绝通用接口 (Concrete Type Dependency)
|
||||
|
||||
这是 Go 语言工程实践中关于日志的一个特殊共识,也是反直觉的地方。
|
||||
|
||||
- 传统误区 (The Java/Interface Way):
|
||||
|
||||
定义一个 type ILogger interface { Info(msg string, args …interface{}) }。
|
||||
|
||||
- _后果_:`args …interface{}` 会导致大量的反射(Reflection)和装箱(Boxing),这直接抹杀了 Zap 存在的意义。Zap 的核心设计哲学就是通过 `zap.Field` 避免使用 `interface{}`。
|
||||
- 我们的决策 (Concrete Type):
|
||||
|
||||
直接依赖 *zap.Logger 具体类型。
|
||||
|
||||
- _原则_:在 Handler、Service、Repository 层,注入的类型就是 `*zap.Logger`。
|
||||
- _测试怎么办_:不要 Mock 日志接口。在单元测试中,直接传入 `zap.NewNop()`(什么都不做)或者 `zap.NewExample()`(输出到测试控制台)。这比 Mock 一个接口要简单且真实得多。
|
||||
|
||||
# 3. 访问模式:混合单例与依赖注入 (The Hybrid Accessor)
|
||||
|
||||
结合之前讨论的 Option A+B,我们通过设计模式来解决“初始化顺序”和“热加载”的问题。
|
||||
|
||||
- 设计挑战:
|
||||
|
||||
如果 main.go 还没来得及读配置初始化 Logger,其他 init() 函数里就调用了日志,程序会 Panic。
|
||||
|
||||
- **我们的决策 (Thread-Safe Proxy)**:
|
||||
- **原子替换 (Atomic Swap)**:全局变量 `globalLogger` 不会直接暴露给外部修改。我们将使用 `unsafe.Pointer` 或 `atomic.Value` (配合 Zap 的 `ReplaceGlobals`) 来保证在运行时重新加载配置(如动态修改 Log Level)时,不会发生并发读写冲突。
|
||||
- **懒汉式兜底 (Lazy Fallback)**:在 `internal/pkg/log` 的 `init()` 中,我们会默认初始化一个 `Console Logger`。这样即使 `main` 函数一行代码都没跑,只要引用了包,日志功能就是可用的(虽然配置是默认的)。这极大提升了开发体验(DX)。
|
||||
|
||||
# 4. 字段构建模式:结构化优先 (Field-First API)
|
||||
|
||||
这关乎团队的编码规范,属于 API 设计模式。
|
||||
|
||||
- 传统误区 (Printf Style):
|
||||
|
||||
使用 SugaredLogger 的 Infof("User %s login failed, error: %v", user, err)。
|
||||
|
||||
- _后果_:日志分析系统(ELK)只能拿到一串文本,无法对 `user` 进行聚合统计。
|
||||
- 我们的决策 (Structured Style):
|
||||
|
||||
默认只暴露 Logger(强类型),在必要时才暴露 SugaredLogger。
|
||||
|
||||
- _强制规范_:代码中必须写成 `log.Info("user login failed", zap.String("user", user), zap.Error(err))`。
|
||||
- _设计意图_:通过 API 的设计,“强迫”开发者思考每一个字段的语义。这虽然写起来繁琐一点,但对于后期的运维和排查是无价的。
|
||||
|
||||
---
|
||||
|
||||
# 总结:设计规格书的基调
|
||||
|
||||
基于以上讨论,在接下来的规格说明书中,我们将确立以下基调:
|
||||
|
||||
1. **不造轮子**:核心逻辑全权委托给 `zap` 和 `lumberjack`。
|
||||
2. **薄封装**:`pkg/log` 代码行数应控制在 200 行以内,只做配置解析和 Context 桥接。
|
||||
3. **强类型**:严禁在核心路径使用 `interface{}`。
|
||||
4. **显式传递**:通过 `WithContext` 显式传递上下文,而不是依赖某些黑魔法(如 Goroutine Local Storage)。
|
||||
```
|
||||
|
||||
==== 02_ 日志\04_ 架构逻辑.md ====
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 代码组织方式 (Code Organization)
|
||||
date created: 星期三, 十二月 10日 2025, 10:42:21 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:38:44 晚上
|
||||
---
|
||||
|
||||
# 1. 代码组织方式 (Code Organization)
|
||||
|
||||
我们将遵循 **“高内聚、低耦合”** 的原则,将日志模块放置在 `internal/pkg/log` 下。这里是所有日志逻辑的物理家园。
|
||||
|
||||
建议的文件结构如下(逻辑分层):
|
||||
|
||||
- **`log.go` (Facade/Entry Point)**:
|
||||
- 这是对外暴露的统一入口。包含全局单例的定义、初始化函数 (`Init`)、以及最常用的静态方法代理(如 `Info`, `Error`, `WithContext`)。
|
||||
- **设计意图**: 让其他模块只 import 这一个包就能完成 90% 的工作。
|
||||
- **`options.go` (Configuration)**:
|
||||
- 定义配置结构体(Level, Filename, MaxSize, MaxAge 等)。
|
||||
- **设计意图**: 将配置解析逻辑与日志初始化逻辑分离,方便单元测试。
|
||||
- **`zap.go` (Core Implementation)**:
|
||||
- 负责 `zap.Logger` 的具体构建。包含 Encoder 配置(JSON vs Console)、Writer 配置(Lumberjack 集成)和 Level 动态调整逻辑。
|
||||
- 这是“脏活累活”集中的地方,屏蔽 Zap 的复杂构建细节。
|
||||
- **`context.go` (The Bridge)**:
|
||||
- **核心组件**。实现 `TraceID` 的提取逻辑。
|
||||
- 定义如何从 `context.Context` 中挖掘元数据,并将其转化为 `zap.Field`。
|
||||
|
||||
---
|
||||
|
||||
# 2. 调用方式与依赖注入 (Invocation & DI)
|
||||
|
||||
这里有一个经典的架构冲突:**Singleton(单例) vs Dependency Injection(依赖注入)**。我们的策略是 **“依赖注入为主,单例为辅”**,但在具体使用上有一个极其重要的**反直觉设计**。
|
||||
|
||||
## A. 为什么 Service 层不应保存 Request Logger?
|
||||
|
||||
你可能会想在 Service 初始化时注入一个带 Context 的 Logger。
|
||||
|
||||
- **错误做法**: `type UserService struct { logger *zap.Logger }`,然后在请求进来时试图把 request-scoped 的 logger 塞进去。
|
||||
- **架构事实**: 在 Wire 依赖注入中,`Service`、`Repository` 通常是 **单例 (Singleton)** 的(即整个应用生命周期只有一个实例)。
|
||||
- **结论**: 你**不能**把属于某一次 HTTP 请求的 `TraceID` 注入到单例的 Struct 成员变量中。
|
||||
|
||||
## B. 正确的调用范式 (The Best Practice)
|
||||
|
||||
Logger 作为**工具能力**被注入,Context 作为**请求参数**被传递。
|
||||
|
||||
1. **依赖注入 (Setup Phase)**:
|
||||
|
||||
- 在 `NewUserUsecase` 时,注入基础的 `*zap.Logger`(不带 TraceID)。
|
||||
- 这个 Logger 配置好了输出路径、Level 等全局属性。
|
||||
|
||||
2. **方法调用 (Runtime Phase)**:
|
||||
|
||||
- 在具体的方法(如 `Register`)中,使用 `log.WithContext(ctx)` 来“临时”生成一个带有 TraceID 的 Logger 实例。
|
||||
|
||||
**示例逻辑流**:
|
||||
|
||||
- **Struct 定义**: `struct { baseLogger *zap.Logger }`
|
||||
- **方法内部**: `l := log.WithContext(ctx, u.baseLogger)` -> `l.Info("user registered")`
|
||||
- **说明**: 这里的 `WithContext` 是一个纯内存操作(浅拷贝),开销极小,可以放心高频调用。
|
||||
|
||||
## C. 高性能场景:作用域复用 (Scoped Logger)
|
||||
|
||||
虽然 `log.WithContext` 是浅拷贝,但在循环或长链路中频繁调用仍会产生大量临时对象,增加 GC 压力。
|
||||
|
||||
- **反模式 (Anti-Pattern)**: 在 `for` 循环内部调用 `log.WithContext(ctx)`。
|
||||
- **最佳实践 (Best Practice)**: **作用域提升**。在函数或循环入口处调用一次 `WithContext`,生成局部变量 `l` (Logger),随后全程复用该变量。
|
||||
|
||||
---
|
||||
|
||||
# 3. 数据流与 TraceID 传递 (Data Flow)
|
||||
|
||||
这是实现“全链路可观测性”的生命线。数据流必须打通以下四个关卡:
|
||||
|
||||
## 关卡 1:入口 (Entry - Middleware)
|
||||
|
||||
- **位置**: `internal/middleware/trace.go` (需新建) 或集成在 `response` 包中。
|
||||
- **行为**: 当 HTTP 请求到达,生成一个 UUID。
|
||||
- **动作**: 使用 `c.Set("X-Trace-ID", uuid)` 将其放入 Gin 的上下文存储中。同时,将其放入 HTTP Response **动作**:
|
||||
1. 调用 `pkg/log.WithTraceID(ctx, uuid)` 将 `UUID` 注入标准 `Context`。
|
||||
2. 执行 `c.Request = c.Request.WithContext(newCtx)` 将其回写。
|
||||
3. (可选) 同时放入 Gin 上下文存储和 Response Header 供前端使用。
|
||||
|
||||
## 关卡 2:桥接 (Bridge - Context Adapter)
|
||||
|
||||
- **位置**: `internal/pkg/log/context.go`
|
||||
- **设计原则**: `pkg/log` **不依赖** `gin`,只识别标准库 `context.Context`。
|
||||
- **行为**: `log.WithContext(ctx) 调用内部帮助函数 GetTraceID(ctx) 获取 TraceID。`
|
||||
- **前置条件**: 必须依赖上游(Middleware)将 TraceID 提前注入到标准 Context 中。
|
||||
- **输出**: 返回一个预置了 `zap.String("trace_id", id)` 字段的 Logger。
|
||||
|
||||
## 关卡 3:穿透 (Propagation - Service/Repo)
|
||||
|
||||
- **行为**: 所有的业务方法签名必须包含 `ctx context.Context` 作为第一个参数。
|
||||
- **动作**: 严禁在层级调用中丢弃 Context(例如使用 `context.Background()` 替代传入的 ctx),这会导致链路断裂。
|
||||
|
||||
## 关卡 4:异步与后台边界 (Async & Background Boundary)
|
||||
|
||||
- **高危场景**: 在 Handler 中启动 Goroutine 处理耗时任务。
|
||||
- **陷阱**: `gin.Context` 是非线程安全的。如果 Goroutine 执行时 HTTP 请求已结束,Gin 会重置该 Context,导致数据竞争或脏读。
|
||||
- **解决方案**: 必须在主协程中执行 `ctx.Copy()`,将副本传递给 Goroutine。日志模块必须支持处理这种副本 Context。
|
||||
- **新增场景:后台任务 (Background Tasks)**
|
||||
- **场景**: 定时任务 (Cron)、消息队列消费者 (MQ Consumer)、系统初始化。
|
||||
- **问题**: 初始 `context.Background()` 不包含 TraceID。
|
||||
- **动作**: 必须调用 `log.StartBackgroundTrace(ctx)` 进行“播种”。该函数会检测 Context,若无 TraceID 则生成新 ID 并注入,确保链路可追踪。
|
||||
|
||||
---
|
||||
|
||||
# 4. 关键架构思考:防腐层 (Anti-Corruption Layer)
|
||||
|
||||
我们在设计时还需考虑一层“防腐”。
|
||||
|
||||
- **问题**: 如果未来我们想给所有的日志加一个字段,比如 `env=prod`,或者想把所有的 `trace_id` 改名为 `traceId`。
|
||||
- **对策**: 所有的业务代码**严禁**直接手动构建 `zap.String("trace_id", …)`。
|
||||
- **约束**: 这个字段的 Key 必须定义在 `pkg/log` 的常量中,且只能由 `WithContext` 内部逻辑自动附加。业务开发者只负责传 Context,不负责管 ID 怎么拼写。
|
||||
|
||||
---
|
||||
|
||||
# 总结
|
||||
|
||||
- **代码位置**: `internal/pkg/log`,包含 `log.go` (入口), `zap.go` (实现), `context.go` (桥接)。
|
||||
- **调用方式**: 注入 Base Logger -> 方法内 `WithContext(ctx)` -> 打印。
|
||||
- **数据流**: Middleware 生成 -> Gin Context 携带 -> Log Adapter 提取 -> Zap Field 输出。
|
||||
- **并发安全**: 警惕 Gin Context 在 Goroutine 中的误用,强调 `Copy()` 机制。
|
||||
```
|
||||
|
||||
==== 02_ 日志\05_ 目录结构与职责.md ====
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 目录结构与职责
|
||||
date created: 星期三, 十二月 10日 2025, 10:45:40 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:40:48 晚上
|
||||
---
|
||||
|
||||
# 目录结构与职责
|
||||
|
||||
## 1. 目录结构设计 (Directory Structure)
|
||||
|
||||
该结构旨在实现 **“配置分离”**、**“核心隐藏”** 与 **“上下文桥接”**。
|
||||
|
||||
```Plaintext
|
||||
internal/
|
||||
├── middleware/ # [Global] 全局中间件层
|
||||
│ ├── access_log.go # [New] HTTP 请求访问日志 (请求入/出记录, 耗时统计)
|
||||
│ └── trace.go # [New] 链路追踪 (生成/透传 TraceID -> 注入 Context)
|
||||
│
|
||||
└── pkg/
|
||||
└── log/ # [Level 0] 全局日志核心包 (基于 Zap)
|
||||
├── log.go # [Facade] 对外入口 (Init, Global L(), Static Proxies)
|
||||
├── options.go # [Config] 配置定义 (Level, FilePath, MaxSize)
|
||||
├── zap.go # [Core] Zap 实例构建 (Encoder, Core, AtomicLevel)
|
||||
├── writer.go # [IO] 输出源管理 (Lumberjack 轮转, Console/File 双写)
|
||||
├── context.go # [Bridge] 上下文桥接 (WithContext, TraceID 提取)
|
||||
└── standard.go # [Schema] 标准字段定义 (Standardized Field Constructors)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 文件职责详解 (Responsibilities)
|
||||
|
||||
### A. `internal/pkg/log` (核心日志包)
|
||||
|
||||
这是一个基础设施包,不应依赖任何业务逻辑(User, Order 等)。
|
||||
|
||||
| **文件名** | **职责描述** | **关键设计点 (Design Decisions)** |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`log.go`** | **门面 (Facade) 与单例管理**。<br>1. 维护私有全局变量 `globalLogger`。<br>2. 提供 `Init(opts)` 初始化入口。<br>3. 提供 `L()` 获取底层 `*zap.Logger`。<br>4. 提供 `Info/Error` 等静态代理方法。 | **单例兜底**:在 `init()` 中初始化一个默认的 `Nop` 或 `Console` Logger,防止未初始化调用导致 Panic。<br>**Caller 修正**:<br>1. 底层 `globalLogger` 配置 `AddCallerSkip(0)`。<br>2. 静态代理方法 (`Info`, `Error`) 内部使用 `WithOptions(AddCallerSkip(1))`。<br>3. `L()` 和 `WithContext()` 返回原生 Logger (Skip 0),确保业务层直接调用时行号正确。 |
|
||||
| **`options.go`** | **配置对象 (DTO)**。<br>定义 `Options` 结构体,用于接收 Viper 的配置映射。 | **配置解耦**:只定义 struct,不包含逻辑。支持从 `config.yaml` 的 `log` 节点自动 Unmarshal。 |
|
||||
| **`zap.go`** | **核心构建工厂 (Factory)**。<br>负责组装 Encoder (JSON/Console)、Writer 和 Level。<br>实现 `New(opts)` 函数。 | **环境隔离**:<br>- Dev: ConsoleEncoder + StackTrace (Warn 级)<br>- Prod: JsonEncoder + StackTrace (Panic 级) |
|
||||
| **`writer.go`** | **IO 输出管理**。<br>封装 `lumberjack.Logger`。<br>实现 `zapcore.WriteSyncer` 接口。 | **可靠性**:配置 `Lumberjack` 的 `Compress: true` 和 `MaxSize: 100MB`。实现 Console + File 的 **Tee (双写)** 模式。 |
|
||||
| **`context.go`** | **上下文装饰器与播种器 (Decorator & Seeder)**。<br>1. `WithContext(ctx)`: 提取 TraceID。<br>2. **[New] `StartBackgroundTrace(ctx)`**: 为后台任务生成并注入根 TraceID。 | **零侵入**:仅通过 `zap.With()` 附加字段,返回 **派生 Logger**,不修改全局 Logger,线程安全。 |
|
||||
| **`standard.go`** | **标准化字段与存取器 (Schema & Accessor)**。<br>1. 定义**私有** Context Key 类型,防止碰撞。<br>2. 提供 `WithTraceID(ctx, id)` 和 `GetTraceID(ctx)` 公开方法。<br>3. 定义标准字段构造器 (如 `zap.String("trace_id", …)`)。 | **规范约束**:<br>- 统一使用 snake_case。<br>- 防止拼写错误 (如 `uid` vs `user_id`)。 |
|
||||
|
||||
### B. `internal/middleware` (中间件集成)
|
||||
|
||||
这是日志模块与 HTTP 框架 (Gin) 结合的触点。
|
||||
|
||||
| **文件名** | **职责描述** | **交互逻辑** |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| **`trace.go`** | **链路起点**。<br>链路追踪 (生成 TraceID -> **注入标准 Context** -> 挂载回 Gin Request) | **上下游打通**:保证 TraceID 在微服务或网关间的透传能力。 |
|
||||
| **`access_log.go`** | **流量审计**。<br>1. 记录 `Start Time`。<br>2. 执行 `c.Next()`。<br>3. 计算 `Latency`。<br>4. 打印结构化日志。 | **字段映射**:<br>`path`, `method`, `status`, `client_ip`, `latency`, `user_agent`。**必须使用 `log.WithContext(c)`**。 |
|
||||
| `recovery.go` | 结构化灾难恢复。<br>1. `defer recover()` 捕获 Panic。<br>2. 获取 Stack Trace。<br>3. **调用 `pkg/log` 记录 JSON 格式的 Error 日志** (包含 `stack` 字段)。<br>4. 返回 500 响应。 | **替代 Gin 默认组件**:必须使用 `gin.New()` 启动,手动注册此中间件,杜绝默认的控制台文本打印。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据流转图示 (Data Flow)
|
||||
|
||||
为了确保你理解“上下文”是如何流动的,以下是逻辑路径:
|
||||
|
||||
1. **Request In** -> `middleware/trace.go` -> 生成 `trace_id` -> **Wrap 进 `std.Context`**。 …
|
||||
2. **`pkg/log/context.go`** -> 从 **`std.Context`** 取出 `trace_id` -> …
|
||||
3. **`pkg/log/context.go`** -> 从 `gin.Context` 取出 `trace_id` -> 创建带字段的 `zap.Logger`。
|
||||
4. **`pkg/log/zap.go`** -> 序列化为 JSON `{…"trace_id":"xyz"…}`。
|
||||
5. **`pkg/log/writer.go`** -> 写入 `app.log` 文件 (由 Lumberjack 轮转)。
|
||||
|
||||
## 4. 依赖关系检查 (Dependency Check)
|
||||
|
||||
- `pkg/log` **不依赖** `middleware` (防止循环依赖)。
|
||||
- `middleware` **依赖** `pkg/log` (调用日志打印)。
|
||||
- `pkg/log` **仅依赖** `uber-go/zap`, `natefinch/lumberjack`。**严禁依赖** `gin` 或其他 Web 框架。所有 Context 操作均基于 Go 标准库接口。
|
||||
|
||||
这个结构完全穷尽了我们在前几轮讨论中确定的技术决策。如果确认无误,我们将在下一步生成具体的代码实现。
|
||||
|
||||
```bash
|
||||
|
||||
==== 02_日志\06_日志模块开发规范与质量保证手册.md ====
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 《日志模块开发规范与质量保证手册》
|
||||
- 一、 核心开发规范 (The Golden Rules)
|
||||
date created: 星期三, 十二月 10日 2025, 10:53:19 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:31:04 晚上
|
||||
---
|
||||
|
||||
# 《日志模块开发规范与质量保证手册》
|
||||
|
||||
---
|
||||
|
||||
## 一、 核心开发规范 (The Golden Rules)
|
||||
|
||||
这部分是“软约束”,属于团队共识,通过 Code Review 和 AI 辅助检查来执行。
|
||||
|
||||
### 1. 键名命名公约 (Key Naming Convention)
|
||||
|
||||
日志是给机器(ELK/Loki)读的,键名必须统一,方便建立索引。
|
||||
|
||||
- **规则**: 严禁使用 CamelCase (小驼峰) 或 PascalCase (大驼峰),**必须且只能使用 snake_case (下划线命名)**。
|
||||
- **反例**: `userId`, `IPAddress`, `httpStatus`
|
||||
- **正例**: `user_id`, `client_ip`, `http_status`
|
||||
- **理由**: 多数数据库和搜索引擎(如 Elasticsearch)的分词器对下划线更友好,且 SQL 查询习惯也是下划线。
|
||||
|
||||
### 2. 类型安全铁律 (Type Safety Strictness)
|
||||
|
||||
利用 Zap 的强类型优势,拒绝隐式转换。
|
||||
|
||||
- **规则**: 在业务热点路径(Hot Path)中,**严禁使用 `zap.Any`、`zap.Reflect` 或 `Sugar` 模式**。
|
||||
- **例外**: 仅在应用启动(Init)、Panic 恢复或非高频的配置加载阶段允许使用 `SugaredLogger`。
|
||||
- **理由**: `zap.Any` 会触发反射(Reflection),导致内存逃逸和 GC 压力。这是高性能系统的“隐形杀手”。
|
||||
|
||||
### 3. 上下文优先原则 (Context First)
|
||||
|
||||
日志不是孤岛,必须依附于请求上下文。
|
||||
|
||||
- **规则**: 所有 Controller、Service、Repository 层的方法,如果需要打印日志,**必须**使用 `log.WithContext(ctx).Info(…)` 及其变体。
|
||||
- **禁止**: 严禁在业务流程中直接调用全局的 `log.Info(…)`(除非是系统级事件,如定时任务启动)。
|
||||
- **理由**: 只有通过 `WithContext`,才能将 TraceID 串联起来。
|
||||
|
||||
### 4. 哨兵值与魔法字符串 (Sentinels & Magic Strings)
|
||||
|
||||
- **规则**: 核心日志字段的 Key 必须定义为常量(Constant)。
|
||||
- **实现**: 在 `pkg/log/standard.go` 中定义 `const TraceIDKey = "trace_id"`。
|
||||
- **禁止**: 代码中出现手写的 `zap.String("trace_id", …)`,防止拼写错误(如写成 `traceid`)。
|
||||
|
||||
### 5. 热点路径复用原则 (Hot Path Reuse)
|
||||
|
||||
针对循环(Loop)或复杂长流程函数,严禁重复构建 Context Logger。
|
||||
|
||||
- **规则**: 必须在作用域入口处初始化 Logger 实例,并在该作用域内复用。
|
||||
- **反例 (Bad)**:
|
||||
|
||||
```Go
|
||||
for _, item := range items {
|
||||
// ❌ 每次循环都分配内存
|
||||
log.WithContext(ctx).Info("processing", zap.String("id", item.ID))
|
||||
}
|
||||
```
|
||||
|
||||
- **正例 (Good)**:
|
||||
|
||||
```Go
|
||||
// ✅ 只分配一次,复用 l
|
||||
l := log.WithContext(ctx)
|
||||
for _, item := range items {
|
||||
l.Info("processing", zap.String("id", item.ID))
|
||||
}
|
||||
```
|
||||
|
||||
- **理由**: 减少大量临时的 `zap.Logger` 结构体分配,降低 GC 的 Scavenge 阶段耗时。
|
||||
|
||||
### 6. 后台任务播种原则 (Background Trace Seeding)
|
||||
|
||||
所有非 HTTP 触发的后台任务入口(Goroutine, Cron, MQ Handler),必须是“有状态”的。
|
||||
|
||||
- **规则**: 任务的第一行代码必须调用 `StartBackgroundTrace`。
|
||||
- **反例 (Bad)**:
|
||||
|
||||
```Go
|
||||
func ProcessOrder(msg []byte) {
|
||||
ctx := context.Background()
|
||||
// ❌ 此时 ctx 空空如也,日志将丢失 TraceID
|
||||
log.WithContext(ctx).Info("processing order")
|
||||
}
|
||||
```
|
||||
|
||||
- **正例 (Good)**:
|
||||
|
||||
```Go
|
||||
func ProcessOrder(msg []byte) {
|
||||
// ✅ 自动生成一个新的 TraceID 注入 ctx
|
||||
ctx := log.StartBackgroundTrace(context.Background())
|
||||
log.WithContext(ctx).Info("processing order")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、 Linter 规则配置 (Automated Enforcement)
|
||||
|
||||
这部分是“硬约束”,我们将在 `.golangci.yml` 中配置这些规则,强行阻断不合规代码的提交。
|
||||
|
||||
### 1. 禁用标准库日志 (`depguard`)
|
||||
|
||||
防止开发人员手滑使用了 Go 原生的 `log` 或 `fmt` 打印日志。
|
||||
|
||||
Linter: depguard
|
||||
|
||||
配置策略:
|
||||
|
||||
- **Deny**:
|
||||
- `log`: 标准库日志(无结构化,无法分级)。
|
||||
- `fmt.Print*`: 控制台打印(生产环境绝对禁止)。
|
||||
- `github.com/sirupsen/logrus`: 防止引入其他日志库。
|
||||
|
||||
### 2. 强制错误处理 (`errcheck`)
|
||||
|
||||
Zap 的 `Sync()` 方法可能会返回错误(特别是在 Linux 的 `/dev/stdout` 上),通常需要忽略,但写入文件的错误不能忽略。
|
||||
|
||||
Linter: errcheck / gosec
|
||||
|
||||
配置策略:
|
||||
|
||||
- 对 `logger.Sync()` 的错误处理进行豁免(Exclude),因为在某些 OS 下 stdout sync 必然报错,这是已知 issue。
|
||||
- 但对 `logger.Info` 等方法的 IO 错误,原则上 Zap 内部处理了,不需要业务层捕获。
|
||||
|
||||
### 3. 自定义规则 (`ruleguard` - 高级)
|
||||
|
||||
标准的 Linter 无法检测“键名必须是 snake_case”。如果需要极致的管控,我们可以引入 `ruleguard`。
|
||||
|
||||
AI 辅助检查逻辑:
|
||||
|
||||
由于配置 ruleguard 较复杂,我们约定在 AI 生成代码阶段 执行此逻辑:
|
||||
|
||||
- **Check 1**: 正则匹配所有 `zap.String("([a-z]+[A-Z][a-z]+)", …)` 模式,如果发现驼峰命名,立刻自我修正。
|
||||
- **Check 2**: 扫描代码中是否存在 `fmt.Print`,如有则报错。
|
||||
|
||||
---
|
||||
|
||||
## 三、 安全与脱敏规范 (Security & Masking)
|
||||
|
||||
这是日志系统的“红线”。
|
||||
|
||||
### 1. PII (个人敏感信息) 零容忍
|
||||
|
||||
- **黑名单字段**: `password`, `token`, `access_token`, `refresh_token`, `credit_card`, `id_card`.
|
||||
- **处理方式**:
|
||||
- **方案 A (拦截器)**: 在 `zapcore` 层加 Hook,但这会损耗性能。
|
||||
- **方案 B (显式脱敏)**: 要求 AI 在生成代码时,对于敏感字段,自动包裹脱敏函数。例如 `zap.String("mobile", mask.Mobile(u.Mobile))`。
|
||||
- **决策**: 采用 **方案 B**。依赖编码时的自觉和 AI 的辅助,性能最优。
|
||||
|
||||
### 2. 大字段截断
|
||||
|
||||
- **规则**: 禁止将 Base64 图片数据、巨大的 HTML 内容直接打入日志。
|
||||
- **限制**: 单个 Field 的 Value 长度建议限制在 2KB 以内。
|
||||
|
||||
---
|
||||
|
||||
## 四、 AI 辅助编码的“质量契约” (AI Quality Contract)
|
||||
|
||||
为了确保我(AI)生成的代码符合上述规范,请你(用户)在审查我的代码时,使用以下 **Checklist** 进行验证。这也是我对你的承诺:
|
||||
|
||||
1. **Imports 检查**: 确认没有引入 `log` 或 `fmt`。
|
||||
2. **Context 检查**: 确认 `log.WithContext(ctx)` 是日志调用的唯一起手式。
|
||||
3. **Keys 检查**: 确认所有 JSON Key 都是 `snake_case`。
|
||||
4. **Args 检查**: 确认使用的是 `zap.String/Int` 等强类型构造器,而非 `zap.Any`。
|
||||
5. **Config 检查**: 确认没有硬编码的路径(如 `/var/log`),必须来自 `options.go`。
|
||||
|
||||
---
|
||||
|
||||
## 五、 总结与下一步
|
||||
|
||||
我们确立了:
|
||||
|
||||
1. **命名**: 强制 snake_case。
|
||||
2. **类型**: 拒绝 `zap.Any`,拒绝 `fmt`。
|
||||
3. **上下文**: 强制 `WithContext`。
|
||||
4. **安全**: 显式脱敏。
|
||||
```
|
||||
|
||||
==== 02_ 日志\07_ 日志模块工程化实施标准.md ====
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 日志模块工程化实施标准
|
||||
date created: 星期三, 十二月 10日 2025, 10:58:53 晚上
|
||||
date modified: 星期三, 十二月 10日 2025, 11:42:26 晚上
|
||||
---
|
||||
|
||||
# 日志模块工程化实施标准
|
||||
|
||||
---
|
||||
|
||||
## 一、 注释与文档规范 (Documentation Standards)
|
||||
|
||||
目标:“中文友好 (Chinese Friendly)” 且 “符合 GoDoc 标准”。
|
||||
|
||||
我们采用 混合语言策略:结构定义用英文(为了 IDE 兼容性),业务解释用中文(为了团队协作)。
|
||||
|
||||
### 1. 导出的包与函数 (Exported Symbols)
|
||||
|
||||
所有对外暴露的函数(首字母大写),必须编写文档注释。
|
||||
|
||||
- **格式要求**:
|
||||
- 第一行:`// FunctionName 简短的英文或中文摘要` (符合 Go Lint 检查)。
|
||||
- 空一行。
|
||||
- 详细说明:**必须使用中文**,解释函数的行为、副作用(Side Effects)和潜在风险。
|
||||
- 参数说明:如果有复杂参数,使用 `// - param: explanation` 格式。
|
||||
- **范例 (Style Guide)**:
|
||||
|
||||
> // WithContext returns a logger with the trace ID injected.
|
||||
>
|
||||
> //
|
||||
>
|
||||
> // [功能]: 从 context.Context 中提取 TraceID 并附加到 Logger 字段中。
|
||||
>
|
||||
> // [注意]: 这是一个轻量级操作,但如果 ctx 为 nil,将返回原始 Logger 的 fallback。
|
||||
>
|
||||
> // [场景]: 务必在 Controller 或 Service 的入口处优先调用。
|
||||
|
||||
### 2. 内部实现细节 (Internal Logic)
|
||||
|
||||
对于 `internal/pkg/log` 内部复杂的逻辑(如 `lumberjack` 的配置转换),必须在代码块上方添加中文注释。
|
||||
|
||||
- **原则**:解释 **“为什么这么做 (Why)”**,而不是“做了什么 (What)”。代码本身已经展示了做了什么。
|
||||
- **范例**:
|
||||
|
||||
> // [Why]: 这里不使用 zap.NewProduction 自带的 OutputPaths,
|
||||
>
|
||||
> // 因为我们需要同时输出到控制台 (为了 Docker 采集) 和文件 (为了本地容灾),
|
||||
>
|
||||
> // 且文件输出需要通过 Lumberjack 进行轮转控制。
|
||||
|
||||
### 3. README 维护
|
||||
|
||||
在 `internal/pkg/log/README.md` 中维护一份**“速查手册”**。
|
||||
|
||||
- **必填内容**:
|
||||
- 如何在 `config.yaml` 中配置(给出默认值)。
|
||||
- 如何动态调整日志级别(如通过信号或 API)。
|
||||
- 常见错误码(Code)与日志关键字的对应关系。
|
||||
|
||||
---
|
||||
|
||||
## 二、 可拓展性设计 (Extensibility Design)
|
||||
|
||||
虽然我们拒绝“过度封装”,但必须为未来的变化预留接口(Hook Points)。
|
||||
|
||||
### 1. 配置扩展:Functional Options 模式
|
||||
|
||||
我们在 `Init` 函数中,不应列出所有参数,而应使用 `Option` 模式。
|
||||
|
||||
- **设计**: `func Init(opts …Option) error`
|
||||
- **预留能力**: 未来如果需要添加“发送日志到 Kafka”或“开启 Sentry 报警”,只需新增一个 `WithKafka(addr)` 的 Option,而无需修改 `Init` 的函数签名,保证了对旧代码的兼容性。
|
||||
|
||||
### 2. 核心扩展:Zap Hooks
|
||||
|
||||
Zap 原生支持 `Hooks`。我们的封装必须暴露这一能力。
|
||||
|
||||
- **场景**: 当日志级别为 `Error` 或 `Fatal` 时,可能需要同步触发飞书/钉钉报警。
|
||||
- **实现标准**: 在 `zap.go` 的构建逻辑中,检查配置是否定义了 Hooks。这允许我们在不侵入日志核心代码的情况下,挂载报警逻辑。
|
||||
|
||||
### 3. 字段扩展:Context Key Registry
|
||||
|
||||
随着业务发展,需要记录的元数据会增加(如 `TenantID`, `RequestID`, `SpanID`)。
|
||||
|
||||
- **标准**: 不要在 `context.go` 里写死 key 的提取逻辑。
|
||||
- **设计**: 定义一个 `type ContextExtractor func(ctx) []Field` 类型。默认提供 `TraceIDExtractor`。允许在初始化时注册新的 Extractor。这使得业务线可以自定义需要提取的 Context 字段。
|
||||
|
||||
---
|
||||
|
||||
## 三、 查漏补缺 (Gap Analysis)
|
||||
|
||||
在之前的讨论中,有几个隐蔽但致命的工程细节尚未覆盖,这里作为最后防线进行补充。
|
||||
|
||||
### 1. 关于 `Logger.Fatal` 的使用禁令
|
||||
|
||||
- **风险**: `zap.Logger.Fatal` 会在打印日志后调用 `os.Exit(1)`。
|
||||
- **工程标准**: **在 Web 服务(HTTP Server)中,严禁在业务逻辑层调用 `Fatal`。**
|
||||
- _原因_: 这会直接杀死整个进程,导致所有正在处理的请求中断(没有 Graceful Shutdown)。
|
||||
- _替代_: 遇到不可恢复错误,使用 `Error` 级别日志,并返回 `500` 错误给客户端,由上层中间件处理。
|
||||
- _例外_: 仅在 `main.go` 启动阶段(如连不上数据库、读不到配置)可以使用 `Fatal`。
|
||||
|
||||
### 2. 时间格式的一致性
|
||||
|
||||
- **问题**: Zap 默认的时间格式可能是浮点数(Unix Epoch)或非标准字符串。
|
||||
- **标准**: 生产环境统一配置为 **`ISO8601` (2025-12-10T22:00:00.000Z)**。
|
||||
- _理由_: 这种格式跨时区友好,且能被几乎所有日志分析工具(ELK, Splunk, CloudWatch)自动识别并建立时间索引。
|
||||
|
||||
### 3. 动态日志级别 (Hot Reload)
|
||||
|
||||
- **需求**: 线上出 Bug 时,需要临时把 Level 调成 Debug,查完再调回 Info,且不能重启服务。
|
||||
- **实现标准**: 利用 `zap.AtomicLevel`。
|
||||
- 我们需要暴露一个 HTTP 接口(如 `PUT /admin/log/level`)或监听配置文件的 `fsnotify` 事件。
|
||||
- 收到变更信号后,直接调用 `atomicLevel.SetLevel(zap.DebugLevel)`。这是线程安全的,无需重启实例。
|
||||
|
||||
### 4. 测试支持 (Testing Support)
|
||||
|
||||
- **问题**: 单元测试时,不仅不想看到日志刷屏,有时还需要断言“是否打印了某条错误日志”。
|
||||
- **标准**:
|
||||
- 提供 `pkg/log/test_helper.go`。
|
||||
- 封装 `zaptest/observer`。
|
||||
- 允许测试代码通过 `log.NewTestLogger()` 获取一个观察者对象,从而断言 `logs.FilterMessage("error").Len() == 1`。
|
||||
|
||||
### 5. 链路完整性保障
|
||||
|
||||
- **风险**: 开发者容易遗忘在 `go func()` 中传递 Context。
|
||||
- **标准**: 在 Code Review 时,重点检查所有 `go` 关键字后是否跟随了 Context 的传递或播种操作。
|
||||
|
||||
### 6. 框架初始化与 Panic 处理
|
||||
|
||||
- **风险**: `gin.Default()` 会自动注册只打印文本日志的 Recovery 中间件,破坏 JSON 格式。
|
||||
- **标准**:
|
||||
1. 必须使用 `gin.New()` 初始化 Engine。
|
||||
2. 必须手动注册我们自定义的 `middleware.Recovery` 和 `middleware.AccessLog`。
|
||||
3. 确保 Panic 日志中包含 TraceID(从 `c.Request.Context` 中尝试恢复)。
|
||||
|
||||
---
|
||||
|
||||
## 四、 总结与就绪确认
|
||||
|
||||
至此,我们已经完成了日志模块的**全生命周期设计**:
|
||||
|
||||
1. **架构**: 基础设施层,无业务依赖。
|
||||
2. **技术栈**: Zap + Lumberjack + Context Adapter。
|
||||
3. **模式**: 单例兜底 + 依赖注入,强类型约束。
|
||||
4. **规范**: Snake_case 键名,中文友好文档,严禁 Fatal。
|
||||
```
|
||||
80
Go项目实战/中间件/推荐的中间件目录演进策略.md
Normal file
80
Go项目实战/中间件/推荐的中间件目录演进策略.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🏗️ 推荐的中间件目录演进策略
|
||||
date created: 星期三, 十二月 10日 2025, 11:08:17 上午
|
||||
date modified: 星期三, 十二月 10日 2025, 11:08:58 上午
|
||||
---
|
||||
这是一个非常有前瞻性的问题。你现在的顾虑完全符合 **“高内聚、低耦合”** 的架构直觉。
|
||||
|
||||
在企业级 Go 项目中,如果把所有中间件(Auth, CORS, RateLimit, Logger, Recovery, Trace…)都堆在 `internal/middleware/` 根目录下,确实会变成一个“大杂烩”,导致文件结构不明晰。
|
||||
|
||||
基于 **Modular Clean Architecture** 和 **Go 最佳实践**,我推荐采用 **“按复杂度分层 (Complexity-Based Layering)”** 的混合策略。
|
||||
|
||||
# 🏗️ 推荐的中间件目录演进策略
|
||||
|
||||
我们不需要一开始就过度设计(Over-engineering),而是制定一条**规则**:
|
||||
|
||||
1. **通用基础设施 (Universal Infra):** 逻辑简单、无状态、单文件即可解决的,直接放在根目录。
|
||||
2. **复杂业务逻辑 (Complex Logic):** 需要定义配置结构体、辅助函数、或依赖外部库较重的,**建立子目录**。
|
||||
|
||||
## 修正后的目录蓝图
|
||||
|
||||
```Plaintext
|
||||
internal/
|
||||
└── middleware/
|
||||
│
|
||||
│ # --- Level 1: 通用基础设施 (直接放根目录) ---
|
||||
├── recovery.go # [现有] 及其简单,就一个函数
|
||||
├── not_found.go # [现有] 就一个 Handler
|
||||
├── cors.go # [未来] CORS 配置通常单文件搞定
|
||||
├── trace.go # [未来] 注入 TraceID
|
||||
│
|
||||
│ # --- Level 2: 复杂中间件 (独立子包) ---
|
||||
├── auth/ # [未来] 认证模块很复杂
|
||||
│ ├── jwt.go # JWT 解析逻辑
|
||||
│ └── context.go # 将 UserID 注入 Context 的辅助函数
|
||||
│
|
||||
├── ratelimit/ # [未来] 限流可能涉及 Redis 脚本
|
||||
│ ├── ip_limiter.go
|
||||
│ └── lua_script.go
|
||||
│
|
||||
└── log/ # [未来] 访问日志
|
||||
└── access_logger.go
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 💡 为什么要这样设计?(Rationale)
|
||||
|
||||
## 1. Go 的包导入体验 (Import DX)
|
||||
|
||||
- 对于通用中间件:
|
||||
|
||||
我们在 main.go 里的调用通常希望长这样:
|
||||
|
||||
```Go
|
||||
// 读起来很通顺:使用中间件 -> 恢复、日志、CORS
|
||||
r.Use(middleware.Recovery())
|
||||
r.Use(middleware.Logger())
|
||||
r.Use(middleware.Cors())
|
||||
```
|
||||
|
||||
如果强行把 `recovery.go` 放到 `middleware/recovery/recovery.go`,调用就会变成 `recovery.New()`,这在语义上丢失了“这是一个中间件”的上下文,而且引入了太多的包名。
|
||||
|
||||
- 对于复杂中间件:
|
||||
|
||||
认证通常需要配置:
|
||||
|
||||
```Go
|
||||
// 读起来清楚:这是 Auth 相关的中间件
|
||||
r.Use(auth.Middleware(auth.Config{…}))
|
||||
```
|
||||
|
||||
这时候引入 `auth` 子包是合理的。
|
||||
|
||||
## 2. 避免循环依赖
|
||||
|
||||
`recovery.go` 和 `not_found.go` 属于**基础设施的底座**,它们几乎不依赖其他业务代码(除了我们刚才定义的 `app` 包)。将它们放在根目录,可以方便地被其他子包引用(虽然中间件之间很少互相引用)。
|
||||
|
||||
---
|
||||
149
Go项目实战/产品需求规格说明书 (PRD) - V1.1.md
Normal file
149
Go项目实战/产品需求规格说明书 (PRD) - V1.1.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 📝 产品需求规格说明书 (PRD) - V1.1
|
||||
date created: 星期日, 十二月 7日 2025, 12:14:41 中午
|
||||
date modified: 星期日, 十二月 7日 2025, 12:49:19 下午
|
||||
---
|
||||
|
||||
# 📝 产品需求规格说明书 (PRD) - V1.1
|
||||
|
||||
> **更新日志:**
|
||||
>
|
||||
> - v1.0: 初始版本,定义功能列表。
|
||||
>
|
||||
> - **v1.1:** [2025-12-07] 补充项目战略背景;优化软删除与缓存策略的灵活性;明确长文本存储类型。
|
||||
|
||||
项目名称: Enterprise-CMS-Core (企业级内容管理系统核心)
|
||||
|
||||
版本: 1.1.0
|
||||
|
||||
状态: [✅ 已锁定]
|
||||
|
||||
适用对象: 后端开发人员、架构师、测试人员
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目战略概述 (Strategic Overview)
|
||||
|
||||
### 1.1 项目背景与目标
|
||||
|
||||
本项目并非单纯为了交付一个 CMS 软件,而是为了构建一个**“Go 语言企业级后端架构样板间”**。
|
||||
|
||||
- **核心目标:** 验证并固化一套“模块化整洁架构”工程实践,使其具备**高可维护性**、**可扩展性**和**安全性**。
|
||||
- **衍生价值:** 产出的源码将作为团队未来的“SaaS 启动脚手架 (Boilerplate)”,或作为独立的高价值技术资产(源码付费产品)进行商业变现。
|
||||
|
||||
### 1.2 核心用户与价值
|
||||
|
||||
- **系统管理员 (Admin):** 痛点是“安全与失控风险”。核心价值是提供**银行级的 RBAC 权限控制**,确保没人能越权操作。
|
||||
- **内容编辑 (Editor):** 痛点是“流程混乱”。核心价值是提供**状态明确的内容流转机制**(草稿 ->审核 ->发布),防止误发。
|
||||
- **二开开发者 (Developer):** 痛点是“屎山代码”。核心价值是提供**清晰的依赖边界**和**开箱即用的基础设施**。
|
||||
|
||||
### 1.3 成功指标 (Success Metrics)
|
||||
|
||||
1. **业务完整性:** 必须完整支持 3 种标准角色(Admin/Editor/Subscriber)的权限隔离,且文章状态流转无逻辑漏洞。
|
||||
2. **工程质量:** 核心业务模块(User/Auth)单元测试覆盖率 > 80%;通过静态代码分析,无循环依赖。
|
||||
3. **性能基线:** 在单机 2C4G 配置下,并发 100 QPS 时,API P99 响应时间 < 200ms。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心功能范围 (In-Scope)
|
||||
|
||||
### 2.1 认证与鉴权模块 (Auth & IAM)
|
||||
|
||||
**唯一来源:** 必须使用 JWT 双令牌机制 + RBAC 模型。
|
||||
|
||||
- **F-AUTH-01 用户注册:** 仅支持“用户名 + 密码”注册。密码必须经过 Argon2 或 Bcrypt 哈希存储。
|
||||
- **F-AUTH-02 用户登录:** 校验账号密码,返回 `Access Token` (短效 15min) 和 `Refresh Token` (长效 7 天)。
|
||||
- **F-AUTH-03 令牌刷新:** 使用有效的 Refresh Token 换取新的 Access Token。**旧的 Refresh Token 若被复用需触发安全警报(可选)或直接失效**。
|
||||
- **F-AUTH-04 统一登出:** 强制使 Refresh Token 失效(需在 Redis 中建立黑名单或白名单机制)。
|
||||
- **F-AUTH-05 密码重置:** 登录状态下修改密码,修改成功后强制吊销所有 Token。
|
||||
|
||||
### 2.2 用户与权限模块 (User & RBAC)
|
||||
|
||||
**预设角色:** 系统初始化必须包含以下三种角色。
|
||||
|
||||
|**角色代码**|**名称**|**权限描述**|
|
||||
|---|---|---|
|
||||
|`admin`|超级管理员|拥有系统所有权限 (用户管理、角色分配、内容强制删除)。|
|
||||
|`editor`|内容编辑|拥有文章发布、审核、标签管理权限。不可管理用户。|
|
||||
|`subscriber`|普通用户|仅拥有修改自身资料、发布评论、查看公开文章权限。|
|
||||
|
||||
- **F-USER-01 个人资料:** 查询与更新当前登录用户的昵称、头像 URL、简介。
|
||||
- **F-USER-02 用户管理 (Admin):** 管理员可查看用户列表,封禁/解封用户状态。
|
||||
- **F-RBAC-01 角色分配 (Admin):** 管理员可修改用户的角色(如将 User 提权为 Editor)。
|
||||
|
||||
### 2.3 内容核心模块 (CMS Core)
|
||||
|
||||
**核心逻辑:** 文章必须包含状态流转。
|
||||
|
||||
- **F-ART-01 文章 CRUD:**
|
||||
- **创建:** 默认为 `Draft` (草稿) 状态。
|
||||
- **字段:** 标题、内容、封面图 URL、作者 ID。
|
||||
- **数据类型约束:** 文章内容字段在数据库层面建议使用 `TEXT` 或 `LONGTEXT` 类型,以完整承载 Markdown/HTML 长文本。
|
||||
- **F-ART-02 文章状态流转:**
|
||||
- 支持状态: `Draft` (草稿) -> `Pending` (待审核) -> `Published` (已发布) -> `Archived` (归档/软删除)。
|
||||
- **F-ART-03 分类与标签:**
|
||||
- 文章必须归属一个分类 (Category)。
|
||||
- 文章可关联多个标签 (Tags)。
|
||||
- **F-ART-04 内容审核 (Editor/Admin):**
|
||||
- 拥有审核权限的角色可将 `Pending` 状态的文章改为 `Published` 或驳回至 `Draft`。
|
||||
- **F-ART-05 公开检索:**
|
||||
- 仅 `Published` 状态的文章对外接口可见。支持按 分类、标签、标题关键词 搜索。
|
||||
|
||||
### 2.4 互动模块 (Interaction)
|
||||
|
||||
- **F-CMT-01 评论发布:** 登录用户可对 `Published` 文章发表评论。
|
||||
- **F-CMT-02 评论管理:** 作者可删除自己文章下的评论;Admin/Editor 可删除任何违规评论。
|
||||
|
||||
---
|
||||
|
||||
## 3. 非功能性需求 (Non-Functional Requirements)
|
||||
|
||||
**开发人员必须严格遵守以下技术约束:**
|
||||
|
||||
### 3.1 数据一致性
|
||||
|
||||
- **删除策略 [优化]:** 核心业务数据(用户、文章)原则上必须使用 Soft Delete (`deleted_at` 字段)。
|
||||
- _例外条款:_ 涉及法律合规(如 GDPR 用户遗忘权)或垃圾数据清理时,经系统管理员明确审批操作后,允许提供物理删除接口。
|
||||
- **事务:** 文章发布与标签关联必须在同一个 Database Transaction 中完成。
|
||||
|
||||
### 3.2 性能与缓存
|
||||
|
||||
- **API 响应:** 95% 的请求响应时间需 < 200ms (不含网络延迟)。
|
||||
- **缓存策略:**
|
||||
- 建议对 **高频读取且低频修改** 的数据(如用户信息 `/profile`、热门文章详情 `/article/:id`)实施缓存策略。
|
||||
- 具体的缓存实现(Redis Key 设计、TTL 时长、Cache-Aside 或 Write-Through 模式)由开发团队根据实际压测结果灵活调整,不强制硬编码 TTL。
|
||||
|
||||
### 3.3 安全性
|
||||
|
||||
- **SQL 注入:** 严禁拼接 SQL,必须使用 GORM 参数化查询。
|
||||
- **敏感数据:** 密码、RefreshToken 严禁明文出现在日志中。
|
||||
- **接口保护:** 除登录、注册、公开文章列表外,所有接口必须通过 JWT 中间件校验。
|
||||
|
||||
### 3.4 工程规范
|
||||
|
||||
- **Schema:** 数据库表结构变更必须提供 Up/Down SQL 迁移脚本。
|
||||
- **Doc:** 所有 API 必须自动生成 Swagger 文档。
|
||||
|
||||
---
|
||||
|
||||
## 4. 不在范围 (Out of Scope)
|
||||
|
||||
**以下功能明确不包含在本次 Phase 1 开发中:**
|
||||
|
||||
1. **❌ 第三方登录:** 不做微信/GitHub/Google 登录。
|
||||
2. **❌ 消息推送/通知:** 不做系统内通知。
|
||||
3. **❌ 文件存储服务 (OSS):** 仅处理 URL 字符串,不处理文件流上传。
|
||||
4. **❌ 复杂的富文本处理:** 后端仅存储字符串,不解析 HTML。
|
||||
5. **❌ 支付与订单:** 不包含任何电商逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心数据实体关系图 (ER 简述)
|
||||
|
||||
- **User** (1) <-> (N) **Article**
|
||||
- **User** (1) <-> (N) **Comment**
|
||||
- **Article** (1) <-> (N) **Comment**
|
||||
- **Article** (N) <-> (1) **Category**
|
||||
- **Article** (N) <-> (N) **Tag** (Many-to-Many)
|
||||
111
Go项目实战/用户模块/01_实体关系图.md
Normal file
111
Go项目实战/用户模块/01_实体关系图.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- ER 图
|
||||
date created: 星期二, 十二月 9日 2025, 10:45:43 晚上
|
||||
date modified: 星期二, 十二月 9日 2025, 10:58:01 晚上
|
||||
---
|
||||
|
||||
# ER 图
|
||||
|
||||
**设计思路分析:**
|
||||
|
||||
1. **RBAC 模型选择:** 为了满足“银行级权限控制”及“企业级样板间”的扩展性要求,我采用了标准的 **RBAC Level 1 (Flat RBAC)** 变体。虽然当前只有 3 个固定角色,但使用 **多对多 (Many-to-Many)** 的关联表 (`user_roles`) 能够支持未来某用户既是 "Editor" 又是 "TechLeader" 的混合权限场景,避免后续重构。
|
||||
2. **双令牌机制落地:** 专门设计了 `refresh_tokens` 表。JWT 的 Access Token 是无状态的(不入库),但 Refresh Token 必须入库以实现“吊销”、“防重放”和“设备管理”功能。
|
||||
3. **软删除与审计:** 所有核心表(`users`, `roles`)均继承了 Base Model,包含 `deleted_at` 字段。
|
||||
|
||||
---
|
||||
|
||||
## 📊 阶段二:概念验证 (Conceptual Modeling - ER Diagram)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
%% ---------------------------------------------------------
|
||||
%% 1. 用户核心表 (Users)
|
||||
%% 核心聚合根,包含认证凭证与个人资料
|
||||
%% ---------------------------------------------------------
|
||||
users {
|
||||
bigint id PK "主键"
|
||||
string username "用户名, unique, not null"
|
||||
string password_hash "Argon2/Bcrypt 哈希值, not null"
|
||||
string nickname "用户昵称 (Profile)"
|
||||
string avatar_url "头像链接 (Profile)"
|
||||
string bio "个人简介 (Profile)"
|
||||
smallint status "状态: 1=Active, 0=Banned"
|
||||
timestamptz created_at "创建时间"
|
||||
timestamptz updated_at "更新时间"
|
||||
timestamptz deleted_at "软删除时间 (Soft Delete)"
|
||||
}
|
||||
|
||||
%% ---------------------------------------------------------
|
||||
%% 2. 角色表 (Roles)
|
||||
%% 存储 Admin, Editor, Subscriber 等定义
|
||||
%% ---------------------------------------------------------
|
||||
roles {
|
||||
bigint id PK "主键"
|
||||
string code "角色编码 (e.g. 'admin'), unique"
|
||||
string name "角色显示名称 (e.g. '超级管理员')"
|
||||
string description "备注"
|
||||
timestamptz created_at
|
||||
timestamptz updated_at
|
||||
timestamptz deleted_at
|
||||
}
|
||||
|
||||
%% ---------------------------------------------------------
|
||||
%% 3. 用户-角色关联表 (User Roles)
|
||||
%% 中间表,实现 RBAC 多对多关系
|
||||
%% ---------------------------------------------------------
|
||||
user_roles {
|
||||
bigint user_id FK "关联 users.id"
|
||||
bigint role_id FK "关联 roles.id"
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ---------------------------------------------------------
|
||||
%% 4. 刷新令牌表 (Refresh Tokens)
|
||||
%% 用于双令牌机制的续期与风控
|
||||
%% ---------------------------------------------------------
|
||||
refresh_tokens {
|
||||
bigint id PK "主键"
|
||||
bigint user_id FK "关联 users.id"
|
||||
string token_hash "Refresh Token 的哈希值 (安全考虑不存明文)"
|
||||
string family_id "令牌家族ID (用于检测重用/Rotation)"
|
||||
string parent_token_id "父令牌ID (用于溯源)"
|
||||
boolean is_revoked "是否已撤销 (黑名单机制)"
|
||||
timestamptz expires_at "过期时间 (7天)"
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ---------------------------------------------------------
|
||||
%% 关系定义 (Relationships)
|
||||
%% ---------------------------------------------------------
|
||||
|
||||
%% 一个用户可以拥有多个 Refresh Token (多设备登录)
|
||||
users ||--o{ refresh_tokens : "has_many (sessions)"
|
||||
|
||||
%% 一个用户可以拥有多个角色 (扩展性设计)
|
||||
users ||--o{ user_roles : "assigned_to"
|
||||
|
||||
%% 一个角色可以包含多个用户
|
||||
roles ||--o{ user_roles : "includes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 关键关系业务含义说明
|
||||
|
||||
1. **Users `||--o{` Refresh Tokens (1 对 多):**
|
||||
|
||||
- **业务含义:** 一个用户可以在多个设备(手机、PC)同时登录。
|
||||
- **风控逻辑:** 当用户执行“修改密码”或“严重违规封号”时,后端会根据 `user_id` 批量将该用户下的所有 `is_revoked` 设为 `true`,从而实现**强制下线**。
|
||||
- **令牌轮转:** `family_id` 和 `parent_token_id` 用于检测令牌复用攻击。如果一个旧的 RT 被使用,系统可以根据 `family_id` 级联作废整个令牌链。
|
||||
|
||||
2. **Users `}|--|{` Roles (多 对 多,通过 `user_roles`):**
|
||||
|
||||
- **业务含义:** 虽然 PRD 定义了三个层级,但物理模型采用“多对多”设计是架构上的**防御性编程**。
|
||||
- **场景:** 如果未来需要赋予某个 Editor 临时的 Admin 权限,或者创建一个 "SuperSubscriber"(付费会员),我们只需在 `user_roles` 插入一条记录,而无需修改表结构。
|
||||
|
||||
3. **Users 表的混合设计:**
|
||||
|
||||
- **决策:** 我没有将 `Profile` (昵称/头像) 拆分为独立表。
|
||||
- **理由:** 遵循“勿增实体”原则。在当前 V1 阶段,Profile 字段较少(仅 3 个),且在列表页展示时(如“评论列表”)属于高频连表查询。合并不仅减少了 JOIN 开销,也简化了 GORM 的模型编写。
|
||||
212
Go项目实战/用户模块/02_SQL DDL 脚本.md
Normal file
212
Go项目实战/用户模块/02_SQL DDL 脚本.md
Normal file
@@ -0,0 +1,212 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- SQL DDL 脚本
|
||||
- 🛡️ 设计说明与合规性检查
|
||||
date created: 星期二, 十二月 9日 2025, 11:04:59 晚上
|
||||
date modified: 星期二, 十二月 9日 2025, 11:53:32 晚上
|
||||
---
|
||||
这里是为您生成的 **PostgreSQL 15** 生产级 SQL DDL 脚本。
|
||||
|
||||
# SQL DDL 脚本
|
||||
|
||||
## 🛡️ 设计说明与合规性检查
|
||||
|
||||
1. **架构一致性:** 严格遵循之前确认的 Mermaid ER 图逻辑。
|
||||
2. **软删除优化:** 所有包含 `deleted_at` 的表均增加了部分索引 (Partial Index),确保在使用 `WHERE deleted_at IS NULL` 时查询性能最优,同时减少索引体积。
|
||||
3. **安全性:**
|
||||
|
||||
- 所有外键均使用 `ON DELETE RESTRICT`,防止误删用户导致级联删除关联数据(如订单、日志)。
|
||||
- `refresh_tokens` 表的 `token_hash` 设为唯一,防止哈希碰撞。
|
||||
|
||||
4. **类型规范:** 遵循宪法,使用 `TIMESTAMPTZ` 处理时间,`TEXT` 处理字符串,`SMALLINT` 处理枚举状态。
|
||||
|
||||
---
|
||||
|
||||
### 📝 修改说明文档 (Changelog v1.1)
|
||||
|
||||
本次更新核心聚焦于 **“千万级数据下的查询性能”** 与 **“高频鉴权的延迟优化”**。
|
||||
|
||||
|**修改点**|**变更内容 (What)**|**预期效果 (Effect)**|**架构师理由 (Why)**|
|
||||
|---|---|---|---|
|
||||
|**1. 反范式化缓存**|`users` 表新增字段 `cached_role_codes TEXT[]`。|**鉴权性能提升 10x**。API 网关/中间件在鉴权时,无需关联查询 `user_roles` 和 `roles` 表,直接读取当前行即可拿到权限。|**Space for Time (以空间换时间)**。鉴权是系统最高频(QPS 10,000+)的操作,3 表 JOIN 在高并发下是 CPU 杀手。虽然增加了写入时的维护成本,但换来了极高的读取性能。|
|
||||
|**2. 深分页索引**|新增索引 `idx_users_active_created_at` (针对 `created_at DESC` + `deleted_at IS NULL`)。|**后台列表秒开**。即使翻到第 10 万页,数据库也能利用索引直接定位数据,避免全表扫描和内存排序。|**B 端体验保障**。原索引仅优化了登录(按用户名查),未覆盖管理后台最常用的“按注册时间倒序查看”场景。|
|
||||
|**3. 分区策略标注**|在 `refresh_tokens` 表头添加了关于 **Range Partitioning** 的架构警告与注释。|**防止 IO 爆炸**。明确告知 DBA/运维团队,在数据量突破 5000 万前必须实施表分区。|**生命周期管理**。日志类/令牌类数据如果不做分区,在执行批量删除(清理过期 Token)时会导致数据库 WAL 日志爆满甚至锁死。|
|
||||
|
||||
---
|
||||
|
||||
### 📜 SQL DDL 脚本 (v1.1 Production-Ready)
|
||||
|
||||
```SQL
|
||||
-- =========================================================
|
||||
-- Enterprise-CMS-Core Schema v1.1
|
||||
-- Architecture: Modular Clean Architecture
|
||||
-- Database: PostgreSQL 15+
|
||||
-- =========================================================
|
||||
|
||||
-- ⚠️ 警告: 生产环境请移除 DROP 语句
|
||||
DROP TABLE IF EXISTS refresh_tokens CASCADE;
|
||||
DROP TABLE IF EXISTS user_roles CASCADE;
|
||||
DROP TABLE IF EXISTS roles CASCADE;
|
||||
DROP TABLE IF EXISTS users CASCADE;
|
||||
|
||||
-- 1. 全局设置
|
||||
-- 确保时间存储统一,避免应用层时区转换混乱
|
||||
SET timezone = 'Asia/Shanghai';
|
||||
|
||||
-- =========================================================
|
||||
-- 2. 用户核心表 (users)
|
||||
-- =========================================================
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL, -- Argon2/Bcrypt Hash
|
||||
|
||||
-- Profile 字段 (允许 NULL,应用层需处理指针)
|
||||
nickname TEXT,
|
||||
avatar_url TEXT,
|
||||
bio TEXT,
|
||||
|
||||
-- 状态: 1=Active, 0=Banned (应用层枚举)
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
|
||||
-- [v1.1 新增] 反范式化字段: 缓存角色编码
|
||||
-- 目的: 让鉴权中间件实现 Zero-Join 查询
|
||||
-- 默认值: 空数组 '{}',避免 NULL 指针异常
|
||||
cached_role_codes TEXT[] NOT NULL DEFAULT '{}',
|
||||
|
||||
-- Base Model 字段
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- 2.1 约束定义
|
||||
ALTER TABLE users
|
||||
ADD CONSTRAINT uniq_users_username UNIQUE (username);
|
||||
|
||||
-- 2.2 索引策略
|
||||
-- [Index] 软删除查询优化 (BRIN / Partial Index)
|
||||
-- 场景: 绝大多数业务只查“未删除”数据,此过滤条件能大幅减小索引体积
|
||||
CREATE INDEX idx_users_deleted_at_brin ON users (deleted_at)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- [Index] 登录查询优化
|
||||
-- 场景: 根据用户名登录,且必须未被删除
|
||||
CREATE INDEX idx_users_username_active ON users (username)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- [v1.1 新增] [Index] 后台管理列表/深分页优化
|
||||
-- 场景: SELECT * FROM users WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT N OFFSET M
|
||||
-- 理由: 消除 FileSort,直接利用索引顺序扫描
|
||||
CREATE INDEX idx_users_active_created_at ON users (created_at DESC)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- 2.3 注释
|
||||
COMMENT ON TABLE users IS '用户核心表';
|
||||
COMMENT ON COLUMN users.cached_role_codes IS '[冗余字段] 缓存用户当前拥有的角色Code (e.g. {admin, editor}),用于提升鉴权性能';
|
||||
|
||||
-- =========================================================
|
||||
-- 3. 角色定义表 (roles)
|
||||
-- =========================================================
|
||||
CREATE TABLE roles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL, -- 业务唯一标识: 'admin', 'editor'
|
||||
name TEXT NOT NULL, -- 显示名称: '超级管理员'
|
||||
description TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
ALTER TABLE roles
|
||||
ADD CONSTRAINT uniq_roles_code UNIQUE (code);
|
||||
|
||||
COMMENT ON TABLE roles IS '系统角色定义表 (元数据)';
|
||||
|
||||
-- =========================================================
|
||||
-- 4. 用户-角色关联表 (user_roles)
|
||||
-- =========================================================
|
||||
CREATE TABLE user_roles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 4.1 外键约束 (确保数据一致性,防止孤儿数据)
|
||||
ALTER TABLE user_roles
|
||||
ADD CONSTRAINT fk_user_roles_users FOREIGN KEY (user_id)
|
||||
REFERENCES users(id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE user_roles
|
||||
ADD CONSTRAINT fk_user_roles_roles FOREIGN KEY (role_id)
|
||||
REFERENCES roles(id) ON DELETE RESTRICT;
|
||||
|
||||
-- 4.2 唯一约束 (防止重复授权)
|
||||
ALTER TABLE user_roles
|
||||
ADD CONSTRAINT uniq_user_roles_pair UNIQUE (user_id, role_id);
|
||||
|
||||
-- 4.3 索引
|
||||
-- 场景: 当管理员更新某用户角色时,需要快速查找到关联记录
|
||||
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
|
||||
|
||||
COMMENT ON TABLE user_roles IS '用户与角色的多对多关联表 (Write Source of Truth)';
|
||||
|
||||
-- =========================================================
|
||||
-- 5. 刷新令牌表 (refresh_tokens)
|
||||
-- =========================================================
|
||||
-- [v1.1 架构备注]
|
||||
-- ⚠️ Scaling Policy:
|
||||
-- 当单表行数预计超过 5000 万时,必须启用 Range Partitioning。
|
||||
-- 建议策略: PARTITION BY RANGE (created_at),按月分表,定期 DROP 旧分区。
|
||||
-- 当前 V1 阶段保持标准表结构。
|
||||
CREATE TABLE refresh_tokens (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
|
||||
-- 风控与轮转字段
|
||||
family_id TEXT NOT NULL, -- 令牌家族,用于检测复用攻击
|
||||
parent_token_id TEXT NOT NULL DEFAULT '', -- 溯源链
|
||||
is_revoked BOOLEAN NOT NULL DEFAULT FALSE, -- 黑名单开关
|
||||
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE refresh_tokens
|
||||
ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id)
|
||||
REFERENCES users(id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE refresh_tokens
|
||||
ADD CONSTRAINT uniq_refresh_tokens_hash UNIQUE (token_hash);
|
||||
|
||||
-- [Index] 安全风控查询
|
||||
-- 场景 1: 用户改密码 -> 吊销所有设备 (WHERE user_id = ?)
|
||||
-- 场景 2: 检测到令牌盗用 -> 吊销整个家族 (WHERE family_id = ?)
|
||||
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
|
||||
CREATE INDEX idx_refresh_tokens_family_id ON refresh_tokens(family_id);
|
||||
|
||||
COMMENT ON TABLE refresh_tokens IS '双令牌机制存储表 (高频写入/删除,需关注膨胀问题)';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 索引策略自辩 (Why These indexes?)
|
||||
|
||||
1. **`users.idx_users_deleted_at_brin`**:
|
||||
|
||||
- **场景**: 系统中几乎 100% 的查询(List, Get)都会带上 `deleted_at IS NULL`。
|
||||
- **优势**: 这是一个“部分索引” (Partial Index)。它只索引活跃用户,随着时间推移,被软删除的用户越来越多,这个索引依然会保持很小,查询速度极快。
|
||||
|
||||
2. **`refresh_tokens.idx_refresh_tokens_family_id`**:
|
||||
|
||||
- **场景**: 触发“令牌复用”警报时。
|
||||
- **逻辑**: 黑客利用旧的 Refresh Token 尝试刷新 -> 系统检测到该 Token 已被用过 -> 系统根据 `family_id` 瞬间查出该链条上所有新颁发的 Token -> 全部标记为 `is_revoked = TRUE`。如果没有这个索引,高并发下这一步风控逻辑会锁表或超时。
|
||||
|
||||
3. **`user_roles.idx_user_roles_user_id`**:
|
||||
|
||||
- **场景**: 每次 HTTP 请求经过 `AuthMiddleware`。
|
||||
- **逻辑**: 中间件必须校验当前 User 是否为 `Admin`。这需要高频执行 `SELECT role_id FROM user_roles WHERE user_id = ?`。此索引是系统吞吐量的关键。
|
||||
205
Go项目实战/用户模块/03_entity 代码.md
Normal file
205
Go项目实战/用户模块/03_entity 代码.md
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
tags: []
|
||||
date created: 星期二, 十二月 9日 2025, 11:56:32 晚上
|
||||
date modified: 星期二, 十二月 9日 2025, 11:57:24 晚上
|
||||
---
|
||||
|
||||
# 03_entity 代码
|
||||
|
||||
根据 **v1.1 SQL DDL** 的变更(主要是增加了反范式化字段 `cached_role_codes`)以及 **“按领域分包”** 的架构要求,以下是完整的、生产级的 GORM Entity 代码。
|
||||
|
||||
请注意:为了处理 PostgreSQL 的 `text[]` 数组类型,我们在 Go 中通常直接使用 `[]string` 并配合 GORM 的 `type:text[]` 标签(GORM v2 的 Postgres 驱动支持此特性)。
|
||||
|
||||
---
|
||||
|
||||
## 📂 1. 用户领域实体
|
||||
|
||||
**文件路径:** `internal/user/entity.go`
|
||||
|
||||
```Go
|
||||
package user
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// =================================================================================
|
||||
// 核心实体 (Core Entities)
|
||||
// 遵循 "Pragmatic Entity" 模式: 既是业务实体也是 GORM 模型
|
||||
// =================================================================================
|
||||
|
||||
// User 聚合根
|
||||
type User struct {
|
||||
// ID 使用 int64 对应 BigSerial
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 认证与安全
|
||||
// -------------------------------------------------------------------------
|
||||
Username string `gorm:"column:username;type:text;not null;unique" json:"username"`
|
||||
PasswordHash string `gorm:"column:password_hash;type:text;not null" json:"-"` // 🔒 安全: 永不序列化
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 个人资料 (Profile)
|
||||
// 使用指针 (*string) 以区分 DB 中的 NULL 和 空字符串
|
||||
// -------------------------------------------------------------------------
|
||||
Nickname *string `gorm:"column:nickname;type:text" json:"nickname"`
|
||||
AvatarURL *string `gorm:"column:avatar_url;type:text" json:"avatarUrl"`
|
||||
Bio *string `gorm:"column:bio;type:text" json:"bio"`
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 状态与权限
|
||||
// -------------------------------------------------------------------------
|
||||
// Status: 1=Active, 0=Banned
|
||||
Status int16 `gorm:"column:status;type:smallint;not null;default:1" json:"status"`
|
||||
|
||||
// [v1.1 新增] 反范式化字段: 缓存角色编码
|
||||
// GORM Postgres 驱动通常能自动处理 []string <-> text[]
|
||||
// 作用: 鉴权中间件读取此字段即可,无需 Join 角色表
|
||||
CachedRoleCodes StringArray `gorm:"column:cached_role_codes;type:text[];not null;default:'{}'" json:"cachedRoleCodes"`
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 审计与时间
|
||||
// -------------------------------------------------------------------------
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:now()" json:"updatedAt"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"` // 启用软删除
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 关联 (Associations) - 仅用于 Preload,非物理字段
|
||||
// -------------------------------------------------------------------------
|
||||
Roles []Role `gorm:"many2many:user_roles;joinForeignKey:user_id;joinReferences:role_id" json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
// TableName 显式定义表名
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// Role 角色实体
|
||||
type Role struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Code string `gorm:"column:code;type:text;not null;unique" json:"code"` // e.g. "admin"
|
||||
Name string `gorm:"column:name;type:text;not null" json:"name"` // e.g. "管理员"
|
||||
Description *string `gorm:"column:description;type:text" json:"description"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:now()" json:"updatedAt"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"`
|
||||
}
|
||||
|
||||
func (Role) TableName() string {
|
||||
return "roles"
|
||||
}
|
||||
|
||||
// UserRole 关联实体 (中间表)
|
||||
// 即使是中间表,也定义实体以便于管理(如审计创建时间)
|
||||
type UserRole struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserID int64 `gorm:"column:user_id;not null" json:"userId"`
|
||||
RoleID int64 `gorm:"column:role_id;not null" json:"roleId"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"createdAt"`
|
||||
}
|
||||
|
||||
func (UserRole) TableName() string {
|
||||
return "user_roles"
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// 辅助类型 (Helper Types)
|
||||
// 确保 PostgreSQL 数组类型能正确 Scanner/Valuer
|
||||
// =================================================================================
|
||||
|
||||
// StringArray 自定义类型以处理 PostgreSQL 的 text[]
|
||||
// 虽然 GORM 高版本支持 []string,但显式实现接口更兼容不同的 Driver
|
||||
type StringArray []string
|
||||
|
||||
func (a *StringArray) Scan(value interface{}) error {
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("type assertion to []byte failed")
|
||||
}
|
||||
// 处理空数组情况 "{}"
|
||||
str := string(bytes)
|
||||
if str == "{}" {
|
||||
*a = []string{}
|
||||
return nil
|
||||
}
|
||||
// 这里使用简单处理,生产环境建议使用 lib/pq 或 pgx 的解析逻辑
|
||||
// 简单实现: 依赖 GORM 默认行为,或者使用 JSON 兼容
|
||||
return json.Unmarshal(bytes, a) // 注意:PG 数组格式不是 JSON,此处通常依赖 Driver 的自动转换
|
||||
}
|
||||
|
||||
func (a StringArray) Value() (driver.Value, error) {
|
||||
if len(a) == 0 {
|
||||
return "{}", nil
|
||||
}
|
||||
// 同样,依赖 GORM 传递给 Driver
|
||||
return a, nil
|
||||
}
|
||||
```
|
||||
|
||||
> **👨💻 架构师注 (User Entity):**
|
||||
>
|
||||
> 1. **`CachedRoleCodes`**: 我定义了一个 `StringArray` 类型。虽然现代 GORM (`gorm.io/driver/postgres`) 可以直接把 `[]string` 映射为 `text[]`,但在代码中显式标记类型(甚至未来引入 `lib/pq` 的 `pq.StringArray`)会让类型转换逻辑更健壮,防止驱动更新导致的兼容性问题。
|
||||
>
|
||||
> 2. **`json:"-"`**: 再次确认 `PasswordHash` 和 `DeletedAt` 已被隐藏,防止敏感信息泄露。
|
||||
|
||||
---
|
||||
|
||||
## 📂 2. 认证领域实体
|
||||
|
||||
**文件路径:** `internal/auth/entity.go`
|
||||
|
||||
```Go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// RefreshToken 令牌实体
|
||||
// 负责双令牌机制中的长效令牌存储与风控
|
||||
// 对应 SQL 表: refresh_tokens
|
||||
type RefreshToken struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserID int64 `gorm:"column:user_id;not null;index" json:"userId"` // 仅存储 ID,保持领域解耦
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 安全核心
|
||||
// -------------------------------------------------------------------------
|
||||
// TokenHash: 存哈希值,防止数据库泄露后 Token 直接被利用
|
||||
TokenHash string `gorm:"column:token_hash;type:text;not null;unique" json:"-"`
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 风控与轮转 (Token Rotation & Risk Control)
|
||||
// -------------------------------------------------------------------------
|
||||
// FamilyID: 标记一整条 Token 链,用于检测重放攻击时级联吊销
|
||||
FamilyID string `gorm:"column:family_id;type:text;not null;index" json:"familyId"`
|
||||
|
||||
// ParentTokenID: 溯源上一个 Token,构建链式关系
|
||||
ParentTokenID string `gorm:"column:parent_token_id;type:text;not null;default:''" json:"parentTokenId"`
|
||||
|
||||
// IsRevoked: 黑名单开关
|
||||
IsRevoked bool `gorm:"column:is_revoked;not null;default:false" json:"isRevoked"`
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 生命周期
|
||||
// -------------------------------------------------------------------------
|
||||
ExpiresAt time.Time `gorm:"column:expires_at;not null" json:"expiresAt"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"createdAt"`
|
||||
}
|
||||
|
||||
// TableName 显式定义表名
|
||||
func (RefreshToken) TableName() string {
|
||||
return "refresh_tokens"
|
||||
}
|
||||
```
|
||||
|
||||
> **👨💻 架构师注 (Auth Entity):**
|
||||
>
|
||||
> 1. **领域边界**: 这里没有引入 `User` 结构体指针。在 `auth` 领域内,我们只关心 `UserID`。如果业务逻辑需要获取用户详情(例如封禁检查),应由 `auth.Service` 调用 `user.Service` 或 `user.Repository`,而不是在 Entity 层面强耦合。这符合 Clean Architecture 的原则。
|
||||
88
Go项目实战/用户模块/04_业务逻辑功能清单.md
Normal file
88
Go项目实战/用户模块/04_业务逻辑功能清单.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 04_ 业务逻辑功能清单
|
||||
date created: 星期三, 十二月 10日 2025, 12:04:34 凌晨
|
||||
date modified: 星期三, 十二月 10日 2025, 12:05:53 凌晨
|
||||
---
|
||||
|
||||
# 04_ 业务逻辑功能清单
|
||||
|
||||
## TL;DR (摘要)
|
||||
|
||||
- **基础版 (MVP):** 仅满足最基本的“注册 - 登录 - 看自己”流程,适合快速打通前后端联调,但**不符合** PRD 的安全标准。
|
||||
- **完整版 (Enterprise):** 严格对应 PRD V1.1,包含双令牌刷新、强制登出、RBAC 提权及管理员封禁功能,符合生产环境安全要求。
|
||||
|
||||
---
|
||||
|
||||
## 方案一:基础版 (MVP / Prototype)
|
||||
|
||||
适用场景: 项目初期快速搭建原型 (PoC),验证核心业务流程(如文章发布),暂时忽略复杂的安全合规。
|
||||
|
||||
局限性: 仅使用单 Access Token(长效),无刷新机制,无法强制踢人下线,无管理员管理界面。
|
||||
|
||||
|**模块**|**方法**|**API 路径**|**核心功能描述**|**鉴权要求**|
|
||||
|---|---|---|---|---|
|
||||
|**Auth**|POST|`/api/v1/register`|用户注册 (仅用户名 + 密码)|无|
|
||||
|**Auth**|POST|`/api/v1/login`|用户登录 (返回长效 JWT)|无|
|
||||
|**User**|GET|`/api/v1/user/profile`|获取当前登录用户信息|JWT|
|
||||
|**User**|PUT|`/api/v1/user/profile`|修改自己的昵称、简介|JWT|
|
||||
|
||||
> 自我反驳 (基础版):
|
||||
> 此方案虽然简单,但直接违反了 PRD 中 F-AUTH-03 (令牌刷新) 和 F-AUTH-04 (统一登出) 的要求。若项目进入 Alpha 测试阶段,必须立刻废弃此方案,否则存在严重的安全隐患(Token 泄露即完全失控)。
|
||||
|
||||
---
|
||||
|
||||
## 方案二:完整版 (Enterprise / PRD Compliant)
|
||||
|
||||
**适用场景:** 正式开发与生产环境交付。严格遵循“银行级 RBAC”和“双令牌”机制。
|
||||
|
||||
### 1. 认证服务 (Auth Service) - 公开/基础域
|
||||
|
||||
对应 PRD 章节: 2.1 认证与鉴权模块
|
||||
|
||||
|**需求编号**|**方法**|**API 路径**|**功能描述**|**输入参数**|**鉴权**|
|
||||
|---|---|---|---|---|---|
|
||||
|**F-AUTH-01**|POST|`/api/v1/auth/register`|用户注册 (密码需 Hash 存储)|`username`, `password`|无|
|
||||
|**F-AUTH-02**|POST|`/api/v1/auth/login`|登录 (颁发 Access + Refresh Token)|`username`, `password`|无|
|
||||
|**F-AUTH-03**|POST|`/api/v1/auth/refresh`|**令牌刷新** (旧换新,防复用机制)|`refresh_token`|无|
|
||||
|**F-AUTH-04**|POST|`/api/v1/auth/logout`|**统一登出** (将 Refresh Token 加入黑名单)|`refresh_token`|JWT|
|
||||
|**F-AUTH-05**|POST|`/api/v1/auth/password`|**重置密码** (成功后吊销所有 Token)|`old_pwd`, `new_pwd`|JWT|
|
||||
|
||||
### 2. 用户自服务 (User Self-Service) - 个人域
|
||||
|
||||
对应 PRD 章节: 2.2 用户与权限模块 (F-USER-01)
|
||||
|
||||
|**需求编号**|**方法**|**API 路径**|**功能描述**|**备注**|**鉴权**|
|
||||
|---|---|---|---|---|---|
|
||||
|**F-USER-01**|GET|`/api/v1/users/me`|获取我的详细资料|**建议增加 Redis 缓存**|JWT|
|
||||
|**F-USER-01**|PUT|`/api/v1/users/me`|修改资料 (昵称, 头像 URL, 简介)|更新后需清除缓存|JWT|
|
||||
|
||||
### 3. 管理员运维 (Admin Dashboard) - 管理域
|
||||
|
||||
对应 PRD 章节: 2.2 用户与权限模块 (F-USER-02, F-RBAC-01)
|
||||
|
||||
|**需求编号**|**方法**|**API 路径**|**功能描述**|**关键逻辑**|**鉴权**|
|
||||
|---|---|---|---|---|---|
|
||||
|**F-USER-02**|GET|`/api/v1/admin/users`|**用户列表查询**|支持分页、按用户名搜索、按状态筛选|**Admin Only**|
|
||||
|**F-USER-02**|PATCH|`/api/v1/admin/users/:id/status`|**封禁/解封用户**|修改状态为 `active`/`banned`,若封禁需强制踢下线|**Admin Only**|
|
||||
|**F-RBAC-01**|PATCH|`/api/v1/admin/users/:id/role`|**角色变更 (提权)**|修改角色为 `editor`/`admin`|**Admin Only**|
|
||||
|
||||
---
|
||||
|
||||
## 关键设计决策说明 (Technical Decisions)
|
||||
|
||||
1. **关于 PATCH vs PUT:**
|
||||
|
||||
- 在**完整版**的管理接口中,我使用了 `PATCH` 而不是 `PUT`。
|
||||
- **理由:** `PUT` 语义上是全量替换。在修改用户状态(如封禁)或角色时,我们只修改单个字段,使用 `PATCH` 更符合 RESTful 语义,且能避免管理员无意中覆盖了用户的其他信息(如昵称)。
|
||||
|
||||
2. **关于路径设计 (URI Design):**
|
||||
|
||||
- 区分了 `/users/me` (当前用户) 和 `/admin/users/:id` (管理特定用户)。
|
||||
- **理由:** 这种分离能清晰地界定权限边界。`/me` 接口永远不需要传 ID(从 Token 解析),杜绝了普通用户通过遍历 ID 窃取他人信息的越权风险 (IDOR)。
|
||||
|
||||
3. **关于缓存 (Cache):**
|
||||
|
||||
- **自我反驳:** 虽然 PRD 建议对 `/profile` 进行缓存,但在 API 定义阶段不需要体现在 URL 上。
|
||||
- **补充:** 但作为后端设计,你需要在 `GET /users/me` 的 Controller 层实现 Cache-Aside 模式(先查 Redis,无则查 DB 并回写)。
|
||||
960
Go项目实战/通用上下文.md
Normal file
960
Go项目实战/通用上下文.md
Normal file
@@ -0,0 +1,960 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- Project Context Aggregation
|
||||
date created: 星期三, 十二月 10日 2025, 12:10:59 凌晨
|
||||
date modified: 星期三, 十二月 10日 2025, 12:06:29 中午
|
||||
---
|
||||
|
||||
# Project Context Aggregation
|
||||
|
||||
> Source Items: 6
|
||||
|
||||
# ⚙️ Go 模块根路径约束 (Module Root Path Constraint)
|
||||
|
||||
**核心约束:**
|
||||
|
||||
- **项目 Go Module 路径 (Root Path):** `gitea-aliyun/Klein/enterprise-cms-core`
|
||||
- **用途:** 所有内部导入(Internal Imports)必须以此路径作为前缀。
|
||||
- **示例:**
|
||||
- **错误:** `import "internal/pkg/ecode"`
|
||||
- **正确:** `import "gitea-aliyun/Klein/enterprise-cms-core/internal/pkg/ecode"`
|
||||
|
||||
**AI 约束实施规则:**
|
||||
|
||||
1. 在生成任何包含 `import` 语句的代码时,必须检查并使用上述 Root Path。
|
||||
2. 若代码位于 `internal` 目录下,且引用了另一个 `internal` 目录下的包,必须使用完整的 Root Path。
|
||||
|
||||
==== 00_ 软件产品全生命周期管理规范.md ====
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 📘 软件产品全生命周期管理规范 (PDLC Guidelines)
|
||||
date created: 星期日, 十二月 7日 2025, 12:49:19 下午
|
||||
date modified: 星期日, 十二月 7日 2025, 12:49:54 下午
|
||||
---
|
||||
这是一个通用的、标准化的《互联网软件产品全生命周期(PDLC)管理规范》。此文档旨在为从灵感到交付的全过程提供顶层指导,适用于中大型项目或追求工程卓越的小型团队。
|
||||
|
||||
---
|
||||
|
||||
# 📘 软件产品全生命周期管理规范 (PDLC Guidelines)
|
||||
|
||||
版本: 2.0 (通用标准版)
|
||||
|
||||
适用范围: 全栈开发、SaaS 产品、企业级应用系统
|
||||
|
||||
核心目标: 降低不确定性,确保交付质量,实现可预测的工程化产出。Shutterstock
|
||||
|
||||
---
|
||||
|
||||
## 阶段概览 (Phase Overview)
|
||||
|
||||
我们将产品落地过程划分为 7 个核心阶段(P0 - P6)。每个阶段都有明确的准入(Entry)和准出(Exit)标准。
|
||||
|
||||
|**阶段代号**|**阶段名称**|**核心角色**|**关键产出物**|
|
||||
|---|---|---|---|
|
||||
|**P0**|**立项与价值验证 (Inception)**|PM, Tech Lead, Stakeholder|BRD, 可行性分析报告|
|
||||
|**P1**|**需求定义与原型 (Definition)**|PM, UI/UX|PRD, 原型图 (Figma)|
|
||||
|**P2**|**技术方案设计 (Technical Design)**|Architect, Backend, Frontend|TDD, API 契约, ER 图|
|
||||
|**P3**|**开发与实现 (Development)**|Developers|源代码, 单元测试|
|
||||
|**P4**|**质量保障与验证 (Verification)**|QA, Developers|测试报告, Bug 清单|
|
||||
|**P5**|**发布与部署 (Release)**|DevOps, Tech Lead|镜像, Release Note|
|
||||
|**P6**|**运维与迭代 (Operations)**|SRE, Ops, PM|监控面板, 运营数据报告|
|
||||
|
||||
---
|
||||
|
||||
## 📅 详细阶段拆解
|
||||
|
||||
### P0: 立项与价值验证 (Inception & Strategy)
|
||||
|
||||
**目的:** 明确“为什么要做”。防止团队在伪需求或技术不可行的方向上浪费资源。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **商业需求分析:** 确定业务痛点、目标用户及商业价值。
|
||||
2. **技术可行性预研 (PoC):** 针对关键技术难点(如 AI 模型效果、高并发瓶颈)进行快速验证。
|
||||
3. **资源评估:** 粗略估算所需人力、时间及服务器成本。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `BRD (Business Requirement Document)`:商业需求文档。
|
||||
- `PoC Demo`:概念验证原型(如有必要)。
|
||||
- **决策门 (Gate):** **Go / No-Go**。如果 ROI(投入产出比)过低,在此阶段终止。
|
||||
|
||||
### P1: 需求定义与产品设计 (Product Definition)
|
||||
|
||||
**目的:** 明确“要做成什么样”。将模糊的想法转化为具象的功能逻辑和视觉形态。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **需求细化:** 编写详细的功能列表、用户故事 (User Stories) 和验收标准 (AC)。
|
||||
2. **交互设计 (UX):** 绘制用户流程图 (User Flow)、低保真线框图。
|
||||
3. **视觉设计 (UI):** 输出高保真设计稿、UI 切图、设计规范 (Design System)。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `PRD (Product Requirement Document)`:产品需求规格说明书(唯一真理来源)。
|
||||
- `Figma/Sketch Files`:高保真设计稿。
|
||||
- **决策门 (Gate):** **需求评审 (PRD Review)**。开发团队确认需求逻辑闭环,无歧义。
|
||||
|
||||
### P2: 技术方案设计 (Technical Design)
|
||||
|
||||
**目的:** 明确“怎么实现”。**这是程序员最重要的规划阶段,严禁跳过此阶段直接编码。**
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **架构设计:** 确定微服务拆分、技术选型、中间件依赖(Redis/MQ/DB)。
|
||||
2. **数据建模 (Schema Design):** 绘制 ER 图,编写 DDL (SQL 建表语句),确定索引策略。
|
||||
3. **接口定义 (API Contract):** 定义 URL、Method、Request/Response JSON 结构、错误码。
|
||||
4. **详细设计 (TDD):** 核心算法逻辑、状态机流转图、时序图、缓存策略设计。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `TDD (Technical Design Document)`:技术设计文档。
|
||||
- `ER Diagram & SQL Scripts`:数据库模型与迁移脚本。
|
||||
- `OpenAPI/Swagger Spec`:API 接口定义文档。
|
||||
- **决策门 (Gate):** **技术评审 (Design Review)**。架构师或 Tech Lead 确认方案具备扩展性、安全性及性能达标。
|
||||
|
||||
### P3: 开发与实现 (Implementation)
|
||||
|
||||
**目的:** 将设计转化为代码。注重代码质量与规范。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **环境准备:** 本地开发环境搭建、Mock 数据生成。
|
||||
2. **编码 (Coding):** 后端 API 开发、前端组件开发、业务逻辑实现。
|
||||
3. **单元测试 (Unit Test):** 编写核心逻辑的单元测试,确保覆盖率。
|
||||
4. **代码审查 (Code Review):** 提交 Merge Request,进行同行评审。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `Source Code`:符合规范的源码。
|
||||
- `Unit Test Report`:单元测试通过报告。
|
||||
- **决策门 (Gate):** **代码合并 (Merge)**。CI 流水线检查通过(Lint, Test, Build)。
|
||||
|
||||
### P4: 质量保障与验证 (Quality Assurance)
|
||||
|
||||
**目的:** 确保交付物符合需求且无重大缺陷。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **集成测试:** 前后端联调,确保接口数据交互正常。
|
||||
2. **系统测试:** QA 团队根据测试用例进行全量测试。
|
||||
3. **非功能测试:** 性能测试 (Load Test)、安全扫描 (Security Scan)。
|
||||
4. **Bug 修复:** 开发修复 QA 发现的问题并回归。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `Test Cases`:测试用例。
|
||||
- `Bug List`:缺陷清单及修复记录。
|
||||
- `Performance Report`:压测报告(可选)。
|
||||
- **决策门 (Gate):** **验收评审 (UAT)**。Bug 清零或无 P0/P1 级 Bug,PM 验收通过。
|
||||
|
||||
### P5: 发布与部署 (Release & Deployment)
|
||||
|
||||
**目的:** 安全、平滑地将产品推向生产环境。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **构建交付:** 编译二进制文件、构建 Docker 镜像。
|
||||
2. **预发布验证 (Staging):** 在仿真环境中进行最后一次冒烟测试。
|
||||
3. **正式部署 (Production):** 灰度发布 (Canary) 或 蓝绿部署,执行数据库迁移。
|
||||
4. **回滚预案:** 准备好一旦失败的一键回滚脚本。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `Release Note`:发布说明(变更日志)。
|
||||
- `Docker Image / Binaries`:制品。
|
||||
- **决策门 (Gate):** **上线检查清单 (Checklist)**。确认配置、密钥、数据库备份均已就绪。
|
||||
|
||||
### P6: 运维与持续迭代 (Operations & Maintenance)
|
||||
|
||||
**目的:** 保障系统稳定性,根据反馈进行优化。
|
||||
|
||||
- **主要工作:**
|
||||
|
||||
1. **监控告警:** 配置 CPU/内存、QPS、错误率监控,设置 PagerDuty 告警。
|
||||
2. **日志审计:** 收集与分析运行日志 (ELK/Loki)。
|
||||
3. **数据复盘:** 分析用户行为数据,验证 P0 阶段的商业假设。
|
||||
4. **事故复盘 (Post-mortem):** 若发生故障,撰写复盘报告,制定改进措施。
|
||||
|
||||
- **关键产出 (Artifacts):**
|
||||
- `SLA Report`:服务可用性报告。
|
||||
- `User Analytics`:用户数据分析报表。
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 关键支撑体系 (Supporting Pillars)
|
||||
|
||||
除了上述流程,以下三个支撑体系贯穿始终:
|
||||
|
||||
1. **项目管理 (Project Management):** 使用 Jira/Trello 管理任务看板,每日站会同步进度,识别风险。
|
||||
2. **配置管理 (Configuration Management):** 代码版本控制 (Git Flow),环境配置隔离 (Env Vars)。
|
||||
3. **文档工程 (Documentation):** 保持 BRD, PRD, API 文档与代码的同步更新,避免“文档腐烂”。
|
||||
```
|
||||
|
||||
==== 关于个人开发者的开发模式.md ====
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 渐进式开发最佳实践
|
||||
date created: 星期一, 十二月 8日 2025, 12:04:31 凌晨
|
||||
date modified: 星期一, 十二月 8日 2025, 12:05:12 凌晨
|
||||
---
|
||||
|
||||
# 渐进式开发最佳实践
|
||||
|
||||
## 1. 必须在写代码前锁定的“硬约束” (The Non-Negotiables)
|
||||
|
||||
即使是后规划细节,但这 **4 样东西** 是一旦开工就很难改的,必须在骨架阶段定死:
|
||||
|
||||
1. **目录结构 (Directory Layout):** `cmd`, `internal`, `pkg` 怎么分。这决定了你能不能顺畅地加代码。
|
||||
|
||||
- _现状:_ 我们已经定好了 (Modular Clean Architecture)。
|
||||
|
||||
2. **核心技术栈与基础设施:** 选 Gin 还是 Echo,用 GORM 还是 SQLX,依赖注入用 Wire 还是手写。
|
||||
|
||||
- _现状:_ 我们已经定好了 (Gin+GORM+Wire+Viper)。
|
||||
|
||||
3. **统一的交互规范:** API 怎么返回错误?数据库怎么管理变更?日志打在哪里?
|
||||
|
||||
- _现状:_ 我们已经定好了 (JSON Envelope, Golang-Migrate, Zap)。
|
||||
|
||||
4. **核心领域模型 (Core Schema):** 最关键的表(User, Role)。
|
||||
|
||||
- _原因:_ 它们是系统的地基,地基不稳,后面写 Service 逻辑会反复推倒重来。
|
||||
|
||||
---
|
||||
|
||||
## 2. 可以(且应该)推迟设计的“软逻辑” (The Deferrables)
|
||||
|
||||
这些内容不要现在想,想了也是白想,等写到那个函数时再具体的“具体问题具体分析”:
|
||||
|
||||
1. **复杂的业务算法:** 比如“文章的热度排名算法”、“复杂的权限递归校验逻辑”。
|
||||
|
||||
- _策略:_ 先写个 `return true` 或简单的逻辑占位,跑通流程再说。
|
||||
|
||||
2. **极致的性能优化:** 比如“这里要不要加 Redis 缓存?”、“这里 SQL 要不要分表?”。
|
||||
|
||||
- _策略:_ 先跑通功能 (Make it work),再优化性能 (Make it fast)。
|
||||
|
||||
3. **非核心字段的定义:** 比如文章表里要不要加 `seo_keywords`,用户表要不要加 `wechat_id`。
|
||||
|
||||
- _策略:_ 用到了再加 migration,不要为了“未来可能用到”而过度设计。
|
||||
|
||||
4. **具体的 API 参数细节:** 比如“更新文章是传 ID 还是传 UUID”。
|
||||
|
||||
- _策略:_ 写 Handler 的时候,顺手定义 DTO 就行。
|
||||
|
||||
---
|
||||
|
||||
## 3. 个人开发者的“曳光弹”开发流 (The Tracer Bullet Workflow)
|
||||
|
||||
不要试图“横向”开发(先把所有 50 张表设计完,再把所有 API 定义完)。
|
||||
|
||||
要进行 “纵向”切片开发(Vertical Slice)。
|
||||
|
||||
**推荐的实操步骤:**
|
||||
|
||||
### 第一阶段:搭建“行走的骨架” (Day 1 - 也就是你现在该做的)
|
||||
|
||||
**目标:** 一个能跑起来、能连数据库、有日志、有 Swagger 文档,但没有任何业务功能的空壳。
|
||||
|
||||
1. **初始化项目:** 建立文件夹,`go mod init`。
|
||||
2. **配置基础设施:** 放入 `Makefile`, `docker-compose.yml`, `config.yaml`。
|
||||
3. **连通性测试:** 写一个简单的 `GET /ping` 接口,确保能读到配置,能连上 DB,能打出日志。
|
||||
4. **依赖注入跑通:** 确保 `wire` 能自动生成代码并启动 Server。
|
||||
5. **结果:** 你拥有了一个**“生产级”的 Hello World**。
|
||||
|
||||
### 第二阶段:第一个“核心切片” (Day 2-3)
|
||||
|
||||
**目标:** 打通 User 模块的完整闭环。
|
||||
|
||||
1. **DB:** 设计 `users` 表,执行 Migrate。
|
||||
2. **Entity:** 生成 User Struct。
|
||||
3. **Repo:** 写 `CreateUser` 和 `FindUser` 的 CRUD。
|
||||
4. **Service:** 写 `Register` 逻辑(这里涉及到密码加密,此时再去查 Argon2 怎么用)。
|
||||
5. **API:** 定义 `RegisterReq` DTO,写 Handler。
|
||||
6. **测试:** 用 Postman/Swagger 调通注册接口。
|
||||
|
||||
### 第三阶段:具体问题具体分析 (Day 4+)
|
||||
|
||||
**目标:** 遇到什么写什么。
|
||||
|
||||
- _场景:_ 比如你做到了“文章发布”,突然发现需要“敏感词过滤”。
|
||||
- _动作:_ 停下来,在 `pkg/util` 里设计一个敏感词过滤函数(或者找个库),然后在 Service 层调用它。
|
||||
- _这就是你说的:_ “进行到具体的业务逻辑的时候进行设计”。
|
||||
|
||||
---
|
||||
|
||||
## 4. 总结给你的建议
|
||||
|
||||
你现在的焦虑在于试图**预见未来**。
|
||||
|
||||
**作为个人开发者,你的代码就是你的设计文档。**
|
||||
|
||||
**你的下一步行动:**
|
||||
|
||||
1. **停止**思考复杂的业务场景。
|
||||
2. **立即**按照我们之前确定的目录结构,新建文件夹。
|
||||
3. **立即**把 `docker-compose` 跑起来,把 `main.go` 跑起来。
|
||||
4. **立即**实现“用户注册”这一个功能。
|
||||
|
||||
只要“骨架”(架构分层、依赖注入、数据库管理方式)是对的,后面你往里面填什么肉(业务逻辑),怎么填,都不会把楼盖歪。
|
||||
|
||||
**准备好开始初始化项目文件夹了吗?**
|
||||
```
|
||||
|
||||
==== 关于项目的顶层设计模式和风格.md ====
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🏗️ Project Architecture & Design Guidelines (v1.0)
|
||||
date created: 星期日, 十二月 7日 2025, 11:57:43 中午
|
||||
date modified: 星期二, 十二月 9日 2025, 11:00:14 晚上
|
||||
---
|
||||
|
||||
# 🏗️ Project Architecture & Design Guidelines (v1.0)
|
||||
|
||||
项目代号: Enterprise-CMS-Core
|
||||
|
||||
架构风格: 模块化整洁架构 (Modular Clean Architecture)
|
||||
|
||||
核心原则: 实用主义 (Pragmatic)、Go 原生思维 (Idiomatic)、领域驱动 (DDD-Lite)
|
||||
|
||||
## 1. 技术栈约束 (Tech Stack Constraints)
|
||||
|
||||
- **Language:** Go 1.21+
|
||||
- **Web Framework:** Gin
|
||||
- **Database:** PostgreSQL (Primary), Redis (Cache)
|
||||
- **ORM:** GORM (With Migration Tools)
|
||||
- **Dependency Injection:** Google Wire
|
||||
- **Configuration:** Viper (YAML)
|
||||
- **Observability:** Zap (Log), Prometheus (Metrics), Jaeger (Trace)
|
||||
- **Documentation:** Swagger / OpenAPI 3.0
|
||||
|
||||
---
|
||||
|
||||
## 2. 目录结构规范 (Directory Structure)
|
||||
|
||||
采用 **“按领域分包 (Package by Domain)”** 的扁平化结构,而非传统的按层分包。
|
||||
|
||||
```Plaintext
|
||||
root/
|
||||
├── cmd/server/
|
||||
│ ├── main.go # 仅包含 wire 初始化与 app.Run()
|
||||
│ └── wire.go # 顶层依赖注入定义
|
||||
├── config/ # 配置文件模板 (config.yaml)
|
||||
├── internal/
|
||||
│ ├── api/ # [API层] 全局通用的 HTTP DTO (Request/Response)
|
||||
│ ├── middleware/ # [中间件] Gin 中间件 (Auth, CORS, Logger)
|
||||
│ ├── pkg/ # [基础设施] 内部通用组件 (AppResult, ErrorCode)
|
||||
│ │
|
||||
│ │ # --- 核心业务领域 (Domain Modules) ---
|
||||
│ │ # 每个领域包内部扁平化,自包含所有逻辑
|
||||
│ ├── user/ # [示例] 用户领域
|
||||
│ │ ├── entity.go # 核心实体 (GORM Model)
|
||||
│ │ ├── repository.go # 仓储接口定义 + GORM 实现
|
||||
│ │ ├── service.go # 业务逻辑 (Service Struct)
|
||||
│ │ ├── handler.go # HTTP 控制器 (Controller)
|
||||
│ │ └── provider.go # Wire ProviderSet
|
||||
│ │
|
||||
│ └── article/ # [示例] 文章领域 (结构同上)
|
||||
│
|
||||
├── pkg/ # [外部库] 可抽离的通用工具 (Hash, JWT, Logger封装)
|
||||
├── migrations/ # 数据库迁移 SQL 文件 (up/down)
|
||||
├── go.mod
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心架构设计规则 (Architectural Rules)
|
||||
|
||||
### 3.1. 依赖倒置与注入 (IoC & DI)
|
||||
|
||||
- **规则:** 严禁在业务代码中手动 `New()` 依赖对象。
|
||||
- **实现:** 所有依赖关系必须通过 `NewStruct(dep Interface)` 构造函数声明,并由 `Google Wire` 在编译期自动组装。
|
||||
- **模块化注入:** 每个领域包(如 `internal/user`)必须包含一个 `provider.go`,导出 `var ProviderSet = wire.NewSet(…)`,供顶层 `cmd/server/wire.go` 聚合。
|
||||
|
||||
### 3.2. 接口策略 (Interface Strategy)
|
||||
|
||||
- **Repository (必须):** 仓储层**必须**定义接口(例如 `UserRepository`),以支持 Mock 测试和数据库切换。
|
||||
- **Service (按需):** 默认**不需要**定义 Service 接口,直接使用 Struct。仅在以下情况提取接口:
|
||||
|
||||
1. 出现循环依赖。
|
||||
2. 需要对 Service 进行 Mock 测试。
|
||||
3. 该 Service 存在多种策略实现(如 `PaymentService` 有支付宝/微信两种实现)。
|
||||
|
||||
### 3.3. 领域包扁平化 (Flat Domain Package)
|
||||
|
||||
- **规则:** 在 `internal/user/` 等领域包内,**不再**建立 `service/`, `repo/` 子目录。
|
||||
- **原因:** 利用 Go 的 `package` 级私有可见性,隐藏领域内部细节(如辅助函数、内部 DTO),仅暴露必要的 Handler 和 Service 方法。
|
||||
|
||||
### 3.4. 数据模型 (Model Vs Entity)
|
||||
|
||||
- **策略:** 采用 **"Pragmatic Entity"** 模式。
|
||||
- **定义:** `entity.go` 中的结构体既是业务实体,也是 GORM 模型(带 `gorm:"…"` 标签)。
|
||||
- **例外:** 只有当数据库存储结构与业务逻辑结构差异巨大时,才在 Repository 内部引入独立的 PO (Persistent Object) 并进行转换。
|
||||
|
||||
---
|
||||
|
||||
## 4. 编码实施标准 (Implementation Standards)
|
||||
|
||||
### 4.1. 错误处理 (Error Handling)
|
||||
|
||||
- **禁止:** 严禁直接返回 `error` 字符串给前端。
|
||||
- **必须:** Service 层返回标准 `error`,Controller 层通过 `pkg/app` 将其转换为统一响应格式。
|
||||
- **格式:**
|
||||
|
||||
```Go
|
||||
// Response JSON
|
||||
{
|
||||
"code": 20001,
|
||||
"msg": "User already exists",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2. 数据库交互 (Database Interaction)
|
||||
|
||||
- **禁止:** Controller 层严禁导入 `gorm` 包,严禁执行 SQL。
|
||||
- **迁移:** 生产环境严禁使用 `AutoMigrate`。必须使用 `migrations/` 目录下的版本化 SQL 脚本进行变更。
|
||||
|
||||
### 4.3. 路由注册 (Router Registration)
|
||||
|
||||
- **规则:** 路由不再集中管理。
|
||||
- **实现:** 每个领域包暴露一个 `RegisterRoutes(r *gin.RouterGroup)` 方法。在 `main.go` 启动时,统一调用各模块的注册方法。
|
||||
|
||||
---
|
||||
|
||||
## 5. AI 编程指令 (Instruction for AI Agent)
|
||||
|
||||
> **当作为 AI 助手编写代码时,请严格遵守以下指令:**
|
||||
|
||||
1. **Context Check:** 在生成代码前,检查当前目录结构是否符合 `Section 2`。如果不符,请优先建议重构或遵循现有结构。
|
||||
2. **No Logic Leak:** 确保 HTTP 处理逻辑(解析参数、校验参数)留在 `handler.go`,业务规则(判断权限、计算)留在 `service.go`,SQL 操作留在 `repository.go`。
|
||||
3. **Wire Awareness:** 每当新增 Service 或 Repository,必须自动更新同目录下的 `provider.go`,并在 `cmd/server/wire.go` 中检查是否需要重新生成。
|
||||
4. **Testability:** 编写 Repository 代码时,优先考虑“如何 Mock”。
|
||||
|
||||
```bash
|
||||
|
||||
==== 规范数据库设计 & 变更管理及工程流操作.md ====
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 🛠️ Database Engineering & Migration Standard (v1.0)
|
||||
date created: 星期日, 十二月 7日 2025, 10:31:59 晚上
|
||||
date modified: 星期二, 十二月 9日 2025, 10:14:44 晚上
|
||||
---
|
||||
|
||||
# 🛠️ Database Engineering & Migration Standard (v1.0)
|
||||
|
||||
文档用途: 规范数据库设计、变更管理及工程流操作。
|
||||
|
||||
适用范围: 所有涉及 Schema 变更的后端开发任务。
|
||||
|
||||
核心原则: Code First (Logic) but SQL First (Schema). 严禁生产环境使用 ORM 自动建表。
|
||||
|
||||
---
|
||||
|
||||
## 1. 基础设施与工具链 (Infrastructure & Tools)
|
||||
|
||||
本项目采用 **“容器化数据库 + 版本化迁移工具”** 的架构。
|
||||
|
||||
| **组件** | **选型** | **说明** |
|
||||
| --------------- | ------------------ | ----------------------------------------- |
|
||||
| **Database** | **PostgreSQL 15+** | 运行于 Docker 容器中,保证开发/生产环境一致。 |
|
||||
| **Schema Mgmt** | **Golang-Migrate** | CLI 工具,用于生成和执行版本化 SQL 脚本。 |
|
||||
| **GUI Client** | **Navicat** | 推荐 Navicat / DataGrip / DBeaver,仅用于设计和验证。 |
|
||||
| **Automation** | **Make** | 封装常用命令,屏蔽底层复杂参数。 |
|
||||
|
||||
### 1.1 目录结构规范
|
||||
|
||||
Plaintext
|
||||
|
||||
```bash
|
||||
project-root/
|
||||
├── migrations/ # [Source of Truth] 存放所有 SQL 变更文件
|
||||
│ ├── 000001_init_users.up.sql
|
||||
│ └── 000001_init_users.down.sql
|
||||
├── internal/
|
||||
│ └── {domain}/ # 领域包
|
||||
│ └── entity.go # [Code Mapping] GORM 结构体定义
|
||||
├── docker-compose.yml # 定义本地 DB 容器
|
||||
└── Makefile # 集成迁移命令
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据库设计规范 (Design Standards)
|
||||
|
||||
### 2.1 命名约定
|
||||
|
||||
- **表名:** 必须使用**复数**形式,`snake_case` (e.g., `users`, `order_items`).
|
||||
- **字段名:** 全小写,`snake_case` (e.g., `created_at`, `user_id`).
|
||||
- **索引名:**
|
||||
- 普通索引: `idx_tablename_column`
|
||||
- 唯一索引: `uniq_tablename_column`
|
||||
- **外键名:** `fk_tablename_ref_tablename`
|
||||
|
||||
### 2.2 关键字段约束
|
||||
|
||||
所有业务表**必须**包含以下基础字段:
|
||||
|
||||
```SQL
|
||||
id BIGSERIAL PRIMARY KEY, -- 或 UUID
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ -- 仅在需要软删除时添加
|
||||
```
|
||||
|
||||
### 2.3 设计禁忌
|
||||
|
||||
1. **严禁** 使用物理外键的级联删除 (`ON DELETE CASCADE`),除非是关联性极强的子表(如文章标签关联)。核心业务数据必须使用 `ON DELETE RESTRICT`。
|
||||
2. **严禁** 在涉及金额的字段使用 `FLOAT` 或 `DOUBLE`,必须使用 `DECIMAL` 或 `BIGINT` (分)。
|
||||
3. **严禁** 将 `NULL` 作为布尔值的第三种状态。布尔字段必须设置 `NOT NULL DEFAULT FALSE`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 标准作业流程 (SOP)
|
||||
|
||||
开发人员需严格遵循以下 **5 步闭环** 进行数据库变更:
|
||||
|
||||
### Step 1: 启动环境
|
||||
|
||||
确保本地 Docker 数据库正在运行。
|
||||
|
||||
```Bash
|
||||
make network # 对应 docker-compose up -d
|
||||
```
|
||||
|
||||
### Step 2: 创建迁移文件 (Create)
|
||||
|
||||
使用 Makefile 生成成对的 `.sql` 文件(up/down)。
|
||||
|
||||
- `name` 参数应简短描述变更内容(如 `add_avatar_to_users`)。
|
||||
|
||||
```Bash
|
||||
make new_migration name=init_schema
|
||||
# 输出:
|
||||
# Created migrations/000001_init_schema.up.sql
|
||||
# Created migrations/000001_init_schema.down.sql
|
||||
```
|
||||
|
||||
### Step 3: 编写 SQL (Edit)
|
||||
|
||||
- **UP 文件:** 填入 `CREATE TABLE`, `ALTER TABLE`, `CREATE INDEX` 等正向操作。
|
||||
- _技巧:_ 可在 GUI 工具中设计好表结构,复制生成的 DDL 语句粘贴至此。
|
||||
- **DOWN 文件:** 填入对应的回滚操作(如 `DROP TABLE`, `DROP INDEX`)。
|
||||
|
||||
### Step 4: 执行变更 (Apply)
|
||||
|
||||
将 SQL 应用到本地数据库。
|
||||
|
||||
```Bash
|
||||
make migrate_up
|
||||
```
|
||||
|
||||
_验证:_ 使用 GUI 工具连接数据库,确认表结构已更新。
|
||||
|
||||
### Step 5: 代码映射 (Mapping)
|
||||
|
||||
在 `internal/{domain}/entity.go` 中编写对应的 Go Struct。
|
||||
|
||||
- 确保 `gorm` tag 与数据库定义一致。
|
||||
- 确保 `json` tag 符合 API 契约。
|
||||
|
||||
---
|
||||
|
||||
## 4. 自动化配置 (Automation)
|
||||
|
||||
将以下内容固化到项目根目录的 `Makefile` 中。
|
||||
|
||||
> **注意:** 确保 `DB_DSN` 与 `docker-compose.yml` 中的配置完全一致。
|
||||
|
||||
```Makefile
|
||||
# ==============================================================================
|
||||
# Database & Migration Logic
|
||||
# ==============================================================================
|
||||
|
||||
# Database Connection String
|
||||
# 格式: postgres://user:password@host:port/dbname?sslmode=disable
|
||||
DB_DSN := postgres://postgres:secret@localhost:5432/cms_core?sslmode=disable
|
||||
|
||||
.PHONY: network new_migration migrate_up migrate_down migrate_force
|
||||
|
||||
# 1. 启动本地环境
|
||||
network:
|
||||
docker-compose up -d
|
||||
|
||||
# 2. 创建新的迁移文件 (Usage: make new_migration name=create_users)
|
||||
new_migration:
|
||||
@if [ -z "$(name)" ]; then echo "Error: name is required"; exit 1; fi
|
||||
migrate create -ext sql -dir migrations -seq $(name)
|
||||
|
||||
# 3. 执行所有未执行的迁移 (Up)
|
||||
migrate_up:
|
||||
migrate -path migrations -database "$(DB_DSN)" up
|
||||
|
||||
# 4. 回滚上一次迁移 (Down 1 step)
|
||||
migrate_down:
|
||||
migrate -path migrations -database "$(DB_DSN)" down 1
|
||||
|
||||
# 5. 强制修复版本 (当 dirty database 时使用, version 为具体的版本号)
|
||||
migrate_force:
|
||||
migrate -path migrations -database "$(DB_DSN)" force $(version)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 故障排查 (Troubleshooting)
|
||||
|
||||
**Q: 执行 migrate_up 时报错 "Dirty database version x".**
|
||||
|
||||
- **原因:** 上一次迁移执行到一半失败了(可能是 SQL 语法错误),导致版本锁死。
|
||||
- **解决:**
|
||||
|
||||
1. 手动修复 SQL 文件中的语法错误。
|
||||
2. 执行 `make migrate_force version=x` (x 是失败前的那个版本号)。
|
||||
3. 再次执行 `make migrate_up`。
|
||||
|
||||
**Q: 多人协作时产生版本冲突。**
|
||||
|
||||
- **现象:** 你有一个 `0003_add_xx.up.sql`,同事提交代码后也有一个 `0003_add_yy.up.sql`。
|
||||
- **解决:** 重命名你的迁移文件编号为 `0004`,确保序列号在时间轴上是递增且唯一的。
|
||||
|
||||
```bash
|
||||
|
||||
==== 七七八八的接口设计相关问题.md ====
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 1. 接口版本控制策略 (API Versioning Strategy)
|
||||
date created: 星期日, 十二月 7日 2025, 11:38:52 晚上
|
||||
date modified: 星期日, 十二月 7日 2025, 11:42:18 晚上
|
||||
---
|
||||
|
||||
# 七七八八的接口设计相关问题
|
||||
|
||||
## 1. 接口版本控制策略 (API Versioning Strategy)
|
||||
|
||||
**核心问题:** 当你发布了 V1 版本后,某天需要修改接口字段(比如把 `name` 拆分为 `first_name` 和 `last_name`),如何保证老版本的 App 不会崩溃?
|
||||
|
||||
**三种主流流派:**
|
||||
|
||||
1. **URI Path Versioning (推荐):**
|
||||
|
||||
- **格式:** `https://api.example.com/v1/users`
|
||||
- **优点:** 直观、易于调试、缓存友好。这也是 GitHub, Twitter, Google API 采用的主流方案。
|
||||
- **落地:** 我们在 Gin 的 Router Group 中直接体现:
|
||||
|
||||
Go
|
||||
|
||||
```bash
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
v1.GET("/users", ...)
|
||||
}
|
||||
```
|
||||
|
||||
2. **Header Versioning:**
|
||||
|
||||
- **格式:** Header 中添加 `Accept: application/vnd.myapi.v1+json`
|
||||
- **优点:** URL 干净。
|
||||
- **缺点:** 调试麻烦(浏览器直接访问 URL 看不到结果),CDN 缓存配置复杂。**不推荐 MVP 阶段使用。**
|
||||
|
||||
3. **Query Parameter:**
|
||||
|
||||
- **格式:** `/users?version=1`
|
||||
- **评价:** 看起来很土,通常不用于 RESTful API。
|
||||
|
||||
**👉 你的策略:** 坚定选择 **URI Path Versioning (`/api/v1`)**。只在发生**破坏性变更 (Breaking Change)** 时才升级到 v2。新增字段不算破坏性变更,不需要升级版本。
|
||||
|
||||
---
|
||||
|
||||
## 2. HTTP 方法的精准语义 (Verbs Semantics)
|
||||
|
||||
很多新手只会用 `GET` 和 `POST`。企业级 API 必须精准区分以下方法的含义:
|
||||
|
||||
|**方法**|**语义**|**幂等性 (Idempotency)**|**典型场景**|
|
||||
|---|---|---|---|
|
||||
|**GET**|获取资源|✅ 是|获取文章列表、详情|
|
||||
|**POST**|新建资源|❌ 否|发布新文章、提交评论|
|
||||
|**PUT**|**全量替换**资源|✅ 是|修改文章(客户端发送文章的完整 JSON,没传的字段会被置空)|
|
||||
|**PATCH**|**局部更新**资源|❌ 否 (理论上)|修改文章状态(只传 `{"status": "published"}`,其他字段不变)|
|
||||
|**DELETE**|删除资源|✅ 是|删除文章|
|
||||
|
||||
⚠️ 重点关注 PUT vs PATCH:
|
||||
|
||||
在 Go 语言中实现 PATCH 有点麻烦(因为 Go 的结构体默认值问题,你很难区分用户是传了 0 还是没传这个字段)。
|
||||
|
||||
- **最佳实践:** 对于 CMS 这种表单复杂的系统,**修改接口首选 `PUT` (全量)**,或者针对特定状态修改提供独立接口(如 `POST /articles/:id/publish`)。如果必须做 `PATCH`,DTO 需使用指针类型 `*string` 来判断是否为 `nil`。
|
||||
|
||||
---
|
||||
|
||||
## 3. RESTful URL 设计模式 (Resource Naming)
|
||||
|
||||
**原则:URL 中只出现名词,不出现动词。**
|
||||
|
||||
- ❌ **反例 (RPC 风格 - 不要这么做):**
|
||||
- `/api/getUsers`
|
||||
- `/api/createUser`
|
||||
- `/api/deleteArticle?id=1`
|
||||
- ✅ **正例 (REST 风格):**
|
||||
- `GET /api/v1/users` (获取列表)
|
||||
- `POST /api/v1/users` (创建)
|
||||
- `DELETE /api/v1/articles/1` (删除 ID 为 1 的文章)
|
||||
|
||||
**复杂关系的嵌套设计:**
|
||||
|
||||
- _场景:_ 获取某篇文章下的评论。
|
||||
- _设计:_ `GET /api/v1/articles/{article_id}/comments`
|
||||
- _场景:_ 获取某个作者的所有文章。
|
||||
- _设计:_ `GET /api/v1/users/{user_id}/articles`
|
||||
|
||||
---
|
||||
|
||||
## 4. 列表接口三剑客:分页、排序、筛选 (Pagination, Sorting, Filtering)
|
||||
|
||||
你的 CMS 一定会有“文章列表”页面,这个接口是最复杂的。不要为每种查询都写一个新接口,要设计一个**通用的查询接口**。
|
||||
|
||||
**最佳实践标准:**
|
||||
|
||||
1. **分页 (Pagination):**
|
||||
|
||||
- 使用 `page` (页码) 和 `page_size` (每页条数)。
|
||||
- URL 示例: `/articles?page=2&page_size=20`
|
||||
- **注意:** 要限制 `page_size` 的最大值(如 100),防止恶意用户一次请求 100 万条数据把数据库打挂。
|
||||
|
||||
2. **排序 (Sorting):**
|
||||
|
||||
- 使用 `sort` 参数。`-` 代表降序,无符号代表升序。
|
||||
- URL 示例: `/articles?sort=-created_at` (按创建时间倒序)
|
||||
- URL 示例: `/articles?sort=view_count,-created_at` (先按浏览量升序,再按时间倒序)
|
||||
|
||||
3. **筛选 (Filtering):**
|
||||
|
||||
- 直接使用字段名作为参数。
|
||||
- URL 示例: `/articles?category_id=1&status=published`
|
||||
|
||||
---
|
||||
|
||||
## 5. 状态码与错误处理 (Status Codes & Error Handling)
|
||||
|
||||
**不要永远只返回 200 OK!**
|
||||
|
||||
前端开发最恨的就是:HTTP 状态码是 200,结果 Body 里写着 `{"code": 500, "msg": "Error"}`。这会让监控系统失效。
|
||||
|
||||
**你需要遵守的“HTTP 状态码地图”:**
|
||||
|
||||
- **2xx (成功):**
|
||||
- `200 OK`: 通用成功。
|
||||
- `201 Created`: 创建成功 (POST 返回)。
|
||||
- `204 No Content`: 删除成功 (DELETE 返回,不带 Body)。
|
||||
- **4xx (客户端错误 - 前端背锅):**
|
||||
- `400 Bad Request`: 参数校验失败(如邮箱格式不对)。
|
||||
- `401 Unauthorized`: 未登录/Token 过期。
|
||||
- `403 Forbidden`: 登录了,但没权限(如普通用户想删文章)。
|
||||
- `404 Not Found`: 资源不存在。
|
||||
- **5xx (服务端错误 - 你背锅):**
|
||||
- `500 Internal Server Error`: 代码崩了/数据库挂了。
|
||||
|
||||
统一错误响应格式 (JSON Envelope):
|
||||
|
||||
无论发生什么错误,Body 必须保持结构一致,方便前端拦截:
|
||||
|
||||
JSON
|
||||
|
||||
```bash
|
||||
{
|
||||
"code": 40001, // 业务错误码 (你在 pkg/e 定义的)
|
||||
"message": "标题已存在", // 给用户看的提示
|
||||
"request_id": "abc-123" // 方便查日志的 Trace ID
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 进阶知识:幂等性 (Idempotency)
|
||||
|
||||
**概念:** 用户因为网络卡顿,连续点击了 3 次“发布文章”按钮,你的系统会创建 1 篇文章还是 3 篇?
|
||||
|
||||
- **非幂等:** `POST`。点 3 次创建 3 篇。
|
||||
- **幂等:** `PUT` / `DELETE`。改 3 次和改 1 次结果一样;删 3 次和删 1 次结果一样(都是没了)。
|
||||
|
||||
如何处理 POST 的幂等性?
|
||||
|
||||
对于关键业务(如支付、核心数据创建),通常要求前端在 Header 带一个 Idempotency-Key (UUID)。后端 Redis 记录这个 Key,如果短时间内收到相同的 Key,直接返回上次的结果,不执行数据库插入。
|
||||
|
||||
(注:MVP 阶段可以先不做,但面试或者做高可靠系统时必须知道)
|
||||
|
||||
---
|
||||
|
||||
## 总结:在你开始定义 API 之前
|
||||
|
||||
请在心中建立这套标准:
|
||||
|
||||
1. **URL:** `/api/v1/名词`
|
||||
2. **Method:** 查询用 GET,新建用 POST,修改用 PUT,删除用 DELETE。
|
||||
3. **List:** 永远记得预留 `page`, `page_size`。
|
||||
4. **Response:** 永远返回统一的 JSON 结构,报错必须用 4xx/5xx 状态码。
|
||||
|
||||
**准备好了吗?我们可以开始基于这些原则,设计 User 模块的 API 了。**
|
||||
|
||||
```bash
|
||||
|
||||
==== 产品需求规格说明书 (PRD) - V1.1.md ====
|
||||
```markdown
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 📝 产品需求规格说明书 (PRD) - V1.1
|
||||
date created: 星期日, 十二月 7日 2025, 12:14:41 中午
|
||||
date modified: 星期日, 十二月 7日 2025, 12:49:19 下午
|
||||
---
|
||||
|
||||
# 📝 产品需求规格说明书 (PRD) - V1.1
|
||||
|
||||
> **更新日志:**
|
||||
>
|
||||
> - v1.0: 初始版本,定义功能列表。
|
||||
>
|
||||
> - **v1.1:** [2025-12-07] 补充项目战略背景;优化软删除与缓存策略的灵活性;明确长文本存储类型。
|
||||
|
||||
项目名称: Enterprise-CMS-Core (企业级内容管理系统核心)
|
||||
|
||||
版本: 1.1.0
|
||||
|
||||
状态: [✅ 已锁定]
|
||||
|
||||
适用对象: 后端开发人员、架构师、测试人员
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目战略概述 (Strategic Overview)
|
||||
|
||||
### 1.1 项目背景与目标
|
||||
|
||||
本项目并非单纯为了交付一个 CMS 软件,而是为了构建一个**“Go 语言企业级后端架构样板间”**。
|
||||
|
||||
- **核心目标:** 验证并固化一套“模块化整洁架构”工程实践,使其具备**高可维护性**、**可扩展性**和**安全性**。
|
||||
- **衍生价值:** 产出的源码将作为团队未来的“SaaS 启动脚手架 (Boilerplate)”,或作为独立的高价值技术资产(源码付费产品)进行商业变现。
|
||||
|
||||
### 1.2 核心用户与价值
|
||||
|
||||
- **系统管理员 (Admin):** 痛点是“安全与失控风险”。核心价值是提供**银行级的 RBAC 权限控制**,确保没人能越权操作。
|
||||
- **内容编辑 (Editor):** 痛点是“流程混乱”。核心价值是提供**状态明确的内容流转机制**(草稿 ->审核 ->发布),防止误发。
|
||||
- **二开开发者 (Developer):** 痛点是“屎山代码”。核心价值是提供**清晰的依赖边界**和**开箱即用的基础设施**。
|
||||
|
||||
### 1.3 成功指标 (Success Metrics)
|
||||
|
||||
1. **业务完整性:** 必须完整支持 3 种标准角色(Admin/Editor/Subscriber)的权限隔离,且文章状态流转无逻辑漏洞。
|
||||
2. **工程质量:** 核心业务模块(User/Auth)单元测试覆盖率 > 80%;通过静态代码分析,无循环依赖。
|
||||
3. **性能基线:** 在单机 2C4G 配置下,并发 100 QPS 时,API P99 响应时间 < 200ms。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心功能范围 (In-Scope)
|
||||
|
||||
### 2.1 认证与鉴权模块 (Auth & IAM)
|
||||
|
||||
**唯一来源:** 必须使用 JWT 双令牌机制 + RBAC 模型。
|
||||
|
||||
- **F-AUTH-01 用户注册:** 仅支持“用户名 + 密码”注册。密码必须经过 Argon2 或 Bcrypt 哈希存储。
|
||||
- **F-AUTH-02 用户登录:** 校验账号密码,返回 `Access Token` (短效 15min) 和 `Refresh Token` (长效 7 天)。
|
||||
- **F-AUTH-03 令牌刷新:** 使用有效的 Refresh Token 换取新的 Access Token。**旧的 Refresh Token 若被复用需触发安全警报(可选)或直接失效**。
|
||||
- **F-AUTH-04 统一登出:** 强制使 Refresh Token 失效(需在 Redis 中建立黑名单或白名单机制)。
|
||||
- **F-AUTH-05 密码重置:** 登录状态下修改密码,修改成功后强制吊销所有 Token。
|
||||
|
||||
### 2.2 用户与权限模块 (User & RBAC)
|
||||
|
||||
**预设角色:** 系统初始化必须包含以下三种角色。
|
||||
|
||||
|**角色代码**|**名称**|**权限描述**|
|
||||
|---|---|---|
|
||||
|`admin`|超级管理员|拥有系统所有权限 (用户管理、角色分配、内容强制删除)。|
|
||||
|`editor`|内容编辑|拥有文章发布、审核、标签管理权限。不可管理用户。|
|
||||
|`subscriber`|普通用户|仅拥有修改自身资料、发布评论、查看公开文章权限。|
|
||||
|
||||
- **F-USER-01 个人资料:** 查询与更新当前登录用户的昵称、头像 URL、简介。
|
||||
- **F-USER-02 用户管理 (Admin):** 管理员可查看用户列表,封禁/解封用户状态。
|
||||
- **F-RBAC-01 角色分配 (Admin):** 管理员可修改用户的角色(如将 User 提权为 Editor)。
|
||||
|
||||
### 2.3 内容核心模块 (CMS Core)
|
||||
|
||||
**核心逻辑:** 文章必须包含状态流转。
|
||||
|
||||
- **F-ART-01 文章 CRUD:**
|
||||
- **创建:** 默认为 `Draft` (草稿) 状态。
|
||||
- **字段:** 标题、内容、封面图 URL、作者 ID。
|
||||
- **数据类型约束:** 文章内容字段在数据库层面建议使用 `TEXT` 或 `LONGTEXT` 类型,以完整承载 Markdown/HTML 长文本。
|
||||
- **F-ART-02 文章状态流转:**
|
||||
- 支持状态: `Draft` (草稿) -> `Pending` (待审核) -> `Published` (已发布) -> `Archived` (归档/软删除)。
|
||||
- **F-ART-03 分类与标签:**
|
||||
- 文章必须归属一个分类 (Category)。
|
||||
- 文章可关联多个标签 (Tags)。
|
||||
- **F-ART-04 内容审核 (Editor/Admin):**
|
||||
- 拥有审核权限的角色可将 `Pending` 状态的文章改为 `Published` 或驳回至 `Draft`。
|
||||
- **F-ART-05 公开检索:**
|
||||
- 仅 `Published` 状态的文章对外接口可见。支持按 分类、标签、标题关键词 搜索。
|
||||
|
||||
### 2.4 互动模块 (Interaction)
|
||||
|
||||
- **F-CMT-01 评论发布:** 登录用户可对 `Published` 文章发表评论。
|
||||
- **F-CMT-02 评论管理:** 作者可删除自己文章下的评论;Admin/Editor 可删除任何违规评论。
|
||||
|
||||
---
|
||||
|
||||
## 3. 非功能性需求 (Non-Functional Requirements)
|
||||
|
||||
**开发人员必须严格遵守以下技术约束:**
|
||||
|
||||
### 3.1 数据一致性
|
||||
|
||||
- **删除策略 [优化]:** 核心业务数据(用户、文章)原则上必须使用 Soft Delete (`deleted_at` 字段)。
|
||||
- _例外条款:_ 涉及法律合规(如 GDPR 用户遗忘权)或垃圾数据清理时,经系统管理员明确审批操作后,允许提供物理删除接口。
|
||||
- **事务:** 文章发布与标签关联必须在同一个 Database Transaction 中完成。
|
||||
|
||||
### 3.2 性能与缓存
|
||||
|
||||
- **API 响应:** 95% 的请求响应时间需 < 200ms (不含网络延迟)。
|
||||
- **缓存策略:**
|
||||
- 建议对 **高频读取且低频修改** 的数据(如用户信息 `/profile`、热门文章详情 `/article/:id`)实施缓存策略。
|
||||
- 具体的缓存实现(Redis Key 设计、TTL 时长、Cache-Aside 或 Write-Through 模式)由开发团队根据实际压测结果灵活调整,不强制硬编码 TTL。
|
||||
|
||||
### 3.3 安全性
|
||||
|
||||
- **SQL 注入:** 严禁拼接 SQL,必须使用 GORM 参数化查询。
|
||||
- **敏感数据:** 密码、RefreshToken 严禁明文出现在日志中。
|
||||
- **接口保护:** 除登录、注册、公开文章列表外,所有接口必须通过 JWT 中间件校验。
|
||||
|
||||
### 3.4 工程规范
|
||||
|
||||
- **Schema:** 数据库表结构变更必须提供 Up/Down SQL 迁移脚本。
|
||||
- **Doc:** 所有 API 必须自动生成 Swagger 文档。
|
||||
|
||||
---
|
||||
|
||||
## 4. 不在范围 (Out of Scope)
|
||||
|
||||
**以下功能明确不包含在本次 Phase 1 开发中:**
|
||||
|
||||
1. **❌ 第三方登录:** 不做微信/GitHub/Google 登录。
|
||||
2. **❌ 消息推送/通知:** 不做系统内通知。
|
||||
3. **❌ 文件存储服务 (OSS):** 仅处理 URL 字符串,不处理文件流上传。
|
||||
4. **❌ 复杂的富文本处理:** 后端仅存储字符串,不解析 HTML。
|
||||
5. **❌ 支付与订单:** 不包含任何电商逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心数据实体关系图 (ER 简述)
|
||||
|
||||
- **User** (1) <-> (N) **Article**
|
||||
- **User** (1) <-> (N) **Comment**
|
||||
- **Article** (1) <-> (N) **Comment**
|
||||
- **Article** (N) <-> (1) **Category**
|
||||
- **Article** (N) <-> (N) **Tag** (Many-to-Many)
|
||||
```
|
||||
Reference in New Issue
Block a user