From 388d4439de3edfebb39e2aac17646a8a16d414c7 Mon Sep 17 00:00:00 2001 From: wonder Date: Wed, 22 Apr 2026 10:10:19 +0800 Subject: [PATCH] vault backup: 2026-04-22 10:10:19 --- CLAUDE.md | 17 + CS/NET/HeavyKeeper.md | 136 ++--- CS/TOOLS/Docker 安装.md | 132 +++++ DEV/GO/GORM.md | 470 +++++++++++++++ config/agent/CLAUDE.md | 30 - config/agent/DOCUMENT_OPERATION.md | 42 ++ config/agent/agent.md | 63 -- config/agent/policies/allowlist.md | 38 -- config/agent/policies/blocklist.md | 40 -- config/agent/policies/standards.md | 66 --- config/template/default-template.md | 18 - 金山办公作业/Week05/PLAN.md | 6 +- 金山办公作业/Week05/db in docker.md | 362 ++++++++++++ 金山办公作业/Week05/viper 和 godotenv 区别.md | 545 ++++++++++++++++++ 金山办公作业/Week05/数据库设计.md | 46 +- 15 files changed, 1656 insertions(+), 355 deletions(-) create mode 100644 CLAUDE.md create mode 100644 CS/TOOLS/Docker 安装.md create mode 100644 DEV/GO/GORM.md delete mode 100644 config/agent/CLAUDE.md create mode 100644 config/agent/DOCUMENT_OPERATION.md delete mode 100644 config/agent/agent.md delete mode 100644 config/agent/policies/allowlist.md delete mode 100644 config/agent/policies/blocklist.md delete mode 100644 config/agent/policies/standards.md delete mode 100644 config/template/default-template.md create mode 100644 金山办公作业/Week05/db in docker.md create mode 100644 金山办公作业/Week05/viper 和 godotenv 区别.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a380550 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,17 @@ +# Claude Code 配置 - Obsidian 知识库 + +此知识库级别的配置为 Claude 操作提供额外的指导说明。 + +## 新增和完善文件 + +在初始化和完成文件时,必须**完整**读取参照 `./config/agent/DOCUMENT_OPERATION.md +了解结构、规范和惯例。 +在初次完善后,读取文件并二次检查是否符合文档的每一条内容规范—— +- **教学者模式** 提问设计 +- **代码示例** 合理注释 +- **图表** Mermaid 格式正确性 +## 行为规范 + +- 不要读取多余的无关文件,以减少 Token 消耗,这非常重要。 +- 当你不确定某个文件应该放在什么地方时,应当优先询问用户,而不是自己探索。 + diff --git a/CS/NET/HeavyKeeper.md b/CS/NET/HeavyKeeper.md index 92b2f70..1c1b704 100644 --- a/CS/NET/HeavyKeeper.md +++ b/CS/NET/HeavyKeeper.md @@ -244,89 +244,77 @@ graph LR ## 参考实现 -### 伪代码 +### 核心实现(伪代码) ```python class HeavyKeeper: - def __init__(self, m, L, threshold, decay_factor): - self.m = m # 每层桶数 - self.L = L # 哈希层数 - self.threshold = threshold - self.decay_factor = decay_factor # 衰减因子 - - # 初始化多层 sketch - self.buckets = [[KeeperBucket() for _ in range(m)] - for _ in range(L)] - - # 初始化哈希函数 - self.hash_funcs = [get_hash_func(i) for i in range(L)] - - def insert(self, item, timestamp): + def __init__(self, m, L, decay_factor): + self.m, self.L = m, L + self.decay_factor = decay_factor + self.buckets = [[Bucket() for _ in range(m)] for _ in range(L)] + + def insert(self, item): for layer in range(self.L): - idx = self.hash_funcs[layer](item) % self.m - bucket = self.buckets[layer][idx] - + bucket = self.buckets[layer][hash(item) % self.m] if bucket.item == item: - # 守护项匹配,直接计数(大象流强化) - bucket.count += 1 - bucket.last_seen = timestamp + bucket.count += 1 # 守护项匹配,直接计数 else: - # 计算替换概率 - estimated_freq = self._estimate_freq(item) - replace_prob = min(1, estimated_freq / bucket.count) - - if random.random() < replace_prob: - # 替换为新项(大象流夺权) - bucket.item = item - bucket.count = 1 - bucket.error = bucket.count - bucket.last_seen = timestamp + # 空桶直接占领;否则按概率争夺 + prob = 1 if bucket.count == 0 else min(1, self._estimate(item) / bucket.count) + if random() < prob: + bucket.item, bucket.count, bucket.error = item, 1, 0 # 替换为新项 else: - # 不替换,仅增加误差(老鼠流被阻拦) - bucket.error += 1 - - def apply_decay(self, current_time): - """应用衰减机制""" - for layer in range(self.L): - for bucket in self.buckets[layer]: - elapsed = current_time - bucket.last_seen - if elapsed > DECAY_INTERVAL: - bucket.count *= self.decay_factor - bucket.error *= self.decay_factor - - # 归零清理 - if bucket.count < 1: - bucket.item = None - bucket.count = 0 - bucket.error = 0 - - def query(self, item): - """查询Item的频率估计""" - min_count = float('inf') - for layer in range(self.L): - idx = self.hash_funcs[layer](item) % self.m - bucket = self.buckets[layer][idx] - if bucket.item == item: - min_count = min(min_count, bucket.count) - - return min_count if min_count != float('inf') else 0 - - def get_top_k(self, k): - """获取Top K大象流""" - candidates = {} - for layer in range(self.L): - for bucket in self.buckets[layer]: - if bucket.count >= self.threshold and bucket.item: - item = bucket.item - candidates[item] = max(candidates.get(item, 0), - bucket.count) - - # 返回 Top-k - return sorted(candidates.items(), - key=lambda x: x[1], - reverse=True)[:k] + bucket.error += 1 # 不替换,仅增加误差 ``` +### Go 参考实现 + +```go +package heavykeeper + +type Bucket struct { + Item []byte + Count uint32 + Error uint32 +} + +type HeavyKeeper struct { + m, L uint32 + decayFactor float64 + buckets [][]Bucket + hashFuncs []func([]byte) uint64 +} + +func (hk *HeavyKeeper) Insert(item []byte) { + for layer := uint32(0); layer < hk.L; layer++ { + idx := hk.hashFuncs[layer](item) % hk.m + bucket := &hk.buckets[layer][idx] + + if bytes.Equal(bucket.Item, item) { + bucket.Count++ + continue + } + + var prob float64 + if bucket.Count == 0 { + prob = 1 // 空桶直接占领 + } else { + prob = float64(hk.estimate(item)) / float64(bucket.Count) + if prob > 1 { + prob = 1 + } + } + if rand.Float64() < prob { + bucket.Item, bucket.Count, bucket.Error = item, 1, 0 + } else { + bucket.Error++ + } + } +} +``` + +> **说明**:`insert` 是核心逻辑——对每层哈希找桶,匹配守护项则计数,否则按概率争夺。伪代码省略了衰减、查询、Top-K 等辅助方法,实际部署时按需补充。 + ## 相关算法对比 | 算法 | 空间复杂度 | 大象流准确性 | 老鼠流过滤 | 衰减支持 | 适用场景 | diff --git a/CS/TOOLS/Docker 安装.md b/CS/TOOLS/Docker 安装.md new file mode 100644 index 0000000..8f71fca --- /dev/null +++ b/CS/TOOLS/Docker 安装.md @@ -0,0 +1,132 @@ +--- +tags: [CS, TOOLS, Docker, 容器化] +create time: 2026-04-21 +--- + +# Docker 安装指南 + +## 概述 + +Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。 + +## 系统要求 + +- Ubuntu 24.04 LTS (Jammy Jellyfish) +- 64-bit architecture +- 内核版本 4.15 或更高版本 + +## 安装步骤 + +### 1. 更新系统包索引 + +```bash +sudo apt update +``` + +### 2. 安装必要的依赖包 + +```bash +sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release +``` + +### 3. 添加 Docker 官方 GPG 密钥 + +```bash +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +``` + +### 4. 添加 Docker APT 仓库 + +```bash +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +``` + +### 5. 更新包索引 + +```bash +sudo apt update +``` + +### 6. 安装 Docker Engine + +```bash +sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin +``` + +### 7. 启动并启用 Docker 服务 + +```bash +sudo systemctl start docker +sudo systemctl enable docker +``` + +### 8. 将当前用户添加到 docker 组(可选,但推荐) + +```bash +sudo usermod -aG docker $USER +``` + +> 请注意:执行此命令后,需要重新登录或运行 `newgrp docker` 命令使组成员身份生效。 + +## 验证安装 + +执行以下命令来验证 Docker 是否正确安装: + +```bash +docker --version +``` + +运行一个简单的测试容器: + +```bash +docker run hello-world +``` + +## 常用命令 + +| 命令 | 说明 | +|------|------| +| `docker pull [镜像名]` | 拉取镜像 | +| `docker run [镜像名]` | 运行容器 | +| `docker ps` | 显示正在运行的容器 | +| `docker ps -a` | 显示所有容器 | +| `docker images` | 显示所有镜像 | +| `docker stop [容器名/ID]` | 停止容器 | +| `docker rm [容器名/ID]` | 删除容器 | +| `docker rmi [镜像名/ID]` | 删除镜像 | + +## 后续操作 + +- **Docker Compose**:如果您需要使用 Docker Compose,在安装 Docker 时已包含,可直接使用 +- **配置 Docker**:可以修改 `/etc/docker/daemon.json` 文件进行配置 + +## 安全说明 + +- 不要使用 root 用户运行 Docker +- 为非 root 用户添加 docker 组时要小心,因为该用户将具有 root 权限 +- 定期更新 Docker 和容器镜像 + +## 故障排除 + +### 权限问题 + +如果遇到权限问题,确保当前用户已被添加到 docker 组中: + +```bash +sudo usermod -aG docker $USER +``` + +然后注销并重新登录。 + +### 服务启动失败 + +如果 Docker 服务无法启动,可以检查其状态: + +```bash +sudo systemctl status docker +``` + +## 参考资料 + +- [Docker 官方安装指南](https://docs.docker.com/engine/install/ubuntu/) +- [Ubuntu 24.04 Docker 安装文档](https://ubuntu.com/server/docs/container-docker) diff --git a/DEV/GO/GORM.md b/DEV/GO/GORM.md new file mode 100644 index 0000000..9da6e9d --- /dev/null +++ b/DEV/GO/GORM.md @@ -0,0 +1,470 @@ +--- +tags: [DEV, GO, golang, GORM, backend, ORM, database] +create time: 2026-04-22 10:00 +--- + +# GORM - Go 语言 ORM 框架 + +## 概述 + +GORM 是 Go 语言中最流行的 ORM(对象关系映射)框架,由 [@jinzhu](https://github.com/jinzhu) 创建。它基于 `database/sql` 构建,提供了链式 API、关联预加载、插件扩展等特性,让 Go 开发者以符合 Go 风格的方式高效操作数据库。 + +> **思考**:为什么在有了 `database/sql` 标准库之后,还需要 GORM 这样的 ORM?ORM 的价值是什么?它的代价又是什么? + +ORM 的核心价值在于**将数据库表映射为结构体**,用面向对象的方式操作数据,减少样板 SQL 代码。代价是:理解 ORM 生成的 SQL 至关重要,复杂查询场景下仍需手写 SQL。 + +## 安装与初始化 + +```bash +go get -u gorm.io/gorm +go get -u gorm.io/driver/mysql # MySQL 驱动 +go get -u gorm.io/driver/postgres # PostgreSQL 驱动 +go get -u gorm.io/driver/sqlite # SQLite 驱动 +``` + +```go +import ( + "gorm.io/gorm" + "gorm.io/driver/mysql" +) + +// MySQL 连接 +db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) +if err != nil { + panic("failed to connect database") +} +``` + +## 定义模型 + +```go +type User struct { + ID uint `gorm:"primaryKey"` // 主键,自动命名为 id + Name string `gorm:"size:255;not null"` // 非空 + 字段长度 + Email string `gorm:"uniqueIndex"` // 唯一索引 + Age int `gorm:"default:0"` // 默认值 + Role string `gorm:"default:'user'"` + CreatedAt time.Time // 自动管理 CreatedAt + UpdatedAt time.Time // 自动管理 UpdatedAt + DeletedAt gorm.DeletedAt // 软删除支持 +} +``` + +**GORM 约定**: + +| 约定 | 说明 | +|------|------| +| 表名 | 结构体名的复数形式(`User` → `users`),可通过 `TableName()` 重写 | +| 主键 | 名为 `ID` 的字段自动作为主键,类型推断为 `INT UNIQUE PRIMARY KEY` | +| 字段名 | Go 字段名 → snake_case 的列名(`UserName` → `user_name`) | +| CreatedAt/UpdatedAt | 自动填充时间戳(可通过 `DisableCreateTimeIndex` / `DisableUpdateTimeIndex` 关闭) | + +## CRUD 操作 + +### 创建 + +```go +user := User{Name: "Alice", Age: 25, Role: "admin"} +result := db.Create(&user) // 返回 *gorm.Result + +// 批量创建(一次 INSERT) +users := []User{ + {Name: "Bob", Age: 30}, + {Name: "Carol", Age: 28}, +} +db.Create(&users) // GORM 自动优化为批量 INSERT +``` + +### 查询 + +```go +// 单一查询 +var user User +db.First(&user, 10) // 主键查询 +db.First(&user, "name = ?", "Alice") // 条件查询 +db.Where("age > ?", 20).First(&user) // Where 链式 + +// 多结果查询 +var users []User +db.Where("role = ?", "user").Find(&users) + +// 链式条件 +db.Where("role = ?", "admin"). + Where("age >= ?", 25). + Order("age desc"). + Limit(10). + Offset(20). + Find(&users) +``` + +**GORM 查询 API 速查**: + +| 方法 | 等价 SQL | +|------|----------| +| `First` / `Take` | `LIMIT 1`,找第一条 | +| `Last` | `ORDER BY PRIMARY KEY DESC LIMIT 1` | +| `Find` | `SELECT * FROM ...` | +| `Where` | `WHERE ...` | +| `Or` | `OR ...` | +| `Not` | `NOT ...` | +| `Order` | `ORDER BY ...` | +| `Select` | `SELECT col1, col2` | +| `Group` / `Having` | `GROUP BY` / `HAVING` | +| `Joins` | `JOIN ...` | +| `Scan` | 扫描到自定义结构体 | + +> **注意**:`First`、`Last` 在没有数据时返回 `gorm.ErrRecordNotFound`,务必处理此错误。 + +```mermaid +flowchart LR + A["db.Model(&User{})"] --> B["Where 条件过滤"] + B --> C["Order 排序"] + C --> D["Select 字段选择"] + D --> E["Limit/Offset 分页"] + E --> F["Group/Having 分组"] + F --> G["Preload 预加载关联"] + G --> H["Find/First/Last 执行"] + H --> I["Scan 扫描到结构体"] + + classDef start fill:#4285f4,color:#fff + classDef step fill:#f1f3f4,stroke:#333 + classDef finish fill:#34a853,color:#fff + class A start + class I finish + class B,C,D,E,F,G,H step +``` + +> **链式 API 的本质**:每个方法返回 `*gorm.DB` 对象,不断累积查询条件,最终 `Find`/`First` 时才组装并执行 SQL。 + +### 更新 + +```go +// 更新单个字段 +db.Model(&user).Update("name", "Alice Updated") + +// 更新多个字段 +db.Model(&user).Updates(User{Name: "Alice V2", Age: 26, Role: "superadmin"}) + +// 带条件的更新 +db.Where("age > ?", 18).Model(&user).Update("role", "adult") + +// 用 map 更新(会跳过零值字段,慎用) +db.Model(&user).Updates(map[string]interface{}{"name": "New", "age": 30}) +``` + +### 删除 + +```go +// 软删除(推荐) +db.Delete(&user) // 设置 DeletedAt = now(),而非物理删除 + +// 硬删除 +db.Unscoped().Delete(&user) // 物理删除 + +// 批量删除 +db.Where("age < ?", 18).Delete(&User{}) +``` + +## 关联(Associations) + +GORM 支持三种关联关系,通过标签定义。 + +```go +type User struct { + gorm.Model + Name string + Role string + CreditCards []CreditCard `gorm:"foreignKey:UserID"` // HasMany +} + +type CreditCard struct { + gorm.Model + Number string + UserID uint `gorm:"not null"` // 外键 +} +``` + +### 三种关联类型 + +```mermaid +graph LR + subgraph User 用户 + U1["ID\nName\nRole"] + end + + subgraph CreditCard 信用卡 + C1["ID\nNumber\nUserID (FK)"] + end + + subgraph Profile 个人资料 + P1["ID\nBio\nUserID (FK)"] + end + + subgraph Language 语言[关联表: user_languages] + L1["ID\nName"] + UM["user_id"] + LM["language_id"] + end + + U1 -->|"1:N HasMany"| C1 + U1 -->|"1:1 HasOne"| P1 + U1 -.->|"N:M BelongsToMany"| L1 + + classDef user fill:#4285f4,stroke:#336,stroke-width:2px,color:#fff + classDef card fill:#ea4335,stroke:#c33,stroke-width:2px,color:#fff + classDef profile fill:#34a853,stroke:#264,stroke-width:2px,color:#fff + classDef lang fill:#fbbc04,stroke:#cc8,stroke-width:2px,color:#333 + + class U1 user + class C1 card + class P1 profile + class L1 lang +``` + +> **关键理解**:标签写在**一端**,GORM 会根据外键位置自动判断关系方向。 + +- **HasOne**:外键在对方模型(`Profile.UserID`)→ 写在 `User` 上用 `hasOne` +- **HasMany**:外键在对方模型(`CreditCard.UserID`)→ 写在 `User` 上用 `hasMany` +- **BelongsTo**:外键在当前模型 → 写在「从属」一方 +- **BelongsToMany**:需要中间关联表(`user_languages`) + +| 关联类型 | 标签方式 | 示例 | +|----------|----------|------| +| **HasOne** | 外键在对方模型 | `Profile` 包含 `UserID`,`User` 通过 `HasOne` 关联 | +| **HasMany** | 外键在对方模型 | `CreditCards` 数组,`CreditCard` 包含 `UserID` | +| **BelongsTo** | 外键在当前模型 | `CreditCard` 包含 `UserID`,`BelongsTo` 指向 `User` | +| **BelongsToMany** | 需要通过关联表 | `User` ↔ `Languages`(多对多) | + +### 预加载(Preloading) + +```go +// N+1 问题:一次查询 N 条关联记录 +db.Preload("CreditCards").Find(&users) +// 生成 2 条 SQL:SELECT * FROM users; SELECT * FROM credit_cards WHERE user_id IN (...) + +// 嵌套预加载 +db.Preload("CreditCards"). + Preload("Profile"). + Find(&users) + +// 带条件的预加载 +db.Preload("CreditCards", "type = ?", "premium").Find(&users) + +// 使用 Joins 预加载(生成 INNER JOIN,减少查询次数) +db.Joins("Profile").Find(&users) +``` + +> **核心原则**:批量查询时务必使用 `Preload`,否则会在循环中触发 N+1 查询问题。 + +```mermaid +flowchart LR + subgraph Bad ["❌ 不使用 Preload - N+1 问题"] + direction TB + S1["SQL 1: SELECT * FROM users"] --> Loop["循环 N 次"] + Loop --> S2["SQL 2: SELECT * FROM credit_cards WHERE user_id=1"] + Loop --> S3["SQL 3: SELECT * FROM credit_cards WHERE user_id=2"] + Loop --> S4["SQL N: SELECT * FROM credit_cards WHERE user_id=N"] + end + + subgraph Good ["✅ 使用 Preload - 2 条 SQL"] + direction TB + G1["SQL 1: SELECT * FROM users"] --> G2["SQL 2: SELECT * FROM credit_cards WHERE user_id IN (1,2,...,N)"] + end +``` + +## 多对多关联(BelongsToMany) + +```go +type User struct { + gorm.Model + Name string + Languages []Language `gorm:"many2many:user_languages;"` +} + +type Language struct { + gorm.Model + Name string +} + +// 自动创建关联表 user_languages(字段:user_id, language_id) +// 插入关联数据 +db.Model(&user).Association("Languages").Append(&Language{Name: "Go"}) + +// 查询关联 +db.Preload("Languages").Find(&users) + +// 替换关联 +db.Model(&user).Association("Languages").Replace( + &Language{Name: "Python"}, &Language{Name: "Rust"}, +) +``` + +## 高级查询技巧 + +### Raw SQL & 条件构造 + +```go +// 原生 SQL +db.Raw("SELECT name, SUM(amount) FROM orders WHERE status = ? GROUP BY name", "PAID"). + Scan(&results) + +// 条件表达式 +db.Where( + "name LIKE ? AND age BETWEEN ? AND ?", "%jon%", 20, 30, +).Find(&users) + +// IN 查询 +db.Where("id IN ?", []int{1, 2, 3}).Find(&users) + +// 子查询 +subQuery := db.Model(&Order).Select("user_id").Where("amount > ?", 100) +db.Where("id IN (?)", subQuery).Find(&users) +``` + +### 事务 + +```go +tx := db.Begin() // 注意:事务中需使用 tx 而非 db + +defer func() { + if r := recover(); r != nil { + tx.Rollback() + } +}() + +if err := tx.Create(&user).Error; err != nil { + tx.Rollback() + return err +} + +if err := tx.Create(&order).Error; err != nil { + tx.Rollback() + return err +} + +return tx.Commit().Error +``` + +> **关键点**:事务内的所有操作必须使用事务句柄 `tx`,而非全局 `db`。 + +### 自定义表名与列名 + +```go +func (User) TableName() string { + return "app_users" // 不使用复数形式 +} + +// 列名自定义 +type User struct { + ID int `gorm:"column:uid"` + Name string `gorm:"column:username"` +} +``` + +### Scope(可复用查询片段) + +```go +func AmountGT(amount int) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + return db.Where("amount > ?", amount) + } +} + +func Active() func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + return db.Where("status = ?", "active") + } +} + +// 使用 +db.Scopes(AmountGT(100), Active()).Find(&orders) +``` + +## 性能优化 + +### 1. Select 指定字段 + +```go +// 只查询需要的字段,避免加载大字段(如 text/blob) +db.Select("name", "age").Find(&users) +``` + +### 2. 批量操作 + +```go +// 批量写入:1次 INSERT N行,而非 N次 INSERT +db.Create(&users) + +// 批量更新:1次 UPDATE +db.Model(&user).Where("id IN ?", ids).Update("status", "active") + +// 批量删除 +db.Delete(&User{}, ids) +``` + +### 3. 关闭预编译(极端性能场景) + +```go +db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + PrepareStmt: true, // 开启预编译,适合高并发同一 SQL +}) +``` + +开启 `PrepareStmt` 后,GORM 会使用 `sql.DB.Prepare` + `sql.Stmt` 缓存预编译语句,适合大量相同 SQL 的执行场景。 + +## 企业级注意事项 + +### 🚫 严禁使用 AutoMigrate + +在团队协作和企业级项目中,**禁止使用 `db.AutoMigrate()`**。原因: + +1. **不可控的 DDL**:自动建表/改表可能导致生产数据丢失 +2. **DBA 审计要求**:所有表结构变更需经过 SQL 审查 +3. **版本管理**:数据库结构应通过迁移脚本(如 `golang-migrate`)管理 + +**推荐做法**:通过 SQL 脚本初始化数据库,与 [数据库设计](file:///home/wonder/Code/obsidian/金山办公作业/Week05/数据库设计.md) 文档保持一致。 + +### 事务与并发 + +```go +// ❌ 错误:多个 goroutine 共享同一个 db 连接 +// db.Create(&user1) // 并发不安全 + +// ✅ 正确:事务自带连接池,每个 goroutine 开启独立事务 +func saveUser(tx *gorm.DB, user *User) error { + return tx.Create(user).Error +} +``` + +GORM 底层使用 `sql.DB` 连接池,本身是并发安全的,但 `db.Begin()` 返回的事务对象不可跨 goroutine 共享。 + +### 错误处理 + +```go +result := db.Where("id = ?", id).First(&user) +if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + // 记录不存在 + } else { + // 其他数据库错误 + return result.Error + } +} +``` + +## GORM vs 原生 SQL + +| 场景 | 推荐方案 | 原因 | +|------|----------|------| +| 简单 CRUD | GORM | 代码简洁,开发效率高 | +| 复杂 JOIN | GORM `Joins` | GORM 链式写法比手写 SQL 更直观 | +| 聚合查询(GROUP BY + HAVING) | GORM `Select/Group/Having` | 原生 SQL 也可,看团队偏好 | +| 复杂子查询/CTE | 原生 SQL `Raw/Scan` | GORM 对复杂 SQL 支持有限 | +| 高性能写入(大批量) | GORM 批量 | 或直接用 `sql.Batch` | +| 数据库迁移 | SQL 脚本 / golang-migrate | GORM AutoMigrate 禁用于生产 | + +## 关联笔记 + +- [[DEV/GO/README]] +- [[金山办公作业/Week05/数据库设计]] +- [[金山办公作业/Week05/全栈开发实战:AI 智能单词本]] diff --git a/config/agent/CLAUDE.md b/config/agent/CLAUDE.md deleted file mode 100644 index 4a083ee..0000000 --- a/config/agent/CLAUDE.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -tags: [config, claude, standards] ---- - -# Claude Code 项目配置 - -本项目是 Obsidian vault,Claude Code 作为 AI 协作者协助知识管理。 - -## 核心原则 - -1. **Obsidian Native**: 理解 Markdown、YAML frontmatter、Wiki-links -2. **Safety First**: 从不未经理解覆盖数据,始终使用相对路径 -3. **Proactive**: 预期潜在问题(如断链、缺失文件) -4. **Clarity**: 精准变更,最小化笔记噪音 - -## 操作规范 - -- 详见 [[config/agent/policies/standards.md]] -- 正面清单:[[config/agent/policies/allowlist.md]] -- 负面清单:[[config/agent/policies/blocklist.md]] - -## 可用 Skills - -- [[config/agent/skills/note-creation.md]]: 笔记创建 -- [[config/agent/skills/file-edit.md]]: 文件编辑 -- [[config/agent/skills/link-management.md]]: 链接管理 - -## 集成方式 - -Claude 会在每次任务开始时自动读取此文件,遵循定义的规范和约束。 \ No newline at end of file diff --git a/config/agent/DOCUMENT_OPERATION.md b/config/agent/DOCUMENT_OPERATION.md new file mode 100644 index 0000000..492db7f --- /dev/null +++ b/config/agent/DOCUMENT_OPERATION.md @@ -0,0 +1,42 @@ + +# 文件创建/修改规范 + +## 格式 + +```yaml +--- +tags: [] +create time: YYYY-MM-DD HH:mm +--- + +# <标题> + +## 概述 +[简短描述] + +## 正文 +[按照内容规范生成的内容] + +## 关联笔记 +- [[相关笔记-可不填-无需链接README]] +``` + +## 必填项 + +| 字段 | 格式 | 说明 | +|------|------|------| +| `tags` | 数组 | 必须存在 `[]`,智能填充相关标签 | +| `create time` | `YYYY-MM-DD HH:mm` | 当前系统时间 | + +## 文档结构 + +1. **## 概述**: 简短描述文档内容,不要保留占位文本 +2. **## 正文**: 主要内容,详略得当,注重拓展进阶 +3. **## 关联笔记**: 相关笔记 Wiki-links,若无可移除 + +## 内容规范【IMPORTANT】 + +- **教学者模式**: 假设你是教学者,你需要先规划如何记录这个知识点,让读者能够容易理解。你也可以穿插问题在文档中,启发学生的思考。 +- **代码示例**: 优先 Go (后端) + React/TS (前端),代码示例点到为止,不要过于冗长。可以适当通过注释省略一部分代码增强可读性,体现核心逻辑即可。当你给出代码时,一定要给出对应的文本解释。 +- **图表**: 遇到关键概念、流程,仅仅靠文字不容易清晰说明,此时应该使用 Mermaid。 +- **风格**: 详略得当,注重实用性。文风严谨但是不失“活人感”,循序渐进,深入浅出。 diff --git a/config/agent/agent.md b/config/agent/agent.md deleted file mode 100644 index 8059226..0000000 --- a/config/agent/agent.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -tags: [config, agent, standards] -create time: 2026-04-17 11:29 -update time: 2026-04-18 ---- - -# 文件创建规范 - -## 核心规则 - -**所有新文件必须遵循以下结构:** - -```yaml ---- -tags: [] -create time: YYYY-MM-DD HH:mm ---- - -# <标题> - -## 概述 -[简短描述] - -## 正文 -[内容] - -## 关联笔记 -- [[相关笔记]] -``` - -## 必填项 - -| 字段 | 格式 | 说明 | -|------|------|------| -| `tags` | 数组 | 必须存在 `[]`,智能填充相关标签 | -| `create time` | `YYYY-MM-DD HH:mm` | 当前系统时间 | - -## 文档结构 - -1. **## 概述**: 简短描述文档内容,不要保留占位文本 -2. **## 正文**: 主要内容,详略得当,注重拓展进阶 -3. **## 关联笔记**: 相关笔记 Wiki-links,若无可移除 - -## 内容规范 - -- **代码示例**: 优先 Go (后端) + React/TS (前端) -- **图表**: 选择性使用 Mermaid -- **风格**: 详略得当,注重实用性 - -## Agent 执行步骤 - -1. 读取模板: `[[config/template/default-template.md]]` -2. 创建文件并填充 `create time` -3. 智能填充 `tags` -4. 替换占位文本为实际内容 -5. 添加相关 Wiki-links - -## 特殊情况 - -配置文件、开发文件等可灵活调整结构,但需保留: -- `tags` 字段 -- `create time` 记录 -- 清晰的组织结构 diff --git a/config/agent/policies/allowlist.md b/config/agent/policies/allowlist.md deleted file mode 100644 index b325800..0000000 --- a/config/agent/policies/allowlist.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -tags: [config, agent, policy] ---- - -# Claude 操作正面清单 - -以下操作为明确允许的操作: - -## 文件操作 -- ✅ 读取 vault 内任何 Markdown 文件 -- ✅ 创建新文件(遵循 [[config/agent/agent.md]] 规范) -- ✅ 编辑现有文件(使用 Edit 工具进行精准变更) -- ✅ 使用相对路径访问文件 - -## 内容结构 -- ✅ 添加/更新 YAML frontmatter -- ✅ 使用 Wiki-links `[[note-name]]` -- ✅ 使用标签 `#tag-name` -- ✅ 保留或简化 Dataview 查询 - -## 搜索与导航 -- ✅ 使用 Grep 搜索内容 -- ✅ 使用 Glob 查找文件 -- ✅ 分析链接关系和依赖 - -## Git 操作(受限) -- ✅ 运行 `git status` 查看状态 -- ✅ 运行 `git diff` 查看变更 -- ⚠️ 提交、推送等操作需要用户明确请求 - -## 资产管理 -- ✅ 读取本地图片(PNG, JPG, GIF, WebP) -- ✅ 下载外部图片到 media 文件夹 -- ✅ 替换图片链接为本地 wiki-link - -## 处理特殊情况 -- ✅ 配置文件和开发文件可灵活调整结构 -- ✅ 在用户明确要求时使用非标准结构 \ No newline at end of file diff --git a/config/agent/policies/blocklist.md b/config/agent/policies/blocklist.md deleted file mode 100644 index 9e275e8..0000000 --- a/config/agent/policies/blocklist.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -tags: [config, agent, policy] ---- - -# Claude 操作负面清单 - -以下操作为明确禁止的操作: - -## 文件操作 -- ❌ 未经 Read 工具直接 Edit 文件 -- ❌ 使用绝对路径访问 vault 文件 -- ❌ 覆盖 `.obsidian/` 配置(除非明确要求) -- ❌ 删除文件(除非用户明确要求) - -## 内容变更 -- ❌ 添加 emoji(除非用户明确要求) -- ❌ 创建文档文件(README.md, *.md)(除非明确要求) -- ❌ 破坏 Dataview 查询语法 -- ❌ 移除必要的 YAML frontmatter 字段 - -## 路径规范 -- ❌ 在 vault 内使用 `/path/to/file` 格式 -- ❌ 使用 `/home/user/...` 绝对路径引用 vault 文件 -- ❌ 混淆 Wiki-links 和 Markdown 链接 - -## Git 操作 -- ❌ 跳过 hooks(--no-verify) -- ❌ 跳过签名(--no-gpg-sign) -- ❌ 强制 push 到 main/master(警告用户) -- ❌ 使用 --interactive 或 --amend 标志 - -## 系统命令 -- ❌ 运行 find、grep、cat(使用专用工具代替) -- ❌ 创建空 git commit -- ❌ 修改全局 git 配置 -- ❌ 运行破坏性命令(reset --hard, clean -f 等) - -## 数据安全 -- ❌ 提交包含凭据的文件(.env, credentials.json) -- ❌ 分享 API 密钥或敏感信息 \ No newline at end of file diff --git a/config/agent/policies/standards.md b/config/agent/policies/standards.md deleted file mode 100644 index 5f3597c..0000000 --- a/config/agent/policies/standards.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -tags: [config, agent, policy] ---- - -# Claude 操作规范 - -## 通用原则 - -1. **遵循现有结构**: 严格遵循项目中已有的文件结构和格式 -2. **安全第一**: 在修改任何文件前都必须先读取,维护现有的内容和结构 -3. **清晰性**: 所有操作必须清晰、可理解,避免混乱或不明确的变更 - -## 内容创作规范 - -1. **结构**: - - 优先使用 Markdown 标题格式 - - 保持一致的段落结构 - - 合理使用列表来组织信息 - -2. **标签**: - - 使用 YAML frontmatter 包含适当的标签 - - 标签必须是数组形式 `tags: [tag1, tag2]` - - 尽可能使用相关领域的标签 - -3. **链接**: - - 内部链接使用 Wiki-links: `[[note-name]]` - - 外部链接使用标准 Markdown 格式: `[text](url)` - - 确保所有内部链接都指向实际存在的文件 - -4. **内容质量**: - - 避免使用占位文本,如 "[内容]"、"[描述]" 等 - - 保持写作风格一致和专业 - - 根据内容添加相关图表或 Mermaid 图表 - -## 文件和路径规范 - -1. **路径使用**: - - 仅使用相对路径进行文件访问 - - 路径不应包含绝对路径(如 `/home/...`) - -2. **文件操作**: - - 不应在 Vault 内创建 README.md、*.md 等文档文件 - - 创建新文件时,应遵循现有的文件结构规范 - -## Git 和版本控制 - -1. **提交操作**: - - 不应自动执行任何 Git 提交或推送操作 - - 如需执行 Git 相关操作,必须让用户明确授权 - -2. **变更检查**: - - 检查文件变更以维护链接一致性 - - 发现断链时应记录并报告 - -## 特殊情况处理 - -1. **知识库层面的处理**: - - 若涉及高层架构、系统设计等内容,应优先根据已有文档进行整理补充 - - 不应创造新的结构或模式,而应遵循既有规范 - -2. **代码实现**: - - 当需要编写代码示例时,优先使用 Go 语言(后端)或 React/TS(前端) - - 所有实现示例应在当前环境中实际可行 - -3. **数据可视化**: - - 使用 Mermaid 图表,但需确保其正确性和可读性 \ No newline at end of file diff --git a/config/template/default-template.md b/config/template/default-template.md deleted file mode 100644 index 7c1e7f2..0000000 --- a/config/template/default-template.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -tags: [] -create time: <% tp.date.now("YYYY-MM-DD HH:mm") %> ---- - -# <% tp.file.title %> - -## 概述 - -添加笔记的概述或简短描述。 - -## 正文 - -在这里写下你的笔记内容... - -## 关联笔记 - -- 添加关联笔记的链接 diff --git a/金山办公作业/Week05/PLAN.md b/金山办公作业/Week05/PLAN.md index 35bb1bf..3c5a08d 100644 --- a/金山办公作业/Week05/PLAN.md +++ b/金山办公作业/Week05/PLAN.md @@ -138,7 +138,7 @@ erDiagram string password "密码(hash)" datetime created_at "创建时间" datetime updated_at "更新时间" - datetime deleted_at "删除时间(软删除)" + tinyint is_deleted "软删除标记" } WORD { uuid id PK "单词记录ID" @@ -149,7 +149,7 @@ erDiagram string ai_provider "AI模型来源" datetime created_at "创建时间" datetime updated_at "更新时间" - datetime deleted_at "删除时间(软删除)" + tinyint is_deleted "软删除标记" } ``` @@ -160,7 +160,7 @@ erDiagram - 用户名设置唯一索引,防止重复注册 - 密码字段存储 bcrypt 哈希值,严禁明文 - 包含标准时间戳字段 - - 支持软删除(deleted_at) + - 支持软删除(is_deleted) 2. **单词表 (words)** - 使用 UUID 作为主键 diff --git a/金山办公作业/Week05/db in docker.md b/金山办公作业/Week05/db in docker.md new file mode 100644 index 0000000..f5da1e1 --- /dev/null +++ b/金山办公作业/Week05/db in docker.md @@ -0,0 +1,362 @@ +# Docker 网络数据库连接指南 + +## 问题描述 + +在后端服务连接运行在 Docker 网络中的数据库时,**不能使用 `localhost`** 作为连接地址。 + +## 原因分析 + +### 为什么使用 `localhost` 是错误的? + +在 Docker 网络中,每个容器都有**独立的网络栈**: + +- **`localhost` 在容器内部指向容器自身**(127.0.0.1 指向容器内部) +- **不是指向同一网络中的其他容器** + +当你的后端容器尝试连接 `localhost:3306` 时,它实际上是在寻找**自己的 3306 端口**,而不是数据库容器的端口。 + +#### 直观理解 + +``` +┌─────────────────────────────────────────────────┐ +│ Docker 网络(我的网络) │ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ Backend 容器 │ │ Database 容器 │ │ +│ │ │ │ │ │ +│ │ localhost ───────┼──→ │ 自己的 8000 端口 │ │ +│ │ (127.0.0.1) │ │ │ │ +│ │ │ │ │ │ +│ │ database ────────┼──→ │ 正确!指向 DB │ │ +│ │ (服务名) │ │ │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ ↑ ↑ │ +│ └──────────┬───────────────┘ │ +│ │ │ +│ 只能通过服务名通信 │ +└─────────────────────────────────────────────────┘ +``` + +#### 实际会发生什么 + +```bash +# 在 backend 容器内执行 +# 尝试连接 localhost:3306 +# 结果:Connection refused +# 原因:backend 自己没有在 3306 端口监听服务 +``` + +#### 类比说明 + +这就好比: +- 你和朋友住在同一个大楼(Docker 网络) +- 你想找朋友 +- 你大喊"我在这里"(使用 `localhost`) +- 但实际上你需要喊的是"小明"(使用服务的名字) +- 因为 `"我在这里"` 指的是你自己,不是小明 + +### Docker 的网络隔离机制 + +Docker 容器默认使用以下网络模式: + +1. **Bridge 网络模式**(默认): + - 每个容器有独立的 IP 地址 + - 容器间通过服务名互相发现 + - `localhost` 仅指向容器自身 + +2. **容器间的 DNS 解析**: + ``` + # 在 backend 容器内 + $ ping database + PING database (172.19.0.2) // 解析到数据库容器的 IP + + $ ping localhost + PING localhost (127.0.0.1) // 指向自己 + ``` + +3. **端口监听位置**: + ``` + Database 容器内部:MySQL 监听 0.0.0.0:3306 + Database 容器 IP:172.19.0.2 + Backend 容器不能访问:127.0.0.1:3306(自己的端口) + Backend 容器应该访问:172.19.0.2:3306(通过服务名解析) + ``` + +### 结论 + +**使用 `localhost` 连接数据库必然失败**,因为它: +- 永远指向调用方容器自己 +- 无法跨容器通信 +- 违反了 Docker 的网络隔离设计 + +**正确做法:使用服务名或容器名**,这样 Docker 的内置 DNS 才能正确解析到目标容器。 + +## 正确做法 + +### 1. 使用服务名(Service Name) + +在 `docker-compose.yml` 中定义的服务名即可用作主机名: + +```yaml +services: + backend: + build: ./backend + ports: + - "8000:8000" + depends_on: + - database + + database: + image: mysql:8.0 + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: rootpassword +``` + +**连接配置:** +```python +# 后端连接配置 +DATABASE_URL = "mysql://user:password@database:3306/dbname" +# ^^^^^^^^ 使用服务名 "database" +``` + +### 2. 使用容器名 + +在 docker-compose 中为容器指定名称: + +```yaml +database: + image: mysql:8.0 + container_name: my-mysql-db # 显式指定容器名 +``` + +**连接配置:** +```python +DATABASE_URL = "mysql://user:password@my-mysql-db:3306/dbname" +``` + +### 3. 网络别名(Network Alias) + +如果有多个服务需要连接,可以设置网络别名: + +```yaml +database: + image: mysql:8.0 + networks: + appnet: + aliases: + - db + - mysql + +networks: + appnet: + driver: bridge +``` + +**连接配置:** +```python +# 可以使用任一别名 +DATABASE_URL = "mysql://user:password@db:3306/dbname" +# 或 +DATABASE_URL = "mysql://user:password@mysql:3306/dbname" +``` + +## 常见错误 + +### ❌ 错误写法 +```python +# 这会导致连接失败 +DATABASE_URL = "mysql://user:password@localhost:3306/dbname" +# ^^^^^^^^^ 指向 backend 自己 +``` + +### ✅ 正确写法 +```python +# 使用 docker-compose 中的服务名 +DATABASE_URL = "mysql://user:password@database:3306/dbname" +# ^^^^^^^^ 数据库服务名 +``` + +## 实际示例 + +### 示例 1:Spring Boot + MySQL + +```yaml +# docker-compose.yml +version: '3.8' +services: + springboot-app: + build: . + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://mysql-db:3306/mydb + SPRING_DATASOURCE_USERNAME: user + SPRING_DATASOURCE_PASSWORD: password + depends_on: + - mysql-db + + mysql-db: + image: mysql:8.0 + environment: + MYSQL_DATABASE: mydb + MYSQL_USER: user + MYSQL_PASSWORD: password + MYSQL_ROOT_PASSWORD: rootpassword + ports: + - "3306:3306" +``` + +### 示例 2:Node.js + PostgreSQL + +```yaml +# docker-compose.yml +version: '3.8' +services: + api: + build: ./api + ports: + - "3000:3000" + environment: + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + DATABASE_NAME: myapp + depends_on: + - postgres + + postgres: + image: postgres:14 + environment: + POSTGRES_DB: myapp + POSTGRES_USER: user + POSTGRES_PASSWORD: password + ports: + - "5432:5432" +``` + +```javascript +// .env +DATABASE_URL=postgres://user:password@postgres:5432/myapp +// ^^^^^^^^ 服务名 +``` + +### 示例 3:Python/Django + MongoDB + +```yaml +# docker-compose.yml +version: '3.8' +services: + web: + build: . + ports: + - "8000:8000" + environment: + MONGO_HOST: mongodb + MONGO_PORT: 27017 + depends_on: + - mongodb + + mongodb: + image: mongo:6.0 + ports: + - "27017:27017" +``` + +```python +# settings.py +MONGO_URI = "mongodb://mongodb:27017/mydb" +# ^^^^^^^^ 使用服务名 +``` + +## 调试技巧 + +### 1. 进入容器测试连接 +```bash +# 进入后端容器 +docker exec -it bash + +# 测试数据库连接 +ping database +telnet database 3306 +nc -zv database 3306 +``` + +### 2. 查看网络信息 +```bash +# 查看容器网络 +docker network inspect + +# 查看容器 IP 和别名 +docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +``` + +### 3. 使用 docker network 连接 +```bash +# 从宿主机通过容器 IP 连接(不推荐) +docker inspect | grep IPAddress + +# 正确方式:从另一个容器连接 +docker run --rm --network mysql:8.0 \ + mysql -h database -u user -ppassword dbname +``` + +## 特殊情况 + +### 从宿主机连接数据库 + +如果需要从**宿主机**(非 Docker 容器)连接数据库,使用: + +```yaml +database: + image: mysql:8.0 + ports: + - "3306:3306" # 映射到宿主机 +``` + +**连接方式:** +```python +# 从宿主机运行的应用 +DATABASE_URL = "mysql://user:password@localhost:3306/dbname" +``` + +**注意:** 这是唯一可以使用 `localhost` 的情况——从宿主机连接暴露端口的容器。 + +### 开发环境 vs 生产环境 + +```python +# 开发环境:可能直接使用本地数据库 +if os.getenv("ENVIRONMENT") == "development": + DATABASE_URL = "mysql://user:password@localhost:3306/devdb" +else: + # 生产环境:使用 Docker 网络中的数据库 + DATABASE_URL = "mysql://user:password@database:3306/proddb" +``` + +## 最佳实践 + +1. **使用环境变量**:将数据库连接配置为环境变量,便于在不同环境间切换 +2. **使用服务名**:优先使用 docker-compose 中的服务名,保持配置一致性 +3. **健康检查**:使用 `depends_on` + 健康检查确保数据库就绪后再启动应用 +4. **网络隔离**:为不同环境使用不同的网络 +5. **安全性**:不要在代码中硬编码密码,使用 secrets 或环境变量 + +## 故障排查清单 + +- [ ] 检查 docker-compose.yml 中数据库服务的名称 +- [ ] 确认后端使用的是服务名而非 `localhost` +- [ ] 确认数据库服务在同一个网桥网络中 +- [ ] 检查端口映射配置(如果从宿主机连接) +- [ ] 验证容器间的网络连通性(使用 `ping` 或 `telnet`) +- [ ] 检查防火墙规则 +- [ ] 查看容器日志确认数据库是否正常启动 + +## 参考资源 + +- [Docker Networking](https://docs.docker.com/network/) +- [Docker Compose Service Discovery](https://docs.docker.com/compose/networking/) +- [Connect to MySQL from another container](https://hub.docker.com/_/mysql) + +--- + +**最后更新:** 2026-04-21 diff --git a/金山办公作业/Week05/viper 和 godotenv 区别.md b/金山办公作业/Week05/viper 和 godotenv 区别.md new file mode 100644 index 0000000..3bbe65e --- /dev/null +++ b/金山办公作业/Week05/viper 和 godotenv 区别.md @@ -0,0 +1,545 @@ +--- +tags: [go, configuration, environment, viper, godotenv, assignment] +create time: 2026-04-21 +--- + +# Viper 和 Godotenv 对比分析 + +## 概述 + +Go 语言中有两个常用的配置管理库:**Viper** 和 **Godotenv**。虽然它们都与配置管理相关,但设计目标和使用场景存在显著差异。 + +- **Viper**: 完整的配置解决方案,支持多种配置源 +- **Godotenv**: 专注于从 `.env` 文件加载环境变量 + +## 核心对比 + +| 特性 | Viper | Godotenv | +|-----|-------|----------| +| **主要用途** | 配置管理框架 | 环境变量加载 | +| **配置源数量** | 多种(JSON/YAML/ENV/Flags) | 仅 `.env` 文件 | +| **功能范围** | 完整配置系统 | 单一功能库 | +| **代码复杂度** | 较大 | 简洁 | +| **学习曲线** | 较复杂 | 简单 | +| **适用场景** | 复杂应用配置 | 简单环境变量管理 | + +## 设计理念对比 + +```mermaid +graph TB + subgraph "Viper - 配置管理框架" + A1[JSON] --> A[统一配置 API] + B1[YAML] --> A + C1[TOML] --> A + D1[环境变量] --> A + E1[命令行参数] --> A + A --> F[配置读取/更新/监听] + end + + subgraph "Godotenv - 环境变量加载" + G[.env 文件] --> H[解析] + H --> I[设置环境变量] + end + + style A fill:#e1f5ff + style I fill:#fff4e1 +``` + +## Viper 详解 + +### 功能特性 + +```mermaid +mindmap + root((Viper)) + 配置源 + JSON + YAML + TOML + 环境变量 + 命令行参数 + 功能 + 自动重载 + 远程配置中心 + 配置合并 + 结构体绑定 + 优势 + 多源配置 + 优先级管理 + 类型安全 +``` + +### 基础使用 + +```go +package main + +import ( + "fmt" + "github.com/spf13/viper" +) + +func viperExample() { + // 1. 初始化 + v := viper.New() + + // 2. 设置配置文件 (自动查找 app.json/app.yaml 等) + v.SetConfigName("config") + v.SetConfigType("yaml") + v.AddConfigPath(".") + v.AddConfigPath("./config") + + // 3. 读取配置文件 + if err := v.ReadInConfig(); err != nil { + panic(fmt.Errorf("读取配置失败: %w", err)) + } + + // 4. 读取配置值 + dbHost := v.GetString("database.host") + dbPort := v.GetInt("database.port") + debug := v.GetBool("debug") + + fmt.Printf("数据库: %s:%d, Debug: %v\n", dbHost, dbPort, debug) +} +``` + +### 结构体绑定 + +```go +func structBindingExample() { + type Config struct { + AppName string `mapstructure:"app_name"` + Debug bool `mapstructure:"debug"` + + Database struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + } `mapstructure:"database"` + } + + var config Config + + if err := viper.Unmarshal(&config); err != nil { + panic(err) + } + + fmt.Printf("应用: %s, 数据库: %s:%d\n", + config.AppName, config.Database.Host, config.Database.Port) +} +``` + +### 多配置源优先级 + +```mermaid +graph LR + A[默认值] --> B[配置文件] + B --> C[环境变量] + C --> D[命令行参数] + D --> E[最终配置] + + style A fill:#f9f9f9 + style E fill:#66bb6a,color:#fff +``` + +```go +func multiSourceExample() { + v := viper.New() + + // 1. 设置默认值 (最低优先级) + v.SetDefault("server.port", 8080) + v.SetDefault("log.level", "info") + + // 2. 读取配置文件 + v.SetConfigFile("config.yaml") + v.ReadInConfig() + + // 3. 绑定环境变量 (可覆盖配置文件) + v.SetEnvPrefix("APP") // APP_SERVER_PORT + v.BindEnv("server.port") + + // 4. 绑定命令行参数 (最高优先级, 使用 pflag) + pflag.Int("port", 0, "服务器端口") + pflag.Parse() + v.BindPFlags(pflag.CommandLine) + + // 最终取值: 命令行参数 > 环境变量 > 配置文件 > 默认值 +} +``` + +### 自动重载配置 + +```go +import ( + "github.com/fsnotify/fsnotify" +) + +func watchConfig() { + v := viper.New() + v.SetConfigFile("config.yaml") + v.ReadInConfig() + + // 监听配置文件变化 + v.WatchConfig() + v.OnConfigChange(func(e fsnotify.Event) { + fmt.Printf("配置文件已更改: %s\n", e.Name) + // 自动重载, 无需手动调用 ReadInConfig + }) +} +``` + +### 配置文件示例 (config.yaml) + +```yaml +app_name: "my-app" +debug: true + +database: + host: "localhost" + port: 5432 + user: "admin" + password: "secret" + +redis: + addr: "localhost:6379" + pool_size: 10 + +log: + level: "debug" + format: "json" +``` + +## Godotenv 详解 + +### 功能特性 + +- ✅ 从 `.env` 文件加载环境变量 +- ✅ 支持注释 (# 开头) +- ✅ 支持引号和换行 +- ❌ 不支持配置优先级 +- ❌ 不支持远程配置 +- ❌ 不支持自动重载 + +### 基础使用 + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/joho/godotenv" +) + +func godotenvExample() { + // 1. 加载 .env 文件到环境变量 + if err := godotenv.Load(); err != nil { + log.Fatal("加载 .env 文件失败", err) + } + + // 2. 直接读取环境变量 + dbHost := os.Getenv("DB_HOST") + dbPort := os.Getenv("DB_PORT") + apiKey := os.Getenv("API_KEY") + + fmt.Printf("数据库: %s:%s, API Key: %s\n", dbHost, dbPort, apiKey) +} +``` + +### 指定文件路径 + +```go +// 加载指定路径的 .env 文件 +godotenv.Load(".env.production") +godotenv.Load("/path/to/.env") + +// 加载多个 .env 文件 (后加载的会覆盖先加载的) +godotenv.Load(".env", ".env.local", ".env.secrets") +``` + +### .env 文件示例 + +```bash +# 数据库配置 +DB_HOST=localhost +DB_PORT=5432 +DB_USER=admin +DB_PASSWORD=secret123 + +# API 配置 +API_BASE_URL=https://api.example.com +API_KEY=sk-1234567890abcdef + +# 应用配置 +APP_NAME=my-app +APP_ENV=production +APP_DEBUG=false +``` + +### 配合 Viper 使用 + +```go +func combinedExample() { + // 1. 先用 godotenv 加载 .env + godotenv.Load() + + // 2. 再用 viper 读取环境变量 + viper.AutomaticEnv() // 自动读取环境变量 + + // 3. 可以设置默认值 + viper.SetDefault("DB_PORT", 5432) + + // 4. 读取配置 + dbHost := viper.GetString("DB_HOST") + dbPort := viper.GetInt("DB_PORT") +} +``` + +## 使用场景分析 + +### 适用 Viper 的场景 + +```mermaid +graph TD + A[复杂应用配置] --> B[多配置源] + A --> C[需要优先级管理] + A --> D[结构化配置文件] + A --> E[配置热更新] + + B --> B1[JSON/YAML/TOML] + C --> C1[默认值 < 文件 < ENV < CLI] + D --> D1[嵌套结构] + E --> E1[监听文件变化] +``` + +**典型应用:** +- 微服务配置管理 +- 需要多种配置来源的应用 +- 需要配置优先级和覆盖机制 +- 需要远程配置中心集成的应用 + +### 适用 Godotenv 的场景 + +```mermaid +graph LR + A[简单环境变量管理] --> B[12-Factor App] + A --> C[Docker 容器化] + A --> D[开发环境隔离] + + B --> B1[配置即代码] + C --> C1[容器环境变量] + D --> D1[.env.dev / .env.prod] +``` + +**典型应用:** +- 12-Factor 应用 +- Docker/Kubernetes 部署 +- 简单脚本或小型应用 +- 仅需要环境变量的项目 + +## 代码对比示例 + +### 场景: 从配置文件读取数据库配置 + +#### 使用 Viper + +```go +// config.yaml +/* +database: + host: localhost + port: 5432 + user: admin + password: secret123 +*/ + +type DatabaseConfig struct { + Host string + Port int + User string + Password string +} + +// 代码 + viper.SetConfigFile("config.yaml") + viper.ReadInConfig() + + var config DatabaseConfig + viper.UnmarshalKey("database", &config) + + db, err := sql.Open("postgres", + fmt.Sprintf("%s:%s@%s:%d/mydb", + config.User, config.Password, config.Host, config.Port)) +``` + +#### 使用 Godotenv + +```go +// .env +/* +DB_HOST=localhost +DB_PORT=5432 +DB_USER=admin +DB_PASSWORD=secret123 +*/ + +// 代码 +godotenv.Load() + +db, err := sql.Open("postgres", + fmt.Sprintf("%s:%s@%s:%s/mydb", + os.Getenv("DB_USER"), + os.Getenv("DB_PASSWORD"), + os.Getenv("DB_HOST"), + os.Getenv("DB_PORT"))) +``` + +## 最佳实践 + +### Viper 最佳实践 + +```go +// 1. 封装配置加载 +func LoadConfig() (*Config, error) { + v := viper.New() + + // 设置配置文件默认路径 + v.SetConfigFile("config/config.yaml") + + // 允许环境变量覆盖 + v.SetEnvPrefix("APP") + v.AutomaticEnv() + + if err := v.ReadInConfig(); err != nil { + return nil, err + } + + var cfg Config + if err := v.Unmarshal(&cfg); err != nil { + return nil, err + } + + return &cfg, nil +} + +// 2. 配置验证 +func (c *Config) Validate() error { + if c.Database.Host == "" { + return errors.New("数据库主机不能为空") + } + if c.Database.Port <= 0 || c.Database.Port > 65535 { + return errors.New("端口号无效") + } + return nil +} +``` + +### Godotenv 最佳实践 + +```bash +# .env.example (提交到版本控制) +DB_HOST=localhost +DB_PORT=5432 +DB_USER=admin +DB_PASSWORD= + +# .env (不提交,加入 .gitignore) +DB_HOST=production-db.example.com +DB_PORT=5432 +DB_USER=admin +DB_PASSWORD=real_secret_password +``` + +```go +// 1. 提供默认值 +func getEnv(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} + +// 2. 使用 +dbHost := getEnv("DB_HOST", "localhost") +dbPort := getEnv("DB_PORT", "5432") +``` + +### 混合使用策略 + +```go +// config.yaml (结构化配置) +app: + name: "my-app" + version: "1.0.0" + +server: + port: 8080 + +// .env (敏感信息) +DB_PASSWORD=secret123 +API_SECRET=xyz789 + +// 代码 +func main() { + // 1. 加载环境变量 (敏感配置) + godotenv.Load() + + // 2. 加载配置文件 (结构化配置) + viper.SetConfigFile("config.yaml") + viper.ReadInConfig() + + // 3. 合并配置 + viper.BindEnv("database.password", "DB_PASSWORD") + viper.BindEnv("api.secret", "API_SECRET") + + // 4. 读取最终配置 + config := loadConfig() + + // 在代码中,结构化配置在 YAML,敏感配置在 .env +} +``` + +## 项目选择建议 + +### 选择 Viper,如果你需要: + +- ✅ 支持多种配置格式 (JSON/YAML/TOML) +- ✅ 配置优先级管理 +- ✅ 结构体绑定和类型安全 +- ✅ 配置热更新 +- ✅ 远程配置中心集成 +- ✅ 命令行参数绑定 + +### 选择 Godotenv,如果你需要: + +- ✅ 12-Factor 应用规范 +- ✅ Docker/Kubernetes 部署 +- ✅ 极简配置管理 +- ✅ 环境变量即可满足需求 +- ✅ 团队熟悉 `.env` 工作流 +- ✅ 依赖最小化 + +## 学习资源 + +### Viper + +- [Viper 官方文档](https://github.com/spf13/viper) +- [Viper 使用指南](https://www.sohamkamani.com/golang/viper-guide/) +- [Go 配置管理最佳实践](https://sagikazarmark.hu/blog/2022/03/02/manage-go-configuration/) + +### Godotenv + +- [Godotenv 官方文档](https://github.com/joho/godotenv) +- [12-Factor App - Config](https://12factor.net/config) +- [.env 文件规范](https://dotenv.org/) + +## 相关笔记 + +- [[CS/LANG/Go/标准库/标准库]] - Go 语言标准库 +- [[CS/DevOps/Docker/基础]] - Docker 容器化 +- [[CS/NET/Auth/密钥管理]] - 配置安全最佳实践 +- [[金山办公作业/Week05/数据库设计]] - 数据库配置详解 diff --git a/金山办公作业/Week05/数据库设计.md b/金山办公作业/Week05/数据库设计.md index 8aad7a7..1f1fe03 100644 --- a/金山办公作业/Week05/数据库设计.md +++ b/金山办公作业/Week05/数据库设计.md @@ -20,7 +20,7 @@ erDiagram string password "密码(hash)" datetime created_at "创建时间" datetime updated_at "更新时间" - datetime deleted_at "删除时间(软删除)" + tinyint is_deleted "软删除标记" } WORD { uuid id PK "单词记录ID" @@ -31,7 +31,7 @@ erDiagram string ai_provider "AI模型来源" datetime created_at "创建时间" datetime updated_at "更新时间" - datetime deleted_at "删除时间(软删除)" + tinyint is_deleted "软删除标记" } ``` @@ -49,7 +49,7 @@ erDiagram | `password` | VARCHAR(255) | NOT NULL | - | 用户密码,存储 bcrypt 哈希值,严禁明文存储 | | `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | - | 账户创建时间 | | `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | - | 账户最后更新时间 | -| `deleted_at` | DATETIME | DEFAULT NULL | INDEX | 软删除标记,NULL 表示未删除 | +| `is_deleted` | TINYINT | DEFAULT 0 | INDEX | 软删除标记,0 表示未删除,1 表示已删除 | **索引设计:** - 主键索引:`id` @@ -60,23 +60,23 @@ erDiagram | 单词表名 | `words` | |-----------|---------| -| 字段名 | 数据类型 | 约束 | 索引 | 说明 | -| ------------- | ------------ | --------------------------- | ------------------------- | ------------------------------------------ | -| `id` | CHAR(36) | PRIMARY KEY | - | 单词记录唯一标识符(UUID 格式) | -| `user_id` | CHAR(36) | NOT NULL, FOREIGN KEY | - | 所属用户 ID,关联 users.id | -| `word` | VARCHAR(100) | NOT NULL | INDEX idx_word_search(20) | 单词文本,支持前缀搜索优化 | -| `definition` | TEXT | NOT NULL | - | AI 生成的单词释义 | -| `examples` | JSON | NOT NULL | - | 例句列表,存储 JSON 数组格式 | -| `ai_provider` | ENUM | NOT NULL | - | AI 模型来源标识(`deepseek`=DeepSeek,`qwen`=通义千问) | -| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | - | 单词记录创建时间 | -| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | - | 单词记录最后更新时间 | -| `deleted_at` | DATETIME | DEFAULT NULL | INDEX idx_user_word | 软删除标记 | +| 字段名 | 数据类型 | 约束 | 索引 | 说明 | +|--------|----------|------|------|------| +| `id` | CHAR(36) | PRIMARY KEY | - | 单词记录唯一标识符(UUID 格式) | +| `user_id` | CHAR(36) | NOT NULL, FOREIGN KEY | - | 所属用户 ID,关联 users.id | +| `word` | VARCHAR(100) | NOT NULL | INDEX idx_word_search(20) | 单词文本,支持前缀搜索优化 | +| `definition` | TEXT | NOT NULL | - | AI 生成的单词释义 | +| `examples` | JSON | NOT NULL | - | 例句列表,存储 JSON 数组格式 | +| `ai_provider` | ENUM | NOT NULL | - | AI 模型来源标识(`deepseek`=DeepSeek,`qwen`=通义千问) | +| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | - | 单词记录创建时间 | +| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | - | 单词记录最后更新时间 | +| `is_deleted` | TINYINT | DEFAULT 0 | INDEX idx_user_word | 软删除标记 | **索引设计:** - 主键索引:`id` - 外键索引:`user_id`(自动创建) - 唯一索引:`uk_user_word` (user_id, word(50)) - 防止同一用户保存重复单词 -- 复合索引:`idx_user_word` (user_id, deleted_at) - 优化用户单词列表分页查询 +- 复合索引:`idx_user_word` (user_id, is_deleted) - 优化用户单词列表分页查询 - 前缀索引:`idx_word_search` (word(20)) - 支持单词前缀搜索优化 ## 四、表关联关系 @@ -97,15 +97,15 @@ erDiagram ```sql -- 查询某用户的所有单词(排除已删除) SELECT * FROM words -WHERE user_id = ? AND deleted_at IS NULL +WHERE user_id = ? AND is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?; -- 联表查询用户信息及单词数量 SELECT u.id, u.username, COUNT(w.id) as word_count FROM users u -LEFT JOIN words w ON u.id = w.user_id AND w.deleted_at IS NULL -WHERE u.deleted_at IS NULL +LEFT JOIN words w ON u.id = w.user_id AND w.is_deleted = 0 +WHERE u.is_deleted = 0 GROUP BY u.id; ``` @@ -126,7 +126,7 @@ GROUP BY u.id; -- 查询:检查用户是否已保存该单词 SELECT id, word, definition, examples, ai_provider FROM words -WHERE user_id = ? AND word = ? AND deleted_at IS NULL; +WHERE user_id = ? AND word = ? AND is_deleted = 0; ``` ### 5.2 手动保存单词 @@ -147,14 +147,14 @@ VALUES (?, ?, ?, ?, ?, ?); -- 查询:分页获取单词列表(按创建时间倒序) SELECT id, word, definition, examples, ai_provider, created_at FROM words -WHERE user_id = ? AND deleted_at IS NULL +WHERE user_id = ? AND is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?; -- 计算总数(用于分页器) SELECT COUNT(*) as total FROM words -WHERE user_id = ? AND deleted_at IS NULL; +WHERE user_id = ? AND is_deleted = 0; ``` ### 5.4 删除单词(软删除) @@ -164,8 +164,8 @@ WHERE user_id = ? AND deleted_at IS NULL; ```sql -- 更新:软删除单词记录 UPDATE words -SET deleted_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP -WHERE id = ? AND user_id = ? AND deleted_at IS NULL; +SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP +WHERE id = ? AND user_id = ? AND is_deleted = 0; ``` ## 六、索引优化说明