From fc72a7312e4597cf818aee01935ca32d290fd579 Mon Sep 17 00:00:00 2001 From: zeaslity Date: Thu, 20 Nov 2025 16:18:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E9=87=8F=E7=9A=84=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/dataSources.xml | 12 + 0-pandoc-失败/epub-失败/convert.ps1 | 4 + .../epub-失败}/epub-style.css | 2 +- .../fonts/SourceHanSansCN-Medium.otf | Bin 0-pandoc-失败/pdf/chinese-template.tex | 9 + 0-pandoc-失败/pdf/convert.ps1 | 7 + 0-pandoc-失败/转换pandoc.md | 17 + 0-pandoc电子书转换/convert.ps1 | 1 - 0-pandoc电子书转换/perplexity.md | 1152 --------- 0-pandoc电子书转换/转换pandoc.md | 168 -- 1-Golang项目/go-gin-gorm-style.md | 2 +- .../0-SQL语法/1-基础学习/gemini-SQL学习全.md | 1223 ++++++++++ .../{select语法详解-2.md => select语法详解-全.md} | 4 +- 101-数据库学习/0-SQL语法/prompt.md | 5 +- 101-数据库学习/0-SQL语法/wdd-sqlite3.db | Bin 0 -> 45056 bytes 101-数据库学习/2-MySQL/0-基础数据结构.md | 582 +++++ .../2-MySQL/9-题目/MySQL-基础考察题目.md | 700 ++++++ 101-数据库学习/2-MySQL/prompt.md | 12 +- .../3-SQLite/9-题目/SQLite基础考察.md | 640 +++++ .../3-SQLite/9-题目/SQLite基础考察.sql | 271 +++ 101-数据库学习/3-SQLite/prompt.md | 11 +- .../0-需求规格书/0-产品经理-prompt.md | 11 + .../0-需求规格书/1-初始需求稿.md | 118 + .../0-需求规格书/1.1-初始需求稿.md | 151 ++ .../0-需求规格书/2-优化产品需求文档PRD.md | 505 ++++ .../0-需求规格书/2.1-优化产品需求文档PRD.md | 798 +++++++ .../1-概要详细设计/0-概要设计prompt.md | 26 + .../1-概要详细设计/3-详细设计说明书.md | 1525 ++++++++++++ .../1-概要详细设计/3.1-详细设计说明书.md | 2122 +++++++++++++++++ promt.md | 4 + 30 files changed, 8755 insertions(+), 1327 deletions(-) create mode 100644 .idea/dataSources.xml create mode 100644 0-pandoc-失败/epub-失败/convert.ps1 rename {0-pandoc电子书转换 => 0-pandoc-失败/epub-失败}/epub-style.css (88%) rename {0-pandoc电子书转换 => 0-pandoc-失败}/fonts/SourceHanSansCN-Medium.otf (100%) create mode 100644 0-pandoc-失败/pdf/chinese-template.tex create mode 100644 0-pandoc-失败/pdf/convert.ps1 create mode 100644 0-pandoc-失败/转换pandoc.md delete mode 100644 0-pandoc电子书转换/convert.ps1 delete mode 100644 0-pandoc电子书转换/perplexity.md delete mode 100644 0-pandoc电子书转换/转换pandoc.md create mode 100644 101-数据库学习/0-SQL语法/1-基础学习/gemini-SQL学习全.md rename 101-数据库学习/0-SQL语法/1-基础学习/{select语法详解-2.md => select语法详解-全.md} (99%) create mode 100644 101-数据库学习/0-SQL语法/wdd-sqlite3.db create mode 100644 101-数据库学习/2-MySQL/0-基础数据结构.md create mode 100644 101-数据库学习/2-MySQL/9-题目/MySQL-基础考察题目.md create mode 100644 101-数据库学习/3-SQLite/9-题目/SQLite基础考察.md create mode 100644 101-数据库学习/3-SQLite/9-题目/SQLite基础考察.sql create mode 100644 4-ProjectNaughtyMan/0-需求规格书/0-产品经理-prompt.md create mode 100644 4-ProjectNaughtyMan/0-需求规格书/1-初始需求稿.md create mode 100644 4-ProjectNaughtyMan/0-需求规格书/1.1-初始需求稿.md create mode 100644 4-ProjectNaughtyMan/0-需求规格书/2-优化产品需求文档PRD.md create mode 100644 4-ProjectNaughtyMan/0-需求规格书/2.1-优化产品需求文档PRD.md create mode 100644 4-ProjectNaughtyMan/1-概要详细设计/0-概要设计prompt.md create mode 100644 4-ProjectNaughtyMan/1-概要详细设计/3-详细设计说明书.md create mode 100644 4-ProjectNaughtyMan/1-概要详细设计/3.1-详细设计说明书.md diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..bd2a872 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/101-数据库学习/0-SQL语法/wdd-sqlite3.db + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/0-pandoc-失败/epub-失败/convert.ps1 b/0-pandoc-失败/epub-失败/convert.ps1 new file mode 100644 index 0000000..39fb38a --- /dev/null +++ b/0-pandoc-失败/epub-失败/convert.ps1 @@ -0,0 +1,4 @@ +#pandoc perplexity.md --from markdown+pipe_tables+fenced_code_blocks+fenced_code_attributes --to epub3 --filter mermaid-filter.cmd --epub-embed-font="C:\\Users\\wddsh\\Documents\\IdeaProjects\\ProjectAGiPrompt\\0-pandoc电子书转换\\fonts\\SourceHanSansCN-Medium.otf" --css="epub-style.css" --syntax-highlighting=pygments --standalone --toc --toc-depth=3 --output output.epub + + +pandoc perplexity.md -f markdown -t pdf -o output.pdf --pdf-engine=xelatex -V geometry:margin=0.5in --syntax-highlighting=kate --filter=mermaid-filter.cmd --standalone --table-of-contents --number-sections diff --git a/0-pandoc电子书转换/epub-style.css b/0-pandoc-失败/epub-失败/epub-style.css similarity index 88% rename from 0-pandoc电子书转换/epub-style.css rename to 0-pandoc-失败/epub-失败/epub-style.css index cecf93e..4e742d5 100644 --- a/0-pandoc电子书转换/epub-style.css +++ b/0-pandoc-失败/epub-失败/epub-style.css @@ -2,7 +2,7 @@ font-family: "Source Han Sans CN"; font-style: normal; font-weight: 500; - src: url("../fonts/SourceHanSansCN-Medium.otf"); + src: url("C:\\Users\\wddsh\\Documents\\IdeaProjects\\ProjectAGiPrompt\\0-pandoc电子书转换\\fonts\\SourceHanSansCN-Medium.otf"); } body { diff --git a/0-pandoc电子书转换/fonts/SourceHanSansCN-Medium.otf b/0-pandoc-失败/fonts/SourceHanSansCN-Medium.otf similarity index 100% rename from 0-pandoc电子书转换/fonts/SourceHanSansCN-Medium.otf rename to 0-pandoc-失败/fonts/SourceHanSansCN-Medium.otf diff --git a/0-pandoc-失败/pdf/chinese-template.tex b/0-pandoc-失败/pdf/chinese-template.tex new file mode 100644 index 0000000..164ea81 --- /dev/null +++ b/0-pandoc-失败/pdf/chinese-template.tex @@ -0,0 +1,9 @@ +\documentclass{article} +\usepackage{xeCJK} +\setCJKmainfont{C:\Users\wddsh\Documents\IdeaProjects\ProjectAGiPrompt\0-pandoc\fonts\SourceHanSansCN-Medium.otf} +\usepackage{geometry} +\geometry{margin=0.5in} +\usepackage{listings} +\usepackage{xcolor} +\usepackage{longtable} +\usepackage{booktabs} diff --git a/0-pandoc-失败/pdf/convert.ps1 b/0-pandoc-失败/pdf/convert.ps1 new file mode 100644 index 0000000..1895049 --- /dev/null +++ b/0-pandoc-失败/pdf/convert.ps1 @@ -0,0 +1,7 @@ +pandoc C:\Users\wddsh\Documents\IdeaProjects\ProjectAGiPrompt\0-pandoc\perplexity.md -f markdown -t pdf -o output.pdf --pdf-engine=xelatex --syntax-highlighting=kate --filter=mermaid-filter.cmd --standalone -V CJKmainfont="C:/Users/wddsh/Documents/IdeaProjects/ProjectAGiPrompt/0-pandoc/fonts/SourceHanSansCN-Medium.otf" -V CJKsansfont="C:/Users/wddsh/Documents/IdeaProjects/ProjectAGiPrompt/0-pandoc/fonts/SourceHanSansCN-Medium.otf" -V CJKmonofont="C:/Users/wddsh/Documents/IdeaProjects/ProjectAGiPrompt/0-pandoc/fonts/SourceHanSansCN-Medium.otf" --variable=CJKoptions:BoldFont="C:/Users/wddsh/Documents/IdeaProjects/ProjectAGiPrompt/0-pandoc/fonts/SourceHanSansCN-Medium.otf" --variable=numbersections:false --variable=secnumdepth:0 + + + + + + diff --git a/0-pandoc-失败/转换pandoc.md b/0-pandoc-失败/转换pandoc.md new file mode 100644 index 0000000..ab05091 --- /dev/null +++ b/0-pandoc-失败/转换pandoc.md @@ -0,0 +1,17 @@ +在windows系统中,使用pandoc将一个markdown文件转换为epub文件,请基于如下的条件转换 +1. 完整保留markdown的语法,去除掉特殊符号 +2. epub中的字体,使用思源黑体CN-Medium +3. markdown中的表格,注意在epub中有合适的表格结构 +4. markdown中的mermaid图像,请解析为合适的图片嵌入至epub中 +5. markdown中的代码块,请合理的解析,在epub中有引用或者突出显示 + +请基于上面的要求,给出转换命令 + + +在windows系统中,使用pandoc将一个markdown文件转换为pdf文件,请基于如下的条件转换 +1. 完整的解析markdown渲染后的全部格式,去除掉特殊符号 +2. pdf的页边距使用窄边距 +3. markdown中的代码块、表格需要被正确的渲染 +4. markdown中的mermaid图像,需要被正确的渲染 + +请基于上面的要求,给出转换命令 diff --git a/0-pandoc电子书转换/convert.ps1 b/0-pandoc电子书转换/convert.ps1 deleted file mode 100644 index a98c776..0000000 --- a/0-pandoc电子书转换/convert.ps1 +++ /dev/null @@ -1 +0,0 @@ -pandoc perplexity.md --from markdown+pipe_tables+fenced_code_blocks+fenced_code_attributes --to epub3 --filter mermaid-filter.cmd --epub-embed-font="SourceHanSansCN-Medium.otf" --css="epub-style.css" --syntax-highlighting=pygments --standalone --toc --toc-depth=3 --output output.epub diff --git a/0-pandoc电子书转换/perplexity.md b/0-pandoc电子书转换/perplexity.md deleted file mode 100644 index ec859b7..0000000 --- a/0-pandoc电子书转换/perplexity.md +++ /dev/null @@ -1,1152 +0,0 @@ -# SQL完整使用指南:从数据定义到复杂查询 - -## 前言 - -作为一名SQL语言大师,我将带您深入理解SQL的精髓。SQL不仅仅是语法规则的集合,更是一门与数据对话的艺术。让我们从数据库设计开始,循序渐进地掌握SQL的各个方面。 - -## 示例数据库设计 - -在开始学习之前,我们先构建一个完整的业务场景:**在线教育平台数据库**。这个案例将贯穿我们整个学习过程。 - -```mermaid -erDiagram - USERS ||--o{ ENROLLMENTS : "enrolls" - COURSES ||--o{ ENROLLMENTS : "enrolled_in" - COURSES ||--o{ LESSONS : "contains" - USERS ||--o{ USER_PROGRESS : "tracks" - LESSONS ||--o{ USER_PROGRESS : "progressed_in" - CATEGORIES ||--o{ COURSES : "categorizes" - - USERS { - int user_id PK - varchar name - varchar email - date birth_date - enum gender - datetime created_at - datetime updated_at - boolean is_active - } - - CATEGORIES { - int category_id PK - varchar name - text description - datetime created_at - } - - COURSES { - int course_id PK - varchar title - text description - int category_id FK - decimal price - int duration_hours - enum difficulty_level - datetime created_at - boolean is_published - } - - LESSONS { - int lesson_id PK - int course_id FK - varchar title - text content - int duration_minutes - int sequence_number - datetime created_at - } - - ENROLLMENTS { - int enrollment_id PK - int user_id FK - int course_id FK - datetime enrollment_date - decimal amount_paid - enum status - } - - USER_PROGRESS { - int progress_id PK - int user_id FK - int lesson_id FK - datetime completed_at - int time_spent_minutes - } -``` - -*** - -# 一、数据定义语言 (DDL) - 构建数据世界的基石 - -> **核心理念**:DDL就像建筑师的图纸,它定义了数据的"骨架"和"规则"。每一个CREATE、ALTER、DROP操作都在塑造数据的结构和约束。 - -## 1.1 CREATE - 创造数据结构 - -### 创建数据库 -```sql --- 创建数据库(某些数据库系统需要) -CREATE DATABASE online_education_platform -CHARACTER SET utf8mb4 -COLLATE utf8mb4_unicode_ci; -``` - -### 创建表结构 - -```sql --- 1. 用户表 - 展示完整的列定义和约束 -CREATE TABLE users ( - user_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 主键,自动增长 - name VARCHAR(100) NOT NULL, -- 非空约束 - email VARCHAR(255) NOT NULL UNIQUE, -- 唯一约束 - birth_date DATE, -- 可为空 - gender ENUM('male', 'female', 'other') DEFAULT 'other', -- 枚举类型 - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 默认当前时间 - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - is_active BOOLEAN DEFAULT true, - - -- 表级约束 - CONSTRAINT chk_age CHECK (birth_date <= date('now', '-13 years')), -- 检查约束:至少13岁 - INDEX idx_email (email), -- 普通索引 - INDEX idx_created_at (created_at) -); - --- 2. 课程分类表 -CREATE TABLE categories ( - category_id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(50) NOT NULL UNIQUE, - description TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - --- 3. 课程表 - 展示外键约束 -CREATE TABLE courses ( - course_id INTEGER PRIMARY KEY AUTOINCREMENT, - title VARCHAR(200) NOT NULL, - description TEXT, - category_id INTEGER NOT NULL, - price DECIMAL(10,2) DEFAULT 0.00, - duration_hours INTEGER DEFAULT 0, - difficulty_level ENUM('beginner', 'intermediate', 'advanced') DEFAULT 'beginner', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - is_published BOOLEAN DEFAULT false, - - -- 外键约束 - FOREIGN KEY (category_id) REFERENCES categories(category_id) - ON DELETE RESTRICT ON UPDATE CASCADE, - - -- 检查约束 - CONSTRAINT chk_price CHECK (price >= 0), - CONSTRAINT chk_duration CHECK (duration_hours >= 0), - - -- 索引 - INDEX idx_category (category_id), - INDEX idx_price (price), - INDEX idx_difficulty (difficulty_level) -); -``` - -### 创建索引 - 性能优化的秘密武器 - -```sql --- 创建单列索引 -CREATE INDEX idx_users_name ON users(name); - --- 创建复合索引(顺序很重要!) -CREATE INDEX idx_course_category_price ON courses(category_id, price); - --- 创建唯一索引 -CREATE UNIQUE INDEX idx_users_email_unique ON users(email); - --- 创建条件索引(SQLite特性) -CREATE INDEX idx_active_users ON users(name) WHERE is_active = true; -``` - -### 创建视图 - 数据的逻辑窗口 - -```sql --- 创建视图:活跃用户的课程统计 -CREATE VIEW active_user_course_stats AS -SELECT - u.user_id, - u.name, - u.email, - COUNT(e.course_id) as enrolled_courses, - SUM(e.amount_paid) as total_spent, - AVG(c.price) as avg_course_price -FROM users u -LEFT JOIN enrollments e ON u.user_id = e.user_id -LEFT JOIN courses c ON e.course_id = c.course_id -WHERE u.is_active = true -GROUP BY u.user_id, u.name, u.email; -``` - -## 1.2 ALTER - 修改现有结构 - -```sql --- 添加新列 -ALTER TABLE users ADD COLUMN phone VARCHAR(20); -ALTER TABLE users ADD COLUMN profile_picture_url TEXT; - --- 修改列定义 -ALTER TABLE courses ALTER COLUMN title SET NOT NULL; -- 设置非空 -ALTER TABLE courses ALTER COLUMN description TYPE TEXT; -- 修改数据类型 - --- 添加约束 -ALTER TABLE users ADD CONSTRAINT chk_phone_format -CHECK (phone IS NULL OR length(phone) >= 10); - --- 删除约束 -ALTER TABLE users DROP CONSTRAINT chk_phone_format; - --- 重命名列 -ALTER TABLE users RENAME COLUMN phone TO phone_number; - --- 重命名表 -ALTER TABLE users RENAME TO platform_users; -ALTER TABLE platform_users RENAME TO users; -- 改回来 -``` - -## 1.3 DROP - 删除结构 - -```sql --- 删除索引 -DROP INDEX idx_users_name; - --- 删除视图 -DROP VIEW active_user_course_stats; - --- 删除表(注意外键约束的影响) --- DROP TABLE courses; -- 这会失败,因为有外键约束 - --- 删除数据库(谨慎使用!) --- DROP DATABASE online_education_platform; -``` - -## 1.4 TRUNCATE - 清空表数据 - -```sql --- 快速清空表数据,保留结构 -TRUNCATE TABLE user_progress; -- 比DELETE FROM table 更快 - --- 注意:TRUNCATE不会触发DELETE触发器,也不能回滚单条记录 -``` - -**💡 DDL最佳实践** -1. **命名规范**:表名用复数,列名用snake_case -2. **约束设计**:优先使用数据库约束而不是应用层检查 -3. **索引策略**:根据查询模式创建索引,避免过度索引 -4. **版本控制**:所有DDL变更都应该有迁移脚本 - -*** - -# 二、数据操作语言 (DML) - 数据的生命周期管理 - -> **核心理念**:DML是数据的生命线。INSERT赋予数据生命,UPDATE让数据成长,DELETE让数据安息。每个操作都要考虑性能、一致性和业务逻辑。 - -## 2.1 INSERT - 数据的诞生 - -### 基础插入操作 - -```sql --- 1. 插入完整记录 -INSERT INTO categories (name, description) -VALUES ('编程开发', '包含各种编程语言和开发技术的课程'); - -INSERT INTO categories (name, description) VALUES -('数据科学', '数据分析、机器学习、统计学相关课程'), -('设计创意', '平面设计、UI/UX、插画等创意类课程'), -('商业管理', '市场营销、项目管理、创业等商业课程'); - --- 2. 插入用户数据 -INSERT INTO users (name, email, birth_date, gender) VALUES -('张三', 'zhang.san@email.com', '1995-05-15', 'male'), -('李四', 'li.si@email.com', '1992-08-22', 'female'), -('王五', 'wang.wu@email.com', '1988-12-03', 'male'), -('赵六', 'zhao.liu@email.com', '1990-03-18', 'female'); - --- 3. 插入课程数据 -INSERT INTO courses (title, description, category_id, price, duration_hours, difficulty_level, is_published) VALUES -('Python从入门到精通', '零基础学Python,包含基础语法、数据结构、面向对象编程', 1, 199.00, 40, 'beginner', true), -('数据分析实战', '使用Python进行数据分析,包含pandas、numpy、matplotlib', 2, 299.00, 30, 'intermediate', true), -('UI设计基础教程', 'Figma和Sketch工具使用,界面设计原则', 3, 159.00, 25, 'beginner', true); -``` - -### 高级插入技巧 - -```sql --- 1. 基于查询的插入(INSERT ... SELECT) -INSERT INTO user_progress (user_id, lesson_id, completed_at, time_spent_minutes) -SELECT - u.user_id, - l.lesson_id, - datetime('now', '-' || abs(random()) % 30 || ' days'), -- 随机过去30天内 - 30 + abs(random()) % 60 -- 30-90分钟随机时长 -FROM users u -CROSS JOIN lessons l -WHERE u.user_id <= 2 AND l.lesson_id <= 3; -- 限制数据量 - --- 2. 插入时处理冲突(UPSERT) -INSERT INTO users (email, name, updated_at) -VALUES ('zhang.san@email.com', '张三(更新)', datetime('now')) -ON CONFLICT(email) DO UPDATE SET - name = excluded.name, - updated_at = excluded.updated_at; - --- 3. 批量插入优化 -INSERT INTO enrollments (user_id, course_id, enrollment_date, amount_paid, status) VALUES -(1, 1, datetime('now'), 199.00, 'active'), -(1, 2, datetime('now'), 299.00, 'active'), -(2, 1, datetime('now'), 199.00, 'active'), -(3, 3, datetime('now'), 159.00, 'completed'), -(4, 1, datetime('now'), 199.00, 'active'); -``` - -## 2.2 UPDATE - 数据的成长 - -### 基础更新操作 - -```sql --- 1. 单表更新 -UPDATE users -SET name = '张三丰', updated_at = datetime('now') -WHERE email = 'zhang.san@email.com'; - --- 2. 条件更新 -UPDATE courses -SET price = price * 0.8 -- 全场8折 -WHERE category_id = 1 AND is_published = true; - --- 3. 使用CASE进行条件更新 -UPDATE courses -SET difficulty_level = - CASE - WHEN duration_hours < 20 THEN 'beginner' - WHEN duration_hours BETWEEN 20 AND 40 THEN 'intermediate' - ELSE 'advanced' - END -WHERE difficulty_level IS NULL; -``` - -### 高级更新技巧 - -```sql --- 1. 基于子查询的更新 -UPDATE users -SET is_active = false -WHERE user_id IN ( - SELECT DISTINCT u.user_id - FROM users u - LEFT JOIN enrollments e ON u.user_id = e.user_id - WHERE e.enrollment_date < date('now', '-365 days') - OR e.enrollment_date IS NULL -); - --- 2. 使用JOIN的更新(如果数据库支持) -UPDATE courses -SET price = c.price * 1.1 -FROM courses c -JOIN categories cat ON c.category_id = cat.category_id -WHERE cat.name = '编程开发' AND c.created_at < date('now', '-180 days'); -``` - -## 2.3 DELETE - 数据的清理 - -### 基础删除操作 - -```sql --- 1. 条件删除 -DELETE FROM user_progress -WHERE completed_at < date('now', '-365 days'); - --- 2. 使用子查询删除 -DELETE FROM enrollments -WHERE course_id IN ( - SELECT course_id - FROM courses - WHERE is_published = false AND created_at < date('now', '-90 days') -); -``` - -### 安全删除策略 - -```sql --- 1. 软删除(推荐生产环境使用) --- 添加deleted_at列而不是真正删除数据 -ALTER TABLE users ADD COLUMN deleted_at DATETIME NULL; - --- 软删除操作 -UPDATE users -SET deleted_at = datetime('now'), is_active = false -WHERE user_id = 1; - --- 查询时排除已删除的记录 -SELECT * FROM users WHERE deleted_at IS NULL; -``` - -**⚠️ DML常见陷阱和最佳实践** - -```mermaid -flowchart TD - A[DML操作] --> B{是否需要事务?} - B -->|是| C[BEGIN TRANSACTION] - B -->|否| D[直接执行] - C --> E[执行DML语句] - E --> F{检查结果} - F -->|成功| G[COMMIT] - F -->|失败| H[ROLLBACK] - D --> I[记录日志] - G --> I - H --> I -``` - -1. **批量操作优化**:大量数据操作时使用批量语句 -2. **索引影响**:UPDATE和DELETE会影响索引性能 -3. **外键约束**:删除时注意外键约束的限制 -4. **备份策略**:重要数据操作前先备份 - -*** - -# 三、事务控制语言 (TCL) - 数据一致性的守护者 - -> **核心理念**:TCL是数据完整性的守护神。它确保要么全部成功,要么全部失败,绝不允许数据处于不一致的中间状态。 - -## 3.1 ACID特性深度解析 - -```mermaid -mindmap - root((ACID)) - Atomicity - 原子性 - 全部成功或全部失败 - 不可分割的操作单元 - Consistency - 一致性 - 数据完整性约束 - 业务规则满足 - Isolation - 隔离性 - 并发事务相互独立 - 隔离级别控制 - Durability - 持久性 - 已提交数据永久保存 - 系统故障不影响 -``` - -## 3.2 基础事务控制 - -### 事务的生命周期 - -```sql --- 1. 显式事务控制 -BEGIN TRANSACTION; -- 开始事务 - --- 用户注册业务逻辑 -INSERT INTO users (name, email, birth_date, gender) -VALUES ('新用户', 'newuser@email.com', '1995-01-01', 'male'); - --- 获取刚插入的用户ID(SQLite方式) --- 在实际应用中,通常通过程序获取last_insert_rowid() - --- 初始化用户数据 -INSERT INTO user_progress (user_id, lesson_id, completed_at) -SELECT last_insert_rowid(), lesson_id, NULL -FROM lessons WHERE course_id = 1; - --- 检查操作是否成功 --- 如果一切正常 -COMMIT; -- 提交事务 - --- 如果出现错误 --- ROLLBACK; -- 回滚事务 -``` - -### 保存点(Savepoint)- 事务内的检查点 - -```sql -BEGIN TRANSACTION; - --- 创建保存点 -SAVEPOINT user_creation; - -INSERT INTO users (name, email, birth_date, gender) -VALUES ('测试用户1', 'test1@email.com', '1990-01-01', 'male'); - -INSERT INTO users (name, email, birth_date, gender) -VALUES ('测试用户2', 'test2@email.com', '1991-01-01', 'female'); - --- 创建另一个保存点 -SAVEPOINT course_enrollment; - --- 假设这里发生了错误 -INSERT INTO enrollments (user_id, course_id, enrollment_date, amount_paid) -VALUES (999, 1, datetime('now'), 199.00); -- 用户ID不存在,会失败 - --- 回滚到指定保存点 -ROLLBACK TO SAVEPOINT course_enrollment; - --- 释放保存点 -RELEASE SAVEPOINT course_enrollment; - -COMMIT; -``` - -## 3.3 隔离级别详解 - -```sql --- 设置事务隔离级别(根据数据库而定) --- SQLite默认是可串行化级别 - --- 1. 读未提交 (Read Uncommitted) -PRAGMA read_uncommitted = true; - --- 2. 读已提交 (Read Committed) - 大多数数据库默认 --- 避免脏读,但可能出现不可重复读 - --- 3. 可重复读 (Repeatable Read) --- 避免脏读和不可重复读,但可能出现幻读 - --- 4. 可串行化 (Serializable) - SQLite默认 --- 避免所有并发问题,但性能最低 -``` - -### 并发问题演示 - -```sql --- 演示并发问题的场景 - --- 会话1: -BEGIN TRANSACTION; -SELECT price FROM courses WHERE course_id = 1; -- 假设返回199.00 --- 在这里暂停,让会话2执行 - --- 会话2: -BEGIN TRANSACTION; -UPDATE courses SET price = 299.00 WHERE course_id = 1; -COMMIT; - --- 回到会话1: -SELECT price FROM courses WHERE course_id = 1; -- 可能返回299.00(不可重复读) -COMMIT; -``` - -## 3.4 实际业务场景中的事务应用 - -### 场景1:用户注册完整流程 - -```sql -BEGIN TRANSACTION; - --- 1. 创建用户账户 -INSERT INTO users (name, email, birth_date, gender) -VALUES ('李明', 'liming@email.com', '1992-06-15', 'male'); - --- 2. 记录注册来源(假设有这样一个表) -INSERT INTO user_activity_log (user_id, activity_type, created_at) -VALUES (last_insert_rowid(), 'registration', datetime('now')); - --- 3. 赠送新用户福利课程 -INSERT INTO enrollments (user_id, course_id, enrollment_date, amount_paid, status) -VALUES (last_insert_rowid(), 1, datetime('now'), 0.00, 'active'); - -COMMIT; -``` - -### 场景2:课程购买事务 - -```sql --- 模拟课程购买的复杂事务 -BEGIN TRANSACTION; - -SAVEPOINT before_purchase; - --- 1. 检查课程是否可购买 --- 2. 检查用户是否已购买 --- 3. 创建订单记录 -INSERT INTO enrollments (user_id, course_id, enrollment_date, amount_paid, status) -VALUES (1, 2, datetime('now'), 299.00, 'active'); - --- 4. 更新用户统计信息(假设有这样的字段) -UPDATE users -SET updated_at = datetime('now') -WHERE user_id = 1; - --- 5. 记录支付日志 --- INSERT INTO payment_log (user_id, amount, payment_method, transaction_id) --- VALUES (1, 299.00, 'credit_card', 'TXN123456789'); - --- 如果一切顺利 -COMMIT; - --- 如果出现错误 --- ROLLBACK TO SAVEPOINT before_purchase; -``` - -**🔒 TCL最佳实践** - -```mermaid -flowchart LR - A[开始业务操作] --> B[BEGIN TRANSACTION] - B --> C[执行关键操作] - C --> D{操作成功?} - D -->|是| E[COMMIT] - D -->|否| F[ROLLBACK] - E --> G[记录成功日志] - F --> H[记录错误日志] - G --> I[结束] - H --> I -``` - -1. **事务范围最小化**:只在必要的操作周围使用事务 -2. **避免长事务**:长时间的事务会增加死锁风险 -3. **合理使用保存点**:复杂业务逻辑中使用保存点进行精细控制 -4. **错误处理**:总是准备回滚计划 - -*** - -# 四、数据查询语言 (DQL) - SELECT语句完全指南 - -> **核心理念**:SELECT不仅仅是查询,它是数据思维的体现。理解数据的逻辑处理顺序,就能写出高效、准确的查询语句。 - -## 4.1 SQL逻辑处理顺序 - 引擎的视角 - -```mermaid -flowchart TD - A[FROM 子句] --> B[JOIN 连接] - B --> C[WHERE 行过滤] - C --> D[GROUP BY 分组] - D --> E[HAVING 组过滤] - E --> F[SELECT 选择列] - F --> G[DISTINCT 去重] - G --> H[ORDER BY 排序] - H --> I[LIMIT/OFFSET 限制] - - style A fill:#f9f,stroke:#333,stroke-width:2px - style F fill:#bbf,stroke:#333,stroke-width:2px -``` - -让我们先创建示例数据,然后按照这个顺序深入学习每个子句: - -```sql --- 创建完整的示例数据 -INSERT INTO lessons (course_id, title, content, duration_minutes, sequence_number) VALUES -(1, 'Python基础语法', 'Python的基本语法介绍', 45, 1), -(1, '数据类型和变量', 'Python的数据类型详解', 38, 2), -(1, '控制流程', '条件语句和循环', 52, 3), -(1, '函数和模块', '函数定义和模块使用', 41, 4), -(2, '数据导入导出', 'pandas读取各种格式数据', 35, 1), -(2, '数据清洗', '处理缺失值和异常值', 48, 2), -(2, '数据可视化', 'matplotlib和seaborn使用', 55, 3), -(3, 'Figma基础', 'Figma界面和基本操作', 30, 1), -(3, '组件设计', '创建可重用的设计组件', 42, 2); -``` - -## 4.2 FROM子句 - 数据源的确立 - -### 基础表查询 - -```sql --- 1. 单表查询 -SELECT * FROM users; - --- 2. 表别名使用 -SELECT u.name, u.email, u.created_at -FROM users u -WHERE u.is_active = true; - --- 3. 子查询作为数据源 -SELECT course_stats.* -FROM ( - SELECT - course_id, - COUNT(*) as enrollment_count, - AVG(amount_paid) as avg_paid - FROM enrollments - WHERE status = 'active' - GROUP BY course_id -) course_stats -WHERE course_stats.enrollment_count > 1; -``` - -## 4.3 JOIN连接 - 数据关系的桥梁 - -### JOIN类型详解 - -```mermaid -graph LR - A[LEFT TABLE] - B[RIGHT TABLE] - - subgraph "INNER JOIN" - C[只返回匹配的记录] - end - - subgraph "LEFT JOIN" - D[返回左表所有记录
右表匹配记录] - end - - subgraph "RIGHT JOIN" - E[返回右表所有记录
左表匹配记录] - end - - subgraph "FULL OUTER JOIN" - F[返回两表所有记录] - end -``` - -```sql --- 1. INNER JOIN - 内连接(最常用) -SELECT - u.name as student_name, - c.title as course_title, - e.enrollment_date, - e.amount_paid -FROM users u -INNER JOIN enrollments e ON u.user_id = e.user_id -INNER JOIN courses c ON e.course_id = c.course_id -WHERE u.is_active = true; - --- 2. LEFT JOIN - 左连接(查看所有用户,包括未注册课程的) -SELECT - u.name, - u.email, - COUNT(e.course_id) as course_count, - COALESCE(SUM(e.amount_paid), 0) as total_spent -FROM users u -LEFT JOIN enrollments e ON u.user_id = e.user_id -GROUP BY u.user_id, u.name, u.email; - --- 3. 多表连接的复杂查询 -SELECT - u.name as student_name, - c.title as course_title, - cat.name as category_name, - COUNT(up.progress_id) as lessons_completed, - COUNT(l.lesson_id) as total_lessons, - ROUND( - COUNT(up.progress_id) * 100.0 / COUNT(l.lesson_id), - 2 - ) as completion_percentage -FROM users u -JOIN enrollments e ON u.user_id = e.user_id -JOIN courses c ON e.course_id = c.course_id -JOIN categories cat ON c.category_id = cat.category_id -LEFT JOIN lessons l ON c.course_id = l.course_id -LEFT JOIN user_progress up ON u.user_id = up.user_id - AND l.lesson_id = up.lesson_id -WHERE e.status = 'active' -GROUP BY u.user_id, c.course_id, u.name, c.title, cat.name -ORDER BY completion_percentage DESC; -``` - -### 自连接 - 同表不同角色 - -```sql --- 查找同一课程的其他学员 -SELECT DISTINCT - u1.name as current_user, - u2.name as classmate, - c.title as course_title -FROM users u1 -JOIN enrollments e1 ON u1.user_id = e1.user_id -JOIN enrollments e2 ON e1.course_id = e2.course_id -JOIN users u2 ON e2.user_id = u2.user_id -JOIN courses c ON e1.course_id = c.course_id -WHERE u1.user_id != u2.user_id -- 排除自己 - AND u1.name = '张三' -ORDER BY c.title, u2.name; -``` - -## 4.4 WHERE子句 - 数据的精确过滤 - -### 基础过滤条件 - -```sql --- 1. 基本比较运算符 -SELECT * FROM courses -WHERE price > 200 - AND difficulty_level = 'intermediate' - AND is_published = true; - --- 2. 范围查询 -SELECT * FROM users -WHERE birth_date BETWEEN '1990-01-01' AND '1999-12-31' - AND created_at >= datetime('now', '-30 days'); - --- 3. 模式匹配 -SELECT * FROM courses -WHERE title LIKE '%Python%' -- 包含Python - OR title LIKE '数据%'; -- 以"数据"开头 - --- 4. 列表匹配 -SELECT * FROM courses -WHERE category_id IN (1, 2) -- 编程和数据科学 - AND difficulty_level NOT IN ('advanced'); -``` - -### 高级过滤技巧 - -```sql --- 1. NULL值处理 -SELECT * FROM users -WHERE birth_date IS NOT NULL - AND (phone IS NULL OR length(phone) < 11); - --- 2. 正则表达式(如果支持) -SELECT * FROM users -WHERE email REGEXP '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; - --- 3. 日期和时间函数 -SELECT * FROM enrollments -WHERE date(enrollment_date) = date('now') -- 今天注册的 - OR strftime('%w', enrollment_date) = '0'; -- 周日注册的 - --- 4. 子查询过滤 -SELECT * FROM users -WHERE user_id IN ( - SELECT DISTINCT user_id - FROM enrollments - WHERE amount_paid > 200 -); -``` - -## 4.5 GROUP BY和聚合函数 - 数据的汇总艺术 - -### 基础分组和聚合 - -```sql --- 1. 按课程统计注册情况 -SELECT - c.title, - COUNT(*) as enrollment_count, - AVG(e.amount_paid) as avg_payment, - MIN(e.amount_paid) as min_payment, - MAX(e.amount_paid) as max_payment, - SUM(e.amount_paid) as total_revenue -FROM courses c -JOIN enrollments e ON c.course_id = e.course_id -GROUP BY c.course_id, c.title -ORDER BY enrollment_count DESC; - --- 2. 按年月统计注册趋势 -SELECT - strftime('%Y-%m', enrollment_date) as month, - COUNT(*) as enrollments, - SUM(amount_paid) as monthly_revenue, - COUNT(DISTINCT user_id) as unique_students -FROM enrollments -GROUP BY strftime('%Y-%m', enrollment_date) -ORDER BY month; -``` - -### HAVING子句 - 分组后的过滤 - -```sql --- 找出受欢迎的课程(注册人数 > 2) -SELECT - c.title, - COUNT(*) as enrollment_count, - AVG(e.amount_paid) as avg_payment -FROM courses c -JOIN enrollments e ON c.course_id = e.course_id -GROUP BY c.course_id, c.title -HAVING COUNT(*) > 1 -- 注意:这里不能用别名enrollment_count - AND AVG(e.amount_paid) > 150 -ORDER BY enrollment_count DESC; - --- 复杂的HAVING条件 -SELECT - cat.name as category, - COUNT(DISTINCT c.course_id) as course_count, - COUNT(DISTINCT e.user_id) as student_count, - SUM(e.amount_paid) as category_revenue -FROM categories cat -JOIN courses c ON cat.category_id = c.category_id -JOIN enrollments e ON c.course_id = e.course_id -GROUP BY cat.category_id, cat.name -HAVING course_count >= 1 - AND category_revenue > 200 - AND student_count > 1; -``` - -## 4.6 高级查询技术 - -### 窗口函数 - 数据分析的利器 - -```sql --- 1. 排名函数 -SELECT - u.name, - c.title, - e.amount_paid, - ROW_NUMBER() OVER (ORDER BY e.amount_paid DESC) as payment_rank, - RANK() OVER (ORDER BY e.amount_paid DESC) as payment_rank_with_ties, - DENSE_RANK() OVER (ORDER BY e.amount_paid DESC) as dense_payment_rank -FROM users u -JOIN enrollments e ON u.user_id = e.user_id -JOIN courses c ON e.course_id = c.course_id; - --- 2. 分区窗口函数 -SELECT - u.name, - c.title, - cat.name as category, - e.amount_paid, - AVG(e.amount_paid) OVER (PARTITION BY cat.category_id) as category_avg_payment, - e.amount_paid - AVG(e.amount_paid) OVER (PARTITION BY cat.category_id) as payment_diff_from_avg -FROM users u -JOIN enrollments e ON u.user_id = e.user_id -JOIN courses c ON e.course_id = c.course_id -JOIN categories cat ON c.category_id = cat.category_id; - --- 3. 累计计算 -SELECT - enrollment_date, - amount_paid, - SUM(amount_paid) OVER (ORDER BY enrollment_date) as running_total, - LAG(amount_paid, 1) OVER (ORDER BY enrollment_date) as previous_payment, - LEAD(amount_paid, 1) OVER (ORDER BY enrollment_date) as next_payment -FROM enrollments -ORDER BY enrollment_date; -``` - -### 通用表表达式 (CTE) - -```sql --- 1. 基础CTE -WITH course_stats AS ( - SELECT - c.course_id, - c.title, - COUNT(e.enrollment_id) as enrollment_count, - AVG(e.amount_paid) as avg_payment - FROM courses c - LEFT JOIN enrollments e ON c.course_id = e.course_id - GROUP BY c.course_id, c.title -), -popular_courses AS ( - SELECT * FROM course_stats - WHERE enrollment_count >= 2 -) -SELECT - title, - enrollment_count, - ROUND(avg_payment, 2) as avg_payment -FROM popular_courses -ORDER BY enrollment_count DESC; - --- 2. 递归CTE(课程推荐系统) -WITH RECURSIVE course_recommendations AS ( - -- 基础情况:用户直接购买的课程 - SELECT - u.user_id, - u.name, - c.course_id, - c.title, - 1 as level - FROM users u - JOIN enrollments e ON u.user_id = e.user_id - JOIN courses c ON e.course_id = c.course_id - WHERE u.name = '张三' - - UNION ALL - - -- 递归情况:相同分类的其他课程 - SELECT - cr.user_id, - cr.name, - c2.course_id, - c2.title, - cr.level + 1 - FROM course_recommendations cr - JOIN courses c1 ON cr.course_id = c1.course_id - JOIN courses c2 ON c1.category_id = c2.category_id - WHERE cr.level < 2 - AND c2.course_id NOT IN ( - SELECT course_id FROM course_recommendations - ) -) -SELECT DISTINCT name, title, level -FROM course_recommendations -ORDER BY level, title; -``` - -### 子查询的艺术 - -```sql --- 1. 标量子查询 -SELECT - u.name, - u.email, - (SELECT COUNT(*) FROM enrollments e WHERE e.user_id = u.user_id) as course_count, - (SELECT MAX(amount_paid) FROM enrollments e WHERE e.user_id = u.user_id) as max_payment -FROM users u; - --- 2. 存在性检查 -SELECT u.name, u.email -FROM users u -WHERE EXISTS ( - SELECT 1 FROM enrollments e - WHERE e.user_id = u.user_id - AND e.amount_paid > 200 -); - --- 3. 复杂的相关子查询 -SELECT - c.title, - c.price, - ( - SELECT COUNT(*) - FROM enrollments e - WHERE e.course_id = c.course_id - ) as enrollment_count, - CASE - WHEN (SELECT COUNT(*) FROM enrollments e WHERE e.course_id = c.course_id) > 2 - THEN '热门课程' - WHEN (SELECT COUNT(*) FROM enrollments e WHERE e.course_id = c.course_id) > 0 - THEN '普通课程' - ELSE '冷门课程' - END as popularity -FROM courses c -WHERE c.is_published = true; -``` - -## 4.7 集合操作 - -```sql --- 1. UNION - 合并结果集 -SELECT name as person_name, 'Student' as role FROM users -WHERE user_id IN (SELECT DISTINCT user_id FROM enrollments) -UNION -SELECT title as person_name, 'Course' as role FROM courses -WHERE is_published = true; - --- 2. INTERSECT - 交集 -SELECT user_id FROM enrollments WHERE course_id = 1 -INTERSECT -SELECT user_id FROM enrollments WHERE course_id = 2; - --- 3. EXCEPT - 差集 -SELECT user_id FROM users WHERE is_active = true -EXCEPT -SELECT DISTINCT user_id FROM enrollments; -``` - -## 4.8 ORDER BY和LIMIT - 结果的排序和限制 - -```sql --- 1. 多列排序 -SELECT - u.name, - c.title, - e.amount_paid, - e.enrollment_date -FROM users u -JOIN enrollments e ON u.user_id = e.user_id -JOIN courses c ON e.course_id = c.course_id -ORDER BY - e.amount_paid DESC, -- 按支付金额降序 - e.enrollment_date ASC, -- 相同金额按注册时间升序 - u.name; -- 最后按姓名排序 - --- 2. 使用表达式排序 -SELECT - u.name, - u.birth_date, - CASE - WHEN date('now') - birth_date > 365.25 * 30 THEN '30+' - WHEN date('now') - birth_date > 365.25 * 25 THEN '25-30' - ELSE '25以下' - END as age_group -FROM users u -ORDER BY - CASE - WHEN date('now') - birth_date > 365.25 * 30 THEN 1 - WHEN date('now') - birth_date > 365.25 * 25 THEN 2 - ELSE 3 - END, - u.name; - --- 3. 分页查询 -SELECT - u.name, - c.title, - e.amount_paid -FROM users u -JOIN enrollments e ON u.user_id = e.user_id -JOIN courses c ON e.course_id = c.course_id -ORDER BY e.enrollment_date DESC -LIMIT 10 OFFSET 0; -- 第一页,每页10条 - --- 更复杂的分页示例 -WITH numbered_results AS ( - SELECT - u.name, - c.title, - e.amount_paid, - ROW_NUMBER() OVER (ORDER BY e.enrollment_date DESC) as row_num - FROM users u - JOIN enrollments e ON u.user_id = e.user_id - JOIN courses c ON e.course_id = c.course_id -) -SELECT name, title, amount_paid -FROM numbered_results -WHERE row_num BETWEEN 11 AND 20; -- 第二页 -``` - -## 📊 查询性能优化 - -### 执行计划分析 - -```sql --- 查看查询执行计划 -EXPLAIN QUERY PLAN -SELECT - u.name, - COUNT(e.course_id) as course_count -FROM users u -LEFT JOIN enrollments e ON u.user_id = e.user_id -WHERE u.is_active = true -GROUP BY u.user_id, u.name; -``` - -### 优化建议 - -```mermaid -flowchart TD - A[查询优化] --> B[索引优化] - A --> C[查询重写] - A --> D[数据量控制] - - B --> E[单列索引] - B --> F[复合索引] - B --> G[覆盖索引] - - C --> H[避免SELECT *] - C --> I[合理使用子查询] - C --> J[优化JOIN顺序] - - D --> K[使用LIMIT] - D --> L[分页处理] - D --> M[条件前置] -``` - -**🎯 SELECT最佳实践总结** - -1. **理解执行顺序**:按照FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY的逻辑思考 -2. **合理使用索引**:根据WHERE和JOIN条件创建合适的索引 -3. **避免N+1查询**:使用JOIN替代循环查询 -4. **控制结果集大小**:合理使用LIMIT和分页 -5. **选择合适的连接类型**:根据业务需求选择INNER JOIN或LEFT JOIN -6. **善用窗口函数**:复杂的分析需求使用窗口函数而不是自连接 - -*** - -# 总结 - -通过这份完整的SQL指南,我们从数据定义的基石开始,经历了数据操作的生命周期,掌握了事务控制的精髓,最终深入探索了查询的艺术。每一个SQL语句都不仅仅是代码,更是我们与数据对话的语言。 - -记住,优秀的SQL不仅要正确,还要高效、可读、可维护。在实际工作中,始终考虑性能影响、数据完整性和业务逻辑的完整性。 - -SQL的学习永无止境,但掌握了这些核心概念和技术,你已经具备了处理复杂数据问题的能力。继续实践,不断优化,让SQL成为你数据世界中最得力的工具。 \ No newline at end of file diff --git a/0-pandoc电子书转换/转换pandoc.md b/0-pandoc电子书转换/转换pandoc.md deleted file mode 100644 index bc095ff..0000000 --- a/0-pandoc电子书转换/转换pandoc.md +++ /dev/null @@ -1,168 +0,0 @@ -在windows系统中,使用pandoc将一个markdown文件转换为epub文件,请基于如下的条件转换 -1. 完整保留markdown的语法,去除掉特殊符号 -2. epub中的字体,使用思源黑体CN-Medium -3. markdown中的表格,注意在epub中有合适的表格结构 -4. markdown中的mermaid图像,请解析为合适的图片嵌入至epub中 -5. markdown中的代码块,请合理的解析,在epub中有引用或者突出显示 - -请基于上面的要求,给出转换命令 - - - - -### 3. 创建CSS样式文件 - -创建一个名为`epub-style.css`的文件: - -```css -/* 设置思源黑体为主字体 */ -@font-face { - font-family: "Source Han Sans CN"; - font-style: normal; - font-weight: 500; - src: url("../fonts/SourceHanSansCN-Medium.ttf"); -} - -body { - font-family: "Source Han Sans CN", sans-serif; - font-size: 1.2em; - line-height: 1.6; - margin: 0; - padding: 1em; -} - -/* 表格样式 */ -table { - border-collapse: collapse; - width: 100%; - margin: 1em 0; -} - -th, td { - border: 1px solid #ddd; - padding: 8px 12px; - text-align: left; -} - -th { - background-color: #f5f5f5; - font-weight: bold; -} - -tr:nth-child(even) { - background-color: #f9f9f9; -} - -/* 代码块样式 */ -pre { - background-color: #f4f4f4; - border: 1px solid #ddd; - border-radius: 4px; - padding: 1em; - overflow-x: auto; - margin: 1em 0; -} - -code { - font-family: "Courier New", monospace; - background-color: #f4f4f4; - padding: 2px 4px; - border-radius: 2px; -} - -pre code { - background-color: transparent; - padding: 0; -} - -/* 标题样式 */ -h1, h2, h3, h4, h5, h6 { - font-family: "Source Han Sans CN", sans-serif; - font-weight: bold; - margin-top: 1.5em; - margin-bottom: 0.5em; -} -``` - - -## 完整的转换命令 - -使用以下pandoc命令进行转换: - -```bash -pandoc input.md \ - --from markdown+pipe_tables+fenced_code_blocks+fenced_code_attributes \ - --to epub3 \ - --filter mermaid-filter.cmd \ - --epub-embed-font="SourceHanSansCN-Medium.otf" \ - --css="epub-style.css" \ - --highlight-style=pygments \ - --standalone \ - --toc \ - --toc-depth=3 \ - --epub-metadata=metadata.xml \ - --output output.epub -``` - - -### Windows命令行版本(单行): - -```cmd -pandoc input.md --from markdown+pipe_tables+fenced_code_blocks+fenced_code_attributes --to epub3 --filter mermaid-filter.cmd --epub-embed-font="SourceHanSansCN-Medium.ttf" --css="epub-style.css" --highlight-style=pygments --standalone --toc --toc-depth=3 --output output.epub -``` - - -## 命令参数解释 - -### 输入格式设置 - -- `--from markdown+pipe_tables+fenced_code_blocks+fenced_code_attributes`:启用表格、代码块和代码属性支持[^1][^2] - - -### Mermaid图像处理 - -- `--filter mermaid-filter.cmd`:在Windows下使用mermaid-filter处理mermaid图像[^3][^4] - - -### 字体设置 - -- `--epub-embed-font="SourceHanSansCN-Medium.ttf"`:嵌入思源黑体字体文件[^5] -- `--css="epub-style.css"`:应用自定义CSS样式[^6] - - -### 代码高亮 - -- `--highlight-style=pygments`:使用pygments风格的代码语法高亮[^7][^8] - - -### 输出选项 - -- `--to epub3`:输出为EPUB3格式 -- `--standalone`:生成完整的独立文档 -- `--toc`:生成目录 -- `--toc-depth=3`:目录深度为3级 - - -## 可选的元数据文件 - -创建`metadata.xml`文件来设置EPUB元数据: - -```xml -您的书名 -作者姓名 -zh-CN -2025-09-17 -版权信息 -``` - - -## 注意事项 - -1. **文件路径**:确保所有文件(字体、CSS、输入文件)都在正确的路径下 -2. **mermaid-filter**:在Windows下必须使用`mermaid-filter.cmd`而不是`mermaid-filter`[^3] -3. **字体路径**:CSS中的字体路径使用相对路径`../fonts/`[^9] -4. **表格支持**:pandoc原生支持多种表格格式,包括pipe tables[^10] -5. **代码块**:支持语法高亮和属性设置[^11] - -这个命令将完整保留您的markdown语法,正确处理表格结构,将mermaid图转换为图片,为代码块添加高亮显示,并使用思源黑体作为EPUB的字体。 - diff --git a/1-Golang项目/go-gin-gorm-style.md b/1-Golang项目/go-gin-gorm-style.md index 463fbf9..87cc44a 100644 --- a/1-Golang项目/go-gin-gorm-style.md +++ b/1-Golang项目/go-gin-gorm-style.md @@ -1,4 +1,4 @@ - +`` **1. 核心角色与指令 (Core Persona & Directive)** 你将扮演一个拥有10年以上经验的Golang后端架构师。你不仅精通Golang语言特性(特别是并发模型和标准库),更是GIN、GORM等主流框架的资深用户。你的代码风格严谨、可读性强,并且极度重视项目结构、模块化、日志规范和文档注释。 diff --git a/101-数据库学习/0-SQL语法/1-基础学习/gemini-SQL学习全.md b/101-数据库学习/0-SQL语法/1-基础学习/gemini-SQL学习全.md new file mode 100644 index 0000000..b92c1ca --- /dev/null +++ b/101-数据库学习/0-SQL语法/1-基础学习/gemini-SQL学习全.md @@ -0,0 +1,1223 @@ +# SQLite SQL 权威指南:从入门到精通 + +## 第一部分:基础准备:数据库模式与数据类型 + +在深入探讨结构化查询语言(SQL)的复杂世界之前,建立一个坚实的基础至关重要。这包括定义一个清晰、一致的数据库模式,作为我们所有示例的试验场,并深入理解SQLite独特的数据类型系统。与其他数据库系统相比,SQLite在此处的行为有其独到之处,理解这些差异是避免常见陷阱和充分利用其性能的关键。 + +### 1.1 示例数据库模式定义 + +为了在本指南中提供连贯且实用的示例,我们将使用一个模拟公司运营的数据库模式。该模式包含四个核心表:`departments`(部门)、 +`employees`(员工)、`projects`(项目)以及一个用于连接员工和项目的关联表 `employee_projects`。 + +以下是用于创建这些表的完整数据定义语言(DDL)语句。这些语句不仅定义了表的结构,还通过主键、外键和各种约束来确保数据的完整性和一致性。 + +**`departments` 表:** + +```SQL +CREATE TABLE departments +( + department_id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); +``` + ++ `department_id`: 部门的唯一标识符,作为主键。 + ++ `name`: 部门名称,不能为空且必须是唯一的。 + +**`employees` 表:** + +```SQL +CREATE TABLE employees +( + employee_id INTEGER PRIMARY KEY, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + salary REAL CHECK (salary > 0), + hire_date TEXT, -- Stored as TEXT in 'YYYY-MM-DD' format + department_id INTEGER, + FOREIGN KEY (department_id) REFERENCES departments (department_id) +); +``` + ++ `employee_id`: 员工的唯一标识符,作为主键。 + ++ `first_name`, `last_name`: 员工的姓名,不能为空。 + ++ `salary`: 员工的薪水,必须是正数。 + ++ `hire_date`: 雇佣日期,以文本格式存储。 + ++ `department_id`: 员工所属部门的外键,关联到 `departments` 表。 + +**`projects` 表:** + +```SQL +CREATE TABLE projects +( + project_id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + start_date TEXT, -- Stored as TEXT in 'YYYY-MM-DD' format + end_date TEXT -- Stored as TEXT in 'YYYY-MM-DD' format +); +``` + ++ `project_id`: 项目的唯一标识符,作为主键。 + ++ `name`: 项目名称,不能为空且必须是唯一的。 + ++ `start_date`, `end_date`: 项目的起止日期。 + +**`employee_projects` 关联表:** + +```SQL +CREATE TABLE employee_projects +( + employee_id INTEGER, + project_id INTEGER, + PRIMARY KEY (employee_id, project_id), + FOREIGN KEY (employee_id) REFERENCES employees (employee_id), + FOREIGN KEY (project_id) REFERENCES projects (project_id) +); +``` + ++ 该表建立员工和项目之间的多对多关系。 + ++ `employee_id` 和 `project_id` 共同构成复合主键,确保每个员工在一个项目中只被记录一次。 + +为了更直观地理解这些表之间的关系,可以使用实体关系图(ERD)来表示: + +```mermaid +erDiagram + departments { + int department_id PK + string name UK + } + employees { + int employee_id PK + string first_name + string last_name + decimal salary + date hire_date + int department_id FK + } + projects { + int project_id PK + string name UK + date start_date + date end_date + } + employee_projects { + int employee_id FK + int project_id FK + } + + departments ||--o{ employees : "包含" + employees ||--o{ employee_projects : "参与" + projects ||--o{ employee_projects : "分配" + +``` + +这个清晰定义的模式将是我们探索SQLite SQL强大功能的基石。 + +### 1.2 SQLite 核心概念:存储类与类型亲和性 + +SQLite的数据类型系统是其最显著的特点之一,也是与MySQL或PostgreSQL等其他关系型数据库最根本的区别所在。大多数数据库采用静态、严格的类型系统,即列的数据类型在表创建时被固定,任何存入该列的数据都必须符合此类型。相比之下,SQLite采用的是一个更灵活的动态类型系统 。 + +在SQLite中,数据类型是与值本身相关联的,而不是与包含该值的列相关联 。这意味着,除了特殊情况(如 + +`INTEGER PRIMARY KEY`列),你几乎可以在任何列中存储任何类型的值。为了理解这一机制,必须区分两个核心概念:存储类(Storage +Classes)和类型亲和性(Type Affinity)。 + +#### 存储类 (Storage Classes) + +每个存储在SQLite数据库中的值都属于以下五种存储类之一 : + +1. **`NULL`**: 表示一个缺失或未知的值。 + +2. **`INTEGER`**: 一个有符号的整数,根据其数值大小,可以存储为1、2、3、4、6或8个字节。 + +3. **`REAL`**: 一个浮点数,存储为8字节的IEEE浮点数。 + +4. **`TEXT`**: 一个文本字符串,使用数据库指定的编码(UTF-8, UTF-16BE, or UTF-16LE)进行存储。 + +5. **`BLOB`**: (Binary Large Object) 一块二进制数据,完全按照输入时的原样存储。 + +值得注意的是,SQLite没有专门的`BOOLEAN`或`DATETIME`存储类。布尔值通常存储为`INTEGER`类型的0(假)和1(真)。日期和时间可以根据需要存储为 +`TEXT`(如'YYYY-MM-DD HH:MM:SS')、`REAL`(儒略日数)或`INTEGER`(Unix时间戳)。 + +#### 类型亲和性 (Type Affinity) + +尽管SQLite内部使用存储类,但为了最大程度地兼容其他SQL数据库,它引入了“类型亲和性”的概念。当你为表的列声明一个数据类型时,SQLite会为该列分配一种“亲和性”。这种亲和性是该列期望存储的数据类型的 +*建议*,而不是强制要求 。当数据插入时,SQLite会尝试将数据转换为该列亲和性的类型。 + +列的亲和性有以下五种 : + +1. **`TEXT`**: 当数据插入时,如果不是`NULL`或`BLOB`,会被转换为文本。 + +2. **`NUMERIC`**: 尝试将值存储为`INTEGER`或`REAL`。如果文本值可以无损地转换为整数或浮点数,它就会被转换;否则,它将保持为 + `TEXT`。 + +3. **`INTEGER`**: 与`NUMERIC`类似,但如果值是浮点数,它会尝试去除小数部分并存储为整数。 + +4. **`REAL`**: 与`NUMERIC`类似,但更倾向于将值存储为浮点数。 + +5. **`BLOB`**: 不进行任何类型转换,按原样存储数据。 + +SQLite根据你在`CREATE TABLE`语句中为列声明的类型名称中的特定子字符串来决定其亲和性。规则按以下顺序应用 : + +1. 如果类型名包含 "INT",则亲和性为 `INTEGER`。 + +2. 如果类型名包含 "CHAR"、"CLOB" 或 "TEXT",则亲和性为 `TEXT`。 + +3. 如果类型名包含 "BLOB" 或未指定类型,则亲和性为 `BLOB`。 + +4. 如果类型名包含 "REAL"、"FLOA" 或 "DOUB",则亲和性为 `REAL`。 + +5. 否则,默认亲和性为 `NUMERIC`。 + +下表展示了一些常见的数据类型声明及其在SQLite中对应的亲和性,这对于从其他数据库系统迁移过来的开发者尤其有用 : + +| CREATE TABLE 或 CAST 表达式中的示例类型名 | 最终亲和性 | 确定亲和性的规则 | +| --- | --- | --- | +| `INT`, `INTEGER`, `TINYINT`, `SMALLINT`, `BIGINT` | `INTEGER` | 1 | +| `CHARACTER(20)`, `VARCHAR(255)`, `TEXT`, `CLOB` | `TEXT` | 2 | +| `BLOB`, *未指定数据类型* | `BLOB` | 3 | +| `REAL`, `DOUBLE`, `DOUBLE PRECISION`, `FLOAT` | `REAL` | 4 | +| `NUMERIC`, `DECIMAL(10,5)`, `BOOLEAN`, `DATE`, `DATETIME` | `NUMERIC` | 5 | + +Export to Sheets + +#### 灵活性的陷阱:对数据完整性和迁移的影响 + +SQLite的动态类型系统提供了一种强大的灵活性,特别是在快速原型开发和简单应用场景中。然而,这种灵活性也带来了一个潜在的“陷阱”。由于数据库本身不强制执行严格的类型约束,数据验证的重担几乎完全转移到了应用层 +。如果应用程序逻辑存在缺陷,就可能导致一个本应只包含数字的列(如 + +`salary`)被意外地插入了文本字符串。 + +这种混合类型的数据会给后续的数据查询和分析带来极大的困难。例如,对一个包含文本的`salary` +列计算平均值可能会失败或产生不可预期的结果。更严重的是,这种设计选择对数据库的长期维护和迁移构成了挑战。当需要将一个SQLite数据库迁移到一个严格类型的系统(如PostgreSQL)时,这个过程往往会演变成一个复杂且成本高昂的数据清洗项目 +。必须对每一列的数据进行检查、转换和规范化,以满足目标数据库的严格类型要求——而这正是SQLite最初的灵活性所掩盖的问题。 + +因此,将SQLite的动态类型视为一种架构决策而非简单的技术特性至关重要。它在带来便利的同时,也可能引入一种技术债务。一个成熟的开发实践是,即使SQLite允许,也应在应用程序层面实施严格的数据验证,确保写入数据库的数据类型始终与列的亲和性保持一致。这种审慎的做法能够在享受SQLite灵活性的同时,最大限度地保护数据的长期完整性和可维护性。 + +## 第二部分:数据定义语言 (DDL) - 构建和管理您的数据骨架 + +数据定义语言(DDL)是SQL中用于定义和管理数据库对象(如表、索引和视图)的部分。DDL命令能够构建、修改和删除数据库的结构,为存储数据搭建起骨架。本部分将详细介绍SQLite中核心的DDL命令: +`CREATE TABLE`、`ALTER TABLE`和`DROP TABLE`。 + +### 2.1 创建表 (`CREATE TABLE`) 与数据完整性约束 + +`CREATE TABLE` +是构建数据库结构的基础。除了定义列名和数据类型(亲和性)外,更重要的是通过约束(Constraints)来强制执行数据规则,从而保证数据的准确性、一致性和可靠性 。 + +约束是施加在表列上的规则,用于限制可以存入该列的数据类型和范围。如果任何操作违反了约束,该操作将被中止 。SQLite支持多种标准SQL约束。 + +#### `NOT NULL` 约束 + +默认情况下,任何列都可以存储`NULL`值。`NOT NULL`约束确保一个列永远不能包含`NULL` +值。这对于那些必须存在的关键数据字段(如ID或名称)至关重要 。 + +**语法与示例:** + +```SQL +CREATE TABLE employees +( + employee_id INTEGER PRIMARY KEY, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, .. + . +); +``` + +在此示例中,`first_name`和`last_name`列都不能为`NULL`。 + +#### `DEFAULT` 约束 + +`DEFAULT`约束为列提供一个默认值。当`INSERT`语句没有为该列提供值时,将自动使用这个默认值 。 + +**语法与示例:** + +```SQL +CREATE TABLE projects +( + .. + . + status + TEXT + DEFAULT + 'Pending' +); +``` + +如果在`projects`表中插入一条新记录而没有指定`status`,它的值将自动设为'Pending'。 + +#### `UNIQUE` 约束 + +`UNIQUE`约束确保一列(或多列组合)中的所有值都是唯一的,不允许重复。一个表可以有多个`UNIQUE`约束 。 + +**语法与示例:** + +```SQL +CREATE TABLE departments +( + department_id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); +``` + +这确保了每个部门名称都是独一无二的。 + +#### `PRIMARY KEY` 约束 + +`PRIMARY KEY`约束是`NOT NULL`和`UNIQUE` +的组合,用于唯一标识表中的每一行记录。每个表只能有一个主键。主键是数据库设计的核心,它不仅保证了记录的唯一性,还常被用作外键,在不同表之间建立关系 。 + +**语法与示例:** + +```SQL +CREATE TABLE employees +( + employee_id INTEGER PRIMARY KEY, .. + . +); +``` + +**SQLite的“主键可为NULL”异常** + +关系数据库理论和标准SQL都明确指出,主键必须是唯一且非空的。然而,SQLite存在一个显著的、与标准行为相悖的特性:由于一个“长期的编码疏忽”, +`PRIMARY KEY`列在技术上可以包含`NULL`值 。 + +这种异常行为的根源在于SQLite如何处理`NULL`的唯一性。在进行`UNIQUE`约束检查时,SQLite认为`NULL`与任何其他值(包括另一个 +`NULL`)都不同 。这意味着,一个单列主键理论上可以存储多个 + +`NULL`值,这完全违背了主键作为唯一标识符的核心原则。这种情况可能导致严重的数据完整性问题,因为多行记录可能没有有效的唯一标识。 + +为了避免这种潜在的混乱和数据损坏,开发者在使用SQLite时必须采取一种防御性编程措施。最佳实践是,在定义主键时,* +*总是显式地声明`NOT NULL`约束**,即使理论上`PRIMARY KEY`已经隐含了这一属性。 + +**正确做法:** + +```SQL +CREATE TABLE employees +( + employee_id INTEGER PRIMARY KEY NOT NULL, -- 显式添加 NOT NULL + .. + . +); +``` + +这种做法不是一种风格偏好,而是在SQLite环境中确保数据行为符合标准预期的必要步骤。它强制主键列遵守其应有的规则,从而保护了数据的完整性。 + +#### `CHECK` 约束 + +`CHECK`约束允许你定义一个表达式,用于在插入或更新数据时验证列中的值。只有当表达式的计算结果为真(或`NULL`)时,操作才被允许 +。这是一种强大的自定义数据验证工具。 + +**语法与示例:** + +```SQL +CREATE TABLE employees +( + .. + . + salary + REAL + CHECK +( + salary > + 0 +), + end_date TEXT, + start_date TEXT, + CHECK (end_date IS NULL OR start_date IS NULL OR end_date >= start_date) -- 表级约束 + ); +``` + +第一个`CHECK`约束确保薪水必须是正数。第二个是表级约束,确保如果`end_date`和`start_date`都存在,则结束日期必须晚于或等于开始日期。 + +#### `FOREIGN KEY` 约束 + +`FOREIGN KEY` +约束用于在一个表的列和另一个表的主键之间建立链接,从而强制实现参照完整性。它确保一个表(子表)中的外键值必须在另一个表(父表)的主键中存在 。 + +**重要提示**: 默认情况下,SQLite不强制执行外键约束。为了激活它,必须在每次连接到数据库后执行以下命令: + +```SQL +PRAGMA foreign_keys = ON; +``` + +**语法与示例:** + +```SQL +CREATE TABLE employees +( + .. + . + department_id + INTEGER, + FOREIGN KEY (department_id) REFERENCES departments (department_id) +); +``` + +此约束确保了`employees`表中的`department_id`必须是`departments`表中已存在的`department_id`。 + +### 2.2 修改表 (`ALTER TABLE`):限制与变通 + +在数据库的生命周期中,修改现有表的结构是常见需求。`ALTER TABLE` +命令用于此目的,但SQLite对此命令的支持是有限的,这反映了其设计的简洁性 。 + +SQLite的`ALTER TABLE`直接支持以下几种操作 : + +1. **`RENAME TO`**: 重命名一个表。 + ```SQL + ALTER TABLE old_table_name RENAME TO new_table_name; + ``` + +2. **`ADD COLUMN`**: 向现有表中添加一个新列。新列总是被添加到表的末尾 。 + + ```SQL + ALTER TABLE table_name ADD COLUMN new_column_definition; + ``` + + 添加列时存在一些限制,例如新列不能是`PRIMARY KEY`或`UNIQUE`,并且如果带有`NOT NULL`约束,则必须提供一个非`NULL`的 + `DEFAULT`值 。 + +3. **`RENAME COLUMN TO`**: 重命名一个现有列(自SQLite 3.25.0起支持)。 + + ```SQL + ALTER TABLE table_name RENAME COLUMN old_column_name TO new_column_name; + ``` + +4. **`DROP COLUMN`**: 删除一个现有列(自SQLite 3.35.0起支持)。 + + ```SQL + ALTER TABLE table_name DROP COLUMN column_name; + ``` + +#### `ALTER TABLE` 限制背后的架构原因 + +开发者可能会对SQLite `ALTER TABLE`功能的局限性感到困惑,例如无法直接修改列的数据类型或添加`UNIQUE` +约束。这种局限性并非偶然,而是SQLite核心架构的直接体现。 + +大多数数据库系统(如MySQL、PostgreSQL)将其模式信息以解析后的、结构化的格式存储在多个系统表中 。当执行 + +`ALTER TABLE`时,数据库引擎只需更新这些系统表中的相应记录。 + +然而,SQLite的设计哲学追求极致的简洁。它将整个数据库的模式(即所有`CREATE TABLE`、`CREATE INDEX`等语句)作为原始的SQL文本字符串,存储在一个名为 +`sqlite_schema`的特殊表中 。因此,SQLite执行 + +`ALTER TABLE`命令的本质,是对存储在`sqlite_schema`表中的`CREATE TABLE`文本进行*字符串操作*。 + ++ 重命名表或在末尾添加一列是相对简单的文本编辑。 + ++ 但是,要从表的中间删除一列、更改列的类型或添加复杂的约束,就需要对原始的`CREATE TABLE` + 语句进行复杂的解析、重写和验证,同时还要更新所有依赖于该表的索引、触发器和视图。这种操作复杂且极易出错。 + +SQLite选择了一种更简单、更稳健的策略:它不将这种复杂的逻辑内置于核心引擎中,而是将复杂性交由用户通过一个明确的手动流程来处理。 + +#### 复杂模式变更的变通方案 + +对于SQLite不直接支持的复杂表结构变更(如修改列类型、添加或删除约束、改变列顺序),需要遵循一个标准的手动流程。这个流程虽然繁琐,但安全可靠 : + +1. **禁用外键约束**: `PRAGMA foreign_keys=OFF;` + +2. **开始事务**: `BEGIN TRANSACTION;` + +3. **创建新表**: 创建一个具有期望的新结构的新表(例如 `new_table`)。 + +4. **迁移数据**: 使用`INSERT INTO new_table SELECT... FROM old_table;`将旧表中的数据复制到新表中。在此过程中可能需要进行数据转换。 + +5. **删除旧表**: `DROP TABLE old_table;` + +6. **重命名新表**: `ALTER TABLE new_table RENAME TO old_table;` + +7. **重建索引和触发器**: 在新表上重新创建所有必要的索引、触发器和视图。 + +8. **提交事务**: `COMMIT;` + +9. **重新启用外键约束**: `PRAGMA foreign_keys=ON;` + +这个流程体现了SQLite的设计权衡:通过牺牲部分便利性来换取核心引擎的极度轻量和可靠。理解这一架构选择有助于开发者更好地规划数据库维护策略,并认识到这个手动流程是SQLite设计理念的逻辑延伸,而非功能缺失。 + +### 2.3 删除表 (`DROP TABLE`) + +`DROP TABLE`命令用于从数据库中永久删除一个表及其所有数据、索引、触发器和视图。这是一个不可逆的操作,应谨慎使用。 + +**语法:** + +```SQL +DROP TABLE table_name; +``` + +使用可选的`IF EXISTS`子句可以防止在尝试删除一个不存在的表时发生错误。 + +## 第三部分:数据操作语言 (DML) - 填充与维护您的数据 + +数据操作语言(DML)是SQL中用于管理数据库中数据的部分。在通过DDL定义了数据结构之后,DML命令被用来插入、更新和删除数据记录。这些是数据库日常操作中最常用的命令。 + +### 3.1 插入数据 (`INSERT INTO`) + +`INSERT INTO`语句用于向表中添加新的数据行。 + +#### 基本语法 + +最常见的形式是指定列和要插入的值。 + +```SQL +INSERT INTO table_name (column1, column2, ...) +VALUES (value1, value2, ...); +``` + +**示例:** 向`departments`表添加一个新部门。 + +```SQL +INSERT INTO departments (name) +VALUES ('Human Resources'); +``` + +如果值的顺序与表中列的顺序完全一致,可以省略列名列表。 + +```SQL +INSERT INTO departments +VALUES (2, 'Engineering'); +``` + +#### 插入多行 + +可以在一个`VALUES`子句中提供多个元组来一次性插入多行。 + +```SQL +INSERT INTO projects (name, start_date, end_date) +VALUES ('Project Phoenix', '2023-01-15', '2024-01-15'), + ('Project Titan', '2023-03-01', NULL), + ('Project Nebula', '2023-05-20', '2023-12-31'); +``` + +#### 从查询结果插入 + +`INSERT INTO... SELECT`语句允许你将一个查询的结果集直接插入到另一个表中。这对于数据迁移或汇总非常有用。 + +```SQL +-- 假设有一个 old_employees 表 +INSERT INTO employees (first_name, last_name, salary) +SELECT first_name, last_name, base_salary +FROM old_employees +WHERE status = 'Active'; +``` + +#### 冲突解决 + +当插入操作违反了约束(如`UNIQUE`或`PRIMARY KEY`)时,默认行为是中止操作并返回错误。SQLite提供了冲突解决子句来改变这种行为。 + ++ **`INSERT OR REPLACE`**: 如果发生冲突,先删除导致冲突的现有行,然后再插入新行。 + ++ **`INSERT OR IGNORE`**: 如果发生冲突,则忽略该插入操作,不插入新行,也不返回错误。 + ++ **`INSERT OR ROLLBACK`**: 如果发生冲突,则回滚当前事务。 + ++ **`INSERT OR ABORT`**: (默认行为) 中止当前SQL语句并返回错误,但事务保持活动状态。 + ++ **`INSERT OR FAIL`**: 中止当前SQL语句并返回错误,事务不会回滚。 + +**示例:** + +```SQL +-- 如果 employee_id 为 101 的员工已存在,则忽略此插入 +INSERT OR IGNORE INTO employees (employee_id, first_name, last_name) +VALUES (101, 'Jane', 'Doe'); +``` + +### 3.2 更新数据 (`UPDATE`) + +`UPDATE`语句用于修改表中已存在的数据行。 + +#### 基本语法 + +```SQL +UPDATE table_name +SET column1 = value1, + column2 = value2, ... + WHERE condition; +``` + +`SET`子句指定要修改的列和新值。`WHERE`子句指定要更新哪些行。 + +**警告**: 如果省略`WHERE`子句,`UPDATE`语句将修改表中的**所有行**。这是一个非常危险的操作,必须极其小心。 + +**示例:** 更新特定员工的薪水。 + +```SQL +UPDATE employees +SET salary = 75000.00 +WHERE employee_id = 101; +``` + +**示例:** 给所有工程部门的员工加薪10%。 + +```SQL +UPDATE employees +SET salary = salary * 1.10 +WHERE department_id = (SELECT department_id FROM departments WHERE name = 'Engineering'); +``` + +### 3.3 删除数据 (`DELETE FROM`) + +`DELETE FROM`语句用于从表中删除数据行。 + +#### 基本语法 + +```SQL +DELETE +FROM table_name +WHERE condition; +``` + +`WHERE`子句指定要删除哪些行。 + +**警告**: 与`UPDATE`类似,如果省略`WHERE`子句,`DELETE FROM`语句将删除表中的**所有行**,但表结构本身会保留。这与`DROP TABLE` +不同,后者会删除整个表。 + +**示例:** 删除一个特定的项目。 + +```SQL +DELETE +FROM projects +WHERE name = 'Project Titan'; +``` + +**示例:** 删除所有已离职的员工(假设有一个`status`列)。 + +```SQL +DELETE +FROM employees +WHERE status = 'Terminated'; +``` + +## 第四部分:事务控制语言 (TCL) - 确保数据的原子性与一致性 + +事务控制语言(TCL)是SQL中用于管理事务的命令集合。事务是一系列操作的逻辑单元,这些操作要么全部成功执行,要么全部失败回滚,从而保证数据库从一个一致的状态转换到另一个一致的状态。在多用户并发环境中,事务对于维护数据完整性至关重要。 + +### 4.1 事务的核心:ACID 原则 + +事务的可靠性由四个基本属性来保证,即ACID原则 : + +1. **原子性 (Atomicity)**: + 事务被视为一个不可分割的最小工作单元。事务中的所有操作要么全部完成,要么全部不执行。如果事务在执行过程中发生任何错误,所有已经执行的操作都将被撤销(回滚),数据库将恢复到事务开始前的状态。 + +2. **一致性 (Consistency)**: 事务必须使数据库从一个一致的状态转移到另一个一致的状态。在事务开始和结束时,数据库的完整性约束(如主键、外键、CHECK约束)都必须得到满足。 + +3. **隔离性 (Isolation)**: 多个并发执行的事务之间应该相互隔离,一个事务的执行不应被其他事务干扰。隔离性确保了并发事务的最终结果与这些事务按某种顺序串行执行的结果相同。 + +4. **持久性 (Durability)**: 一旦事务被成功提交(`COMMIT`),它对数据库的更改就是永久性的,即使后续发生系统崩溃或断电等故障,这些更改也不会丢失。 + +### 4.2 事务的基本控制:`BEGIN`, `COMMIT`, `ROLLBACK` + +SQLite通过三个核心命令来控制事务的边界 : + ++ **`BEGIN TRANSACTION`**: 标志着一个事务的开始。在此命令之后的所有SQL操作都将被视为该事务的一部分。 + ++ **`COMMIT`**: 标志着一个事务的成功结束。当执行`COMMIT`时,事务期间所做的所有更改都将被永久写入数据库。 + ++ **`ROLLBACK`**: 用于撤销一个事务。当执行`ROLLBACK`时,事务期间所做的所有更改都将被撤销,数据库将恢复到执行`BEGIN`命令之前的状态。 + +**示例:银行转账** + +一个经典的事务示例是银行转账,它包含两个原子操作:从一个账户扣款,并向另一个账户存款。这两个操作必须同时成功或同时失败。 + +```SQL +-- 假设有一个 accounts 表,包含 account_id 和 balance 列 +BEGIN TRANSACTION; + +-- 尝试从账户A(ID=1)扣款100元 +UPDATE accounts +SET balance = balance - 100 +WHERE account_id = 1; + +-- 检查账户A是否有足够余额(此处为简化逻辑,实际应用中应在更新前检查) +--... + +-- 尝试向账户B(ID=2)存款100元 +UPDATE accounts +SET balance = balance + 100 WHERE account_id = 2; + +-- 假设在此时发生错误,或检查发现账户A余额不足 +-- 我们可以回滚整个事务 +-- ROLLBACK; + +-- 如果所有操作都成功,则提交事务 +COMMIT; +``` + +如果在这两个`UPDATE`操作之间发生任何错误(如系统崩溃),数据库在恢复后将自动回滚未完成的事务,确保资金不会凭空消失或产生。 + +### 4.3 SQLite 事务模式:`DEFERRED`, `IMMEDIATE`, `EXCLUSIVE` + +SQLite的`BEGIN` +命令可以接受一个可选的模式修饰符,用于指定事务何时以及如何获取数据库锁。这个选择对并发性能和应用程序的错误处理逻辑有重大影响。这三种模式代表了在并发控制中乐观锁和悲观锁策略的选择 。 + +1. **`DEFERRED` (延迟模式)** + + + **语法**: `BEGIN DEFERRED TRANSACTION;` 或 `BEGIN;` (这是默认模式) + + + **行为**: 这是最“懒惰”的模式。执行`BEGIN DEFERRED` + 本身不会做任何事情,甚至不访问数据库。它只是将数据库连接置于一个“事务模式”中。直到事务内第一条读或写语句被执行时,SQLite才会去获取相应的锁。 + + + 第一条读操作(`SELECT`)会获取一个**共享锁(SHARED lock)**,允许多个读操作并发进行。 + + + 第一条写操作(`INSERT`, `UPDATE`, `DELETE`)会尝试获取一个**保留锁(RESERVED lock)**,并最终升级为\*\*独占锁(EXCLUSIVE + lock)\*\*来完成写入。 + + + **策略**: 这是一种**乐观锁**策略。它假设在事务的大部分时间内不会有写冲突。`BEGIN` + 操作本身非常快且不会阻塞。但缺点是,当真正需要写入时,获取写锁的操作可能会失败(返回`SQLITE_BUSY` + 错误),因为此时可能有其他连接已经持有了写锁。这意味着应用程序**必须**准备好处理`SQLITE_BUSY`错误,并实现重试逻辑。 + +2. **`IMMEDIATE` (立即模式)** + + + **语法**: `BEGIN IMMEDIATE TRANSACTION;` + + + **行为**: 执行`BEGIN IMMEDIATE`会立即尝试获取一个**保留锁(RESERVED lock)**。这个锁允许其他连接继续读取数据库,但阻止任何其他连接发起新的写事务。 + + + **策略**: 这是一种**悲观锁**策略。它在事务开始时就确保了“写”的权利。如果`BEGIN IMMEDIATE` + 命令成功执行,那么该事务内的所有后续写操作都保证不会因为锁竞争而失败(虽然仍可能因其他原因失败,如约束冲突)。如果 + `BEGIN IMMEDIATE`命令本身失败(返回`SQLITE_BUSY`),则意味着有其他写事务正在进行。 + + + **应用场景**: 对于需要执行写操作的事务,使用`IMMEDIATE` + 模式是一种更健壮的策略。它将并发冲突的处理集中在了事务的起点,简化了应用程序的错误处理逻辑,因为不再需要在每个写操作后都检查 + `SQLITE_BUSY`。 + +3. **`EXCLUSIVE` (独占模式)** + + + **语法**: `BEGIN EXCLUSIVE TRANSACTION;` + + + **行为**: 执行`BEGIN EXCLUSIVE`会立即尝试获取一个**独占锁(EXCLUSIVE lock)**。一旦成功,任何其他连接(无论是读还是写)都无法访问数据库,直到该事务结束。 + + + **策略**: 这是最强的悲观锁策略。它保证了事务执行期间的完全隔离。 + + + **应用场景**: 适用于需要对数据库进行大规模修改或需要确保在事务期间数据库状态绝对不被任何其他连接观察到的情况。 + +**选择合适的事务模式是一个关键的应用程序设计决策**。对于单线程或读密集型的应用,默认的`DEFERRED` +模式通常足够。但对于多线程或写密集型的并发应用,主动使用`BEGIN IMMEDIATE`来启动写事务是一种更可靠的编程范式,它可以显著简化并发控制和错误处理的复杂性。 + +### 4.4 嵌套事务与保存点:`SAVEPOINT` 与 `RELEASE` + +标准的`BEGIN...COMMIT`事务在SQLite中是不能嵌套的。在一个已开始的事务内部再次执行`BEGIN`会导致错误 +。为了实现更细粒度的事务控制,例如在复杂操作中进行部分回滚,SQLite提供了 + +`SAVEPOINT`机制。 + ++ **`SAVEPOINT savepoint_name;`**: 在当前事务中创建一个命名的保存点。 + ++ **`RELEASE SAVEPOINT savepoint_name;`**: 销毁一个保存点。这会将该保存点之后的操作合并到其父事务(或上一个保存点)中。 + ++ **`ROLLBACK TO SAVEPOINT savepoint_name;`**: 撤销自指定的保存点创建以来所做的所有更改,但事务本身仍然保持活动状态,保存点之前所做的更改也仍然保留。 + +**示例:** + +```SQL +BEGIN TRANSACTION; +-- 操作 A +INSERT INTO employees (first_name, last_name) +VALUES ('Alice', 'Smith'); + +SAVEPOINT sp1; +-- 操作 B +UPDATE projects +SET status = 'In Progress' +WHERE project_id = 1; + +SAVEPOINT sp2; +-- 操作 C +DELETE +FROM employees +WHERE employee_id = 99; +-- 假设这是一个错误的操作 + +-- 发现操作C是错误的,回滚到保存点sp2 +ROLLBACK TO SAVEPOINT sp2; + +-- 现在,操作C被撤销了,但操作A和B仍然是事务的一部分 +-- 我们可以继续执行其他操作 +--... + +-- 如果我们想保留操作A和B,可以提交整个事务 +COMMIT; + +-- 或者,如果我们想撤销操作A和B,可以回滚整个事务 +-- ROLLBACK; +``` + +`SAVEPOINT`为复杂的业务逻辑提供了强大的控制能力,允许开发者在不放弃整个事务的情况下,从错误中优雅地恢复。 + +## 第五部分:数据查询语言 (DQL) - `SELECT` 语句的艺术 + +数据查询语言(DQL)的核心是`SELECT`语句,它是SQL中最复杂也最强大的部分。`SELECT`语句用于从数据库中检索数据,其结果是一个或多个数据行组成的临时表,称为结果集。掌握 +`SELECT`语句的各个子句及其执行逻辑,是进行任何有效数据分析和应用开发的基础。 + +本部分将按照`SELECT`语句的逻辑处理顺序,而不是其书写顺序,来详细解析每个子句的功能。逻辑处理顺序大致如下:`FROM` -> +`JOIN` -> `WHERE` -> `GROUP BY` -> `HAVING` -> `SELECT` -> `DISTINCT` -> `ORDER BY` -> `LIMIT`。 + +### 5.1 查询的基石:`SELECT` 与 `FROM` + +`SELECT`和`FROM`是所有查询语句最基本的组成部分。`FROM`子句指定了数据源(一个或多个表),而`SELECT`子句指定了要从这些数据源中检索哪些列。 + +#### 基本语法 + +```SQL +SELECT column1, + column2, ... + FROM table_name; +``` + +**示例:** 从`employees`表中检索员工的姓名和薪水。 + +```SQL +SELECT first_name, last_name, salary +FROM employees; +``` + +#### 选择所有列 (`*`) + +星号(`*`)是选择表中所有列的快捷方式 。 + +```SQL +SELECT * +FROM projects; +``` + +虽然在交互式查询和调试中非常方便,但在生产应用程序代码中,**强烈建议明确列出所需的列**。这样做有几个好处 : + ++ **可读性和可维护性**: 代码清晰地表明了需要哪些数据。 + ++ **性能**: 数据库只需读取和传输必要的列,减少了I/O和网络开销。 + ++ **稳定性**: 如果表的结构发生变化(例如,添加或删除列),明确指定列的查询不会受到影响,而`SELECT *`可能会导致应用程序因列数不匹配而崩溃。 + +#### 使用别名 (Alias) + +可以使用`AS`关键字为列或表指定一个临时的别名,以提高查询的可读性。 + +```SQL +SELECT first_name AS "姓", + last_name AS "名", + salary AS "薪水" +FROM employees AS e; +``` + +### 5.2 关联的艺术:`JOIN` 子句详解 + +当需要的数据分散在多个表中时,`JOIN` +子句就派上了用场。它根据表之间的关联条件,将多个表的行组合起来。SQLite中的所有连接操作都基于左表和右表数据集的笛卡尔积,然后根据连接条件进行筛选 。 + +#### `INNER JOIN` (内连接) + +`INNER JOIN`返回两个表中连接字段相匹配的行。如果某一行在另一个表中没有匹配的行,那么它将不会出现在结果集中 。这是最常用的连接类型。 + +```mermaid +graph TD + subgraph A + d1(Row 1) + d2(Row 2) + d3(Row 3) + end + subgraph B + d4(Row A) + d5(Row B) + d6(Row C) + end + subgraph INNER JOIN Result + d7(Row 2 - Row A) + d8(Row 3 - Row B) + end + d2 -- matches --> d4 + d3 -- matches --> d5 +``` + +**语法:** + +```SQL +SELECT... + FROM table1 + INNER JOIN table2 +ON table1.column_name = table2.column_name; +``` + +`ON`子句指定了连接条件。 + +**示例:** 查询每个员工及其所在的部门名称。 + +```SQL +SELECT e.first_name, + e.last_name, + d.name AS department_name +FROM employees AS e + INNER JOIN + departments AS d ON e.department_id = d.department_id; +``` + +#### `LEFT JOIN` (左连接) + +`LEFT JOIN` (或 `LEFT OUTER JOIN`) 返回左表中的**所有行**,以及右表中与左表匹配的行。如果右表中没有匹配的行,则结果集中右表的列将显示为 +`NULL` 。 + +```mermaid +graph TD + subgraph Left Table A + d1(Row 1) + d2(Row 2) + d3(Row 3) + end + subgraph Right Table B + d4(Row A) + d5(Row B) + end + subgraph LEFT JOIN Result + d6(Row 1 - NULL) + d7(Row 2 - Row A) + d8(Row 3 - Row B) + end + d2 -- matches --> d4 + d3 -- matches --> d5 +``` + +**语法:** + +```SQL +SELECT ... + FROM table1 -- 左表 + LEFT JOIN table2 +ON table1.column_name = table2.column_name; +``` + +**示例:** 列出所有部门,并显示每个部门的员工数量(即使某个部门没有员工)。 + +```SQL +SELECT d.name, + COUNT(e.employee_id) AS number_of_employees +FROM departments AS d + LEFT JOIN + employees AS e ON d.department_id = e.department_id +GROUP BY d.name; +``` + +对于没有任何员工的部门,`number_of_employees`将为0。 + +#### `CROSS JOIN` (交叉连接) + +`CROSS JOIN`返回两个表的笛卡尔积,即左表中的每一行都与右表中的每一行进行组合。它不需要`ON`子句 。在 + +`FROM`子句中用逗号分隔多个表,效果与`CROSS JOIN`相同。 + +**语法:** + +```SQL +SELECT...FROM table1 CROSS JOIN table2; +-- 等效于 +SELECT...FROM table1, table2; +``` + +**`CROSS JOIN`作为查询优化器覆盖工具** + +从数学角度看,`INNER JOIN`、逗号连接和`CROSS JOIN`在没有`ON`子句的情况下都产生相同的结果——笛卡尔积。那么为什么需要 +`CROSS JOIN`这个看似冗余的语法呢? + +答案在于一个SQLite特有的、非标准的行为。通常,数据库的查询优化器会根据统计信息自由地重新排列`JOIN` +的顺序,以找到最高效的执行计划(例如,先连接小表)。然而,当开发者使用明确的`CROSS JOIN`关键字时,SQLite的查询优化器会**禁用连接重排 +** 。 + +这提供了一个“逃生舱口”,允许对数据分布有深入了解的专家用户手动强制一个特定的连接顺序,从而可能覆盖优化器不够理想的决策。这是一种高级性能调优技术,虽然牺牲了可移植性,但在特定场景下,它是解决复杂查询性能问题的强大工具。因此, +`CROSS JOIN`在SQLite中不仅仅是一种语法变体,更是一个为专家级用户提供底层控制的有意设计。 + +### 5.3 精准过滤:`WHERE` 子句 + +`WHERE`子句在`FROM`和`JOIN`子句之后执行,用于根据指定的条件过滤行。只有满足`WHERE`子句中条件的行才会被传递到后续的处理阶段(如 +`GROUP BY`)。 + +**常用操作符:** + ++ 比较操作符: `=`, `!=` (或 `<>`), `>`, `<`, `>=`, `<=` + ++ 逻辑操作符: `AND`, `OR`, `NOT` + ++ 范围操作符: `BETWEEN... AND...` + ++ 列表操作符: `IN (...)`, `NOT IN (...)` + ++ 模式匹配: `LIKE` (配合 `%` 匹配任意字符序列,`_` 匹配单个字符) + ++ 空值检查: `IS NULL`, `IS NOT NULL` + +**示例:** 查询薪水在50000到80000之间,且在2022年之后雇佣的员工。 + +```SQL +SELECT first_name, last_name, salary, hire_date +FROM employees +WHERE salary BETWEEN 50000 AND 80000 + AND hire_date >= '2022-01-01'; +``` + +### 5.4 数据聚合:`GROUP BY` 与聚合函数 + +`GROUP BY`子句通常与聚合函数一起使用,用于将具有相同值的行分组到摘要行中 。例如,按部门对员工进行分组,然后计算每个部门的平均薪水。 + +**常用聚合函数:** + ++ `COUNT()`: 计算行数。 + ++ `SUM()`: 计算数值列的总和。 + ++ `AVG()`: 计算数值列的平均值。 + ++ `MAX()`: 找出列中的最大值。 + ++ `MIN()`: 找出列中的最小值。 + +**语法:** + +```SQL +SELECT column1, aggregate_function(column2) +FROM table_name +WHERE condition +GROUP BY column1; +``` + +**示例:** 计算每个部门的员工人数和平均薪水。 + +```SQL +SELECT d.name AS department, + COUNT(e.employee_id) AS employee_count, + AVG(e.salary) AS average_salary +FROM departments AS d + JOIN + employees AS e ON d.department_id = e.department_id +GROUP BY d.name; +``` + +#### “裸列”的危险与效用 + +标准SQL有一个严格的规则:`SELECT`列表中的任何非聚合函数列都必须出现在`GROUP BY`子句中。否则,数据库无法确定对于一个聚合组,应该显示哪个具体行的值。 + +SQLite打破了这一规则,允许在`SELECT`列表中出现既不在聚合函数中也不在`GROUP BY`子句中的列,这些列被称为“裸列”(bare +columns)。在这种情况下,SQLite会从该组的 + +**任意一行**中选取该裸列的值。这种不确定性行为是潜在错误的来源,因为查询结果可能看起来正确,但实际上是随机的,每次执行都可能不同。 + +然而,这个特性有一个非常重要且有用的例外。如果一个聚合查询中**只包含一个`min()`或`max()`聚合函数** +,那么SQLite保证所有裸列的值都将取自包含该最小值或最大值的**同一行** 。 + +这个例外将“裸列”从一个危险的反模式转变为一个高效的工具。考虑这个需求:找出每个部门中薪水最高的员工的完整信息。在标准SQL中,这通常需要使用子查询或窗口函数,语法较为复杂。但在SQLite中,可以利用这个特性写出非常简洁的查询: + +```SQL +-- 找出每个部门薪水最高的员工的姓名和薪水 +SELECT d.name AS department, + e.first_name, -- 裸列 + e.last_name, -- 裸列 + MAX(e.salary) AS max_salary -- 唯一的 max() 函数 +FROM employees AS e + JOIN + departments AS d ON e.department_id = d.department_id +GROUP BY d.name; +``` + +这个查询不仅简洁,而且执行效率通常更高。因此,在使用SQLite时,应谨慎对待裸列:在一般情况下避免使用以防止不确定性,但在符合 +`min()`/`max()`单一聚合的特定场景下,可以将其作为一种强大的优化技巧。 + +### 5.5 聚合后过滤:`HAVING` 子句 + +`HAVING`子句与`WHERE`子句类似,都用于过滤数据。但它们的作用阶段完全不同: + ++ `WHERE`在**聚合前**过滤行。 + ++ `HAVING`在**聚合后**过滤分组 。 + +因此,`HAVING`子句中可以使用聚合函数,而`WHERE`子句中不能。 + +**示例:** 找出平均薪水超过60000的部门。 + +```SQL +SELECT d.name, + AVG(e.salary) AS average_salary +FROM departments AS d + JOIN + employees AS e ON d.department_id = e.department_id +GROUP BY d.name +HAVING AVG(e.salary) > 60000; +``` + +### 5.6 结果排序:`ORDER BY` 子句 + +`ORDER BY`子句用于对最终的结果集进行排序。如果不指定`ORDER BY`,结果的顺序是未定义的 。 + +**语法:** + +```SQL +ORDER BY column1, column2,...; +``` + ++ `ASC`: 升序(默认)。 + ++ `DESC`: 降序。 + ++ 可以按多个列排序,优先级从左到右。 + +**处理`NULL`值**: 默认情况下,SQLite在排序时认为`NULL`值比任何其他值都小。可以使用`NULLS FIRST`或`NULLS LAST`来明确控制 +`NULL`值的排序位置 。 + +**示例:** 按薪水降序排列员工,薪水相同时按姓氏升序排列。 + +```SQL +SELECT first_name, last_name, salary +FROM employees +ORDER BY salary DESC, last_name ASC; +``` + +### 5.7 数据去重:`DISTINCT` 关键字 + +`DISTINCT`关键字用于从结果集中移除重复的行 。 + +**示例:** 查询所有有员工的部门ID。 + +```SQL +SELECT DISTINCT department_id +FROM employees +WHERE department_id IS NOT NULL; +``` + +`DISTINCT`会作用于`SELECT`列表中的所有列,只有当所有列的值都相同时,才被视为重复行。 + +### 5.8 结果分页:`LIMIT` 与 `OFFSET` + +`LIMIT`和`OFFSET`子句用于实现结果集的分页,这在Web应用中非常常见 。它们是查询中最后被应用的子句。 + ++ `LIMIT N`: 限制结果集最多返回 N 行。 + ++ `OFFSET M`: 从结果集的开头跳过 M 行。 + +**语法:** + +```SQL +SELECT... + ORDER BY ... + LIMIT < count > +OFFSET ; +``` + +**示例:** 查询薪水排在第11到第20位的员工。 + +```SQL +SELECT first_name, last_name, salary +FROM employees +ORDER BY salary DESC +LIMIT 10 OFFSET 10; +``` + +#### `LIMIT M, N` 兼容性语法 + +除了标准的`LIMIT... OFFSET...`语法,SQLite还支持一种替代形式:`LIMIT , ` 。 + +```SQL +-- 与上面的示例等效 +SELECT first_name, last_name, salary +FROM employees +ORDER BY salary DESC +LIMIT 10, 10; -- 注意:第一个数字是OFFSET,第二个是LIMIT +``` + +这种语法的参数顺序是反直觉的,与`OFFSET`关键字的用法相反。它存在的唯一原因是为了“最大化与遗留SQL数据库系统的兼容性”,特别是为了兼容MySQL,后者广泛使用 +`LIMIT , `这种形式 。 + +这个细节揭示了SQLite的一个重要设计哲学:实用主义优先于纯粹性。为了方便开发者从其他流行系统迁移,SQLite愿意采纳一个虽然不一致但被广泛使用的语法。尽管如此,为了代码的清晰性和可移植性, +**强烈建议始终使用明确的`OFFSET`关键字**。 + +### 5.9 高级查询技术 + +除了基本子句,`SELECT`语句还支持多种高级技术,以处理更复杂的查询需求。 + +#### 子查询 (Subqueries) + +子查询是嵌套在另一个查询(主查询)中的`SELECT`语句。子查询可以出现在`SELECT`列表、`FROM`子句、`WHERE`子句等多个位置。 + ++ **标量子查询**: 返回单个值(一行一列),可以用在需要单个值的地方。 + + ```SQLSELECT first_name, salary, + (SELECT AVG(salary) FROM employees) AS avg_company_salary + FROM employees; + ``` + ++ **多行子查询**: 返回多行,常与`IN`, `EXISTS`等操作符一起使用。 + + ```SQLSELECT * FROM employees + WHERE department_id IN (SELECT department_id FROM departments WHERE name LIKE 'Eng%'); + ``` + ++ **派生表**: 子查询出现在`FROM`子句中,其结果集被当作一个临时表。 + + ```SQL + SELECT * FROM ( + SELECT department_id, AVG(salary) as avg_sal + FROM employees + GROUP BY department_id + ) AS dept_salaries + WHERE avg_sal > 55000; + ``` + +#### 公用表表达式 (Common Table Expressions - `WITH` Clause) + +公用表表达式(CTE)通过`WITH` +子句定义,它允许你创建一个临时的、命名的结果集,可以在后续的查询中引用。CTE可以极大地提高复杂查询的可读性和模块化程度 。 + +**示例:** 使用CTE重写上面的派生表示例。 + +```SQL +WITH dept_salaries AS (SELECT department_id, AVG(salary) as avg_sal + FROM employees + GROUP BY department_id) +SELECT d.name, ds.avg_sal +FROM dept_salaries AS ds + JOIN departments AS d ON ds.department_id = d.department_id +WHERE ds.avg_sal > 55000; +``` + +CTE还可以是递归的,用于处理层次结构或图形数据,例如组织结构图或物料清单。 + +#### 集合运算 (`UNION`, `UNION ALL`, `INTERSECT`, `EXCEPT`) + +集合运算符用于合并两个或多个`SELECT`语句的结果集 。 + ++ **`UNION`**: 合并两个结果集,并自动去除重复行。 + ++ **`UNION ALL`**: 合并两个结果集,但保留所有行,包括重复行。性能通常优于`UNION`。 + ++ **`INTERSECT`**: 返回同时存在于两个结果集中的行。 + ++ **`EXCEPT`**: 返回存在于第一个结果集中,但不存在于第二个结果集中的行。 + +**要求**: 参与集合运算的所有`SELECT`语句必须具有相同数量的列,并且对应列的数据类型必须兼容。 + +#### 窗口函数 (`WINDOW` 与 `OVER` 子句) + +窗口函数是一种强大的分析工具,它对与当前行相关的行集(称为“窗口”)进行计算。与聚合函数不同,窗口函数不会将多行折叠为一行,而是在保留原始行数的同时,为每一行计算一个值。 + +窗口由`OVER()`子句定义,其中可以包含: + ++ **`PARTITION BY`**: 将行分成多个分区(组),窗口函数在每个分区内独立计算。 + ++ **`ORDER BY`**: 定义分区内行的排序方式。 + ++ **`frame_spec`**: 定义窗口的范围(例如,“当前行及之前的所有行”)。 + +**示例:** 计算每个员工的薪水,并显示其所在部门的平均薪水,以及薪水在部门内的排名。 + +```SQL +SELECT first_name, + last_name, + d.name AS department, + salary, + AVG(salary) OVER (PARTITION BY department_id) AS avg_dept_salary, + RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank_in_dept +FROM employees e + JOIN + departments d ON e.department_id = d.department_id; +``` + +这个查询无需`GROUP BY`,就能为每一位员工提供丰富的上下文信息,展示了窗口函数的强大威力。 \ No newline at end of file diff --git a/101-数据库学习/0-SQL语法/1-基础学习/select语法详解-2.md b/101-数据库学习/0-SQL语法/1-基础学习/select语法详解-全.md similarity index 99% rename from 101-数据库学习/0-SQL语法/1-基础学习/select语法详解-2.md rename to 101-数据库学习/0-SQL语法/1-基础学习/select语法详解-全.md index e1622b5..5959755 100644 --- a/101-数据库学习/0-SQL语法/1-基础学习/select语法详解-2.md +++ b/101-数据库学习/0-SQL语法/1-基础学习/select语法详解-全.md @@ -56,7 +56,9 @@ erDiagram ```mermaid flowchart TD - A[1. FROM & JOIN: 构建数据源] --> B[2. WHERE: 行级过滤] + A[1. FROM & JOIN: 构建数据源] + B[2. WHERE: 行级过滤] + A --> B B --> C[3. GROUP BY: 数据分组] C --> D[4. HAVING: 分组后过滤] D --> E[5. SELECT: 选取列/计算表达式] diff --git a/101-数据库学习/0-SQL语法/prompt.md b/101-数据库学习/0-SQL语法/prompt.md index 2d32eb4..ca8d8ee 100644 --- a/101-数据库学习/0-SQL语法/prompt.md +++ b/101-数据库学习/0-SQL语法/prompt.md @@ -9,4 +9,7 @@ -## 生成笔记 +gemini直接搜索 +Elements + +markdown markdown-main-panel tutor-markdown-rendering stronger enable-updated-hr-color diff --git a/101-数据库学习/0-SQL语法/wdd-sqlite3.db b/101-数据库学习/0-SQL语法/wdd-sqlite3.db new file mode 100644 index 0000000000000000000000000000000000000000..ed62b8216bfb7bce56ee27a1c7d98a332cfa9b00 GIT binary patch literal 45056 zcmeI5VN4s>9l-BwFa~1Yri@t`WIcsd0uj4mh;Euz1>;;2r(g&eZEPQ`!#?n3_8sw^ zY4V{GhBSmF&`cxEO4t&%q@(EyiMno-K%+(b64DQwH0|3ywDs9WQ^mKfpQioq&Nhb! zY^ypcOa4br{@=a-`@i@9-T%FN_wI7nbFy7fcxFI~$2f&)roKSYG}X#56h+m*S_x}8 ztQJ@atR=AK7dkKO@>@f>J70F#f|T9zCbj1r`CZj5$6S?Camey!Gq<2F%s@F{D0>G?T+mCtZK$PZ zC)CNY)Y3SZ+E|&;3eb(1ji=tbuD-gM9a~)=pHy#7>J}!|%a;rbMiSV`Tw8*j=8Rwv zYL!|&|Ix^7p;nD%jgLlt3hrm_ol`GgQ19G@GSu05^_6+`+G_UFn0oEk>iJjH=|!mV z`gGE8BYXdK!vdH#F#4*Wc!%<%{V#86e6M_7b0-D zvu`BRcSqILA2U5hM9EBcZa%g2Hh7-Cei@um^|G@IchuD@*`L3qUbwOT`pxv!-@wtx zF5WUy(-R{aFoGz^@sDRds7U%8R@w)3g+ObWtM-LO->uu;m{ba z9PR98eaHO8t~4;+>`}Ix^?TW#d`pl`t~M}|$b|VQ4=02WCx^5+&$+zC;wBKYL1V96 zznSW&XsBGb{?XaoOiy|BR-4CaYMI;YvTtd{2e#2TfbAJN1#_dr_KfXWJF|U$?K#)% ziW>CT^Y$`lO$~kCXwbPvpZh7>(r|M-^G#Jd{T3~SuA*=2$ zlHL;=Ww-3HIem3BC5U1EjC?u@mjXd9p-B36(9}x@n@k&(j()0B8=0{cHfLY+PT9BV zw}VaF>?}`ypVo(Vqc7`KGkIqcEn8yCl%U7g=C=7zn4*dC^aeV8elXI(`n8c8Ga$re zB`9*SZ9*x^?U=;JI3c<?_@9Qs5r<-0|P>c4~mIcKOfh%8mj(I9schbmqRwY z#yHtkQs&&hpB|OXXHYrcI!m`;$?w&fgjC{Fb8@e zzCuwB8rA_A6g*dIb9(pxo3N1!u_D{LtTtz3Bi&~ZFv3*kPbYXW#OI*M<3Y_fb z!wHY8`A`d#>T>S;hVxjQ3-QOLp&>pF_aH;OJEF_2XSpb^DSKUCKh*4|!S24pPEV8* z1BtjG4ssy56dPr66Mco`faYJLoF5LAzc5zOOcPsyYPbx5E+>v>W+mMQfob2zT+f-1M<;igMxp!AOR$R1dsp{ zKmter2_OL^fCP{L5uB@;18|TZQKeO%P~`XId!)AN6Y{es!%_4C2_OL^ zfCP{L5bLD51bxA9_N9_F{sTK^s-$WNW+; zE*=&5wz%|?7&2sczqV17|F|H-n?%mS;c@J(@zk_Q{U@Zaoq`%vH|;&b4GZD#aN=NC z6FB!vAhw=9?G8yXPh9yH4_Z!-%M*onPxQnDCBkWvTh_#)Qi^L?K^{=0ButSH$sAcD zNf2>C0!RP}AOR$R1dsp{Kmter2_OL^fCN4Rfm-Ve^iER^!ZTJcz00gZgt4;pZqo*X zP$7H2)bIYXwTb@M>48&UwR-5CrvrHH`TtTPS>XErKDkAvi1Zm8#4Jbv2_OL^fCP{L z5gMqq=Rkk`X4^}hYJ!w0!RP}AOR$R1dsp{ zKmter2_OL^@Z=Mq!A1RhfKR@w=phn70!RP}AOR$R1dsp{Kmter2_S)|kAQan-$owL z>*`G9=*^ie>MkpL1v0!RP}AOR$R1dsp{Kmter2|Vru?6wlxLL$_f$6lsM z0`<^qFVUny>QNhIw;SSGTe$_KFY?p}ZH;zX+sRW8Jr&@djXa{rr(}bq$zRAL@~6k$ zW2_$%Kmter2_OL^fCP{L52YxC#>m{0W?ej MShT5va+m=4Cn{zj$N&HU literal 0 HcmV?d00001 diff --git a/101-数据库学习/2-MySQL/0-基础数据结构.md b/101-数据库学习/2-MySQL/0-基础数据结构.md new file mode 100644 index 0000000..37c651c --- /dev/null +++ b/101-数据库学习/2-MySQL/0-基础数据结构.md @@ -0,0 +1,582 @@ +# MySQL 数据类型权威指南:从基础到最佳实践 + +## 第一章:MySQL 数据类型核心概念 + +### 1.1 数据类型的重要性:性能、存储与数据完整性 + +在数据库架构设计中,数据类型的选择是一项基础而关键的决策,其影响深远,远超初学者所能想象。它并非简单的占位符声明,而是直接关系到数据库三大核心支柱的基石:性能、存储效率和数据完整性。每一个字段的数据类型定义,都会对整个系统的响应速度、资源消耗和数据可靠性产生连锁反应。 + +**性能**是数据类型选择最直接的影响领域。数据库的性能瓶颈通常在于I/O操作,即从磁盘读取数据到内存的过程。选择更小、更合适的数据类型,意味着每一行数据占用的存储空间更少。在InnoDB存储引擎中,数据以页(Page)为单位进行管理,通常每页大小为16 KB。更小的数据行意味着单个页可以容纳更多的记录。当执行查询时,数据库需要读取的页数量就会减少,从而显著降低物理I/O操作,提升查询速度。此外,更小的数据类型还能更有效地利用InnoDB的缓冲池(Buffer Pool),这是MySQL缓存数据和索引的核心内存区域。在有限的内存中缓存更多“热”数据,可以极大地提高缓存命中率,使查询操作更多地在内存中完成,而不是依赖缓慢的磁盘 。   + +**存储效率**是另一个显而易见的考量因素。随着数据量的爆炸式增长,磁盘空间不再是无限廉价的资源。尤其在云数据库环境中,存储成本与使用量直接挂钩。通过精确选择数据类型,例如使用 `TINYINT` 存储年龄而非 `INT`,可以将存储需求降低75%。这种优化在拥有数十亿行记录的大表中累积起来,可以节省TB级别的存储空间,直接转化为成本的节约 。   + +**数据完整性**是数据库的生命线。数据类型是保障数据完整性的第一道防线。它通过定义列可以接受的值的类型、范围和格式,从源头上阻止了非法或无效数据的插入。例如,将日期字段定义为 `DATE` 类型,可以确保只有符合'YYYY-MM-DD'格式的有效日期才能被存储,任何无效的输入(如'2024-02-30')都会被拒绝。这比在应用层进行数据校验更为可靠和高效,因为它将约束直接施加在数据存储的核心层 。   + +一个看似微不足道的选择,其影响是系统性的。例如,一个开发者为存储国家代码(如'US', 'CN')的字段选择了 `VARCHAR(255)`。尽管实际存储的值只有2个字符,但MySQL在处理变长字段时,可能会在内存中为其分配更大的固定大小内存块 。这不仅浪费了内存,还增加了行记录的整体长度。更大的行记录导致每页能存储的行数减少。一个需要扫描1000行记录的查询,原本可能只需要读取15个数据页,现在可能需要读取20个。这额外的5个页不仅增加了本次查询的I/O,还可能将缓冲池中其他有用的数据页挤出,导致后续其他查询的缓存命中率下降。因此,一个字段的草率选择,通过增加I/O和降低内存效率,最终可能导致整个数据库系统的性能下降。   + +### 1.2 MySQL 数据类型分类概览 + +MySQL提供了一个丰富的数据类型系统,以满足各种数据存储需求。这些类型可以被划分为几个主要类别,每个类别都针对特定的数据形式进行了优化。理解这些分类是进行高效数据库设计的第一步 。   + ++ **数值类型 (Numeric Types)**:用于存储数字数据。这个大类又可细分为: + + + **整数类型**:用于存储没有小数部分的整数,如 `TINYINT`, `SMALLINT`, `INT`, `BIGINT`。 + + + **定点数类型**:用于存储具有精确小数位的数值,如 `DECIMAL` 和 `NUMERIC`,是金融计算的理想选择。 + + + **浮点数类型**:用于存储近似的小数值,如 `FLOAT` 和 `DOUBLE`,适用于科学计算。 + + + **位值类型**:`BIT` 类型,用于存储位字段值。 + ++ **字符串类型 (String Types)**:用于存储文本或二进制数据。这个类别包括: + + + **字符字符串类型**:如 `CHAR`, `VARCHAR`, `TEXT` 系列,用于存储文本数据,并受字符集和排序规则的影响。 + + + **二进制字符串类型**:如 `BINARY`, `VARBINARY`, `BLOB` 系列,用于存储原始的字节数据,如图片或文件,其比较和排序是基于字节值的。 + + + **枚举和集合类型**:`ENUM` 和 `SET`,用于从预定义的列表中选择值。 + ++ **日期和时间类型 (Date and Time Types)**:专门用于存储时间信息。包括 `DATE`, `TIME`, `YEAR`, `DATETIME`, 和 `TIMESTAMP`。这些类型提供了强大的函数支持,用于处理和计算时间数据。 + ++ **空间数据类型 (Spatial Data Types)**:用于存储地理和空间信息,如点、线和多边形。例如 `GEOMETRY`, `POINT`, `POLYGON` 等。虽然功能强大,但它们属于专业领域,本指南的核心内容不包含对此类的深入探讨 。   + ++ **JSON 数据类型 (JSON Data Type)**:自MySQL 5.7版本引入,允许在单个字段中存储半结构化的JSON文档。这为关系型数据库带来了类似NoSQL的灵活性,适用于存储动态或嵌套的元数据 。   + + +### 1.3 选择正确类型的基本原则 + +在选择数据类型时,应遵循一个核心哲学:“最小化原则”。即总是选择能够安全、可靠地存储所有当前及未来可能值的最小数据类型 。这一原则是实现高性能和高效率存储的基石。   + +具体而言,选择过程应围绕以下几个关键问题展开: + +1. **数据的本质是什么?** 首先要明确存储的是什么样的数据。是整数、精确的小数、近似值,还是文本?这个问题的答案将直接将选择范围缩小到一个特定的数据类型类别。 + +2. **值的范围是多少?** 对于数值类型,需要预估其可能的最大值和最小值。例如,如果一个字段用于存储用户年龄,其值永远不会超过255,那么 `TINYINT UNSIGNED` (范围0-255) 就是最佳选择,而不是默认使用占用4个字节的 `INT`。 + +3. **是否需要精确性?** 这是一个至关重要的问题。如果数据涉及金融交易、货币计算或任何不允许出现舍入误差的场景,那么必须使用 `DECIMAL` 类型。在这些场景下使用 `FLOAT` 或 `DOUBLE` 是一个严重的设计缺陷,可能导致数据不一致和财务损失 。   + +4. **时间代表什么?** 对于时间数据,需要区分其含义。它是一个全球统一的、绝对的时间点(例如,一条消息的发送时间),还是一个与特定时区无关的“日历”或“时钟”上的时间(例如,某人的生日或一个本地活动的开始时间)?前者应使用 `TIMESTAMP`,后者则应使用 `DATETIME` 。   + + +遵循这些基本原则,可以确保数据库模式从一开始就建立在坚实、高效的基础之上,为未来的扩展和性能优化奠定良好的根基。 + +## 第二章:数值类型 + +数值类型是数据库中最基础、最常用的数据类型之一。MySQL提供了全面的数值类型支持,涵盖了从微小的整数到高精度的定点数,再到大范围的浮点数。正确选择数值类型对于节省存储空间、提升查询性能以及保证数据计算的准确性至关重要。 + +### 2.1 整数类型 (Integer Types: `TINYINT`, `SMALLINT`, `MEDIUMINT`, `INT`, `BIGINT`) + +整数类型用于存储不包含小数部分的数字。MySQL提供了五种不同大小的整数类型,以满足不同范围的数据存储需求,这完美体现了“最小化原则”的应用。 + +#### 语法、存储空间与取值范围 + +每种整数类型都有其固定的存储大小和相应的取值范围。选择时,应根据业务需求预估字段可能的最大值,然后选择恰好能覆盖该范围的最小类型。例如,存储一个班级的学生人数,`TINYINT UNSIGNED` (0-255) 通常就足够了。 + +下表详细列出了各种整数类型的核心规格 :   + +| 类型名称 | 存储空间 (字节) | 有符号范围 (Signed Range) | 无符号范围 (Unsigned Range) | +| --- | --- | --- | --- | +| `TINYINT` | 1 | \-128 到 127 | 0 到 255 | +| `SMALLINT` | 2 | \-32,768 到 32,767 | 0 到 65,535 | +| `MEDIUMINT` | 3 | \-8,388,608 到 8,388,607 | 0 到 16,777,215 | +| `INT` | 4 | \-2,147,483,648 到 2,147,483,647 | 0 到 4,294,967,295 | +| `BIGINT` | 8 | −9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 0 到 18,446,744,073,709,551,615 | + +Export to Sheets + +#### `UNSIGNED` 和 `ZEROFILL` 属性详解 + ++ **`UNSIGNED`**:这是一个非常重要的属性。当为整数列指定 `UNSIGNED` 属性时,该列将只允许存储非负数(零和正数)。其效果是,将原本用于表示负数的那一半取值范围,移动到了正数范围的顶端,从而使正数的最大值翻倍。对于那些在业务逻辑上永远不会是负值的字段,如自增ID、物品数量、年龄等,强烈推荐使用 `UNSIGNED` 属性。这不仅是一种数据约束,确保了数据的有效性,也是一种优化,因为它用同样的存储空间提供了更大的正数表示范围 。   + ++ **`ZEROFILL`**:这是一个纯粹的显示格式化属性。当为列指定 `ZEROFILL` 时,MySQL会自动为该列添加 `UNSIGNED` 属性。在显示数值时,如果数字的位数小于在类型定义中指定的显示宽度 `M`(例如 `INT(10)`),MySQL会在左侧用零来填充,直到达到指定的宽度。例如,在一个 `INT(5) ZEROFILL` 的列中存入数字 `123`,查询时会显示为 `00123`。需要特别强调的是,`INT(M)` 中的 `M` **不影响**该整数类型的存储大小或实际可存储的数值范围。`INT(10)` 和 `INT(1)` 都能存储从-21亿到+21亿的完整范围,并都占用4个字节。`M` 仅在与 `ZEROFILL` 结合使用时才有视觉上的意义。这是一个普遍存在的误解,许多开发者错误地认为 `M` 限制了可存储的数字位数 。   + + +### 2.2 定点数类型 (Fixed-Point Types: `DECIMAL`, `NUMERIC`) + +#### 精确计算的基石 + +`DECIMAL` 类型是为需要绝对精确计算的场景而设计的,它是存储货币、利率、财务数据以及任何不允许出现舍入误差的数值的唯一正确选择。与 `FLOAT` 和 `DOUBLE` 将数值存储为二进制近似值不同,`DECIMAL` 将每个数字作为字符串进行内部存储,从而保证了数值的精确性 。   + +`DECIMAL` 的语法是 `DECIMAL(M, D)`,其中: + ++ `M` 代表**精度 (precision)**,即数字的总位数(整数部分+小数部分),最大值为65。 + ++ `D` 代表**标度 (scale)**,即小数点后的位数,最大值为30,且 `D` 必须小于等于 `M`。 + + +例如,`DECIMAL(10, 2)` 可以存储最多10位数字,其中2位是小数,其取值范围为 `-99999999.99` 到 `99999999.99`。 + +`NUMERIC` 类型是 `DECIMAL` 的同义词,在功能上完全等同于 `DECIMAL`,主要是为了遵循SQL标准 。   + +`DECIMAL` 类型的存储空间是可变的,它取决于 `M` 的值,MySQL会根据需要打包数字以节省空间 。   + +### 2.3 浮点数类型 (Floating-Point Types: `FLOAT`, `DOUBLE`) + +#### 近似值的存储与潜在问题 + +`FLOAT` 和 `DOUBLE` 类型用于存储近似的数值。它们遵循IEEE 754标准,使用二进制格式来表示浮点数。这种表示方式使得它们能够存储极大或极小的数值,但代价是牺牲了精度。 + ++ **`FLOAT`**:单精度浮点数,占用4个字节。 + ++ **`DOUBLE`**:双精度浮点数,占用8个字节,具有比 `FLOAT` 更大的范围和更高的精度。`REAL` 是 `DOUBLE` 的同义词。 + + +由于二进制无法精确表示所有十进制小数(例如0.1),浮点数在存储和计算过程中可能会引入微小的舍入误差。这些误差在单个数值上可能微不足道,但在大量聚合计算(如 `SUM()`)中会累积,导致结果与预期不符。因此,浮点数适用于科学计算、工程模拟、传感器读数等场景,在这些场景中,数值范围远比绝对精度重要 。   + +### 2.4 深度比较:`DECIMAL` vs. `FLOAT`/`DOUBLE` + +`DECIMAL` 和 `FLOAT`/`DOUBLE` 之间的选择是数据库设计中一个常见且关键的决策点。它们的差异直接关系到数据的准确性和应用的可靠性。 + +下表清晰地对比了这两种类型的核心特性 :   + +| 特性 | `DECIMAL` | `FLOAT` / `DOUBLE` | +| --- | --- | --- | +| **精度** | **精确**。按原样存储数值,无舍入误差。 | **近似**。存储为二进制浮点数,可能存在微小的舍入误差。 | +| **存储方式** | 内部以变长字符串形式存储,保证精确度。 | 内部以二进制指数形式存储。 | +| **存储空间** | 可变,取决于定义的精度(M)。通常比浮点数占用更多空间。 | 固定。`FLOAT` 为4字节,`DOUBLE` 为8字节。 | +| **计算性能** | 由于内部表示复杂,计算速度相对较慢。 | 基于硬件浮点单元,计算速度通常更快。 | +| **适用场景** | **金融、货币、会计**等任何要求精确计算的领域。 | **科学计算、工程、物理模拟**等对数值范围要求高但可容忍微小误差的领域。 | + +Export to Sheets + +使用 `FLOAT` 或 `DOUBLE` 存储货币值是一个极其危险的常见错误。一个简单的例子可以说明问题:假设一个商品价格为9.99,在 `FLOAT` 类型中可能被存储为 `9.9900000000000002`。当对一百万笔这样的交易进行求和时,累积的误差可能会导致最终总额出现明显的偏差。这种因数据类型选择不当而导致的业务逻辑错误,其后果可能是灾难性的。 + +### 2.5 数值类型使用最佳实践与常见误区 + ++ **最佳实践**: + + 1. **坚持最小化原则**:为年龄字段使用 `TINYINT UNSIGNED`,为文章点赞数使用 `INT UNSIGNED`,而不是一律使用 `INT` 。   + + 2. **为非负ID使用 `UNSIGNED`**:所有自增主键和外键都应定义为 `UNSIGNED`,这不仅符合逻辑,还能将ID的上限提高一倍。 + + 3. **为货币和精确值使用 `DECIMAL`**:这是不可动摇的铁律。 + ++ **常见误区**: + + 1. **滥用 `INT`**:将 `INT` 作为所有整数需求的默认选择,导致不必要的存储浪费。 + + 2. **误用 `FLOAT`/`DOUBLE` 存储货币**:这是最严重的误区之一,直接威胁到数据的准确性和业务的可靠性 。   + + 3. **误解 `INT(M)` 的含义**:错误地认为 `M` 限制了数值的范围或存储大小,而实际上它只与 `ZEROFILL` 属性的显示格式有关。 + + +## 第三章:字符串类型 + +字符串类型是用于存储文本数据的核心构建块,从用户名、地址到文章内容,无处不在。MySQL提供了多种字符串类型,主要分为定长、变长、大文本对象以及特殊的枚举和集合类型。理解它们之间的细微差别,尤其是在存储机制、索引能力和性能影响上的差异,对于构建高效、可扩展的数据库至关重要。 + +### 3.1 深度比较:`CHAR` vs. `VARCHAR` + +`CHAR` 和 `VARCHAR` 是最常用的两种字符类型,它们的核心区别在于长度是固定的还是可变的。 + +#### 存储机制 + ++ **`CHAR(M)`**:代表**定长 (Fixed-Length)** 字符串。当你定义一个 `CHAR(10)` 的列时,无论你存入的字符串是 'hello' (5个字符) 还是 'database' (8个字符),它在磁盘上都将占用10个字符的存储空间。对于短于 `M` 的字符串,MySQL会在其右侧用空格进行填充以达到指定长度。一个重要的特性是,在检索 `CHAR` 值时,除非启用了特定的SQL模式 (`PAD_CHAR_TO_FULL_LENGTH`),否则尾部的填充空格通常会被移除 。   + ++ **`VARCHAR(M)`**:代表**变长 (Variable-Length)** 字符串。当你定义一个 `VARCHAR(255)` 的列时,它存储一个字符串所需的空间是其实际长度加上一个或两个字节的前缀。这个前缀用于记录字符串的字节长度。如果值的字节长度不超过255,则使用1个字节的前缀;如果超过255,则使用2个字节。`VARCHAR` 不会进行空格填充,并且在存储和检索时会保留字符串尾部的空格 。   + + +下表通过一个实例直观地展示了 `CHAR` 和 `VARCHAR` 在存储上的差异 :   + +| 输入值 | 存入 `CHAR(6)` | 存储所需空间 (单字节字符集) | 存入 `VARCHAR(6)` | 存储所需空间 (单字节字符集) | +| --- | --- | --- | --- | --- | +| `''` (空字符串) | `' '` | 6 字节 | `''` | 1 字节 (长度前缀) | +| `'sql'` | `'sql '` | 6 字节 | `'sql'` | 4 字节 (3 + 1) | +| `'mysql '` | `'mysql '` | 6 字节 | `'mysql '` | 7 字节 (6 + 1) | + +Export to Sheets + +#### 性能影响与适用场景 + ++ **`CHAR` 的适用场景**:`CHAR` 最适合存储那些长度几乎完全固定的数据。典型的例子包括: + + + 国家代码 (如 'US', 'CN',固定为2个字符)。 + + + MD5或SHA1哈希值 (固定为32或40个字符)。 + + + 性别字段 (如 'M', 'F',固定为1个字符)。 在这些场景下,由于行记录的长度是固定的,理论上MySQL可以更快地计算行偏移量,从而在某些情况下(尤其是在较老的存储引擎如MyISAM中)获得微弱的性能优势。然而,在现代的InnoDB存储引擎中,由于其基于页的复杂存储结构,这种优势已基本可以忽略不计 。   + ++ **`VARCHAR` 的适用场景**:`VARCHAR` 是绝大多数文本存储场景的理想选择。用户名、电子邮件、地址、文章标题等,这些数据的长度都是可变的。使用 `VARCHAR` 可以极大地节省存储空间。空间的节省意味着更小的表、更小的索引、更高效的缓冲池利用率,最终转化为更少的磁盘I/O和更快的查询性能 。   + + +### 3.2 深度比较:`VARCHAR` vs. `TEXT` + +当需要存储的文本长度可能超过 `VARCHAR` 的常规使用范围,或者非常长时,开发者通常会在 `VARCHAR` 和 `TEXT` 之间进行抉择。这个选择对性能有着至关重要的影响。 + +#### 存储限制与索引策略 + ++ **`VARCHAR`**:其最大长度可以定义到65,535。但需要注意的是,这个限制是**字节**数,并且受MySQL单行最大65,535字节的限制(所有列共享)。实际可存储的**字符**数取决于所使用的字符集(例如,`utf8mb4` 字符集一个字符最多可能占用4个字节)。`VARCHAR` 列可以被完整地索引,这对于查询性能至关重要 。   + ++ **`TEXT`**:`TEXT` 类型家族用于存储更长的文本数据。 + + + `TEXT`:最大长度 65,535 字节 (~64 KB)。 + + + `MEDIUMTEXT`:最大长度 16,777,215 字节 (~16 MB)。 + + + `LONGTEXT`:最大长度 4,294,967,295 字节 (~4 GB)。 `TEXT` 类型的一个关键限制是,不能直接对其整个列创建常规索引。如果需要索引,必须指定一个**前缀长度**,例如 `CREATE INDEX idx_content ON articles(content(255));`。这意味着索引只能利用内容的前255个字符,对于需要全文搜索或精确匹配长内容的查询,这种前缀索引的效率有限 。   + + +#### 对临时表和排序的性能影响 + +这是 `VARCHAR` 和 `TEXT` 之间最关键、也最容易被忽视的性能差异。当一个查询需要使用临时表来处理中间结果时(例如,使用了 `GROUP BY`, `ORDER BY`, `DISTINCT` 等操作),MySQL会首先尝试在内存中创建这个临时表。 + +然而,MySQL的内存存储引擎(`MEMORY` engine)**不支持** `TEXT` 和 `BLOB` 类型。如果临时表中需要包含 `TEXT` 类型的列,MySQL将被迫放弃使用内存临时表,转而在**磁盘**上创建一个基于 `InnoDB` 或 `MyISAM` 的临时表 。   + +这个转换过程对性能的影响是巨大的: + +1. 一个查询,如 `SELECT title FROM articles ORDER BY content;`,需要对 `content` 列进行排序。 + +2. 优化器决定使用一个临时表来存储 `title` 和 `content` 并进行排序。 + +3. 优化器检查到 `content` 列是 `TEXT` 类型。 + +4. 由于内存引擎不支持 `TEXT`,它不能在RAM中创建临时表。 + +5. MySQL转而在磁盘上创建临时表。磁盘I/O的速度比内存操作慢几个数量级。 + +6. 因此,这个原本可能很快的查询,会因为 `TEXT` 类型的存在而变得异常缓慢,并产生大量的磁盘I/O,可能拖慢整个数据库服务器的性能。 + + +结论是:如果文本数据的长度确定不会超过 `VARCHAR` 的65,535字节限制,应**始终优先选择 `VARCHAR` 而不是 `TEXT`**。这可以避免在复杂查询中掉入“磁盘临时表”的性能陷阱 。   + +### 3.3 二进制字符串与大对象 (`BINARY`, `VARBINARY`, `BLOB`) + +`BINARY`, `VARBINARY`, 和 `BLOB` 分别是 `CHAR`, `VARCHAR`, 和 `TEXT` 的二进制(字节)版本。它们的主要区别在于存储和比较的方式: + ++ **存储内容**:它们存储的是原始的字节序列,而不是字符。 + ++ **比较和排序**:对这些类型的比较和排序是基于字节的数值进行的,因此是**区分大小写**的。它们不受字符集和排序规则(collation)的影响。例如,在二进制比较中,字节值 `0x61` ('a') 与 `0x41` ('A') 是不同的。 + ++ **适用场景**:`BINARY` 和 `VARBINARY` 适用于存储那些需要精确字节匹配的数据,如加密密钥或定长的二进制标识符。`BLOB` (Binary Large Object) 系列(`TINYBLOB`, `BLOB`, `MEDIUMBLOB`, `LONGBLOB`)则用于存储大量的二进制数据,如图片、音频或编译后的代码。然而,将大文件直接存储在数据库中通常被认为是一种反模式,因为它会迅速撑大数据库体积,增加备份和恢复的复杂性。更推荐的做法是,将文件存储在专门的文件系统或对象存储(如S3)中,而在数据库中只存储文件的路径或URL 。   + + +### 3.4 枚举与集合类型 (`ENUM`, `SET`) + ++ **`ENUM`**:枚举类型。允许你从一个预定义的字符串值列表中选择**一个**值。例如 `ENUM('active', 'inactive', 'pending')`。`ENUM` 在内部存储时非常高效,通常只用1或2个字节来存储一个指向值列表的索引,而不是存储字符串本身 。   + ++ **`SET`**:集合类型。允许你从一个预定义的字符串值列表中选择**零个或多个**值。例如 `SET('read', 'write', 'execute')`。`SET` 在内部被存储为一个位图(bitmap),每个预定义的值对应一个位。存储空间取决于列表成员的数量,可以是1, 2, 3, 4, 或 8个字节 。   + + +尽管 `ENUM` 和 `SET` 在存储上极为高效,但它们存在一个严重的**维护性陷阱**。这两个类型将允许的值列表硬编码到了表结构(schema)中。如果业务需求变化,需要增加一个新的状态或权限,就必须执行 `ALTER TABLE` 语句来修改列定义。在生产环境的大表上,`ALTER TABLE` 通常是一个阻塞性的、耗时很长的操作,可能导致服务中断。因此,在需要频繁变更或扩展值列表的敏捷开发环境中,使用 `ENUM` 和 `SET` 往往是不明智的。一个更灵活、可扩展的替代方案是创建一个独立的“查找表”(lookup table),并通过外键关联来维护这些值 。   + +### 3.5 字符串类型使用最佳实践与常见误区 + ++ **最佳实践**: + + 1. **`VARCHAR` 是首选**:对于绝大多数长度可变的文本数据(只要在64KB以内),`VARCHAR` 都是最佳选择。 + + 2. **精确定义长度**:避免使用 `VARCHAR(255)` 作为“懒人默认值”。根据实际数据预估一个合理的长度,如 `VARCHAR(50)` 用于用户名。这有助于数据校验,并且可能为MySQL的内部内存管理带来优化 。   + + 3. **为真正定长的数据使用 `CHAR`**:仅在数据长度严格固定的情况下(如MD5哈希)才考虑使用 `CHAR`。 + ++ **常见误区**: + + 1. **误用 `TEXT`**:在数据长度完全可以用 `VARCHAR` 容纳的情况下使用 `TEXT`,这会带来潜在的巨大性能风险,尤其是在涉及排序和分组的查询中 。   + + 2. **滥用 `ENUM`**:为那些可能会随业务发展而变化的值列表(如商品分类)使用 `ENUM`,导致后期维护困难和上线风险 。   + + 3. **在 `VARCHAR` 中存储CSV**:将多个值用逗号分隔存储在一个 `VARCHAR` 字段中(如 '1,5,23'),这破坏了数据库的第一范式,使得查询、更新和维护这些值变得极其困难和低效。正确的做法是使用一个独立的关联表。 + + +## 第四章:日期与时间类型 + +在几乎所有的应用中,时间信息都是不可或缺的数据维度。MySQL提供了一套完整且功能强大的日期和时间数据类型,用于精确记录和处理从年份到微秒的各种时间值。在这些类型中,`DATETIME` 和 `TIMESTAMP` 的选择尤为关键,它们在时区处理上的根本差异决定了其各自的适用场景。 + +### 4.1 基础类型:`DATE`, `TIME`, `YEAR` + +这三种类型用于存储时间信息的特定部分,用途明确。 + ++ **`DATE`**:用于存储日期,不包含时间部分。其标准格式为 `'YYYY-MM-DD'`。它占用3个字节,支持的范围从 `'1000-01-01'` 到 `'9999-12-31'`。`DATE` 类型是存储生日、纪念日、事件发生日期等场景的理想选择 。   + ++ **`TIME`**:用于存储时间,不包含日期部分。其标准格式为 `'HH:MM:SS'`。有趣的是,`TIME` 类型的范围远不止24小时,它可以从 `'-838:59:59'` 到 `'838:59:59'`。这使得 `TIME` 不仅可以表示一天中的某个时间点,还可以用来记录两个事件之间的时间间隔或持续时长 。   + ++ **`YEAR`**:用于存储年份。它只占用1个字节,可以存储从1901到2155的年份。这是一个空间效率极高的类型,但使用场景相对有限 。   + + +### 4.2 核心对决:`DATETIME` vs. `TIMESTAMP` + +`DATETIME` 和 `TIMESTAMP` 都可以存储包含日期和时间的完整时间戳,但它们在内部工作机制、存储范围和时区处理上存在本质区别。 + +#### 存储空间、取值范围与“2038年问题” + ++ **`DATETIME`**: + + + **范围**:支持的范围非常广,从 `'1000-01-01 00:00:00'` 到 `'9999-12-31 23:59:59'` 。   + + + **存储**:自MySQL 5.6.4起,`DATETIME` 的存储空间经过优化,需要5个字节加上小数秒所需的额外存储(0-3字节)。   + ++ **`TIMESTAMP`**: + + + **范围**:其范围相对受限,从 `'1970-01-01 00:00:01'` UTC 到 `'2038-01-19 03:14:07'` UTC 。这个上限被称为\*\*“2038年问题”\*\*,因为它在内部是基于一个有符号的32位Unix时间戳(从1970年1月1日午夜UTC开始的秒数)实现的。当这个32位整数溢出时,时间将无法正确表示 。   + + + **存储**:需要4个字节加上小数秒所需的额外存储 。   + + +#### 时区处理的根本差异 + +这是 `DATETIME` 和 `TIMESTAMP` 之间最核心、最决定性的区别。 + ++ **`DATETIME`:时区无关 (Timezone-Agnostic)** `DATETIME` 存储的是一个**字面量 (literal)** 的日期和时间值。你存入什么,它就记录什么,完全不关心当前数据库服务器或客户端连接的任何时区设置。例如,无论你的服务器在伦敦还是东京,当你执行 `INSERT INTO my_table (dt_col) VALUES ('2024-01-01 10:00:00');` 时,数据库中存储的值就是 `'2024-01-01 10:00:00'`。在任何时区环境下检索这个值,你得到的也永远是这个固定的字符串 。   + ++ **`TIMESTAMP`:时区感知 (Timezone-Aware)** `TIMESTAMP` 存储的是一个**绝对的、全球统一的时间点**。它的工作流程如下: + + 1. **存储时**:MySQL获取你提供的时间值(例如 `'2024-01-01 10:00:00'`),并根据**当前会话的时区设置**(`time_zone`变量),将其转换为**协调世界时 (UTC)** 进行存储。 + + 2. **检索时**:当查询该 `TIMESTAMP` 值时,MySQL会从数据库中取出存储的UTC时间,并再次根据**当前会话的时区设置**,将其转换回本地时间进行显示。 这个自动转换机制确保了 `TIMESTAMP` 记录的是一个无歧义的时刻。一个在东八区(北京时间)上午10点插入的 `TIMESTAMP` 值,对于一个在零时区(伦敦时间)的客户端来说,检索出来会显示为凌晨2点,两者指向的是同一个宇宙时刻 。   + + +#### 概念辨析:全球时刻 vs. 本地约定 + +基于时区处理的差异,可以得出选择 `DATETIME` 和 `TIMESTAMP` 的核心指导思想: + ++ **使用 `TIMESTAMP` 记录“全球时刻” (Global Moment)**:当需要记录一个事件发生的绝对时间点,且这个时间点需要在全球不同地区被正确地理解时,应使用 `TIMESTAMP`。例如: + + + 用户注册时间 + + + 订单创建时间 + + + 消息发送时间 + + + 文章发布时间 + ++ **使用 `DATETIME` 记录“本地约定” (Local Appointment)**:当需要记录一个与特定地理位置或文化背景相关的、不应随时区变化而改变的“日历时间”或“墙上时钟时间”时,应使用 `DATETIME`。例如: + + + 某人的生日(一个人的生日不会因为他去了另一个国家而改变)。 + + + 一个在纽约举行的会议的开始时间(对所有参会者来说,都是纽约当地时间上午9点)。 + + + 法定节假日日期。 + + +下表总结了 `DATETIME` 和 `TIMESTAMP` 的所有关键区别,为技术选型提供清晰的决策依据 :   + +| 特性 | `DATETIME` | `TIMESTAMP` | +| --- | --- | --- | +| **支持范围** | `'1000-01-01'` 到 `'9999-12-31'` | `'1970-01-01'` 到 `'2038-01-19'` | +| **存储空间** | 5 字节 + 小数秒存储 (>= 5.6.4) | 4 字节 + 小数秒存储 | +| **时区处理** | **时区无关**。存储和显示字面值,不进行转换。 | **时区感知**。存储时转换为UTC,检索时从UTC转回会话时区。 | +| **自动初始化/更新** | 可配置 `DEFAULT CURRENT_TIMESTAMP` 和 `ON UPDATE CURRENT_TIMESTAMP` (>= 5.6.5) | 默认行为(在旧版本中)或可配置 `DEFAULT CURRENT_TIMESTAMP` 和 `ON UPDATE CURRENT_TIMESTAMP`。 | +| **核心用例** | 生日、本地事件时间、不应随时区变化的“约定时间”。 | 用户注册、订单创建、日志记录等需要全球统一的“绝对时刻”。 | + +Export to Sheets + +### 4.3 日期时间类型使用最佳实践与常见误区 + ++ **最佳实践**: + + 1. **服务器时区设置为UTC**:强烈建议将MySQL服务器的全局时区设置为UTC (`--default-time-zone='+00:00'`)。这为所有时间存储提供了一个统一、无歧义的基准。应用程序可以根据用户偏好在展示层进行时区转换 。   + + 2. **明确业务需求**:在选择 `DATETIME` 或 `TIMESTAMP` 之前,仔细分析业务场景,问自己:“这个时间值应该随着观察者位置的变化而变化吗?” + + 3. **考虑未来**:对于需要长期存储或记录未来事件的系统,要警惕 `TIMESTAMP` 的“2038年问题”。如果应用的生命周期可能跨越2038年,或者需要存储1970年之前的历史数据,那么使用 `DATETIME` 并以UTC标准存储是更安全的选择。 + ++ **常见误区**: + + 1. **混淆二者**:不理解时区处理的差异,随意选择 `DATETIME` 或 `TIMESTAMP`,导致在跨时区的应用中出现时间混乱。 + + 2. **忽略“2038年问题”**:在新项目中使用 `TIMESTAMP` 而没有评估其时间范围是否满足长远需求,为系统埋下了一颗定时炸弹。 + + 3. **在应用层处理时区混乱**:当数据库层面时区策略不一致时(例如,混合使用 `DATETIME` 和 `TIMESTAMP`,服务器时区未标准化),应用层需要编写大量复杂的代码来弥补,增加了出错的概率和维护成本。 + + +## 第五章:JSON 数据类型 + +自MySQL 5.7版本起,引入了原生的 `JSON` 数据类型,这标志着MySQL在融合关系型数据库的严谨性与NoSQL数据库的灵活性方面迈出了重要一步。`JSON` 类型允许开发者在单个数据库列中存储和操作半结构化的JSON(JavaScript Object Notation)文档。 + +### 5.1 JSON 类型简介:关系型数据库的半结构化能力 + +`JSON` 数据类型为传统的关系型数据模型提供了一种强大的补充。它允许存储包含嵌套对象和数组的复杂数据结构,而无需预先定义一个严格的、扁平化的表结构。这对于以下场景特别有用: + ++ \*\* schema 灵活性\*\*:存储那些结构多变或未来可能频繁扩展的元数据,如产品属性、用户配置、标签等。 + ++ **简化数据模型**:对于一对多的弱关系数据,使用 `JSON` 数组可以避免创建额外的关联表,简化数据库设计。 + ++ **与现代应用集成**:现代Web和移动应用大量使用JSON作为数据交换格式,将JSON文档直接存入数据库可以减少应用层的数据解析和转换开销 。   + + +MySQL对 `JSON` 类型的实现具有一个关键优势:它不仅仅是将其作为普通文本存储。当数据插入 `JSON` 列时,MySQL会**校验**其是否为合法的JSON格式。如果格式无效,插入操作将失败。此外,MySQL会以一种优化的二进制格式存储JSON数据,这使得对文档内部元素的读取和访问更加高效 。   + +### 5.2 创建、操作与查询 JSON 数据 + +MySQL提供了一套丰富的内置函数来处理 `JSON` 数据,涵盖了创建、查询、修改等各个方面。 + ++ **创建JSON值**: + + + `JSON_OBJECT(key1, val1, key2, val2,...)`:根据键值对创建一个JSON对象。 + + + 示例:`SELECT JSON_OBJECT('name', 'Alice', 'age', 30);` -> `{"age": 30, "name": "Alice"}` + + + `JSON_ARRAY(val1, val2,...)`:根据给定的值创建一个JSON数组。 + + + 示例:`SELECT JSON_ARRAY('apple', 'banana', 123);` -> `["apple", "banana", 123]` 这些函数可以直接在 `INSERT` 或 `UPDATE` 语句中使用,以编程方式构建JSON文档 。   + ++ **查询和提取JSON数据**: 查询JSON数据的核心是**JSON路径表达式 (JSON Path)**,它是一种类似文件系统路径的语法,用于定位JSON文档中的特定元素。路径以 `$` 符号代表整个文档。 + + + `JSON_EXTRACT(json_doc, path)`:是最主要的提取函数。 + + + 示例:`SELECT JSON_EXTRACT('{"user": {"name": "Bob", "tags": ["dev", "sql"]}}', '$.user.tags');` -> `"dev"` + + + **列路径操作符 (Column Path Operator)**:MySQL提供了更简洁的语法糖: + + + `column->path`:等价于 `JSON_EXTRACT()`,返回的结果是JSON格式的(例如,字符串会带引号)。 + + + `column->>path`:等价于 `JSON_UNQUOTE(JSON_EXTRACT(...))`,它会提取值并移除JSON的引号,返回一个常规的SQL字符串。这在 `WHERE` 子句中进行值比较时非常有用 。   + ++ **修改JSON数据**: MySQL允许对 `JSON` 文档进行原地部分更新,而无需读取整个文档、在应用层修改、再写回整个文档。 + + + `JSON_SET(json_doc, path, val,...)`:在指定路径插入或更新值。如果路径已存在,则更新;如果不存在,则创建。 + + + `JSON_REPLACE(json_doc, path, val,...)`:仅当指定路径存在时,才更新其值。 + + + `JSON_REMOVE(json_doc, path,...)`:移除指定路径的元素 。   + + +### 5.3 JSON 列的索引策略 + +`JSON` 类型虽然灵活,但其性能上的一个主要挑战是:**不能直接在 `JSON` 列本身上创建常规索引**。如果不对其进行索引优化,那么对JSON内容进行过滤的查询(例如 `WHERE profile->>'$.city' = 'New York'`)将会导致全表扫描,在数据量大时性能极差。 + +为了解决这个问题,MySQL提供了**基于生成列 (Generated Columns) 的索引策略**。这是使用 `JSON` 类型时必须掌握的核心性能优化技术。其步骤如下: + +1. **创建虚拟或存储生成列**:在表中定义一个新列,其值是根据 `JSON` 列中的某个路径表达式动态计算得出的。 + +2. **在该生成列上创建索引**:由于生成列具有明确的标量数据类型(如 `VARCHAR` 或 `INT`),因此可以在其上创建标准的B-Tree索引 。   + + +下面是一个具体的例子,演示了如何为一个存储用户信息的 `JSON` 列中的 `city` 字段创建索引: + +SQL + +``` +CREATE TABLE user_profiles ( + id INT PRIMARY KEY AUTO_INCREMENT, + profile JSON, + + -- 步骤1: 创建一个虚拟生成列,用于提取城市信息。 + -- 'AS (profile->>"$.address.city")' 定义了该列的值。 + -- 'VIRTUAL' 表示该列的值不实际存储,而是在读取时计算。 + city VARCHAR(100) AS (profile->>"$.address.city") VIRTUAL, + + -- 步骤2: 在这个新创建的生成列上建立索引。 + INDEX idx_city (city) +); +``` + +通过这种方式,当执行如下查询时: + +SQL + +``` +SELECT id, profile FROM user_profiles WHERE city = 'San Francisco'; +``` + +MySQL的查询优化器能够利用 `idx_city` 索引快速定位到符合条件的行,极大地提升了查询性能,避免了对 `user_profiles` 表的全表扫描。 + +### 5.4 JSON 类型使用最佳实践 + ++ **明确使用场景**:`JSON` 类型最适合用于存储非关键的、结构易变的元数据。核心的、关系紧密的数据,尤其是那些需要频繁进行 `JOIN` 操作或强制实施严格约束的字段,仍然应该使用传统的关系型列来存储。 + ++ **索引关键路径**:对于 `JSON` 文档中任何需要用于 `WHERE` 子句过滤、排序或分组的字段,都**必须**通过生成列为其创建索引。这是保证 `JSON` 类型高性能查询的前提。 + ++ **避免“上帝对象”**:切忌将一个应用的所有数据都塞进一个巨大的 `JSON` blob中。这样做无异于将MySQL当作一个简单的键值存储来使用,完全丧失了关系型数据库在数据完整性、查询优化和事务处理方面的强大优势。应将结构化数据规范化到独立的列和表中,仅将真正半结构化的部分存入 `JSON` 列。 + + +## 第六章:综合选型策略与总结 + +经过对MySQL主要数据类型的深入剖析,我们已经了解了每种类型的特性、优势和潜在的陷阱。本章旨在将这些知识整合为一个整体的、实用的选型策略,并总结在数据库设计中最常见的错误,以帮助开发者构建出健壮、高效且可维护的数据库模式。 + +### 6.1 综合选型策略:如何为你的应用设计最佳表结构 + +设计表结构时,可以遵循一个决策流程,对每个字段进行审慎的考量。这个过程可以被看作是回答一系列关于数据本质的问题: + +1. **数据是什么性质的?** + + + 是数字、文本、时间还是复杂的半结构化数据?这决定了你将在哪个大的数据类型类别中进行选择(数值、字符串、日期时间、JSON)。 + +2. **对于数值,它的范围和精度要求是什么?** + + + 它是否永远为非负数?如果是,立即添加 `UNSIGNED` 属性。 + + + 预估其可能的最大值是多少?根据这个值,从 `TINYINT`, `SMALLINT`, `MEDIUMINT`, `INT`, `BIGINT` 中选择最小的那个。例如,一个国家的总人口数可能需要 `BIGINT UNSIGNED`,而一个班级的人数 `TINYINT UNSIGNED` 就足够了。 + + + 它是否需要绝对精确,如货币?如果是,**必须**使用 `DECIMAL`。任何其他选择都是错误的。如果是科学测量值,`FLOAT` 或 `DOUBLE` 可能是合适的。 + +3. **对于字符串,它的长度特性是什么?** + + + 长度是否严格固定?例如MD5哈希值。如果是,`CHAR` 是一个合理的选择。 + + + 长度是否可变?这是绝大多数情况。选择 `VARCHAR`。 + + + 预估其最大长度是多少?为 `VARCHAR` 设置一个合理的上限,而不是盲目使用 `VARCHAR(255)`。 + + + 它是否可能超过64KB?如果答案是“是”,那么你可能需要 `MEDIUMTEXT` 或 `LONGTEXT`,但在此之前,要再次确认是否真的有必要,并充分意识到其对排序和临时表操作的潜在性能影响。 + +4. **对于时间,它代表什么意义?** + + + 它是一个全球统一的、绝对的时刻吗?(例如,日志条目、创建时间)如果是,选择 `TIMESTAMP`,并确保服务器时区设置为UTC。 + + + 它是一个与本地时区绑定的“约定”时间吗?(例如,生日、本地会议开始时间)如果是,选择 `DATETIME`。 + + + 是否需要存储1970年之前或2038年之后的时间?如果是,`TIMESTAMP` 将无法满足需求,必须使用 `DATETIME`。 + +5. **对于复杂或动态数据,它是否适合 `JSON`?** + + + 数据是否是结构化的,但结构经常变化或嵌套层次很深?`JSON` 是一个很好的候选者。 + + + 如果选择 `JSON`,哪些内部字段会成为查询条件?为这些字段通过生成列建立索引。 + + + 这些数据是否是应用的核心实体?如果是,应优先考虑将其规范化为传统的列和表,而不是全部塞进 `JSON`。 + + +### 6.2 常见数据类型选型错误汇总与规避方案 + +以下是在实际开发中反复出现的典型数据类型选型错误,以及如何规避它们的最终建议。 + ++ **错误1:过度宽容的类型定义 (Over-provisioning Types)** + + + **表现**:为只能容纳几百个值的状态字段使用 `INT`;为用户ID使用 `BIGINT`,而 `INT UNSIGNED` 已经可以支持超过40亿用户。 + + + **危害**:严重浪费存储空间和内存,降低了缓存效率,拖慢了整个系统。 + + + **规避方案**:严格遵循“最小化原则”。仔细分析每个字段的业务边界,选择恰好满足需求的最小数据类型 。   + ++ **错误2:使用浮点数处理货币 (Using `FLOAT` for Currency)** + + + **表现**:将商品价格、账户余额等字段定义为 `FLOAT` 或 `DOUBLE`。 + + + **危害**:导致不可预测的舍入误差,在聚合计算中误差会被放大,最终造成数据不一致和财务损失。这是一个致命的错误。 + + + **规避方案**:任何涉及精确计算的场景,**必须**使用 `DECIMAL` 或 `NUMERIC` 类型 。   + ++ **错误3:无差别使用 `TEXT` (Unnecessary Use of `TEXT`)** + + + **表现**:只要是长文本,就使用 `TEXT`,即使其长度远未达到 `VARCHAR` 的上限。 + + + **危害**:当涉及 `TEXT` 列的查询需要排序或分组时,可能强制MySQL使用基于磁盘的慢速临时表,造成严重的性能瓶颈 。   + + + **规避方案**:只要数据长度在65,535字节以内,就**始终优先使用 `VARCHAR`**。 + ++ **错误4:混淆 `DATETIME` 和 `TIMESTAMP` 的时区行为** + + + **表现**:在需要全球统一时间戳的应用中使用了 `DATETIME`,或者在需要存储本地“约定”时间时使用了 `TIMESTAMP`。 + + + **危害**:导致跨时区应用的时间显示混乱和逻辑错误。 + + + **规避方案**:深刻理解二者的核心区别——“全球时刻” vs “本地约定”。将服务器时区设为UTC,并根据业务需求审慎选择 。   + ++ **错误5:滥用 `ENUM` 和 `SET` (The `ENUM`/`SET` Maintenance Trap)** + + + **表现**:为那些业务逻辑上可能频繁变更的列表(如商品分类、用户标签)使用 `ENUM` 或 `SET`。 + + + **危害**:每次增加新选项都需要执行 `ALTER TABLE`,这在生产环境的大表上是高风险、高成本的操作,严重影响系统的灵活性和可维护性 。   + + + **规避方案**:对于可能变化的列表,使用一个独立的查找表(lookup table)和外键关联是更优的、更具扩展性的设计模式。 + ++ **错误6:在字符串中存储结构化数据 (Storing Structured Data in Strings)** + + + **表现**:将多个ID用逗号分隔存储在 `VARCHAR` 中(如 `'1,5,23'`);或者将键值对拼接成字符串。 + + + **危害**:完全破坏了关系型数据库的优势,使得查询、更新、数据校验和维护变得极其困难和低效,无法利用索引。 + + + **规避方案**:对于多对多关系,使用标准的关联表。对于半结构化的键值对数据,使用 `JSON` 类型并配合生成列索引。 + + +最终,数据类型的选择是数据库设计艺术与科学的结合。它要求开发者不仅要理解每种类型的技术规格,更要洞察其背后的业务逻辑和数据生命周期。通过遵循本指南中提出的原则和最佳实践,可以为构建高性能、高可靠性的MySQL应用打下坚实的基础。 \ No newline at end of file diff --git a/101-数据库学习/2-MySQL/9-题目/MySQL-基础考察题目.md b/101-数据库学习/2-MySQL/9-题目/MySQL-基础考察题目.md new file mode 100644 index 0000000..339fb6a --- /dev/null +++ b/101-数据库学习/2-MySQL/9-题目/MySQL-基础考察题目.md @@ -0,0 +1,700 @@ +# MySQL综合能力考核:大学信息系统项目 + +## 导言:大学数据库项目 - 一次实践性考核 + +欢迎参加本次MySQL综合能力考核。本次考核旨在通过一个完整的、贴近实际的项目,全面评估您对MySQL数据库的掌握程度。您将扮演一名数据库工程师,负责为一个新成立的大学设计、构建并管理其核心的学生信息系统数据库。 + +这个项目不仅仅是一系列孤立的SQL命令测试,它模拟了数据库开发的整个生命周期:从根据业务需求定义数据结构(DDL),到填充和维护数据(DML),再到从中提取有价值的信息(DQL),最后确保复杂操作的数据一致性(TCL)。 + +### 场景概述与核心业务规则 + +大学信息系统的核心实体及其业务规则如下: + ++ **讲师 (Instructors):** 每位讲师拥有唯一的ID、姓名、电子邮件和入职日期。电子邮件地址必须是唯一的。 + ++ **课程 (Courses):** 每门课程拥有唯一的ID、课程名称、学分,并且由一位讲师负责授课。 + ++ **学生 (Students):** 每位学生拥有唯一的ID、姓名、电子邮件、出生日期和专业。电子邮件地址必须是唯一的。 + ++ **选课记录 (Enrollments):** 这是连接学生和课程的关键。它记录了哪个学生选修了哪门课程,以及他们获得的分数。这体现了学生与课程之间的多对多关系。 + + +### 实体关系描述 (ERD) + ++ 一名 **讲师 (Instructor)** 可以教授多门 **课程 (Courses)**。(一对多关系) + ++ 一名 **学生 (Student)** 可以选修多门 **课程 (Courses)**。 + ++ 一门 **课程 (Course)** 可以被多名 **学生 (Student)** 选修。(学生与课程之间是多对多关系,因此需要`Enrollments`这个中间表来实现) + + +在开始编写任何SQL代码之前,深刻理解这些业务规则和实体间的关系至关重要。例如,“电子邮件必须唯一”这一业务规则将直接转化为数据库表中的`UNIQUE`约束;而“学生与课程的多对多关系”则决定了我们必须创建一个独立的`Enrollments`表,并通过外键将其与`Students`表和`Courses`表关联起来。这种从业务需求到数据库逻辑结构的转换,是数据库设计的核心思想,也是本次考核贯穿始终的考察重点。 + +现在,请根据以下题目,逐步完成这个大学数据库的构建与管理。 + +* * * + +## 第一部分: foundational Architecture - 数据定义语言 (DDL) + +本部分将考察您使用DDL创建和管理数据库基本结构的能力。一个设计良好、结构清晰的数据库是所有数据操作的基础。您将从零开始,构建起整个大学信息系统的骨架。 + +1. 创建一个名为 `university_db` 的新数据库,并设置其默认字符集为 `utf8mb4`。 + +2. 切换到 `university_db` 数据库。创建一个名为 `instructors` 的表,用于存储讲师信息。该表应包含以下列: + + + `instructor_id`: 整数类型,作为主键,自动增长。 + + + `name`: 变长字符串类型,最大长度100,不允许为空。 + + + `email`: 变长字符串类型,最大长度100,必须唯一,不允许为空。 + + + `hire_date`: 日期类型,不允许为空。 + +3. 创建一个名为 `courses` 的表,用于存储课程信息。该表应包含以下列: + + + `course_id`: 整数类型,作为主键,自动增长。 + + + `title`: 变长字符串类型,最大长度150,不允许为空。 + + + `credits`: 整数类型,不允许为空。 + + + `instructor_id`: 整数类型。此列将用于关联 `instructors` 表。 + +4. 创建一个名为 `students` 的表,用于存储学生信息。该表应包含以下列: + + + `student_id`: 整数类型,作为主键,自动增长。 + + + `name`: 变长字符串类型,最大长度100,不允许为空。 + + + `email`: 变长字符串类型,最大长度100,必须唯一,不允许为空。 + + + `date_of_birth`: 日期类型。 + + + `major`: 变长字符串类型,最大长度50。 + +5. 创建最关键的连接表 `enrollments`,用于记录学生的选课情况。该表的设计需要体现多对多关系。它应包含以下列: + + + `enrollment_id`: 整数类型,作为主键,自动增长。 + + + `student_id`: 整数类型,不允许为空。 + + + `course_id`: 整数类型,不允许为空。 + + + `grade`: 小数类型,总共3位,其中1位是小数(例如95.5)。 + + + **约束**: + + + `student_id` 和 `course_id` 的组合必须是唯一的,以防止同一学生重复选修同一门课程。 + + + `student_id` 列应作为外键,引用 `students` 表的 `student_id` 列。 + + + `course_id` 列应作为外键,引用 `courses` 表的 `course_id` 列。 + +6. 在 `students` 表中添加一个新的列 `phone_number`,类型为变长字符串,最大长度为20。 + +7. 修改 `courses` 表中的 `credits` 列,为其添加一个 `CHECK` 约束,确保其值必须大于0。 + +8. 为 `students` 表的 `major` 列添加一个 `DEFAULT` 约束,如果插入新学生时未指定专业,则默认为 'Undeclared'。 + +9. 现在,为 `courses` 表中的 `instructor_id` 列添加一个外键约束,使其引用 `instructors` 表的 `instructor_id` 列。 + +10. 清空 `enrollments` 表中的所有数据,但保留表结构。请写出执行此操作的命令。 + +11. 完整地删除 `instructors` 表。 + +12. 解释 `DELETE FROM table_name;`、`TRUNCATE TABLE table_name;` 和 `DROP TABLE table_name;` 这三个命令在功能、性能和使用场景上的主要区别。 + + +* * * + +## 第二部分:Data Population and Management - 数据操作语言 (DML) + +在数据库结构搭建完毕后,我们需要向其中填充数据,并根据业务变化进行维护。本部分将考察您使用DML语句进行数据插入、更新和删除的能力。您将亲身体验DDL中设置的约束是如何在DML操作中发挥作用的。 + +**准备工作:** 如果您在上一部分执行了第11题(删除了`instructors`表),请重新创建它,以确保后续操作可以正常进行。 + +13. 向 `instructors` 表中插入以下三位讲师的数据: + + + 姓名: '张磊', 邮箱: 'zhang.lei@university.edu', 入职日期: '2020-08-15' + + + 姓名: '李静', 邮箱: 'li.jing@university.edu', 入职日期: '2019-07-01' + + + 姓名: '王浩', 邮箱: 'wang.hao@university.edu', 入职日期: '2021-01-20' + +14. 使用一条 `INSERT` 语句向 `students` 表中插入以下两位学生的数据: + + + 姓名: '赵辰', 邮箱: 'zhao.chen@student.edu', 生日: '2002-05-10', 专业: 'Computer Science' + + + 姓名: '孙悦', 邮箱: 'sun.yue@student.edu', 生日: '2003-09-22', 专业: 'Data Science' + +15. 再向 `students` 表中插入一位学生,但不指定其专业,以测试默认值约束: + + + 姓名: '周毅', 邮箱: 'zhou.yi@student.edu', 生日: '2002-11-30' + +16. 根据 `instructors` 表中的数据,向 `courses` 表中插入以下课程。请确保 `instructor_id` 与讲师姓名对应正确(假设张磊ID为1,李静ID为2,王浩ID为3): + + + 课程名: 'Introduction to Programming', 学分: 4, 讲师ID: 1 + + + 课程名: 'Advanced Databases', 学分: 4, 讲师ID: 2 + + + 课程名: 'Web Development', 学分: 3, 讲师ID: 1 + + + 课程名: 'Machine Learning', 学分: 3, 讲师ID: 3 + +17. 为学生选课。向 `enrollments` 表中插入以下记录(假设赵辰ID为1,孙悦ID为2,周毅ID为3;课程ID从1开始): + + + 赵辰 选修 'Introduction to Programming',成绩为 92.5 + + + 赵辰 选修 'Advanced Databases',成绩为 88.0 + + + 孙悦 选修 'Introduction to Programming',成绩为 95.0 + + + 孙悦 选修 'Machine Learning',成绩为 97.5 + +18. **(挑战题)** 尝试向 `enrollments` 表中插入一条记录,其中 `student_id` 为 99(一个不存在的学生ID),`course_id` 为 1。执行该命令并解释为什么会失败。这个失败说明了什么? + +19. 学生“赵辰”的邮箱地址输入有误,请将其邮箱更新为 'zhao.chen.new@student.edu'。 + +20. 由于教学计划调整,'Web Development' 这门课程的授课教师需要更换为“李静”(假设其ID为2)。请更新 `courses` 表。 + +21. 学校决定将所有 'Computer Science' 专业的学生的专业名称更新为 'Computer Science and Technology'。请执行此批量更新操作。 + +22. 学生“孙悦”决定退选 'Machine Learning' 这门课。请从 `enrollments` 表中删除对应的选课记录。 + +23. **(挑战题)** 尝试从 `instructors` 表中删除讲师“张磊”(假设其ID为1)。执行该命令并解释为什么它很可能会失败。 + +24. 承接上一题,如果要成功删除讲师“张磊”,需要先执行哪些操作?请描述正确的操作步骤。 + + +* * * + +## 第三部分:Information Retrieval and Analysis - 数据查询语言 (DQL) + +数据库的核心价值在于能够快速、准确地提取所需信息。本部分是考核的重点,将全面检验您使用DQL从简单查询到复杂分析的能力。所有查询都基于您在第二部分中插入的数据。 + +### A. 基础查询 (10道) + +25. 查询 `students` 表中的所有学生信息。 + +26. 查询 `instructors` 表中所有讲师的姓名和邮箱地址。 + +27. 查询 `courses` 表中学分大于3的所有课程的完整信息。 + +28. 查询所有专业为 'Data Science' 的学生姓名和出生日期。 + +29. 查询课程名称中包含 'Introduction' 关键字的所有课程。 + +30. 查询所有姓“张”的讲师信息。 + +31. 查询在2020年1月1日之后入职的讲师姓名和入职日期。 + +32. 查询专业为 'Computer Science and Technology' 或 'Data Science' 的所有学生信息。 + +33. 查询 `enrollments` 表中所有成绩在85到95分之间(包含85和95)的选课记录。 + +34. 查询所有课程信息,并按学分从高到低排序。如果学分相同,则按课程名称的字母顺序排序。 + + +### B. 聚合函数与分组查询 (10道) + +35. 统计 `students` 表中共有多少名学生。 + +36. 计算 `courses` 表中所有课程的平均学分。 + +37. 找出 `enrollments` 表中记录的最高成绩。 + +38. 统计每个专业(major)分别有多少名学生。 + +39. 计算每门课程的平均成绩。查询结果应显示课程ID和对应的平均分。 + +40. 统计每位讲师(instructor)负责教授的课程数量。查询结果应显示讲师ID和课程数量。 + +41. 找出选修课程数量超过1门的学生的ID。 + +42. 查询每个专业的学生中,年龄最小的学生的出生日期。 + +43. 统计所有课程中,总共有多少名学生进行了选课(同一个学生选多门课算多次)。 + +44. 找出平均成绩最高的课程的ID。 + + +### C. 连接查询 (10道) + +45. 使用 `INNER JOIN` 查询所有学生的姓名以及他们选修的课程名称。 + +46. 查询所有选课记录,并同时显示学生的姓名、课程的名称以及该课程的分数。 + +47. 查询讲师“张磊”所教授的所有课程的名称和学分。 + +48. 列出所有学生及其选修课程的授课讲师的姓名。查询结果应包含三列:学生姓名, 课程名称, 讲师姓名。 + +49. **(数据准备)** 为了测试 `LEFT JOIN`,请先插入一位没有选修任何课程的新学生: + + SQL + + ``` + INSERT INTO students (name, email, date_of_birth, major) VALUES ('钱坤', 'qian.kun@student.edu', '2004-01-15', 'Physics'); + ``` + + 现在,使用 `LEFT JOIN` 查询所有学生的姓名以及他们选修的课程名称。确保那位没有选课的学生(钱坤)也出现在结果中。 + +50. **(数据准备)** 为了测试 `RIGHT JOIN`,请先插入一门没有任何学生选修的新课程: + + SQL + + ``` + INSERT INTO courses (title, credits, instructor_id) VALUES ('Quantum Physics', 4, 2); + ``` + + 现在,查询所有课程的名称以及选修该课程的学生人数。确保那门没有学生选修的课程也出现在结果中,且学生人数为0。 + +51. 查询所有选修了学分等于4的课程的学生姓名,并去除重复的姓名。 + +52. 查询所有成绩高于90分的学生姓名和他们所选的课程名称。 + +53. 查询与“赵辰”在同一个专业的所有其他学生的姓名。 + +54. 列出所有讲师的姓名以及他们所教授课程的平均成绩。 + + +### D. 高阶查询 (含`HAVING`子句与子查询) (6道) + +55. 使用 `GROUP BY` 和 `HAVING` 子句,找出选修人数超过1人的所有课程的ID和选修人数。 + +56. 解释 `WHERE` 子句和 `HAVING` 子句在功能和使用时机上的核心区别。 + +57. 使用子查询,找出由讲师“李静”教授的所有课程的ID。然后基于这个ID列表,在 `enrollments` 表中找出所有选修了这些课程的学生的ID。 + +58. 使用子查询,查询选修了 'Advanced Databases' 这门课的所有学生的姓名。 + +59. 查询比所有 'Data Science' 专业学生的平均年龄都要大的学生姓名。(提示:需要计算 'Data Science' 学生的平均出生日期,然后进行比较)。 + +60. 查询每门课程中获得最高分的学生的姓名和该课程的名称。 + + +* * * + +## 第四部分:Ensuring Consistency - 事务控制语言 (TCL) + +在真实的数据库应用中,一系列操作往往需要被视为一个不可分割的整体。例如,银行转账包含“A账户扣款”和“B账户收款”两个步骤,必须同时成功或同时失败。本部分将通过场景题,考察您使用TCL来保证复杂业务逻辑下数据一致性的能力。 + +61. **场景一:成功注册** 学生“周毅”(ID为3)希望注册选修“Web Development”(ID为3)这门课程。这个注册过程包含两个步骤: a. 在 `enrollments` 表中插入一条新的选课记录(成绩暂时为NULL)。 b. 假设学校有一个 `student_accounts` 表(我们在此不创建,仅作为逻辑假设),需要更新该学生的账户余额,扣除相应学费。 请编写一个事务,将步骤a的操作包含在内。由于操作会成功,最终提交该事务。 + +62. 在上题的事务中,`COMMIT` 命令起到了什么作用? + +63. 为什么将这两个步骤(或更多步骤)捆绑在一个事务中是至关重要的? + +64. 启动一个事务的SQL命令是什么? + +65. **场景二:注册失败与回滚** 学生“钱坤”(ID为4)尝试注册“Machine Learning”(ID为4)课程,但系统检查发现他未满足前置课程要求,因此注册失败。整个操作流程如下: a. 开始一个事务。 b. 在 `enrollments` 表中插入一条选课记录。 c. 系统检查前置课程,发现不满足条件,因此决定中止操作。 请编写这个事务,并在操作中止时使用 `ROLLBACK` 命令,确保数据库状态恢复到事务开始之前。 + +66. 在上题的场景中,`ROLLBACK` 命令具体做了什么? + +67. 如果在执行 `ROLLBACK` 之前,没有使用 `START TRANSACTION` 或等效命令开启事务,会发生什么? + +68. **场景三:带保存点的部分回滚** 一位管理员正在为学生“孙悦”(ID为2)批量注册三门课程。流程如下: a. 开始一个事务。 b. 成功注册第一门课 'Advanced Databases' (ID 2)。 c. 创建一个保存点,名为 `savepoint_1`。 d. 成功注册第二门课 'Web Development' (ID 3)。 e. 创建一个保存点,名为 `savepoint_2`。 f. 尝试注册第三门课 'Quantum Physics' (ID 5),但发现该课程已满员,注册失败。 管理员决定保留前两次成功的注册,但撤销第三次失败的尝试。请编写SQL序列,使用 `SAVEPOINT` 和 `ROLLBACK TO` 来实现这个需求,并最终提交前两次成功的注册。 + +69. 在上题中,`ROLLBACK TO savepoint_2;` 命令执行后,数据库处于什么状态? + +70. 为什么在这种多步操作中,使用 `SAVEPOINT` 比直接使用 `ROLLBACK` 更加灵活? + +71. `AUTOCOMMIT` 是MySQL中的一个重要概念。请解释它的含义,以及它与我们手动执行 `START TRANSACTION`、`COMMIT`、`ROLLBACK` 之间的关系。 + + +* * * + +## 附录:综合能力考核答案 + +### 第一部分:DDL答案 + +1. SQL + + ``` + CREATE DATABASE university_db CHARACTER SET utf8mb4; + ``` + +2. SQL + + ``` + USE university_db; + CREATE TABLE instructors ( + instructor_id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE, + hire_date DATE NOT NULL + ); + ``` + +3. SQL + + ``` + CREATE TABLE courses ( + course_id INT PRIMARY KEY AUTO_INCREMENT, + title VARCHAR(150) NOT NULL, + credits INT NOT NULL, + instructor_id INT + ); + ``` + +4. SQL + + ``` + CREATE TABLE students ( + student_id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE, + date_of_birth DATE, + major VARCHAR(50) + ); + ``` + +5. SQL + + ``` + CREATE TABLE enrollments ( + enrollment_id INT PRIMARY KEY AUTO_INCREMENT, + student_id INT NOT NULL, + course_id INT NOT NULL, + grade DECIMAL(3, 1), + UNIQUE (student_id, course_id), + FOREIGN KEY (student_id) REFERENCES students(student_id), + FOREIGN KEY (course_id) REFERENCES courses(course_id) + ); + ``` + +6. SQL + + ``` + ALTER TABLE students ADD COLUMN phone_number VARCHAR(20); + ``` + +7. SQL + + ``` + ALTER TABLE courses ADD CONSTRAINT chk_credits CHECK (credits > 0); + ``` + +8. SQL + + ``` + ALTER TABLE students ALTER COLUMN major SET DEFAULT 'Undeclared'; + ``` + +9. SQL + + ``` + ALTER TABLE courses ADD CONSTRAINT fk_instructor + FOREIGN KEY (instructor_id) REFERENCES instructors(instructor_id); + ``` + +10. SQL + +``` +TRUNCATE TABLE enrollments; +``` + +11. SQL + +``` +DROP TABLE instructors; +``` + +12. **`DELETE FROM table_name;`**: + ++ **功能**: 逐行删除表中的数据,可以与 `WHERE` 子句配合使用删除特定行。 + ++ **性能**: 相对较慢,因为它会为每一行记录删除日志。 + ++ **特点**: 触发`DELETE`触发器;不重置自增ID。 **`TRUNCATE TABLE table_name;`**: + ++ **功能**: 快速删除表中的所有行。 + ++ **性能**: 非常快,因为它通过释放数据页来清空表,而不是逐行删除。 + ++ **特点**: 不触发`DELETE`触发器;通常会重置自增ID;不能与`WHERE`子句一起使用。 **`DROP TABLE table_name;`**: + ++ **功能**: 完全删除整个表,包括表结构、数据、索引、约束和触发器。 + ++ **性能**: 非常快。 + ++ **特点**: 表将不复存在,需要`CREATE TABLE`才能重新使用。这是最具破坏性的操作。 + + +### 第二部分:DML答案 + +13. SQL + + ``` + INSERT INTO instructors (name, email, hire_date) VALUES + ('张磊', 'zhang.lei@university.edu', '2020-08-15'), + ('李静', 'li.jing@university.edu', '2019-07-01'), + ('王浩', 'wang.hao@university.edu', '2021-01-20'); + ``` + +14. SQL + + ``` + INSERT INTO students (name, email, date_of_birth, major) VALUES + ('赵辰', 'zhao.chen@student.edu', '2002-05-10', 'Computer Science'), + ('孙悦', 'sun.yue@student.edu', '2003-09-22', 'Data Science'); + ``` + +15. SQL + + ``` + INSERT INTO students (name, email, date_of_birth) VALUES + ('周毅', 'zhou.yi@student.edu', '2002-11-30'); + -- 查询后会发现他的major字段值为'Undeclared' + ``` + +16. SQL + + ``` + INSERT INTO courses (title, credits, instructor_id) VALUES + ('Introduction to Programming', 4, 1), + ('Advanced Databases', 4, 2), + ('Web Development', 3, 1), + ('Machine Learning', 3, 3); + ``` + +17. SQL + + ``` + INSERT INTO enrollments (student_id, course_id, grade) VALUES + (1, 1, 92.5), + (1, 2, 88.0), + (2, 1, 95.0), + (2, 4, 97.5); + ``` + +18. SQL + + ``` + INSERT INTO enrollments (student_id, course_id) VALUES (99, 1); + ``` + + **解释**: 该命令会失败,并报告一个外键约束错误 (`FOREIGN KEY constraint fails`)。 **原因**: 因为我们在 `enrollments` 表的 `student_id` 列上定义了一个外键,它引用了 `students` 表的 `student_id` 列。这意味着任何试图插入到 `enrollments` 表的 `student_id` 的值,都必须首先存在于 `students` 表中。由于 `students` 表中不存在ID为99的学生,数据库为了维护引用完整性,拒绝了这次插入操作。这完美地展示了DDL中定义的约束是如何保证数据一致性和有效性的。 + +19. SQL + + ``` + UPDATE students SET email = 'zhao.chen.new@student.edu' WHERE name = '赵辰'; + ``` + +20. SQL + + ``` + UPDATE courses SET instructor_id = 2 WHERE title = 'Web Development'; + ``` + +21. SQL + + ``` + UPDATE students SET major = 'Computer Science and Technology' WHERE major = 'Computer Science'; + ``` + +22. SQL + + ``` + DELETE FROM enrollments WHERE student_id = 2 AND course_id = 4; + ``` + +23. SQL + + ``` + DELETE FROM instructors WHERE instructor_id = 1; + ``` + + **解释**: 该命令很可能会失败,并报告一个外键约束错误。 **原因**: 因为讲师“张磊”(ID为1)在 `courses` 表中被引用了('Introduction to Programming' 和 'Web Development' 这两门课的 `instructor_id` 都是1)。为了保护数据的引用完整性,数据库禁止删除一个正在被其他表引用的记录。 + +24. **正确操作步骤**: + + 1. 必须先处理掉 `courses` 表中对讲师“张磊”的引用。有两种方式: + + + **方式一(重新分配课程)**: 将他教授的课程的 `instructor_id` 更新为另一位讲师的ID,或者设置为 `NULL`(如果该列允许为NULL)。 + + + **方式二(删除课程)**: 删除所有由他教授的课程(但这可能会引发连锁问题,因为课程可能已被学生选修)。 + + 2. 在解除了所有外键引用之后,才能安全地从 `instructors` 表中删除讲师“张磊”的记录。 例如,先执行更新:`UPDATE courses SET instructor_id = NULL WHERE instructor_id = 1;` 然后执行删除:`DELETE FROM instructors WHERE instructor_id = 1;` + + +### 第三部分:DQL答案 + +25. `SELECT * FROM students;` + +26. `SELECT name, email FROM instructors;` + +27. `SELECT * FROM courses WHERE credits > 3;` + +28. `SELECT name, date_of_birth FROM students WHERE major = 'Data Science';` + +29. `SELECT * FROM courses WHERE title LIKE '%Introduction%';` + +30. `SELECT * FROM instructors WHERE name LIKE '张%';` + +31. `SELECT name, hire_date FROM instructors WHERE hire_date > '2020-01-01';` + +32. `SELECT * FROM students WHERE major IN ('Computer Science and Technology', 'Data Science');` + +33. `SELECT * FROM enrollments WHERE grade BETWEEN 85 AND 95;` + +34. `SELECT * FROM courses ORDER BY credits DESC, title ASC;` + +35. `SELECT COUNT(*) FROM students;` + +36. `SELECT AVG(credits) FROM courses;` + +37. `SELECT MAX(grade) FROM enrollments;` + +38. `SELECT major, COUNT(*) FROM students GROUP BY major;` + +39. `SELECT course_id, AVG(grade) FROM enrollments GROUP BY course_id;` + +40. `SELECT instructor_id, COUNT(*) FROM courses GROUP BY instructor_id;` + +41. `SELECT student_id FROM enrollments GROUP BY student_id HAVING COUNT(course_id) > 1;` + +42. `SELECT major, MAX(date_of_birth) FROM students GROUP BY major;` -- 年龄最小,出生日期最大 + +43. `SELECT COUNT(*) FROM enrollments;` + +44. `SELECT course_id FROM enrollments GROUP BY course_id ORDER BY AVG(grade) DESC LIMIT 1;` + +45. `SELECT s.name, c.title FROM students s INNER JOIN enrollments e ON s.student_id = e.student_id INNER JOIN courses c ON e.course_id = c.course_id;` + +46. `SELECT s.name, c.title, e.grade FROM students s JOIN enrollments e ON s.student_id = e.student_id JOIN courses c ON e.course_id = c.course_id;` + +47. `SELECT c.title, c.credits FROM courses c JOIN instructors i ON c.instructor_id = i.instructor_id WHERE i.name = '张磊';` + +48. `SELECT s.name AS student_name, c.title AS course_title, i.name AS instructor_name FROM students s JOIN enrollments e ON s.student_id = e.student_id JOIN courses c ON e.course_id = c.course_id JOIN instructors i ON c.instructor_id = i.instructor_id;` + +49. `SELECT s.name, c.title FROM students s LEFT JOIN enrollments e ON s.student_id = e.student_id LEFT JOIN courses c ON e.course_id = c.course_id;` + +50. `SELECT c.title, COUNT(e.student_id) AS number_of_students FROM courses c LEFT JOIN enrollments e ON c.course_id = e.course_id GROUP BY c.course_id, c.title;` + +51. `SELECT DISTINCT s.name FROM students s JOIN enrollments e ON s.student_id = e.student_id JOIN courses c ON e.course_id = c.course_id WHERE c.credits = 4;` + +52. `SELECT s.name, c.title FROM students s JOIN enrollments e ON s.student_id = e.student_id JOIN courses c ON e.course_id = c.course_id WHERE e.grade > 90;` + +53. `SELECT s2.name FROM students s1 JOIN students s2 ON s1.major = s2.major WHERE s1.name = '赵辰' AND s2.name!= '赵辰';` + +54. `SELECT i.name, AVG(e.grade) AS average_grade FROM instructors i JOIN courses c ON i.instructor_id = c.instructor_id JOIN enrollments e ON c.course_id = e.course_id GROUP BY i.instructor_id, i.name;` + +55. `SELECT course_id, COUNT(student_id) AS num_students FROM enrollments GROUP BY course_id HAVING COUNT(student_id) > 1;` + +56. **`WHERE` vs `HAVING`**: + + + **作用对象不同**: `WHERE` 子句作用于原始的表数据,在数据分组(`GROUP BY`)之前进行过滤。`HAVING` 子句作用于 `GROUP BY` 之后生成的结果集,对分组后的聚合结果进行过滤。 + + + **使用位置不同**: `WHERE` 必须在 `GROUP BY` 之前,`HAVING` 必须在 `GROUP BY` 之后。 + + + **可使用函数不同**: `WHERE` 子句中不能使用聚合函数(如 `COUNT()`, `AVG()`),而 `HAVING` 子句专门用于配合聚合函数进行条件判断。 + +57. SQL + + ``` + SELECT student_id FROM enrollments + WHERE course_id IN ( + SELECT course_id FROM courses + WHERE instructor_id = ( + SELECT instructor_id FROM instructors WHERE name = '李静' + ) + ); + ``` + +58. SQL + + ``` + SELECT name FROM students + WHERE student_id IN ( + SELECT student_id FROM enrollments + WHERE course_id = ( + SELECT course_id FROM courses WHERE title = 'Advanced Databases' + ) + ); + ``` + +59. SQL + + ``` + SELECT name FROM students + WHERE date_of_birth < ( + SELECT AVG(date_of_birth) FROM students WHERE major = 'Data Science' + ); + ``` + +60. SQL + + ``` + SELECT s.name, c.title + FROM enrollments e + JOIN students s ON e.student_id = s.student_id + JOIN courses c ON e.course_id = c.course_id + WHERE (e.course_id, e.grade) IN ( + SELECT course_id, MAX(grade) + FROM enrollments + GROUP BY course_id + ); + ``` + + +### 第四部分:TCL答案 + +61. SQL + + ``` + START TRANSACTION; + -- 步骤a: 插入选课记录 + INSERT INTO enrollments (student_id, course_id, grade) VALUES (3, 3, NULL); + -- 步骤b: 假设的更新账户操作 + -- UPDATE student_accounts SET balance = balance - 100 WHERE student_id = 3; + COMMIT; + ``` + +62. `COMMIT` 命令将事务中执行的所有更改(在此例中是 `INSERT` 语句)永久性地保存到数据库中。一旦提交,这些更改就成为数据库持久状态的一部分,并且对其他用户可见。 + +63. 事务的重要性在于保证**原子性(Atomicity)**。注册课程这个业务逻辑包含多个数据库操作,它们必须作为一个整体执行。如果只有插入选课记录成功,而扣费失败,那么数据就会处于不一致的状态。事务确保了这一系列操作要么全部成功(`COMMIT`),要么全部失败(`ROLLBACK`),从而维护了业务逻辑和数据的完整性。 + +64. `START TRANSACTION;` (或者它的同义词 `BEGIN;`) + +65. SQL + + ``` + START TRANSACTION; + -- 尝试插入选课记录 + INSERT INTO enrollments (student_id, course_id) VALUES (4, 4); + -- 此时,应用逻辑发现前置课程不满足,决定中止 + ROLLBACK; + ``` + +66. `ROLLBACK` 命令会撤销当前事务中自 `START TRANSACTION` 以来所做的所有未提交的更改。在上题中,它会撤销那条 `INSERT` 语句,使得 `enrollments` 表恢复到事务开始前的状态,就好像那条 `INSERT` 语句从未执行过一样。 + +67. 如果MySQL的 `AUTOCOMMIT` 模式是开启的(默认情况),那么每一条SQL语句都会被视为一个独立的事务并被立即自动提交。在这种情况下,`ROLLBACK` 命令将不起任何作用,因为在它执行之前,前面的 `INSERT` 语句已经永久保存了。因此,要使用 `ROLLBACK`,必须先用 `START TRANSACTION` 显式地开启一个事务。 + +68. SQL + + ``` + START TRANSACTION; + -- 注册第一门课 + INSERT INTO enrollments (student_id, course_id) VALUES (2, 2); + SAVEPOINT savepoint_1; + -- 注册第二门课 + INSERT INTO enrollments (student_id, course_id) VALUES (2, 3); + SAVEPOINT savepoint_2; + -- 尝试注册第三门课 (此操作在真实场景中会失败,这里我们用回滚模拟) + -- INSERT INTO enrollments (student_id, course_id) VALUES (2, 5); -- 假设失败 + -- 发现失败,回滚到第二个保存点 + ROLLBACK TO savepoint_2; + -- 最终提交事务,保留前两次成功的注册 + COMMIT; + ``` + +69. 执行 `ROLLBACK TO savepoint_2;` 之后,数据库的状态是:为孙悦注册第二门课的操作(`INSERT` course\_id 3)被撤销了,但注册第一门课的操作(`INSERT` course\_id 2)仍然保留在当前事务中,等待最终的 `COMMIT` 或 `ROLLBACK`。 + +70. `SAVEPOINT` 提供了更细粒度的事务控制。在一个长事务中,如果某个中间步骤失败,我们不必撤销整个事务(`ROLLBACK`),而是可以选择性地回滚到某个已知的良好状态(`ROLLBACK TO SAVEPOINT`),然后继续执行其他操作或提交部分结果。这极大地增强了处理复杂业务逻辑时的灵活性。 + +71. **`AUTOCOMMIT`** 是MySQL的一个系统变量,它控制着事务的提交方式。 + + + **当 `AUTOCOMMIT = 1` (默认值)**: 每一条SQL语句(如 `INSERT`, `UPDATE`, `DELETE`)在执行完毕后都会被立即自动提交,效果等同于在每条语句前后都加上了 `START TRANSACTION` 和 `COMMIT`。 + + + **当 `AUTOCOMMIT = 0`**: 自动提交被禁用。你需要手动执行 `START TRANSACTION` 来开启一个事务,并通过 `COMMIT` 或 `ROLLBACK` 来结束它。在手动结束之前,所有的更改都只在当前会话中可见,并且是未提交的状态。 **关系**: 当我们使用 `START TRANSACTION` 时,它会暂时覆盖当前的 `AUTOCOMMIT` 行为,创建一个手动控制的事务块。在这个块内,只有 `COMMIT` 或 `ROLLBACK` 才能结束事务,`AUTOCOMMIT` 的设置被忽略。这使得我们能够将多条语句捆绑在一起,实现TCL的核心功能。 \ No newline at end of file diff --git a/101-数据库学习/2-MySQL/prompt.md b/101-数据库学习/2-MySQL/prompt.md index 6cff3bb..9dde093 100644 --- a/101-数据库学习/2-MySQL/prompt.md +++ b/101-数据库学习/2-MySQL/prompt.md @@ -1 +1,11 @@ -请针对 \ No newline at end of file +请依照基础[agi_mysql_study.md](agi_mysql_study.md)的要求,修改[0-基础数据结构.md](0-%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.md)中的内容,给出完整的MySQL基础数据结构用法的文档 + + + +## SQLite 题目 +请根据[agi_mysql_study.md](agi_mysql_study.md)的规范,作为一名专业的SQL老师,设计从数据格式,涵盖DDL DML DQL TCL的一系列题目,考察一名初学者对于MySQL学习的掌握能力 +每道题目需要有序号 +题目设计保持上下文连贯,如DQL基于DML插入的数值,DDL创建的表结构。如果数据不够,请在题目中给出插入数据命令 +每个大模块的题目,不少于10道 +DQL需要给出至少30道题目,需要包含inner join, group by, having等高阶查询方法 +要求答案放置于最后面,与前面的题目序号一一对应 \ No newline at end of file diff --git a/101-数据库学习/3-SQLite/9-题目/SQLite基础考察.md b/101-数据库学习/3-SQLite/9-题目/SQLite基础考察.md new file mode 100644 index 0000000..b68b918 --- /dev/null +++ b/101-数据库学习/3-SQLite/9-题目/SQLite基础考察.md @@ -0,0 +1,640 @@ +# SQLite综合能力考核:大学数据库管理实战 + +## 介绍:大学数据库场景 + +为了全面考察您对SQLite的掌握程度,我们将围绕一个清晰、易于理解的场景来设计所有练习。这种叙事性的方法不仅能提升学习的趣味性,还能帮助您理解SQL命令在实际工作中的应用。 + +**场景描述:** 您将为一所小型大学设计并管理一个数据库。该数据库需要追踪讲师信息、他们教授的课程、注册课程的学生以及学生的选课和成绩详情。这是一个经典的关系模型,非常适合用来测试DDL(数据定义语言)、DML(数据操作语言)、DQL(数据查询语言)和TCL(事务控制语言)的各项概念。 + +**实体与关系:** + ++ **讲师 (Instructors):** 每位讲师拥有姓名、邮箱和办公室编号。 + ++ **课程 (Courses):** 每门课程拥有课程名称、所属系别、学分,并由一位讲师授课。 + ++ **学生 (Students):** 每位学生拥有姓名、邮箱和专业。 + ++ **选课记录 (Enrollments):** 这是一个连接表,用于关联学生和课程,并记录学生在特定课程中获得的成绩。 + + +这个场景经过精心选择,因为它天然地包含了**一对多关系**(一位讲师可以教授多门课程)和**多对多关系**(一名学生可以选修多门课程,一门课程也可以被多名学生选修),为测试连接查询和参照完整性提供了坚实的基础。 + +* * * + +## 第一部分:DDL (数据定义语言) - 构建数据库基础 + +本部分旨在考察您将业务需求转化为逻辑数据库结构的能力。这不仅考验您的DDL命令语法,还涉及数据规范化和数据完整性等设计原则。我们将重点考察 `CREATE TABLE`、数据类型与类型亲和性、`PRIMARY KEY`、`FOREIGN KEY`、`NOT NULL`、`UNIQUE`、`DEFAULT` 以及 `ALTER TABLE` 等命令。 + +**题目列表:** + +1. 在SQLite中,外键约束默认是关闭的,这可能导致数据完整性问题 。为了确保后续操作的参照完整性,请写出为当前数据库连接开启外键约束强制执行的命令。   + +2. 创建一个名为 `Instructors` 的表,用于存储讲师信息。该表应包含以下列: + + + `instructor_id`: 整数类型,作为主键且能自动增长。 + + + `first_name`: 文本类型,不能为空。 + + + `last_name`: 文本类型,不能为空。 + + + `email`: 文本类型,不能为空,且必须保证其值唯一。 + +3. 创建一个名为 `Students` 的表,用于存储学生信息。该表应包含以下列: + + + `student_id`: 整数类型,作为主键。 + + + `first_name`: 文本类型,不能为空。 + + + `last_name`: 文本类型,不能为空。 + + + `email`: 文本类型,不能为空,且值唯一。 + + + `major`: 文本类型,如果未指定,则默认值为 'Undeclared'。 + +4. 创建一个名为 `Courses` 的表,用于存储课程信息。该表应包含以下列: + + + `course_id`: 整数类型,作为主键。 + + + `title`: 文本类型,不能为空。 + + + `department`: 文本类型,不能为空(例如 'CSCI')。 + + + `credits`: 整数类型。 + + + `instructor_id`: 整数类型,该列应作为外键,引用 `Instructors` 表的 `instructor_id` 列。 + +5. 修改 `Courses` 表中 `instructor_id` 的外键约束。要求当 `Instructors` 表中的某位讲师记录被删除时,`Courses` 表中由该讲师授课的课程的 `instructor_id` 字段应自动被设置为 `NULL` 。   + +6. 创建一个名为 `Enrollments` 的连接表,用于记录学生选修课程的情况。该表应包含: + + + `student_id`: 整数类型。 + + + `course_id`: 整数类型。 + + + `grade`: 文本类型,用于存储成绩(如 'A', 'B+', 'C' 等)。 + + + 将 `student_id` 和 `course_id` 的组合设置为主键(复合主键)。   + +假设学校规定,每个学生最多只能选修 5 门课程。仅在数据库层面(不依赖应用程序代码),我们如何实现这个约束? + +(提示:查阅 SQLite 关于 CHECK 约束和子查询的文档,思考一个创造性的解决方案。) + +7. 在 `Enrollments` 表上定义外键约束: + + + `student_id` 列引用 `Students` 表的 `student_id` 列。 + + + `course_id` 列引用 `Courses` 表的 `course_id` 列。 + +8. 修改 `Enrollments` 表的外键约束。要求当 `Students` 表中的某个学生记录被删除时,该学生所有的选课记录也应自动从 `Enrollments` 表中被删除(级联删除)。   + +9. 使用 `ALTER TABLE` 命令为 `Instructors` 表添加一个新列,名为 `office_number`,类型为 `TEXT`。 + +10. 使用 `ALTER TABLE` 命令将 `Enrollments` 表重命名为 `Student_Enrollments`。 + +11. 创建一个名为 `Temp_Table` 的临时表(列定义不限),然后写出删除该表的命令。 + + +* * * + +## 第二部分:DML (数据操作语言) - 填充与修改数据 + +本部分旨在考察您能否正确地插入、更新和删除数据,同时遵守在DDL阶段定义的各项约束。DML操作的成功与否,将直接反映您DDL设计的正确性。 + +**题目列表:** + +1. 向 `Instructors` 表中插入以下三位讲师的数据: + + + (1, 'Alan', 'Turing', 'alan.turing@bletchley.edu', 'B-101') + + + (2, 'Grace', 'Hopper', 'grace.hopper@yale.edu', 'C-203') + + + (3, 'John', 'von Neumann', 'john.vn@ias.edu', 'A-305') + +2. 向 `Students` 表中插入至少五名学生的数据,确保他们的 `student_id` 从101开始,并拥有不同的专业。 + + SQL + + ``` + INSERT INTO Students (student_id, first_name, last_name, email, major) VALUES + (101, 'Alice', 'Smith', 'alice.smith@university.edu', 'Computer Science'), + (102, 'Bob', 'Johnson', 'bob.johnson@university.edu', 'Physics'), + (103, 'Charlie', 'Brown', 'charlie.brown@university.edu', 'History'), + (104, 'Diana', 'Prince', 'diana.prince@university.edu', 'Computer Science'), + (105, 'Eve', 'Adams', 'eve.adams@university.edu', 'Physics'); + ``` + +3. 根据第一步插入的讲师ID,向 `Courses` 表中插入以下课程数据: + + SQL + + ``` + INSERT INTO Courses (course_id, title, department, credits, instructor_id) VALUES + (201, 'Introduction to CS', 'CSCI', 3, 1), + (202, 'Advanced Compilers', 'CSCI', 4, 2), + (203, 'Game Theory', 'MATH', 3, 3), + (204, 'Databases', 'CSCI', 4, 1), + (205, 'Quantum Mechanics', 'PHYS', 4, NULL); + ``` + +4. 向 `Student_Enrollments` 表(注意,该表已在DDL部分被重命名)中插入以下选课记录: + + SQL + + ``` + INSERT INTO Student_Enrollments (student_id, course_id, grade) VALUES + (101, 201, 'A'), + (101, 204, 'B+'), + (102, 205, 'A-'), + (103, 203, 'B'), + (104, 201, 'C'), + (104, 202, 'A'); + ``` + +5. **测试 `NOT NULL` 约束**:尝试向 `Instructors` 表中插入一条记录,但 `first_name` 字段留空。此操作预期会失败。请写出这条 `INSERT` 语句。 + +6. **测试 `UNIQUE` 约束**:尝试向 `Students` 表中插入一名新学生,其 `email` 与 'alice.smith@university.edu' 相同。此操作预期会因违反唯一性约束而失败。请写出这条 `INSERT` 语句。 + +7. **测试 `FOREIGN KEY` 约束**:尝试向 `Courses` 表中插入一门新课程,其 `instructor_id` 为999,而这个ID在 `Instructors` 表中并不存在。此操作预期会因违反外键约束而失败。请写出这条 `INSERT` 语句。 + +8. Alan Turing博士更换了办公室。请使用 `UPDATE` 语句,将他的 `office_number` 更新为 'B-105'。 + +9. 学校决定将 'CSCI' 系的缩写统一更改为 'COMP'。请使用 `UPDATE` 语句更新 `Courses` 表中所有 'CSCI' 系的课程。 + +**测试 `连表查询`** 查询学生'Bob Johnson' (student_id=102) 选取的所有课程信息 + +10. **测试 `ON DELETE CASCADE`**:学生 'Bob Johnson' (student\_id=102) 退学。请从 `Students` 表中删除他的记录。删除后,请查询 `Student_Enrollments` 表,验证他的选课记录是否也已被自动删除。 + +11. **测试 `ON DELETE SET NULL`**:讲师 'Grace Hopper' (instructor\_id=2) 离职。请从 `Instructors` 表中删除她的记录。删除后,请查询 `Courses` 表,验证她所教授课程的 `instructor_id` 是否已变为 `NULL`。 + +12. 删除 `Student_Enrollments` 表中所有成绩为 'C' 的记录。 + + +* * * + +## 第三部分:DQL (数据查询语言) - 查询与分析数据 + +本部分将全面考察您从数据库中检索和分析数据的能力,从简单的单表查询逐步过渡到复杂的多表聚合查询。 + +**题目列表:** + +#### 基础检索 (1-10) + +1. 查询并显示 `Students` 表中的所有列及所有行。 + +2. 查询所有讲师的姓(`last_name`)和名(`first_name`)。 + +3. 查询所有属于 'COMP' 系(假设已在DML部分更新)的课程名称(`title`)。 + +4. 查询所有专业为 'Physics' 的学生信息,并按姓氏(`last_name`)升序排列。 + +5. 查询学分(`credits`)最高的3门课程的所有信息。 + +6. 查询 `Students` 表中所有不重复的专业(`major`)名称。 + +7. 查询课程名称(`title`)中包含 'Intro' 关键字的所有课程。 + +8. 查询专业为 'History' 或 'Physics' 的所有学生信息。 + +9. 查询所有办公室编号(`office_number`)未被指定的讲师。 + +10. 查询所有学分在3到4之间(包含3和4)的课程名称和学分。 + + +#### 聚合与分组 (11-20) + +11. 统计 `Students` 表中共有多少名学生。 + +12. 计算 `Courses` 表中所有课程的平均学分是多少。 + +13. 统计选修了课程ID为201('Introduction to CS')的学生总人数。 + +14. 按专业(`major`)分组,统计每个专业的学生人数。 + +15. 按系别(`department`)分组,统计每个系别开设的课程数量。 + +16. 查询学生人数超过1人的专业名称以及对应的人数。 + +17. 按课程(`course_id`)分组,计算每门课程的平均成绩(提示:这在当前数据结构下无法直接计算,但请思考如何统计选课人数)。请统计每门课程的选课人数。 + +18. 查询开设课程总数超过1门的讲师ID及其开设的课程数。 + +19. 找出 `Courses` 表中学分最高和最低的课程的学分值。 + +20. 统计所有 'COMP' 系课程的总学分。 + + +#### 连接查询与复杂查询 (21-32) + +21. 使用 `INNER JOIN` 查询所有选修了 'Introduction to CS' 这门课的学生的姓和名。 + +22. 使用 `INNER JOIN` 连接 `Student_Enrollments` 和 `Students` 表,列出每条选课记录对应的学生全名(姓和名拼接)和成绩。 + +23. 使用 `INNER JOIN` 连接 `Courses` 和 `Instructors` 表,列出每位讲师的全名以及他们所教授的课程名称。 + +24. **`LEFT JOIN` 应用**:查询所有讲师的全名,以及他们教授的课程名称。即使某位讲师没有教授任何课程,也需要显示在结果中(其课程名称显示为 `NULL`)。 + +25. **`LEFT JOIN` 应用**:查询所有没有教授任何课程的讲师的全名。 + +26. **`LEFT JOIN` 应用**:查询所有学生的姓名,以及他们选修的课程名称。即使某个学生没有选修任何课程,也需要显示在结果中。 + +27. **`LEFT JOIN` 应用**:查询所有没有选修任何课程的学生的姓名。 + +28. 使用子查询,查询由 'Alan Turing' 教授的所有课程的名称。 + +29. 使用子查询,查询选修了至少一门 'COMP' 系课程的所有学生的姓名。 + +30. 查询选课数量最多的学生的姓名。 + +31. 使用 `CASE` 语句,查询所有学生的姓名和专业,并添加一列名为 `remark` 的备注。如果专业是 'Computer Science',备注为 'Tech Major';如果专业是 'Physics',备注为 'Science Major';否则备注为 'Arts & Humanities'。 + +32. **逻辑执行顺序理解**:尝试按专业统计学生人数,并将统计结果列命名为 `student_count`。然后,使用 `WHERE student_count > 1` 来筛选结果。这个查询会失败。请解释为什么会失败,并写出使用 `HAVING` 子句的正确查询 。   + + +* * * + +## 第四部分:TCL (事务控制语言) - 保证数据一致性 + +本部分旨在考察您对事务的理解,以及如何使用事务来确保在执行多步操作时数据库的原子性和一致性。ACID原则是数据库管理的核心概念之一 。   + +**题目列表:** + +1. **基本事务**:开启一个事务,向 `Students` 表中插入一名新学生 (student\_id: 106, name: 'Frank', 'Castle', email: 'frank.castle@university.edu', major: 'Law'),然后提交事务,使更改永久生效。 + +2. **`ROLLBACK` 场景**:开启一个事务,删除 `Students` 表中所有专业为 'Undeclared' 的学生,然后执行 `ROLLBACK` 命令。最后,查询 `Students` 表,验证被删除的学生记录是否已恢复。 + +3. **原子操作场景**:学生 'Alice Smith' (student\_id: 101) 决定从 'Databases' (course\_id: 204) 这门课退选,并改选 'Game Theory' (course\_id: 203)。这个操作需要两步:从 `Student_Enrollments` 中删除一条记录,并插入一条新记录。请将这两个操作放在一个事务中,确保它们要么都成功,要么都失败。 + +4. **约束冲突与自动回滚**:开启一个事务。首先,将学生 'Charlie Brown' (student\_id: 103) 的专业更新为 'Philosophy'。然后,尝试插入一名新学生,但其 `email` 与已存在的学生 'Diana Prince' 冲突。由于第二步违反了 `UNIQUE` 约束会失败,整个事务会自动回滚 。请问,'Charlie Brown' 的专业最终是什么?请通过查询验证。   + +5. **`SAVEPOINT` 应用**:在一个事务中,按顺序执行以下操作: a. 插入两名新的 'Art' 专业学生。 b. 创建一个名为 `after_art_students` 的保存点。 c. 插入两名新的 'Music' 专业学生。 d. 回滚到 `after_art_students` 这个保存点。 e. 提交事务。 请问,最终哪些专业的学生被永久保存到了数据库中? + +6. **复杂原子操作**:为一名学生办理毕业手续。这需要:a) 从 `Student_Enrollments` 表中删除该学生的所有选课记录;b) 从 `Students` 表中删除该学生的记录。请为学生 'Diana Prince' (student\_id: 104) 编写一个事务来完成此操作,确保数据的一致性。 + +7. 根据上一题的毕业操作,解释ACID属性中的 'A' (Atomicity - 原子性) 是如何体现的。 + +8. 以 `Student_Enrollments` 表的外键约束为例,解释ACID属性中的 'C' (Consistency - 一致性) 是如何保证的。 + +9. 解释在何种情况下,您可能会选择使用 `BEGIN IMMEDIATE TRANSACTION` 而不是默认的延迟事务(`BEGIN DEFERRED TRANSACTION`)。   + +10. 一个脚本需要更新100门不同课程的学分。相比于让SQLite为每条 `UPDATE` 语句自动提交事务,为什么将这100条语句包裹在一个单独的事务中会更高效? + + +* * * + +## 答案 + +### 第一部分:DDL 答案 + +1. SQL + + ``` + PRAGMA foreign_keys = ON; + ``` + +2. SQL + + ``` + CREATE TABLE Instructors ( + instructor_id INTEGER PRIMARY KEY AUTOINCREMENT, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE + ); + ``` + +3. SQL + + ``` + CREATE TABLE Students ( + student_id INTEGER PRIMARY KEY, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + major TEXT DEFAULT 'Undeclared' + ); + ``` + +4. SQL + + ``` + CREATE TABLE Courses ( + course_id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + department TEXT NOT NULL, + credits INTEGER, + instructor_id INTEGER, + FOREIGN KEY (instructor_id) REFERENCES Instructors(instructor_id) + ); + ``` + +5. \-- 需要先删除旧表,再用新的约束创建。在实际操作中,这意味着需要备份数据。 + + SQL + + ``` + -- 假设表已存在,需要先删除再重建(或使用ALTER TABLE在新版SQLite中) + -- 为简化题目,这里提供带ON DELETE SET NULL的完整创建语句 + DROP TABLE Courses; + CREATE TABLE Courses ( + course_id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + department TEXT NOT NULL, + credits INTEGER, + instructor_id INTEGER, + FOREIGN KEY (instructor_id) REFERENCES Instructors(instructor_id) ON DELETE SET NULL + ); + ``` + +6. SQL + + ``` + CREATE TABLE Enrollments ( + student_id INTEGER, + course_id INTEGER, + grade TEXT, + PRIMARY KEY (student_id, course_id) + ); + ``` + +7. \-- 同样,这里提供带完整外键定义的创建语句 + + SQL + + ``` + DROP TABLE Enrollments; + CREATE TABLE Enrollments ( + student_id INTEGER, + course_id INTEGER, + grade TEXT, + PRIMARY KEY (student_id, course_id), + FOREIGN KEY (student_id) REFERENCES Students(student_id), + FOREIGN KEY (course_id) REFERENCES Courses(course_id) + ); + ``` + +8. \-- 提供带ON DELETE CASCADE的完整创建语句 + + SQL + + ``` + DROP TABLE Enrollments; + CREATE TABLE Enrollments ( + student_id INTEGER, + course_id INTEGER, + grade TEXT, + PRIMARY KEY (student_id, course_id), + FOREIGN KEY (student_id) REFERENCES Students(student_id) ON DELETE CASCADE, + FOREIGN KEY (course_id) REFERENCES Courses(course_id) + ); + ``` + +9. SQL + + ``` + ALTER TABLE Instructors ADD COLUMN office_number TEXT; + ``` + +10. SQL + +``` +ALTER TABLE Enrollments RENAME TO Student_Enrollments; +``` + +11. SQL + +``` +CREATE TABLE Temp_Table (id INTEGER); +DROP TABLE Temp_Table; +``` + + +### 第二部分:DML 答案 + +1. SQL + + ``` + -- 注意:在DDL第9题后,Instructors表结构已改变。 + -- 为确保数据插入成功,需要更新instructor_id=1的记录。 + INSERT INTO Instructors (instructor_id, first_name, last_name, email) VALUES + (1, 'Alan', 'Turing', 'alan.turing@bletchley.edu'), + (2, 'Grace', 'Hopper', 'grace.hopper@yale.edu'), + (3, 'John', 'von Neumann', 'john.vn@ias.edu'); + UPDATE Instructors SET office_number = 'B-101' WHERE instructor_id = 1; + UPDATE Instructors SET office_number = 'C-203' WHERE instructor_id = 2; + UPDATE Instructors SET office_number = 'A-305' WHERE instructor_id = 3; + ``` + +2. SQL + + ``` + INSERT INTO Students (student_id, first_name, last_name, email, major) VALUES + (101, 'Alice', 'Smith', 'alice.smith@university.edu', 'Computer Science'), + (102, 'Bob', 'Johnson', 'bob.johnson@university.edu', 'Physics'), + (103, 'Charlie', 'Brown', 'charlie.brown@university.edu', 'History'), + (104, 'Diana', 'Prince', 'diana.prince@university.edu', 'Computer Science'), + (105, 'Eve', 'Adams', 'eve.adams@university.edu', 'Physics'); + ``` + +3. SQL + + ``` + INSERT INTO Courses (course_id, title, department, credits, instructor_id) VALUES + (201, 'Introduction to CS', 'CSCI', 3, 1), + (202, 'Advanced Compilers', 'CSCI', 4, 2), + (203, 'Game Theory', 'MATH', 3, 3), + (204, 'Databases', 'CSCI', 4, 1), + (205, 'Quantum Mechanics', 'PHYS', 4, NULL); + ``` + +4. SQL + + ``` + INSERT INTO Student_Enrollments (student_id, course_id, grade) VALUES + (101, 201, 'A'), + (101, 204, 'B+'), + (102, 205, 'A-'), + (103, 203, 'B'), + (104, 201, 'C'), + (104, 202, 'A'); + ``` + +5. SQL + + ``` + INSERT INTO Instructors (last_name, email) VALUES ('Curie', 'marie.curie@sorbonne.edu'); + -- 这将返回一个错误,因为 first_name 违反了 NOT NULL 约束。 + ``` + +6. SQL + + ``` + INSERT INTO Students (student_id, first_name, last_name, email, major) + VALUES (106, 'Alex', 'Ray', 'alice.smith@university.edu', 'Biology'); + -- 这将返回一个错误,因为 email 违反了 UNIQUE 约束。 + ``` + +7. SQL + + ``` + INSERT INTO Courses (course_id, title, department, credits, instructor_id) + VALUES (206, 'Artificial Intelligence', 'CSCI', 4, 999); + -- 这将返回一个错误,因为 instructor_id 违反了 FOREIGN KEY 约束。 + ``` + +8. SQL + + ``` + UPDATE Instructors SET office_number = 'B-105' WHERE instructor_id = 1; + ``` + +9. SQL + + ``` + UPDATE Courses SET department = 'COMP' WHERE department = 'CSCI'; + ``` + +10. SQL + +``` +DELETE FROM Students WHERE student_id = 102; +-- 查询验证 (应返回0行) +SELECT * FROM Student_Enrollments WHERE student_id = 102; +``` + +11. SQL + +``` +DELETE FROM Instructors WHERE instructor_id = 2; +-- 查询验证 (instructor_id应为NULL) +SELECT * FROM Courses WHERE course_id = 202; +``` + +12. SQL + +``` +DELETE FROM Student_Enrollments WHERE grade = 'C'; +``` + + +### 第三部分:DQL 答案 + +1. `SELECT * FROM Students;` + +2. `SELECT last_name, first_name FROM Instructors;` + +3. `SELECT title FROM Courses WHERE department = 'COMP';` + +4. `SELECT * FROM Students WHERE major = 'Physics' ORDER BY last_name ASC;` + +5. `SELECT * FROM Courses ORDER BY credits DESC LIMIT 3;` + +6. `SELECT DISTINCT major FROM Students;` + +7. `SELECT * FROM Courses WHERE title LIKE '%Intro%';` + +8. `SELECT * FROM Students WHERE major IN ('History', 'Physics');` + +9. `SELECT * FROM Instructors WHERE office_number IS NULL;` + +10. `SELECT title, credits FROM Courses WHERE credits BETWEEN 3 AND 4;` + +11. `SELECT COUNT(*) FROM Students;` + +12. `SELECT AVG(credits) FROM Courses;` + +13. `SELECT COUNT(*) FROM Student_Enrollments WHERE course_id = 201;` + +14. `SELECT major, COUNT(*) FROM Students GROUP BY major;` + +15. `SELECT department, COUNT(*) FROM Courses GROUP BY department;` + +16. `SELECT major, COUNT(*) FROM Students GROUP BY major HAVING COUNT(*) > 1;` + +17. `SELECT course_id, COUNT(student_id) AS num_students FROM Student_Enrollments GROUP BY course_id;` + +18. `SELECT instructor_id, COUNT(*) FROM Courses GROUP BY instructor_id HAVING COUNT(*) > 1;` + +19. `SELECT MAX(credits), MIN(credits) FROM Courses;` + +20. `SELECT SUM(credits) FROM Courses WHERE department = 'COMP';` + +21. `SELECT s.first_name, s.last_name FROM Students s INNER JOIN Student_Enrollments se ON s.student_id = se.student_id INNER JOIN Courses c ON se.course_id = c.course_id WHERE c.title = 'Introduction to CS';` + +22. `SELECT s.first_name || ' ' | | s.last_name AS full_name, se.grade FROM Students s INNER JOIN Student_Enrollments se ON s.student_id = se.student_id;` + +23. `SELECT i.first_name || ' ' | | i.last_name AS instructor_name, c.title FROM Instructors i INNER JOIN Courses c ON i.instructor_id = c.instructor_id;` + +24. `SELECT i.first_name, i.last_name, c.title FROM Instructors i LEFT JOIN Courses c ON i.instructor_id = c.instructor_id;` + +25. `SELECT i.first_name, i.last_name FROM Instructors i LEFT JOIN Courses c ON i.instructor_id = c.instructor_id WHERE c.course_id IS NULL;` + +26. `SELECT s.first_name, s.last_name, c.title FROM Students s LEFT JOIN Student_Enrollments se ON s.student_id = se.student_id LEFT JOIN Courses c ON se.course_id = c.course_id;` + +27. `SELECT s.first_name, s.last_name FROM Students s LEFT JOIN Student_Enrollments se ON s.student_id = se.student_id WHERE se.course_id IS NULL;` + +28. `SELECT title FROM Courses WHERE instructor_id = (SELECT instructor_id FROM Instructors WHERE first_name = 'Alan' AND last_name = 'Turing');` + +29. `SELECT first_name, last_name FROM Students WHERE student_id IN (SELECT student_id FROM Student_Enrollments WHERE course_id IN (SELECT course_id FROM Courses WHERE department = 'COMP'));` + +30. `SELECT s.first_name, s.last_name FROM Students s JOIN Student_Enrollments se ON s.student_id = se.student_id GROUP BY s.student_id ORDER BY COUNT(se.course_id) DESC LIMIT 1;` + +31. ```sql SELECT first_name, last_name, major, CASE major WHEN 'Computer Science' THEN 'Tech Major' WHEN 'Physics' THEN 'Science Major' ELSE 'Arts & Humanities' END AS remark FROM Students; ``` + +32. **解释**:失败是因为SQL的逻辑执行顺序是`FROM`->`WHERE`->`GROUP BY`->`HAVING`->`SELECT`->`ORDER BY`。`WHERE`子句在`SELECT`子句之前执行,因此在执行`WHERE`时,别名`student_count `尚未被创建。`HAVING`子句在`GROUP BY` 之后执行,用于过滤聚合后的结果,所以可以使用聚合函数。 **正确查询**: `sql SELECT major, COUNT(*) AS student_count FROM Students GROUP BY major HAVING COUNT(*) > 1;` + +### 第四部分:TCL 答案 + +1. SQL + + ``` + BEGIN TRANSACTION; + INSERT INTO Students (student_id, first_name, last_name, email, major) + VALUES (106, 'Frank', 'Castle', 'frank.castle@university.edu', 'Law'); + COMMIT; + ``` + +2. SQL + + ``` + BEGIN TRANSACTION; + -- 假设有一个默认专业为'Undeclared'的学生 + INSERT INTO Students (student_id, first_name, last_name, email) VALUES (107, 'John', 'Doe', 'john.doe@university.edu'); + DELETE FROM Students WHERE major = 'Undeclared'; + ROLLBACK; + -- 查询验证,John Doe 应该仍然存在 + SELECT * FROM Students WHERE student_id = 107; + ``` + +3. SQL + + ``` + BEGIN TRANSACTION; + DELETE FROM Student_Enrollments WHERE student_id = 101 AND course_id = 204; + INSERT INTO Student_Enrollments (student_id, course_id, grade) VALUES (101, 203, NULL); + COMMIT; + ``` + +4. **最终专业**:'History'。因为事务中的第二步失败导致整个事务自动回滚,第一步的 `UPDATE` 操作也被撤销了。 **验证查询**:`SELECT major FROM Students WHERE student_id = 103;` + +5. **最终结果**:只有 'Art' 专业的学生被永久保存。因为事务回滚到了保存点 `after_art_students`,这撤销了插入 'Music' 专业学生的操作,然后提交了之前的操作。 + +6. SQL + + ``` + BEGIN TRANSACTION; + DELETE FROM Student_Enrollments WHERE student_id = 104; + DELETE FROM Students WHERE student_id = 104; + COMMIT; + ``` + +7. **原子性(Atomicity)** 体现在毕业操作的两个步骤(删除选课记录和删除学生记录)被视为一个不可分割的单元。如果其中任何一步失败(例如,由于权限问题),整个事务将回滚,数据库将恢复到操作开始前的状态,从而避免了“学生记录已删除但选课记录仍存在”这种不一致的状态。 + +8. **一致性(Consistency)** 保证了事务将数据库从一个有效的状态转换到另一个有效的状态。`Student_Enrollments` 表的外键约束确保了任何插入的选课记录都必须关联一个真实存在的学生和一个真实存在的课程。任何试图破坏这种关系的DML操作(如为不存在的学生添加选课记录)都会失败,从而维护了数据库在事务前后的一致性。 + +9. 默认的延迟事务(`DEFERRED`)直到第一次实际读写数据库时才加锁。而 `IMMEDIATE` 事务在 `BEGIN` 命令执行时就立即尝试获取一个保留锁(写锁的前置锁)。当您预知即将进行写操作,并希望尽早确保事务不会因为其他写操作而失败(返回 `SQLITE_BUSY`)时,应使用 `IMMEDIATE`。这可以减少事务中途失败的可能性。 + +10. 将100条 `UPDATE` 语句包裹在单个事务中更高效,主要有两个原因: + ++ **减少磁盘I/O**:SQLite的默认模式下,每次提交事务都需要将日志文件同步到磁盘,这是一个相对缓慢的操作。100次自动提交意味着100次磁盘同步。而单个事务只需要在最后 `COMMIT` 时进行一次磁盘同步,大大减少了I/O开销。 + ++ **原子性保证**:如果脚本在更新到第50条时失败,单个事务可以确保所有已做的更改都被回滚,数据库保持一致。而100个独立事务则会导致前49次更新已永久保存,数据库处于一个不完整的中间状态。 \ No newline at end of file diff --git a/101-数据库学习/3-SQLite/9-题目/SQLite基础考察.sql b/101-数据库学习/3-SQLite/9-题目/SQLite基础考察.sql new file mode 100644 index 0000000..1a08443 --- /dev/null +++ b/101-数据库学习/3-SQLite/9-题目/SQLite基础考察.sql @@ -0,0 +1,271 @@ +PRAGMA forien_keys = ON; + +PRAGMA forien_keys; + +CREATE TABLE instructors ( + instructor_id INTEGER PRIMARY KEY AUTOINCREMENT, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE +); + +CREATE TABLE students ( + student_id INTEGER PRIMARY KEY, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + major TEXT DEFAULT "Undeclared" +); + + +CREATE TABLE courses ( + course_id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + department TEXT NOT NULL, + credits INTEGER, + instructor_id INTEGER , + + CONSTRAINT fk_courses_instructor FOREIGN KEY(instructor_id) REFERENCES instructors(instructor_id) +); + +-- 修改 Courses 表中 instructor_id 的外键约束。要求当 Instructors 表中的某位讲师记录被删除时,Courses 表中由该讲师授课的课程的 instructor_id 字段应自动被设置为 NULL 。 +CREATE TABLE courses_back AS SELECT * FROM courses; +DROP TABLE courses; +CREATE TABLE courses ( + course_id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + department TEXT NOT NULL, + credits INTEGER, + instructor_id INTEGER , + + CONSTRAINT fk_courses_instructor FOREIGN KEY(instructor_id) REFERENCES instructors(instructor_id) ON DELETE SET NULL + ); + +INSERT INTO courses SELECT * FROM courses_back; +DROP TABLE courses_back; + +-- 创建一个名为 Enrollments 的连接表,用于记录学生选修课程的情况。该表应包含: +-- +-- student_id: 整数类型。 +-- +-- course_id: 整数类型。 +-- +-- grade: 文本类型,用于存储成绩(如 'A', 'B+', 'C' 等)。 +-- +-- 将 student_id 和 course_id 的组合设置为主键(复合主键)。 +create table enrollments ( + student_id INTEGER NOT NULL, + course_id INTEGER NOT NULL, + grade TEXT, + + -- 想象一下你的身份证号(假设前6位是地区码,后8位是个人编码)。单看地区码无法确定你是谁(一个地区有千万人),单看个人编码也可能重复(不同地区可能有相同个人编码)。但“地区码+个人编码”的组合就能在全国范围内唯一锁定你这个人。这个组合就是“复合主键”。 + -- 定义复合主键:一个学生不能多次选修同一门课程 + PRIMARY KEY (student_id, course_id), + -- 要求当 Students 表中的某个学生记录被删除时,该学生所有的选课记录也应自动从 Enrollments 表中被删除(级联删除) + FOREIGN KEY (student_id) REFERENCES students(student_id) on delete cascade , + FOREIGN KEY (course_id) REFERENCES courses(course_id) +); +PRAGMA foreign_keys = ON; + +-- 使用 ALTER TABLE 命令为 Instructors 表添加一个新列,名为 office_number,类型为 TEXT。 +alter table instructors add column office_number TEXT; + +-- 使用 ALTER TABLE 命令将 Enrollments 表重命名为 Student_Enrollments。 +alter table enrollments rename to student_enrollments; + +drop table student_enrollments; +drop table sqlite_master; + +-- 创建一个名为 Temp_Table 的临时表(列定义不限),然后写出删除该表的命令。 +-- 临时表最大的优点是无需手动管理清理,断开连接后自动消失,避免了垃圾数据累积。 wdd:有何意义? + +-- 向 Instructors 表中插入以下三位讲师的数据: +-- +-- (1, 'Alan', 'Turing', 'alan.turing@bletchley.edu', 'B-101') +-- +-- (2, 'Grace', 'Hopper', 'grace.hopper@yale.edu', 'C-203') +-- +-- (3, 'John', 'von Neumann', 'john.vn@ias.edu', 'A-305') + +insert into instructors values + (1, 'Alan', 'Turing', 'alan.turing@bletchley.edu', 'B-101'), + (2, 'Grace', 'Hopper', 'grace.hopper@yale.edu', 'C-203'), + (3, 'John', 'von Neumann', 'john.vn@ias.edu', 'A-305'); + + +INSERT INTO students VALUES + (101, 'Alice', 'Smith', 'alice.smith@university.edu', 'Computer Science'), + (102, 'Bob', 'Johnson', 'bob.johnson@university.edu', 'Physics'), + (103, 'Charlie', 'Brown', 'charlie.brown@university.edu', 'History'), + (104, 'Diana', 'Prince', 'diana.prince@university.edu', 'Computer Science'), + (105, 'Eve', 'Adams', 'eve.adams@university.edu', 'Physics'); + + +INSERT INTO courses (course_id, title, department, credits, instructor_id) VALUES + (201, 'Introduction to CS', 'CSCI', 3, 1), + (202, 'Advanced Compilers', 'CSCI', 4, 2), + (203, 'Game Theory', 'MATH', 3, 3), + (204, 'Databases', 'CSCI', 4, 1), + (205, 'Quantum Mechanics', 'PHYS', 4, NULL); + +INSERT INTO enrollments (student_id, course_id, grade) VALUES + (101, 201, 'A'), + (101, 204, 'B+'), + (102, 205, 'A-'), + (103, 203, 'B'), + (104, 201, 'C'), + (104, 202, 'A'); + +-- 测试 NOT NULL 约束:尝试向 Instructors 表中插入一条记录,但 first_name 字段留空。此操作预期会失败。请写出这条 INSERT 语句。 +insert into instructors (instructor_id, first_name, last_name, office_number, email) values (233, null, "yes", "CS-1", "ok@qq.com"); + +-- 测试 FOREIGN KEY 约束:尝试向 Courses 表中插入一门新课程,其 instructor_id 为999,而这个ID在 Instructors 表中并不存在。此操作预期会因违反外键约束而失败。请写出这条 INSERT 语句。 +insert into courses (course_id, title, department, credits, instructor_id) values (123, "测试课程", "fts3tokenize", 12, 999); + +-- Alan Turing博士更换了办公室。请使用 UPDATE 语句,将他的 office_number 更新为 'B-105'。 +update instructors set office_number="B-105" where first_name="Alan" and last_name="Turing"; + +-- 学校决定将 'CSCI' 系的缩写统一更改为 'COMP'。请使用 UPDATE 语句更新 Courses 表中所有 'CSCI' 系的课程。 +update courses set title="COMP" where title = "CSCI"; + +-- 测试 ON DELETE CASCADE:学生 'Bob Johnson' (student_id=102) 退学。请从 Students 表中删除他的记录。删除后,请查询 Student_Enrollments 表,验证他的选课记录是否也已被自动删除 +delete from students where student_id=102; + +-- 连表查询 查询学生'Bob Johnson' (student_id=102) 选取的所有课程信息 +select e.student_id, e.course_id, c.title, c.department, c.credits from enrollments e + INNER JOIN courses c on e.course_id = c.course_id where e.student_id = 102; + +-- 测试 ON DELETE SET NULL:讲师 'Grace Hopper' (instructor_id=2) 离职。请从 Instructors 表中删除她的记录。删除后,请查询 Courses 表,验证她所教授课程的 instructor_id 是否已变为 NULL。 +select * from instructors i inner join courses c on i.instructor_id = c.instructor_id where i.instructor_id =2; + +delete from instructors where instructor_id=2; + +-- 删除 Student_Enrollments 表中所有成绩为 'C' 的记录。 +select * from enrollments; +delete from enrollments where grade = 'C'; + +-- 查询并显示 Students 表中的所有列及所有行。 +select * from students; + + +-- 查询所有讲师的姓(last_name)和名(first_name)。 +select last_name, first_name from instructors; + +-- 查询所有属于 'COMP' 系(假设已在DML部分更新)的课程名称(title)。 +select title from courses where department = 'COMP'; + + +-- 查询所有专业为 'Physics' 的学生信息,并按姓氏(last_name)升序排列。 +select * from students where major = 'Physics' order by last_name asc ; + +-- 查询学分(credits)最高的3门课程的所有信息。 +select * from courses order by credits desc limit 3; + +-- 查询 Students 表中所有不重复的专业(major)名称。 +select distinct major from students; + +-- 查询课程名称(title)中包含 'Intro' 关键字的所有课程。 +select * from courses where title like '%Intro%'; + +-- 查询专业为 'History' 或 'Physics' 的所有学生信息。 +select * from students where major = 'History' or 'Physics'; + +-- 查询所有办公室编号(office_number)未被指定的讲师。 +select * from instructors where office_number = '' or null; + +-- 查询所有学分在3到4之间(包含3和4)的课程名称和学分。 +select * from courses where credits between 3 and 4; + + +-- 统计 Students 表中共有多少名学生。 +select count(*) from students; + +-- 计算 Courses 表中所有课程的平均学分是多少。 +select avg(credits) avg_credits from courses; + +-- 统计选修了课程ID为201('Introduction to CS')的学生总人数。 +select count(student_id) from enrollments where course_id = 201; + +-- 按专业(major)分组,统计每个专业的学生人数。 +select count(student_id) student_count, major from students group by major; + +-- 按系别(department)分组,统计每个系别开设的课程数量。 +select count(course_id) course_count, courses.department from courses group by department; + +-- 查询学生人数超过1人的专业名称以及对应的人数。 +SELECT major, COUNT(*) FROM Students GROUP BY major HAVING COUNT(*) > 1;; + +-- 按课程(course_id)分组,计算每门课程的平均成绩(提示:这在当前数据结构下无法直接计算,但请思考如何统计选课人数)。 +-- +-- 请统计每门课程的选课人数。 +select c.course_id, count(e.student_id), e.grade from courses c inner join enrollments e on c.course_id = e.course_id group by c.course_id; + +-- 查询开设课程总数超过1门的讲师ID及其开设的课程数。 +select i.instructor_id, count(c.course_id) from instructors i inner join courses c on i.instructor_id = c.instructor_id group by i.instructor_id; + +-- 找出 Courses 表中学分最高和最低的课程的学分值。 +-- select credits from courses group by credits ; +-- 找出学分最高和最低的值 +SELECT + MAX(credits) AS highest_credits, + MIN(credits) AS lowest_credits +FROM courses; +-- 如果想要同时查看这些极值对应的课程信息 +SELECT * +FROM courses +WHERE credits = (SELECT MAX(credits) FROM courses) + OR credits = (SELECT MIN(credits) FROM courses); + +-- 统计所有 'COMP' 系课程的总学分。 +select sum(credits) from courses where department='CSCI'; + +-- 使用 INNER JOIN 查询所有选修了 'Introduction to CS' 这门课的学生的姓和名。 +select * from courses c inner join enrollments e on c.course_id = e.course_id inner join students s on s.student_id = e.student_id where c.title = 'Introduction to CS'; + +-- 使用 INNER JOIN 连接 Student_Enrollments 和 Students 表,列出每条选课记录对应的学生全名(姓和名拼接)和成绩。 +select s.last_name || ' ' || s.first_name as full_name, e.grade from enrollments e inner join students s on s.student_id = e.student_id; + + +-- 使用 INNER JOIN 连接 Courses 和 Instructors 表,列出每位讲师的全名以及他们所教授的课程名称。 +select s.last_name || ' ' || s.first_name as full_name, c.title from instructors s inner join courses c on s.instructor_id = c.instructor_id; + +-- LEFT JOIN 应用:查询所有讲师的全名,以及他们教授的课程名称。即使某位讲师没有教授任何课程,也需要显示在结果中(其课程名称显示为 NULL)。 +select s.last_name || ' ' || s.first_name as full_name, c.title from instructors s left join courses c on s.instructor_id = c.instructor_id; + +-- LEFT JOIN 应用:查询所有没有教授任何课程的讲师的全名。 +select s.last_name || ' ' || s.first_name as full_name from instructors s left join courses c on s.instructor_id = c.instructor_id where c.instructor_id is null; + +-- LEFT JOIN 应用:查询所有学生的姓名,以及他们选修的课程名称。即使某个学生没有选修任何课程,也需要显示在结果中。 +select s.last_name, s.first_name, c.title from students s left join enrollments e on s.student_id = e.student_id left join courses c on e.course_id = c.course_id; + +-- LEFT JOIN 应用:查询所有没有选修任何课程的学生的姓名。 +select s.last_name, s.first_name , s.student_id from students s where s.student_id not in (select student_id + from enrollments + ); + +-- 使用子查询,查询由 'Alan Turing' 教授的所有课程的名称。 +select c.title from courses c where instructor_id = ( + select instructor_id from instructors where first_name = 'Alan' and last_name = 'Turing' + ); + +-- 使用子查询,查询选修了至少一门 'COMP' 系课程的所有学生的姓名。 +select s.first_name, s.last_name from students s where s.student_id = ( + select student_id from enrollments where course_id = ( + select course_id from courses where department = 'CSCI' + ) + ); + +select count(student_id) from enrollments where course_id = ( + select course_id from courses where department = 'CSCI' +); + +select course_id from courses where department = 'CSCI'; + +-- 查询选课数量最多的学生的姓名。 +select students.first_name, students.last_name +from students where student_id = (select student_id from enrollments group by student_id order by count(course_id) limit 1); + +-- 使用 CASE 语句,查询所有学生的姓名和专业,并添加一列名为 remark 的备注。如果专业是 'Computer Science',备注为 'Tech Major';如果专业是 'Physics',备注为 'Science Major';否则备注为 'Arts & Humanities'。 +select students.first_name, students.last_name + +-- 逻辑执行顺序理解:尝试按专业统计学生人数,并将统计结果列命名为 student_count。然后,使用 WHERE student_count > 1 来筛选结果。这个查询会失败。请解释为什么会失败,并写出使用 HAVING 子句的正确查询 。 \ No newline at end of file diff --git a/101-数据库学习/3-SQLite/prompt.md b/101-数据库学习/3-SQLite/prompt.md index 9e1befe..2dadf68 100644 --- a/101-数据库学习/3-SQLite/prompt.md +++ b/101-数据库学习/3-SQLite/prompt.md @@ -7,4 +7,13 @@ ## SQLite引擎流程 -请根据[agi_sqlite_study.md](agi_sqlite_study.md)的规范,请详细讲解SQLite中的事务,事务对其他进程的读写产生何种影响,请分析极端情况下的事务。SQLite中的读写隔离机制是怎样的。 \ No newline at end of file +请根据[agi_sqlite_study.md](agi_sqlite_study.md)的规范,请详细讲解SQLite中的事务,事务对其他进程的读写产生何种影响,请分析极端情况下的事务。SQLite中的读写隔离机制是怎样的。 + + +## SQLite 题目 +请根据[agi_sqlite_study.md](agi_sqlite_study.md)的规范,作为一名专业的SQL老师,设计从数据格式,涵盖DDL DML DQL TCL的一系列题目,考察一名初学者对于SQLite学习的掌握能力 +每道题目需要有序号 +题目设计保持上下文连贯,如DQL基于DML插入的数值,DDL创建的表结构。如果数据不够,请在题目中给出插入数据命令 +每个大模块的题目,不少于10道 +DQL需要给出至少30道题目,需要包含inner join, group by, having等高阶查询方法 +要求答案放置于最后面,与前面的题目序号一一对应 \ No newline at end of file diff --git a/4-ProjectNaughtyMan/0-需求规格书/0-产品经理-prompt.md b/4-ProjectNaughtyMan/0-需求规格书/0-产品经理-prompt.md new file mode 100644 index 0000000..08519b4 --- /dev/null +++ b/4-ProjectNaughtyMan/0-需求规格书/0-产品经理-prompt.md @@ -0,0 +1,11 @@ +你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。 + + +你是一名出色的产品经理,能够根据用户的初始需求,理解用户需求的真实需求意图,改善客户不够完善的需求,形成专业、简练的需求文档。并且能够在基础需求上优化产品的额设计和功能 + +请根据要求,请参考TelegramBotAPI官方文档,进行深度的思考,优化1-初始需求稿.md,直接给出优化后的PRD + + +你是一名出色的产品经理,能够根据用户的初始需求,理解用户需求的真实需求意图,改善客户不够完善的需求,形成专业、简练的需求文档。并且能够在基础需求上优化产品的额设计和功能 + +你之前根据1-初始需求稿.md输出了2-优化产品需求文档PRD.md, 目前初始需求有一些更新,见1.1-初始需求稿.md,请你详细对比1.1-初始需求稿.md和1-初始需求稿.md之间的差异, 修改2-优化产品需求文档PRD.md,得到新的需求文档 \ No newline at end of file diff --git a/4-ProjectNaughtyMan/0-需求规格书/1-初始需求稿.md b/4-ProjectNaughtyMan/0-需求规格书/1-初始需求稿.md new file mode 100644 index 0000000..10f6c57 --- /dev/null +++ b/4-ProjectNaughtyMan/0-需求规格书/1-初始需求稿.md @@ -0,0 +1,118 @@ +# Telegram详细内容 + +1. 官方API参考内容 + + https://core.telegram.org/bots/api + + +# 模块功能需求 + +## 消息通知 + +1. 功能概述: 能够根据不同的消息来源, 操作TelegramBotAPI发送消息,消息来源如下: + 1. 接口暴露 + 2. AI智能体 + 3. 机器人功能 +2. 实现优先级: 高 + +### 接口暴露 + +1. 通过GIN框架暴露接口,能够方便的进行消息通知 +2. 需要有强Token验证才行 + 1. 设计握手式Token获取法 + 2. 每个Token的有效期为6小时 + +### 消息通知范围 + +1. 单对单消息 +2. 群组消息 + +### 通知消息体 + +1. 通知等级 + 1. 消息具备等级 + 2. 不同等级的消息模板不一样 +2. 通知消息模板 + 1. 服务器类-ProjectOctopus + 2. 金融类-ProjectTonyStack + +### 通知时间 + +1. 与通知等级匹配 +2. 正常通知时间 + 1. 白天时间 8:00-23:00 +3. 紧急通知时间 + 1. 全天 + +### Telegram通知实现 + +1. 向特定用户发送消息 +2. 向特定群组发送消息 + +## 机器人功能 + +1. 模块描述: 按照预设的特定指令,执行操作 +2. 实现优先级: 低 +3. 该模块充分考虑功能拓展性,方便后续拓展其他的功能 + +### 定时提醒功能 + +1. 机器人指令/notify +2. 当输入指令之后,弹出时间选择窗口(参照iOS的提醒事项设计) + 1. 最下方 提醒内容输入框 + 2. 上方显示 日期选项按钮,时间选项按钮,重复选项按钮 + 3. 日期选项选中,可以以日历的形式选择提醒日期 + 4. 时间按钮选中,默认勾选日期为今天,弹出时间选择 + 5. 重复按钮选中,弹出重复提醒选项,每日 每周 每两周 每月 自定义(任意选择星期几) + 6. 显示提交按钮,点击后可以提交 +3. 机器人记录提醒时间及内容,按照设计的时间发送提醒内容到对应的聊天 + 1. 提醒内容需要持久化保存 + + +### 定时提醒查看清除功能 + +1. 机器人执行 /notify-list +2. 当输入指令后,弹出本人的全部定时提醒内容 + 1. 按照如下的顺序显示 + 2. 提醒频率(无则显示 单次提醒) 提醒日期 提醒时间 摘要显示提醒内容 + 3. 最右侧显示删除按键,点击弹出确认删除此定时提醒窗口 + 4. 确认删除后,项目删除此提醒事项 +3. 若无提醒事项 + 1. 显示无提醒事项 + +## 智能体功能 + +1. 模块描述: 响应@ 根据用户的上下文,给出AI智能解答 +2. 实现优先级: 低 +3. 消息回复需要引用被@的那条消息和之前的三条消息 + +### AI来源 + +1. 需要支持如下厂家来源的API + 1. OpenAI chatgpt + 2. Google Gemini + 3. xAI Grok + 4. openRouter +2. 需要支持思考过程和流式响应 + 1. 持续返回消息需要满足消息限制器的要求 + +## 基础架构设计 + +### 白名单功能 + +1. 只能允许白名单内的用户或者群组访问此机器人 + +### 消息限制器 + +1. TelegramAPI的消息频率有限制,请参考官方文档,设计相应的消息发送限制器 +2. 参照单例模式设计,所有与Telegram交互全部通过消息限制器发送 + +### 机器人网络代理 + +1. Telegram机器人构建需要考虑到网络代理的情况,机器人请求TelegramAPI可以接受socks5和http代理 + +### 持久化保存 + +1. 持久化信息采用sqlite保存 + 1. windows保存至 \naughty_man\ + 2. linux保存至 /usr/local/etc/naughty_man/ \ No newline at end of file diff --git a/4-ProjectNaughtyMan/0-需求规格书/1.1-初始需求稿.md b/4-ProjectNaughtyMan/0-需求规格书/1.1-初始需求稿.md new file mode 100644 index 0000000..6c6cd3d --- /dev/null +++ b/4-ProjectNaughtyMan/0-需求规格书/1.1-初始需求稿.md @@ -0,0 +1,151 @@ +# Telegram详细内容 + +1. 官方API参考内容 + + https://core.telegram.org/bots/api + +# 模块功能需求 + +## 消息通知 + +1. 功能概述: 能够根据不同的消息来源, 操作TelegramBotAPI发送消息,消息来源如下: + + 1. 接口暴露 + 2. AI智能体 + 3. 机器人功能 +2. 实现优先级: 高 + +### 接口暴露 + +1. 通过GIN框架暴露接口,能够方便的进行消息通知 +2. 需要有强Token验证才行 + + 1. 设计握手式Token获取法 + 2. 每个Token的有效期为6小时 + +### 消息通知范围 + +1. 单对单消息 +2. 群组消息 + +### 通知消息体 + +1. 通知等级 + + 1. 消息具备等级 + 2. 不同等级的消息模板不一样 +2. 通知消息模板 + + 1. 服务器类-ProjectOctopus + 2. 金融类-ProjectTonyStack + +### 通知时间 + +1. 与通知等级匹配 +2. 正常通知时间 + + 1. 白天时间 8:00-23:00 +3. 紧急通知时间 + + 1. 全天 + +### Telegram通知实现 + +1. 向特定用户发送消息 +2. 向特定群组发送消息 + +## 机器人预定义功能 + +1. 模块描述: 按照预设的特定指令,执行操作 +2. 实现优先级: 低 +3. 该模块充分考虑功能拓展性,方便后续拓展其他的功能 + +### 定时提醒功能 + +1. 机器人指令/notify +2. 当输入指令之后,弹出时间选择窗口(参照iOS的提醒事项设计) + + 1. 最下方 提醒内容输入框 + 2. 上方显示 日期选项按钮,时间选项按钮,重复选项按钮 + 3. 日期选项选中,可以以日历的形式选择提醒日期 + 4. 时间按钮选中,默认勾选日期为今天,弹出时间选择 + 5. 重复按钮选中,弹出重复提醒选项,每日 每周 每两周 每月 自定义(任意选择星期几) + 6. 显示提交按钮,点击后可以提交 +3. 机器人记录提醒时间及内容,按照设计的时间发送提醒内容到对应的聊天 + + 1. 提醒内容需要持久化保存 + +### 定时提醒查看清除功能 + +1. 机器人执行 /notify-list +2. 当输入指令后,弹出本人的全部定时提醒内容 + + 1. 按照如下的顺序显示 + 2. 提醒频率(无则显示 单次提醒) 提醒日期 提醒时间 摘要显示提醒内容 + 3. 最右侧显示删除按键,点击弹出确认删除此定时提醒窗口 + 4. 确认删除后,项目删除此提醒事项 +3. 若无提醒事项 + + 1. 显示无提醒事项 + +## Telegram管理功能 + +### 用户群组管理 + +1. 能够查看所有添加的用户ID 及 群组ID +2. 能够根据用户ID查询用户的相关信息 + 1. 持久化保存 +3. 使用接口的形式向外暴露 + +### 机器人功能管理 + +1. 能够查看机器人现在注册的功能 +2. 能够删除机器人注册的功能 +3. 能够修改机器人注册的功能 +4. 需要将机器人预定义功能注册给Bot,能够正常运行 + +## 智能体功能 + +1. 模块描述: 响应群组中的@ 及用户单独发送至Bot的消息,根据消息的上下文,给出AI智能解答 +2. 实现优先级: 低 +3. 消息回复需要引用被@的那条消息或用户单独询问的那条消息和之前的三条消息 +4. 后台需要能够打印,发送Bot消息的 用户ID/群组ID 用户名/群组名 用户手机号/空 + +### AI来源 +1. 需要支持如下厂家来源的API + 1. OpenAI chatgpt + 2. Google Gemini + 3. xAI Grok + 4. OpenRouter +2. 需要支持思考过程和流式响应 + 1. 持续返回消息需要满足消息限制器的要求 +3. 暂时只实现OpenRouter的API调用方式 + 1. 使用OpenAI SDK的go版本实现功能 + +### AI消息通知 +1. 包装普通消息通知发送器 + 1. 发送普通的消息,非模板消息 +2. 包装AI消息通知发送器 + 1. AI消息为markdown格式 + +## 基础架构设计 + +### 白名单功能 + +1. 只能允许白名单内的用户或者群组访问此机器人 + +### 消息限制器 + +1. TelegramAPI的消息频率有限制,请参考官方文档,设计相应的消息发送限制器 +2. 参照单例模式设计,所有与Telegram交互全部通过消息限制器发送 + +### 机器人网络代理 + +1. Telegram机器人构建需要考虑到网络代理的情况,机器人请求TelegramAPI可以接受socks5和http代理 + +### 持久化保存 + +1. 持久化信息采用sqlite保存 + + 1. windows保存至 \naughty_man\ + 2. linux保存至 /usr/local/etc/naughty_man/ \ No newline at end of file diff --git a/4-ProjectNaughtyMan/0-需求规格书/2-优化产品需求文档PRD.md b/4-ProjectNaughtyMan/0-需求规格书/2-优化产品需求文档PRD.md new file mode 100644 index 0000000..1a3df75 --- /dev/null +++ b/4-ProjectNaughtyMan/0-需求规格书/2-优化产品需求文档PRD.md @@ -0,0 +1,505 @@ +--- + +# Telegram Bot 智能通知与交互系统 PRD + +**项目代号**: NaughtyMan +**文档版本**: v2.0 +**最后更新**: 2025 年 10 月 + +# 一、产品概述 + +## 1.1 产品定位 + +构建一个企业级 Telegram Bot 系统,集成消息通知、智能提醒和 AI 助手功能,为 ProjectOctopus(服务器监控)和 ProjectTonyStack(金融监控)提供统一的实时通知中枢。 + +## 1.2 核心价值 + +- **智能分级推送**: 根据事件紧急程度自动调度通知时间和模板 +- **安全可控**: 白名单 + 动态 Token 双重鉴权机制 +- **高可用性**: 内置速率控制器保障稳定运行 +- **AI 增强**: 多厂商 AI 集成提供智能上下文响应 + +## 1.3 技术栈 + +- **后端框架**: Gin (Go 语言) +- **Bot SDK**: 原生 Telegram Bot API +- **数据持久化**: SQLite +- **网络代理**: 支持 SOCKS5/HTTP + +--- + +# 二、功能模块详细设计 + +## 2.1 核心模块:消息通知系统 [P0-高优先级] + +### 2.1.1 接口设计 + +**API 端点结构** + +```go +POST /api/v1/auth/handshake # 握手获取临时Token +POST /api/v1/auth/token # 换取正式Token +POST /api/v1/message/send # 发送消息 +GET /api/v1/message/status # 查询消息状态 +``` + +**Token 安全机制** + +1. **双阶段认证流程**: + + - 阶段一:客户端携带 API Key 请求握手,服务端返回 challenge 码(60 秒有效期) + - 阶段二:客户端用 challenge+API Secret 生成签名,换取 Access Token(6 小时有效期) +2. **Token 刷新策略**: + + - Token 过期前 30 分钟允许静默续期 + - 每个 API Key 同时只允许一个有效 Token + - Token 中包含加密的白名单用户 ID/群组 ID + +**请求体示例** + +```go +{ + "target_type": "user|group", + "target_id": "123456789", + "level": "info|warning|critical", + "project": "octopus|tonystack", + "content": { + "title": "服务器CPU告警", + "body": "服务器CPU使用率达85%", + "metadata": { + "server": "prod-web-01", + "timestamp": "2025-10-21T10:23:00Z" + } + } +} +``` + +### 2.1.2 消息分级与模板 + +**等级定义** + +| 等级 | 触发条件 | 推送时段 | 响应时效 | 模板标识 | +| ---------- | -------------- | ------------- | ---------------------- | --------------- | +| INFO | 常规事件日志 | 08:00-23:00 | 批量推送(5 分钟汇总) | 📘 蓝色图标 | +| WARNING | 异常但可运行 | 08:00-23:00 | 即时推送 | ⚠️ 黄色图标 | +| CRITICAL | 严重故障 | 全天候 | 即时推送 + 重试 | 🚨 红色图标 | + +**模板配置** + +*ProjectOctopus (服务器监控模板)* + +```go +🖥️ [{{level}}] {{title}} + +📍 服务器: {{metadata.server}} +📊 详情: {{body}} +⏰ 时间: {{timestamp}} + +#服务器监控 #{{level}} +``` + +*ProjectTonyStack (金融监控模板)* + +```go +💹 [{{level}}] {{title}} + +💰 标的: {{metadata.symbol}} +📈 数值: {{body}} +⏰ 时间: {{timestamp}} + +#金融监控 #{{level}} +``` + +### 2.1.3 速率限制器设计 + +**Telegram 官方限速规则** + +- 不同聊天: 30 消息/秒 +- 同一私聊: 1 消息/秒 +- 同一群聊: 20 消息/分钟 +- editMessageText: 共享发送额度 + +**限制器实现** + +```go +// 单例模式令牌桶算法 +type RateLimiter struct { + globalBucket *TokenBucket // 全局30/s + chatBuckets sync.Map // 每个chat独立限流 + editBucket *TokenBucket // 编辑消息单独限流 +} + +// 消息排队机制 +type MessageQueue struct { + priority PriorityQueue // CRITICAL优先 + retryQueue []Message // 失败重试队列(指数退避) + deadLetterBox []Message // 3次失败转死信队列 +} +``` + +--- + +## 2.2 机器人交互功能 [P1-中优先级] + +### 2.2.1 定时提醒功能 + +**指令**: `/notify` + +**交互流程** + +利用 Telegram InlineKeyboard 实现多步交互: + +```go +用户输入 /notify +↓ +Bot返回主界面: +┌─────────────────────────┐ +│ 📅 日期 🕐 时间 🔁 重复 │ +├─────────────────────────┤ +│ 📝 提醒内容: │ +│ [在此输入...] │ +├─────────────────────────┤ +│ ✅ 创建提醒 │ +└─────────────────────────┘ +``` + +**回调数据结构** + +```json +{ + "action": "notify_date|notify_time|notify_repeat|notify_submit", + "state": { + "date": "2025-10-25", + "time": "09:00", + "repeat": "daily|weekly|biweekly|monthly|custom:[1,3,5]", + "content": "团队站会" + } +} +``` + +**日期选择器实现** + +使用 editMessageText 动态更新消息: + +- 日历视图: 显示当前月份,支持上/下月翻页 +- 时间选择: 时/分滚动选择器(步长 15 分钟) +- 重复规则: 预设 + 自定义(RRULE 格式) + +### 2.2.2 提醒管理功能 + +**指令**: `/notify_list` + +**列表展示** + +```go +📋 您的提醒事项 (3) + +🔁 每日 | 09:00 | 团队站会 + [🗑️ 删除] + +📅 10-25 | 14:30 | 客户电话会议 + [🗑️ 删除] + +📅 10-30 | 全天 | 项目交付DDL + [🗑️ 删除] +``` + +**删除确认机制** + +点击删除后 editMessageText 更新为确认界面: + +```go +⚠️ 确认删除提醒? + +🔁 每日 | 09:00 | 团队站会 + +[❌ 取消] [✅ 确认删除] +``` + +### 2.2.3 数据持久化 + +**SQLite 表结构** + +```sql +CREATE TABLE reminders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id BIGINT NOT NULL, + chat_id BIGINT NOT NULL, + content TEXT NOT NULL, + trigger_time DATETIME NOT NULL, + repeat_rule TEXT, -- RRULE格式 + next_trigger DATETIME, + status INTEGER DEFAULT 1, -- 1:活动 0:已删除 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_next_trigger (next_trigger, status) +); +``` + +--- + +## 2.3 AI 智能体功能 [P1-中优先级] + +### 2.3.1 触发机制 + +- 私聊: 直接回复所有消息 +- 群聊: 仅响应 @ 机器人的消息 + +### 2.3.2 上下文管理 + +**消息引用策略** + +```tex +触发消息(被@的消息) +↓ 引用 +前3条历史消息 +↓ 拼接 +发送给AI API +``` + +**上下文窗口** + +```json +{ + "messages": [ + {"role": "system", "content": "你是Telegram群助手..."}, + {"role": "user", "content": "历史消息1", "name": "user123"}, + {"role": "user", "content": "历史消息2", "name": "user456"}, + {"role": "user", "content": "历史消息3", "name": "user123"}, + {"role": "user", "content": "当前@消息", "name": "user789"} + ] +} +``` + +### 2.3.3 多厂商 AI 集成 + +**支持的 AI 提供商** + +| 提供商 | 模型推荐 | 流式支持 | 思考链 | +| ------------ | ---------------- | ---------- | ---------------- | +| OpenAI | gpt-4-turbo | ✅ | ✅ (reasoning) | +| Google | gemini-1.5-pro | ✅ | ✅ (思考步骤) | +| xAI | grok-2 | ✅ | ✅ (推理过程) | +| OpenRouter | 多模型聚合 | ✅ | 视模型而定 | + +**流式响应处理** + +```text +1. 发送初始消息"正在思考..." +2. 累积AI响应分片(每500字符或5秒) +3. 使用editMessageText更新消息 +4. 遵守编辑速率限制(复用全局限流器) +5. 完成后添加"✅ 回答完成"标识 +``` + +**思考过程展示** + +```txt +🤔 思考中... + +【推理步骤1】分析问题... +【推理步骤2】检索知识... + +💡 回答: +[AI生成的完整答复] + +✅ 回答完成 | 用时3.2s +``` + +--- + +# 三、基础架构设计 + +## 3.1 安全控制 + +**白名单机制** + +```sql +CREATE TABLE whitelist ( + id INTEGER PRIMARY KEY, + type TEXT CHECK(type IN ('user', 'group')), + entity_id BIGINT UNIQUE NOT NULL, + alias TEXT, + added_by BIGINT, + added_at DATETIME DEFAULT CURRENT_TIMESTAMP, + status INTEGER DEFAULT 1 +); +``` + +**中间件验证逻辑** + +```go +func WhitelistMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + update := c.MustGet("telegram_update") + + // 提取user_id和chat_id + if !IsInWhitelist(userID, chatID) { + bot.SendMessage(chatID, "⚠️ 未授权访问") + c.Abort() + return + } + c.Next() + } +} +``` + +## 3.2 网络代理配置 + +**环境变量** + +```shell +TELEGRAM_PROXY_TYPE=socks5|http +TELEGRAM_PROXY_HOST=127.0.0.1 +TELEGRAM_PROXY_PORT=1080 +TELEGRAM_PROXY_USER=optional_username +TELEGRAM_PROXY_PASS=optional_password +``` + +**代理客户端初始化** + +```go +proxyURL, _ := url.Parse( + fmt.Sprintf("%s://%s:%s@%s:%d", + proxyType, user, pass, host, port)) + +httpClient := &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + }, +} + +bot, _ := tgbotapi.NewBotAPIWithClient(token, httpClient) +``` + +## 3.3 数据持久化 + +**存储路径规范** + +- Windows: `%USERPROFILE%\naughty_man\bot.db` +- Linux: `/usr/local/etc/naughty_man/bot.db` + +**核心表设计** + +```sql +-- Token管理 +CREATE TABLE api_tokens ( + id INTEGER PRIMARY KEY, + api_key TEXT NOT NULL, + access_token TEXT UNIQUE, + challenge TEXT, + challenge_expire DATETIME, + token_expire DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 消息日志 +CREATE TABLE message_log ( + id INTEGER PRIMARY KEY, + message_id TEXT, + chat_id BIGINT, + level TEXT, + project TEXT, + content TEXT, + status TEXT, -- pending|sent|failed + retry_count INTEGER DEFAULT 0, + sent_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +## 3.4 配置文件 + +所有可变参数都通过**统一的配置文件**(如`config.yaml`)加载,便于部署和维护 + +- 数据库路径 +- 代理设置 +- AI API Keys +- Token 密钥 +- 正常时段定义 + +--- + +# 四、非功能性需求 + +## 4.1 性能指标 + +- API 响应延迟: P95 \< 200ms +- 消息发送成功率: \> 99.5% +- 定时提醒误差: ±30 秒 +- AI 响应首字延迟: \< 3 秒 + +## 4.2 可靠性 + +- 消息队列持久化避免丢失 +- 失败消息自动重试(指数退避,最多 3 次) +- 死信队列人工介入机制 +- 定时任务崩溃恢复(启动时扫描 pending 任务) + +## 4.3 可观测性 + +**日志分级** + +- ERROR: 消息发送失败、数据库错误 +- WARN: 触及速率限制、Token 即将过期 +- INFO: API 调用、定时任务触发 +- DEBUG: 消息队列状态、AI 流式分片 + +**监控指标** + +- 消息发送 QPS +- 令牌桶剩余容量 +- 数据库连接池状态 +- AI API 调用耗时分布 + +--- + +# 五、实施计划 + +## 阶段一:基础设施 (2 周) + +- 速率限制器实现与测试 +- 数据库表结构设计 +- 白名单 +Token 认证系统 +- 代理支持与连接测试 + +## 阶段二:核心功能 (3 周) + +- 消息通知 API 开发 +- 分级模板渲染引擎 +- 消息队列与重试机制 +- 定时提醒 CRUD 功能 + +## 阶段三:高级功能 (3 周) + +- InlineKeyboard 交互流程 +- AI 多厂商适配层 +- 流式响应处理 +- 性能优化与压测 + +## 阶段四:上线准备 (1 周) + +- 集成测试与修复 +- 文档编写 +- 监控告警配置 +- 灰度发布 + +--- + +# 六、风险与对策 + +| 风险项 | 影响 | 对策 | +| ------------------- | --------------- | --------------------------------- | +| Telegram API 限流 | 消息延迟/丢失 | 令牌桶 + 优先级队列 + 重试机制 [byteplus+1](https://www.byteplus.com/en/topic/450604) | +| AI 厂商服务中断 | 智能体不可用 | 自动降级到下一厂商/返回友好提示 | +| SQLite 并发写入 | 数据竞争 | 使用 WAL 模式 + 写操作串行化 | +| 网络代理不稳定 | Bot 离线 | 心跳检测 + 自动重连 + 备用代理 | + +--- + +# 七、后续演进方向 + +1. **多 Bot 实例**: 分布式部署 + 消息路由中心 +2. **富媒体支持**: 图片、文件、语音消息通知 +3. **自定义指令**: 用户自定义快捷命令 +4. **数据看板**: 消息统计、AI 使用分析 +5. **Webhook 模式**: 替代 Long Polling 提升实时性 \ No newline at end of file diff --git a/4-ProjectNaughtyMan/0-需求规格书/2.1-优化产品需求文档PRD.md b/4-ProjectNaughtyMan/0-需求规格书/2.1-优化产品需求文档PRD.md new file mode 100644 index 0000000..4eaed79 --- /dev/null +++ b/4-ProjectNaughtyMan/0-需求规格书/2.1-优化产品需求文档PRD.md @@ -0,0 +1,798 @@ +# Telegram Bot 智能通知与交互系统 PRD + +**项目代号**: NaughtyMan +**文档版本**: v2.1 +**最后更新**: 2025年10月24日 + +*** + +## 一、产品概述 + +### 1.1 产品定位 + +构建一个企业级 Telegram Bot 系统,集成消息通知、智能提醒、管理功能和 AI 助手,为 ProjectOctopus(服务器监控)和 ProjectTonyStack(金融监控)提供统一的实时通知中枢与交互管理平台。 + +### 1.2 核心价值 + +- **智能分级推送**: 根据事件紧急程度自动调度通知时间和模板 +- **安全可控**: 白名单 + 动态 Token 双重鉴权机制 +- **高可用性**: 内置速率控制器保障稳定运行 +- **AI增强**: OpenRouter API 集成提供智能上下文响应 +- **全面管理**: 用户群组管理与机器人功能动态配置 + + +### 1.3 技术栈 + +- **后端框架**: Gin (Go 语言) +- **Bot SDK**: 原生 Telegram Bot API +- **AI SDK**: OpenAI Go SDK (对接OpenRouter) +- **数据持久化**: SQLite +- **网络代理**: 支持 SOCKS5/HTTP + +*** + +## 二、功能模块详细设计 + +### 2.1 核心模块:消息通知系统 [P0-高优先级] + +#### 2.1.1 接口设计 + +**API 端点结构** + +```go +POST /api/v1/auth/handshake # 握手获取临时Token +POST /api/v1/auth/token # 换取正式Token +POST /api/v1/message/send # 发送消息 +GET /api/v1/message/status # 查询消息状态 +POST /api/v1/message/send-plain # 发送普通消息(非模板) +POST /api/v1/message/send-markdown # 发送Markdown消息 +``` + +**Token 安全机制** + +1. **双阶段认证流程**: + - 阶段一: 客户端携带 API Key 请求握手,服务端返回 challenge 码(60秒有效期) + - 阶段二: 客户端用 challenge+API Secret 生成签名,换取 Access Token(6小时有效期) +2. **Token 刷新策略**: + - Token过期前30分钟允许静默续期 + - 每个API Key同时只允许一个有效Token + - Token中包含加密的白名单用户ID/群组ID + +**请求体示例** + +```json +{ + "target_type": "user|group", + "target_id": "123456789", + "level": "info|warning|critical", + "project": "octopus|tonystack", + "content": { + "title": "服务器CPU告警", + "body": "服务器CPU使用率达85%", + "metadata": { + "server": "prod-web-01", + "timestamp": "2025-10-21T10:23:00Z" + } + } +} +``` + + +#### 2.1.2 消息分级与模板 + +**等级定义** + + +| 等级 | 触发条件 | 推送时段 | 响应时效 | 模板标识 | +| :-- | :-- | :-- | :-- | :-- | +| INFO | 常规事件日志 | 08:00-23:00 | 批量推送(5分钟汇总) | 📘 蓝色图标 | +| WARNING | 异常但可运行 | 08:00-23:00 | 即时推送 | ⚠️ 黄色图标 | +| CRITICAL | 严重故障 | 全天候 | 即时推送+重试 | 🚨 红色图标 | + +**模板配置** + +*ProjectOctopus (服务器监控模板)* + +``` +🖥️ [{{level}}] {{title}} + +📍 服务器: {{metadata.server}} +📊 详情: {{body}} +⏰ 时间: {{timestamp}} + +#服务器监控 #{{level}} +``` + +*ProjectTonyStack (金融监控模板)* + +``` +💹 [{{level}}] {{title}} + +💰 标的: {{metadata.symbol}} +📈 数值: {{body}} +⏰ 时间: {{timestamp}} + +#金融监控 #{{level}} +``` + + +#### 2.1.3 消息发送器包装 + +**普通消息发送器** + +用于发送非模板类的纯文本消息: + +```go +type PlainMessageSender struct { + rateLimiter *RateLimiter + whitelist *WhitelistChecker +} + +func (s *PlainMessageSender) Send(chatID int64, text string) error { + // 白名单验证 + // 速率限制 + // 发送消息 +} +``` + +**AI消息发送器** + +专门处理Markdown格式的AI回复: + +```go +type AIMessageSender struct { + baseSender *PlainMessageSender +} + +func (s *AIMessageSender) SendMarkdown(chatID int64, markdown string) error { + // 转换Markdown格式 + // 使用ParseMode: "MarkdownV2" + // 调用baseSender发送 +} +``` + + +#### 2.1.4 速率限制器设计 + +**Telegram官方限速规则** + +- 不同聊天: 30消息/秒 +- 同一私聊: 1消息/秒 +- 同一群聊: 20消息/分钟 +- editMessageText: 共享发送额度 + +**限制器实现** + +```go +// 单例模式令牌桶算法 +type RateLimiter struct { + globalBucket *TokenBucket // 全局30/s + chatBuckets sync.Map // 每个chat独立限流 + editBucket *TokenBucket // 编辑消息单独限流 +} + +// 消息排队机制 +type MessageQueue struct { + priority PriorityQueue // CRITICAL优先 + retryQueue []Message // 失败重试队列(指数退避) + deadLetterBox []Message // 3次失败转死信队列 +} +``` + + +*** + +### 2.2 机器人预定义功能 [P1-中优先级] + +#### 2.2.1 定时提醒功能 + +**指令**: `/notify` + +**交互流程** + +利用 Telegram InlineKeyboard 实现多步交互: + +``` +用户输入 /notify +↓ +Bot返回主界面: +┌─────────────────────────┐ +│ 📅 日期 🕐 时间 🔁 重复 │ +├─────────────────────────┤ +│ 📝 提醒内容: │ +│ [在此输入...] │ +├─────────────────────────┤ +│ ✅ 创建提醒 │ +└─────────────────────────┘ +``` + +**回调数据结构** + +```json +{ + "action": "notify_date|notify_time|notify_repeat|notify_submit", + "state": { + "date": "2025-10-25", + "time": "09:00", + "repeat": "daily|weekly|biweekly|monthly|custom:[1,3,5]", + "content": "团队站会" + } +} +``` + +**日期选择器实现** + +使用 editMessageText 动态更新消息: + +- 日历视图: 显示当前月份,支持上/下月翻页 +- 时间选择: 时/分滚动选择器(步长15分钟) +- 重复规则: 预设+自定义(RRULE格式) + + +#### 2.2.2 提醒管理功能 + +**指令**: `/notify_list` + +**列表展示** + +``` +📋 您的提醒事项 (3) + +🔁 每日 | 09:00 | 团队站会 + [🗑️ 删除] + +📅 10-25 | 14:30 | 客户电话会议 + [🗑️ 删除] + +📅 10-30 | 全天 | 项目交付DDL + [🗑️ 删除] +``` + +**删除确认机制** + +点击删除后 editMessageText 更新为确认界面: + +``` +⚠️ 确认删除提醒? + +🔁 每日 | 09:00 | 团队站会 + +[❌ 取消] [✅ 确认删除] +``` + + +#### 2.2.3 数据持久化 + +**SQLite 表结构** + +```sql +CREATE TABLE reminders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id BIGINT NOT NULL, + chat_id BIGINT NOT NULL, + content TEXT NOT NULL, + trigger_time DATETIME NOT NULL, + repeat_rule TEXT, -- RRULE格式 + next_trigger DATETIME, + status INTEGER DEFAULT 1, -- 1:活动 0:已删除 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_next_trigger (next_trigger, status) +); +``` + + +*** + +### 2.3 Telegram管理功能 [P1-中优先级] **[新增]** + +#### 2.3.1 用户群组管理 + +**API端点** + +```go +GET /api/v1/admin/users # 查看所有用户ID +GET /api/v1/admin/groups # 查看所有群组ID +GET /api/v1/admin/user/:id # 查询用户详细信息 +POST /api/v1/admin/user/:id/update # 更新用户信息 +``` + +**用户信息存储** + +```sql +CREATE TABLE user_profiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id BIGINT UNIQUE NOT NULL, + username TEXT, + first_name TEXT, + last_name TEXT, + phone_number TEXT, + language_code TEXT, + is_bot INTEGER DEFAULT 0, + last_interaction DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE group_profiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id BIGINT UNIQUE NOT NULL, + title TEXT, + type TEXT, -- group, supergroup, channel + member_count INTEGER, + last_interaction DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +**功能特性** + +- 自动记录所有与Bot交互的用户/群组信息 +- 支持通过API查询用户完整档案 +- 持久化保存用户交互历史 + + +#### 2.3.2 机器人功能管理 + +**动态指令注册系统** + +```go +type BotCommand struct { + Command string // 指令名称 (如 "notify") + Description string // 指令描述 + Handler string // 处理器标识 + IsEnabled bool // 是否启用 + Scope string // user/group/all +} +``` + +**API端点** + +```go +GET /api/v1/admin/commands # 查看所有注册指令 +POST /api/v1/admin/commands # 注册新指令 +PUT /api/v1/admin/commands/:cmd # 修改指令 +DELETE /api/v1/admin/commands/:cmd # 删除指令 +POST /api/v1/admin/commands/sync # 同步到Telegram +``` + +**数据表设计** + +```sql +CREATE TABLE bot_commands ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + command TEXT UNIQUE NOT NULL, + description TEXT, + handler_name TEXT NOT NULL, + is_enabled INTEGER DEFAULT 1, + scope TEXT DEFAULT 'all', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +**预定义功能注册** + +系统启动时自动注册核心指令: + +- `/notify` - 创建定时提醒 +- `/notify_list` - 查看提醒列表 +- `/help` - 帮助信息 +- `/start` - 开始对话 + +*** + +### 2.4 AI智能体功能 [P1-中优先级] + +#### 2.4.1 触发机制 **[更新]** + +**双模式触发** + +- **私聊模式**: 直接回复用户发送至Bot的所有消息 +- **群聊模式**: 仅响应@机器人的消息 + +```go +func shouldRespondToMessage(msg *Message) bool { + // 私聊模式: 所有消息都响应 + if msg.Chat.Type == "private" { + return true + } + + // 群聊模式: 检查是否@了机器人 + if msg.Chat.Type == "group" || msg.Chat.Type == "supergroup" { + for _, entity := range msg.Entities { + if entity.Type == "mention" && entity.IsBotMention { + return true + } + } + } + + return false +} +``` + + +#### 2.4.2 上下文管理 + +**消息引用策略** + +``` +触发消息(被@的消息或私聊消息) +↓ 引用 +前3条历史消息 +↓ 拼接 +发送给AI API +``` + +**上下文窗口** + +```json +{ + "messages": [ + {"role": "system", "content": "你是Telegram群助手..."}, + {"role": "user", "content": "历史消息1", "name": "user123"}, + {"role": "user", "content": "历史消息2", "name": "user456"}, + {"role": "user", "content": "历史消息3", "name": "user123"}, + {"role": "user", "content": "当前触发消息", "name": "user789"} + ] +} +``` + + +#### 2.4.3 AI集成实现 **[更新]** + +**首选方案: OpenRouter** + +使用OpenAI Go SDK对接OpenRouter API: + +```go +import ( + "github.com/sashabaranov/go-openai" +) + +func initAIClient() *openai.Client { + config := openai.DefaultConfig(os.Getenv("OPENROUTER_API_KEY")) + config.BaseURL = "https://openrouter.ai/api/v1" + + return openai.NewClientWithConfig(config) +} +``` + +**模型选择建议** + + +| 提供商 | 推荐模型 | 特性 | +| :-- | :-- | :-- | +| OpenRouter | anthropic/claude-3.5-sonnet | 思考链支持 | +| OpenRouter | google/gemini-pro-1.5 | 长上下文 | +| OpenRouter | openai/gpt-4-turbo | 通用平衡 | + +**流式响应处理** + +```go +1. 发送初始消息"🤔 正在思考..." +2. 创建流式请求到OpenRouter +3. 累积AI响应分片(每500字符或5秒) +4. 使用AIMessageSender.SendMarkdown更新消息 +5. 遵守编辑速率限制(复用全局限流器) +6. 完成后添加"✅ 回答完成"标识 +``` + +**思考过程展示** + +``` +🤔 思考中... + +【推理步骤1】分析问题... +【推理步骤2】检索知识... + +💡 回答: +[AI生成的完整答复] + +✅ 回答完成 | 用时3.2s +``` + + +#### 2.4.4 交互日志记录 **[新增]** + +**日志输出格式** + +```go +type AIInteractionLog struct { + Timestamp time.Time + UserID int64 + Username string + PhoneNumber string // 可能为空 + GroupID int64 // 私聊时为0 + GroupName string // 私聊时为空 + MessageText string + AIResponse string + Duration time.Duration +} +``` + +**日志输出示例** + +``` +[2025-10-24 15:23:45] AI交互记录 +用户ID: 123456789 | 用户名: @john_doe | 手机号: +1234567890 +群组ID: 987654321 | 群组名: 开发团队讨论组 +消息: 今天天气怎么样? +AI回复: 我无法获取实时天气信息... +耗时: 2.3s +``` + + +*** + +## 三、基础架构设计 + +### 3.1 安全控制 + +**白名单机制** + +```sql +CREATE TABLE whitelist ( + id INTEGER PRIMARY KEY, + type TEXT CHECK(type IN ('user', 'group')), + entity_id BIGINT UNIQUE NOT NULL, + alias TEXT, + added_by BIGINT, + added_at DATETIME DEFAULT CURRENT_TIMESTAMP, + status INTEGER DEFAULT 1 +); +``` + +**中间件验证逻辑** + +```go +func WhitelistMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + update := c.MustGet("telegram_update") + + // 提取user_id和chat_id + if !IsInWhitelist(userID, chatID) { + bot.SendMessage(chatID, "⚠️ 未授权访问") + c.Abort() + return + } + c.Next() + } +} +``` + + +### 3.2 网络代理配置 + +**环境变量** + +```shell +TELEGRAM_PROXY_TYPE=socks5|http +TELEGRAM_PROXY_HOST=127.0.0.1 +TELEGRAM_PROXY_PORT=1080 +TELEGRAM_PROXY_USER=optional_username +TELEGRAM_PROXY_PASS=optional_password +``` + +**代理客户端初始化** + +```go +proxyURL, _ := url.Parse( + fmt.Sprintf("%s://%s:%s@%s:%d", + proxyType, user, pass, host, port)) + +httpClient := &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + }, +} + +bot, _ := tgbotapi.NewBotAPIWithClient(token, httpClient) +``` + + +### 3.3 数据持久化 + +**存储路径规范** + +- Windows: `%USERPROFILE%\naughty_man\bot.db` +- Linux: `/usr/local/etc/naughty_man/bot.db` + +**核心表设计** + +```sql +-- Token管理 +CREATE TABLE api_tokens ( + id INTEGER PRIMARY KEY, + api_key TEXT NOT NULL, + access_token TEXT UNIQUE, + challenge TEXT, + challenge_expire DATETIME, + token_expire DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 消息日志 +CREATE TABLE message_log ( + id INTEGER PRIMARY KEY, + message_id TEXT, + chat_id BIGINT, + level TEXT, + project TEXT, + content TEXT, + status TEXT, -- pending|sent|failed + retry_count INTEGER DEFAULT 0, + sent_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- AI交互日志 +CREATE TABLE ai_interaction_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id BIGINT NOT NULL, + username TEXT, + phone_number TEXT, + group_id BIGINT, + group_name TEXT, + message_text TEXT, + ai_response TEXT, + duration_ms INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + + +### 3.4 配置文件 + +**统一配置管理** + +所有可变参数通过`config.yaml`加载: + +```yaml +database: + path: "./data/bot.db" + +proxy: + enabled: true + type: "socks5" + host: "127.0.0.1" + port: 1080 + +ai: + provider: "openrouter" + api_key: "${OPENROUTER_API_KEY}" + model: "anthropic/claude-3.5-sonnet" + +auth: + token_secret: "${TOKEN_SECRET}" + token_expire_hours: 6 + +notification: + normal_hours: "08:00-23:00" +``` + + +*** + +## 四、非功能性需求 + +### 4.1 性能指标 + +- API响应延迟: P95 < 200ms +- 消息发送成功率: > 99.5% +- 定时提醒误差: ±30秒 +- AI响应首字延迟: < 3秒 + + +### 4.2 可靠性 + +- 消息队列持久化避免丢失 +- 失败消息自动重试(指数退避,最多3次) +- 死信队列人工介入机制 +- 定时任务崩溃恢复(启动时扫描pending任务) + + +### 4.3 可观测性 + +**日志分级** + +- ERROR: 消息发送失败、数据库错误 +- WARN: 触及速率限制、Token即将过期 +- INFO: API调用、定时任务触发、AI交互记录 +- DEBUG: 消息队列状态、AI流式分片 + +**监控指标** + +- 消息发送QPS +- 令牌桶剩余容量 +- 数据库连接池状态 +- AI API调用耗时分布 +- 用户活跃度统计 + +*** + +## 五、实施计划 + +### 阶段一:基础设施 (2周) + +- 速率限制器实现与测试 +- 数据库表结构设计(包含新增管理表) +- 白名单+Token认证系统 +- 代理支持与连接测试 + + +### 阶段二:核心功能 (3周) + +- 消息通知API开发 +- 分级模板渲染引擎 +- 普通/AI消息发送器包装 +- 消息队列与重试机制 +- 定时提醒CRUD功能 + + +### 阶段三:管理功能 (2周) **[新增]** + +- 用户群组信息采集与存储 +- 管理API接口开发 +- 动态指令注册系统 +- 预定义功能自动注册 + + +### 阶段四:AI功能 (3周) + +- OpenRouter API集成(使用OpenAI SDK) +- 私聊/群聊双模式触发 +- 流式响应处理 +- AI交互日志记录 +- InlineKeyboard交互流程 + + +### 阶段五:上线准备 (1周) + +- 集成测试与修复 +- 文档编写 +- 监控告警配置 +- 灰度发布 + +*** + +## 六、风险与对策 + +| 风险项 | 影响 | 对策 | +| :-- | :-- | :-- | +| Telegram API限流 | 消息延迟/丢失 | 令牌桶+优先级队列+重试机制 | +| OpenRouter服务中断 | 智能体不可用 | 降级提示+本地缓存回复 | +| SQLite并发写入 | 数据竞争 | WAL模式+写操作串行化 | +| 网络代理不稳定 | Bot离线 | 心跳检测+自动重连+备用代理 | +| 用户隐私泄露 | 合规风险 | 手机号加密存储+API鉴权 | + + +*** + +## 七、后续演进方向 + +1. **多厂商AI支持**: 完整实现OpenAI/Gemini/Grok切换 +2. **多Bot实例**: 分布式部署+消息路由中心 +3. **富媒体支持**: 图片、文件、语音消息通知 +4. **自定义指令**: 用户自定义快捷命令 +5. **数据看板**: 消息统计、AI使用分析、用户活跃度 +6. **Webhook模式**: 替代Long Polling提升实时性 + +*** + +## 八、文档变更记录 + +**v2.1 (2025-10-24)** + +- 新增Telegram管理功能模块(用户群组管理/机器人功能管理) +- AI智能体触发机制扩展至私聊模式 +- 明确AI实现为OpenRouter优先,使用OpenAI Go SDK +- 新增AI消息通知发送器包装 +- 新增AI交互日志记录要求 +- 术语更新:"机器人功能"改为"机器人预定义功能" + +**v2.0 (2025-10-21)** + +- 初始PRD文档发布 + + diff --git a/4-ProjectNaughtyMan/1-概要详细设计/0-概要设计prompt.md b/4-ProjectNaughtyMan/1-概要详细设计/0-概要设计prompt.md new file mode 100644 index 0000000..1e89724 --- /dev/null +++ b/4-ProjectNaughtyMan/1-概要详细设计/0-概要设计prompt.md @@ -0,0 +1,26 @@ +你是一名资深的软件系统架构师,具备以下核心职责与能力,绘图请使用mermaid语言: + +## 需求分析与理解 + +- 深度解读产品需求文档(PRD),识别业务目标、功能需求与非功能性需求 +- 分析需求间的依赖关系与优先级,识别潜在的技术风险与挑战 + +## 架构设计与规划 + +- 设计高可用、可扩展、高性能的系统架构,确保系统健壮性与安全性 +- 制定技术选型方案,包括开发语言、框架、中间件、数据库等关键技术栈 +- 绘制多层次架构图(系统架构图、数据流图、组件交互图) +- 定义模块划分、接口规范、数据模型与核心算法策略 +- 规划系统分层结构(展现层、业务层、数据层),明确各层职责边界 + +## 方案设计与输出 + +- 针对核心需求点提供详细的技术解决方案,包含实现路径与备选方案 +- 设计关键业务流程的时序图与状态机,确保逻辑清晰完整 +- 输出规范化的《系统详细设计说明书》,包含架构设计、接口定义、数据库设计等完整文档 + + + +请根据[2-优化产品需求文档PRD.md],按照上述的要求,输出系统详细设计说明书 + +你之前根据[2-优化产品需求文档PRD.md]输出了[3-详细设计说明书.md], 目前PRD需求有一些更新,见[2.1-优化产品需求文档PRD.md],请你详细对比[2.1-优化产品需求文档PRD.md]和[2-优化产品需求文档PRD.md]之间的差异, 修改[3-详细设计说明书.md],得到新的需求文档.要求尽量不改动[3-详细设计说明书.md]的初始设计,只改动差异化部分的设计 \ No newline at end of file diff --git a/4-ProjectNaughtyMan/1-概要详细设计/3-详细设计说明书.md b/4-ProjectNaughtyMan/1-概要详细设计/3-详细设计说明书.md new file mode 100644 index 0000000..6bbc675 --- /dev/null +++ b/4-ProjectNaughtyMan/1-概要详细设计/3-详细设计说明书.md @@ -0,0 +1,1525 @@ +### **项目代号**: NaughtyMan + +**文档版本**: v1.0 + +**编制日期**: 2025年10月21日 + +**技术架构师**: [待填写] + +--- + +## 系统架构设计 + +### 总体架构 + +系统采用**分层微服务架构**,由接口层、业务逻辑层、数据访问层和外部服务层构成。架构设计遵循高内聚低耦合原则,确保各模块独立演进能力。 + +```mermaid +graph TB + subgraph "客户端层" + A1[ProjectOctopus] + A2[ProjectTonyStack] + A3[Telegram用户端] + end + + subgraph "接入层" + B1[RESTful API网关
Gin框架] + B2[Telegram Webhook
Long Polling] + end + + subgraph "业务逻辑层" + C1[认证服务
AuthService] + C2[消息调度服务
MessageDispatcher] + C3[提醒管理服务
ReminderService] + C4[AI交互服务
AIService] + C5[速率控制服务
RateLimiter] + end + + subgraph "数据访问层" + D1[消息队列
PriorityQueue] + D2[SQLite数据库
WAL模式] + D3[缓存层
sync.Map] + end + + subgraph "外部服务层" + E1[Telegram Bot API] + E2[OpenAI API] + E3[Google Gemini] + E4[xAI Grok] + E5[SOCKS5/HTTP代理] + end + + A1 --> B1 + A2 --> B1 + A3 --> B2 + B1 --> C1 + B1 --> C2 + B2 --> C3 + B2 --> C4 + C2 --> C5 + C3 --> D2 + C4 --> D3 + C2 --> D1 + C5 --> E1 + C4 --> E2 + C4 --> E3 + C4 --> E4 + E1 -.代理.-> E5 + +``` + +### 技术选型说明 + +| 技术组件 | 选型方案 | 选型理由 | +| --- | --- | --- | +| 开发语言 | Go 1.21+ | 原生并发支持、高性能、跨平台编译、内存安全 | +| Web框架 | Gin v1.9 | 轻量级、路由高效、中间件生态完善、社区活跃 | +| 数据库 | SQLite 3.40+ | 零配置部署、事务ACID保证、跨平台兼容、适合中小规模数据 | +| Bot SDK | tgbotapi v5 | 官方API完整封装、长连接稳定、支持代理配置 | +| 并发控制 | sync.Map + Channel | 原生并发安全结构、无锁化设计、性能优越 | +| 日志框架 | zap | 结构化日志、高性能、灵活的日志等级控制 | + +--- + +## 核心模块详细设计 + +### 认证与安全模块 + +### 双阶段Token认证流程 + +```mermaid +sequenceDiagram + participant C as 客户端 + participant G as API网关 + participant A as 认证服务 + participant D as 数据库 + + C->>G: POST /auth/handshake
{api_key} + G->>A: 验证API Key + A->>D: 查询白名单 + D-->>A: 返回权限范围 + A->>A: 生成Challenge码
(UUID + 时间戳) + A->>D: 存储Challenge
(60s有效期) + A-->>C: {challenge, expire_at} + + Note over C: 计算签名
HMAC-SHA256(challenge+api_secret) + + C->>G: POST /auth/token
{api_key, challenge, signature} + G->>A: 验证签名 + A->>D: 查询Challenge + D-->>A: Challenge数据 + A->>A: 对比签名
检查时效 + A->>A: 生成JWT Token
(6h有效期) + A->>D: 记录Token映射 + A-->>C: {access_token, refresh_token} + +``` + +### Token数据结构设计 + +**JWT Payload规范**: + +```go +type TokenClaims struct { + ApiKey string `json:"api_key"` + Whitelist []int64 `json:"whitelist"` // 加密的授权目标ID列表 + Permissions []string `json:"permissions"` // send_message, query_status + IssuedAt int64 `json:"iat"` + ExpiresAt int64 `json:"exp"` + RefreshAfter int64 `json:"rfa"` // 允许刷新的时间阈值 +} + +``` + +**数据库表结构**: + +```sql +CREATE TABLE api_credentials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + api_key TEXT UNIQUE NOT NULL, + api_secret TEXT NOT NULL, -- BCRYPT加密存储 + project TEXT NOT NULL, -- octopus/tonystack + whitelist_ids TEXT, -- JSON数组存储 + status INTEGER DEFAULT 1, -- 1:启用 0:禁用 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_used DATETIME, + INDEX idx_api_key (api_key) +); + +CREATE TABLE token_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + api_key TEXT NOT NULL, + challenge TEXT, + challenge_expire DATETIME, + access_token TEXT UNIQUE, + refresh_token TEXT UNIQUE, + token_expire DATETIME, + client_ip TEXT, + user_agent TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_access_token (access_token), + INDEX idx_challenge (challenge, challenge_expire) +); + +``` + +### 白名单验证机制 + +**中间件实现**: + +```go +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + + // 1. 解析JWT Token + claims, err := jwt.Parse(token, secretKey) + if err != nil || time.Now().Unix() > claims.ExpiresAt { + c.JSON(401, gin.H{"error": "无效或过期的Token"}) + c.Abort() + return + } + + // 2. 验证目标ID在白名单内 + var req MessageRequest + c.BindJSON(&req) + + if !contains(claims.Whitelist, req.TargetID) { + c.JSON(403, gin.H{"error": "目标未授权"}) + c.Abort() + return + } + + // 3. 注入上下文 + c.Set("api_key", claims.ApiKey) + c.Set("project", claims.Project) + c.Next() + } +} + +``` + +--- + +### 消息通知系统 + +### 消息调度核心架构 + +```mermaid +graph LR + A[API请求] --> B{消息等级判断} + B -->|INFO| C[时段检查器
08:00-23:00] + B -->|WARNING| D[即时队列] + B -->|CRITICAL| D + C -->|时段内| E[批量聚合器
5分钟窗口] + C -->|时段外| F[延迟队列
次日8:00] + E --> G[优先级队列] + D --> G + F --> G + G --> H[速率限制器] + H --> I[Telegram API] + I -->|成功| J[消息日志] + I -->|失败| K{重试判断} + K -->|次数<3| L[指数退避重试] + K -->|次数≥3| M[死信队列] + L --> H + +``` + +### 优先级队列实现 + +**数据结构设计**: + +```go +type Message struct { + ID string + ChatID int64 + Level string // INFO/WARNING/CRITICAL + Content string + Metadata map[string]interface{} + Priority int // CRITICAL:3, WARNING:2, INFO:1 + RetryCount int + ScheduledAt time.Time + CreatedAt time.Time +} + +type PriorityQueue struct { + heap *priorityHeap // 优先级小顶堆 + mutex sync.RWMutex + notEmpty chan struct{} // 通知消费者 + deadLetter chan Message // 死信队列通道 +} + +// 堆排序规则 +func (pq *priorityHeap) Less(i, j int) bool { + // 1. 优先级高的优先 + if pq[i].Priority != pq[j].Priority { + return pq[i].Priority > pq[j].Priority + } + // 2. 相同优先级按时间戳排序 + return pq[i].ScheduledAt.Before(pq[j].ScheduledAt) +} + +``` + +### 消息模板渲染引擎 + +**模板定义规范**: + +```go +type MessageTemplate struct { + Project string + Level string + Template string +} + +var templates = map[string]MessageTemplate{ + "octopus_critical": { + Project: "octopus", + Level: "CRITICAL", + Template: `🚨 【严重告警】{{.Title}} + +📍 服务器: {{.Metadata.server}} +📊 详情: {{.Body}} +⏰ 时间: {{formatTime .Metadata.timestamp}} +🔗 查看详情: {{.Metadata.dashboard_url}} + +#服务器监控 #CRITICAL`, + }, + "tonystack_warning": { + Project: "tonystack", + Level: "WARNING", + Template: `⚠️ 【风险提示】{{.Title}} + +💰 标的: {{.Metadata.symbol}} +📈 触发值: {{.Body}} +📉 当前价格: {{.Metadata.current_price}} +⏰ 时间: {{formatTime .Metadata.timestamp}} + +#金融监控 #WARNING`, + }, +} + +// 渲染函数 +func RenderTemplate(msg Message) (string, error) { + key := fmt.Sprintf("%s_%s", msg.Project, strings.ToLower(msg.Level)) + tmpl, exists := templates[key] + if !exists { + return "", errors.New("模板不存在") + } + + t := template.New("message").Funcs(template.FuncMap{ + "formatTime": func(ts string) string { + t, _ := time.Parse(time.RFC3339, ts) + return t.Format("2006-01-02 15:04:05") + }, + }) + + t, _ = t.Parse(tmpl.Template) + var buf bytes.Buffer + t.Execute(&buf, msg) + return buf.String(), nil +} + +``` + +### 速率限制器设计 + +**令牌桶算法实现**: + +```go +type TokenBucket struct { + capacity int // 桶容量 + tokens int // 当前令牌数 + refillRate time.Duration // 补充速率 + lastRefill time.Time + mutex sync.Mutex +} + +func (tb *TokenBucket) Take() bool { + tb.mutex.Lock() + defer tb.mutex.Unlock() + + // 计算应补充的令牌数 + now := time.Now() + elapsed := now.Sub(tb.lastRefill) + refillCount := int(elapsed / tb.refillRate) + + if refillCount > 0 { + tb.tokens = min(tb.capacity, tb.tokens + refillCount) + tb.lastRefill = now + } + + // 尝试获取令牌 + if tb.tokens > 0 { + tb.tokens-- + return true + } + return false +} + +type RateLimiter struct { + globalBucket *TokenBucket // 全局30/s + chatBuckets sync.Map // map[int64]*TokenBucket + editBucket *TokenBucket // 编辑消息限流 +} + +func NewRateLimiter() *RateLimiter { + return &RateLimiter{ + globalBucket: &TokenBucket{ + capacity: 30, + tokens: 30, + refillRate: time.Second / 30, + lastRefill: time.Now(), + }, + editBucket: &TokenBucket{ + capacity: 20, + tokens: 20, + refillRate: time.Minute / 20, + lastRefill: time.Now(), + }, + } +} + +func (rl *RateLimiter) AllowSend(chatID int64, isEdit bool) bool { + // 1. 检查全局限流 + if !rl.globalBucket.Take() { + return false + } + + // 2. 编辑消息检查专用桶 + if isEdit && !rl.editBucket.Take() { + rl.globalBucket.tokens++ // 归还全局令牌 + return false + } + + // 3. 检查单聊天限流 + bucket := rl.getChatBucket(chatID) + if !bucket.Take() { + rl.globalBucket.tokens++ + return false + } + + return true +} + +``` + +--- + +### 定时提醒系统 + +### 状态机设计 + +```mermaid +stateDiagram-v2 + [*] --> 等待输入: /notify命令 + 等待输入 --> 选择日期: 点击日期按钮 + 选择日期 --> 选择时间: 确认日期 + 选择时间 --> 配置重复: 确认时间 + 配置重复 --> 输入内容: 选择重复规则 + 输入内容 --> 确认创建: 输入提醒文本 + 确认创建 --> 已激活: 提交成功 + 已激活 --> 已触发: 到达触发时间 + 已触发 --> 已激活: 有重复规则 + 已触发 --> 已完成: 无重复规则 + 已激活 --> 已删除: 用户删除 + 已完成 --> [*] + 已删除 --> [*] + +``` + +### 日历选择器UI实现 + +**InlineKeyboard布局**: + +```go +func GenerateCalendar(year int, month time.Month) [][]tgbotapi.InlineKeyboardButton { + firstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local) + lastDay := firstDay.AddDate(0, 1, -1) + + keyboard := [][]tgbotapi.InlineKeyboardButton{ + // 月份导航行 + { + tgbotapi.NewInlineKeyboardButtonData("◀", fmt.Sprintf("cal_prev_%d_%d", year, month)), + tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%d年%d月", year, month), "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("▶", fmt.Sprintf("cal_next_%d_%d", year, month)), + }, + // 星期标题行 + { + tgbotapi.NewInlineKeyboardButtonData("日", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("一", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("二", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("三", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("四", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("五", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("六", "cal_ignore"), + }, + } + + // 日期网格生成 + currentWeek := []tgbotapi.InlineKeyboardButton{} + for weekday := 0; weekday < int(firstDay.Weekday()); weekday++ { + currentWeek = append(currentWeek, tgbotapi.NewInlineKeyboardButtonData(" ", "cal_ignore")) + } + + for day := 1; day <= lastDay.Day(); day++ { + date := time.Date(year, month, day, 0, 0, 0, 0, time.Local) + callback := fmt.Sprintf("cal_select_%s", date.Format("2006-01-02")) + + // 过期日期禁用 + label := fmt.Sprintf("%d", day) + if date.Before(time.Now().Truncate(24 * time.Hour)) { + callback = "cal_ignore" + label = "×" + } + + currentWeek = append(currentWeek, tgbotapi.NewInlineKeyboardButtonData(label, callback)) + + // 每行7天 + if len(currentWeek) == 7 { + keyboard = append(keyboard, currentWeek) + currentWeek = []tgbotapi.InlineKeyboardButton{} + } + } + + if len(currentWeek) > 0 { + keyboard = append(keyboard, currentWeek) + } + + return keyboard +} + +``` + +### 提醒调度器实现 + +**定时扫描机制**: + +```go +type ReminderScheduler struct { + db *sql.DB + bot *tgbotapi.BotAPI + ticker *time.Ticker + stopChan chan struct{} +} + +func (rs *ReminderScheduler) Start() { + rs.ticker = time.NewTicker(30 * time.Second) + + go func() { + for { + select { + case <-rs.ticker.C: + rs.processPendingReminders() + case <-rs.stopChan: + return + } + } + }() +} + +func (rs *ReminderScheduler) processPendingReminders() { + // 查询需触发的提醒(未来5分钟内) + rows, _ := rs.db.Query(` + SELECT id, chat_id, user_id, content, repeat_rule, next_trigger + FROM reminders + WHERE status = 1 + AND next_trigger <= datetime('now', '+5 minutes') + ORDER BY next_trigger + `) + defer rows.Close() + + for rows.Next() { + var r Reminder + rows.Scan(&r.ID, &r.ChatID, &r.UserID, &r.Content, &r.RepeatRule, &r.NextTrigger) + + // 等待到精确触发时间 + wait := r.NextTrigger.Sub(time.Now()) + if wait > 0 { + time.Sleep(wait) + } + + // 发送提醒消息 + msg := tgbotapi.NewMessage(r.ChatID, fmt.Sprintf("⏰ 提醒\\n\\n%s", r.Content)) + rs.bot.Send(msg) + + // 更新下次触发时间 + if r.RepeatRule != "" { + nextTrigger := calculateNextTrigger(r.NextTrigger, r.RepeatRule) + rs.db.Exec(`UPDATE reminders SET next_trigger = ? WHERE id = ?`, nextTrigger, r.ID) + } else { + rs.db.Exec(`UPDATE reminders SET status = 0 WHERE id = ?`, r.ID) + } + } +} + +func calculateNextTrigger(current time.Time, rule string) time.Time { + switch rule { + case "daily": + return current.AddDate(0, 0, 1) + case "weekly": + return current.AddDate(0, 0, 7) + case "biweekly": + return current.AddDate(0, 0, 14) + case "monthly": + return current.AddDate(0, 1, 0) + default: + // 解析RRULE格式 + return parseRRule(current, rule) + } +} + +``` + +--- + +### AI智能体系统 + +### 多厂商适配层架构 + +```mermaid +graph TB + A[Telegram消息] --> B{触发条件判断} + B -->|私聊| C[直接触发] + B -->|"群聊@Bot"| C + B -->|其他| D[忽略] + C --> E[上下文提取器] + E --> F[消息历史查询
最近3条] + F --> G[提示词构建器] + G --> H[AI路由器] + H --> I1[OpenAI适配器] + H --> I2[Gemini适配器] + H --> I3[Grok适配器] + H --> I4[OpenRouter适配器] + I1 --> J[流式响应处理器] + I2 --> J + I3 --> J + I4 --> J + J --> K[消息分段器
500字符/5秒] + K --> L[editMessageText] + L --> M[速率限制器] + M --> N[Telegram API] + +``` + +### AI适配器接口定义 + +```go +type AIProvider interface { + Name() string + Chat(ctx context.Context, messages []ChatMessage, stream bool) (<-chan string, error) + SupportStream() bool + SupportReasoning() bool +} + +type ChatMessage struct { + Role string `json:"role"` // system/user/assistant + Content string `json:"content"` + Name string `json:"name,omitempty"` + Meta map[string]interface{} `json:"-"` +} + +// OpenAI适配器实现 +type OpenAIProvider struct { + apiKey string + model string + client *http.Client +} + +func (p *OpenAIProvider) Chat(ctx context.Context, messages []ChatMessage, stream bool) (<-chan string, error) { + reqBody := map[string]interface{}{ + "model": p.model, + "messages": messages, + "stream": stream, + } + + if stream { + return p.streamChat(ctx, reqBody) + } + return p.blockingChat(ctx, reqBody) +} + +func (p *OpenAIProvider) streamChat(ctx context.Context, reqBody map[string]interface{}) (<-chan string, error) { + ch := make(chan string, 10) + + req, _ := http.NewRequestWithContext(ctx, "POST", + "", + toJSON(reqBody)) + req.Header.Set("Authorization", "Bearer "+p.apiKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := p.client.Do(req) + if err != nil { + return nil, err + } + + go func() { + defer close(ch) + defer resp.Body.Close() + + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "data: ") { + continue + } + + data := strings.TrimPrefix(line, "data: ") + if data == "[DONE]" { + break + } + + var chunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + } + + json.Unmarshal([]byte(data), &chunk) + if len(chunk.Choices) > 0 { + ch <- chunk.Choices[^0].Delta.Content + } + } + }() + + return ch, nil +} + +``` + +### 流式响应处理策略 + +**分段更新逻辑**: + +```go +type StreamHandler struct { + bot *tgbotapi.BotAPI + rateLimiter *RateLimiter + updateBuffer strings.Builder + lastUpdate time.Time + messageID int + chatID int64 +} + +func (sh *StreamHandler) ProcessStream(ctx context.Context, stream <-chan string) error { + initialMsg := tgbotapi.NewMessage(sh.chatID, "🤔 思考中...") + sent, _ := sh.bot.Send(initialMsg) + sh.messageID = sent.MessageID + sh.lastUpdate = time.Now() + + for { + select { + case chunk, ok := <-stream: + if !ok { + // 流结束,发送最终消息 + sh.finalUpdate("✅ 回答完成") + return nil + } + + sh.updateBuffer.WriteString(chunk) + + // 触发更新条件: 500字符 OR 5秒间隔 + shouldUpdate := sh.updateBuffer.Len() >= 500 || + time.Since(sh.lastUpdate) >= 5*time.Second + + if shouldUpdate { + sh.sendUpdate() + } + + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func (sh *StreamHandler) sendUpdate() { + content := sh.updateBuffer.String() + + // 等待速率限制器允许 + for !sh.rateLimiter.AllowSend(sh.chatID, true) { + time.Sleep(100 * time.Millisecond) + } + + edit := tgbotapi.NewEditMessageText(sh.chatID, sh.messageID, content) + edit.ParseMode = "Markdown" + sh.bot.Send(edit) + + sh.lastUpdate = time.Now() +} + +``` + +--- + +## 数据库设计 + +### ER图 + +```mermaid +erDiagram + API_CREDENTIALS ||--o{ TOKEN_SESSIONS : generates + API_CREDENTIALS ||--o{ MESSAGE_LOG : sends + WHITELIST ||--o{ API_CREDENTIALS : authorizes + REMINDERS }o--|| WHITELIST : belongs_to + + API_CREDENTIALS { + int id PK + string api_key UK + string api_secret + string project + json whitelist_ids + int status + datetime created_at + } + + TOKEN_SESSIONS { + int id PK + string api_key FK + string challenge + datetime challenge_expire + string access_token UK + string refresh_token UK + datetime token_expire + string client_ip + } + + WHITELIST { + int id PK + string type + bigint entity_id UK + string alias + int status + datetime added_at + } + + MESSAGE_LOG { + int id PK + string message_id + bigint chat_id + string level + string project FK + text content + string status + int retry_count + datetime sent_at + } + + REMINDERS { + int id PK + bigint user_id + bigint chat_id FK + text content + datetime trigger_time + string repeat_rule + datetime next_trigger + int status + } + + AI_CONVERSATIONS { + int id PK + bigint chat_id + bigint user_id + text user_message + text ai_response + string provider + string model + int tokens_used + datetime created_at + } + +``` + +### 核心表索引策略 + +```sql +-- 消息日志表优化索引 +CREATE INDEX idx_message_log_composite ON message_log(status, created_at); +CREATE INDEX idx_message_log_chat ON message_log(chat_id, sent_at DESC); + +-- 提醒表优化索引 +CREATE INDEX idx_reminders_trigger ON reminders(next_trigger, status) WHERE status = 1; +CREATE INDEX idx_reminders_user ON reminders(user_id, status); + +-- Token会话表优化索引 +CREATE INDEX idx_token_expire ON token_sessions(token_expire) WHERE token_expire > datetime('now'); + +-- AI对话表优化索引 +CREATE INDEX idx_ai_conversations_chat ON ai_conversations(chat_id, created_at DESC); + +``` + +--- + +## 接口设计 + +### RESTful API规范 + +### 认证接口 + +**1. 握手接口** + +``` +POST /api/v1/auth/handshake +Content-Type: application/json + +{ + "api_key": "octopus_ak_1234567890" +} + +``` + +**响应**: + +```json +{ + "challenge": "uuid-v4-string", + "expire_at": "2025-10-21T17:01:00Z", + "algorithm": "HMAC-SHA256" +} + +``` + +**2. Token获取接口** + +``` +POST /api/v1/auth/token +Content-Type: application/json + +{ + "api_key": "octopus_ak_1234567890", + "challenge": "uuid-v4-string", + "signature": "hex-encoded-hmac-sha256" +} + +``` + +**响应**: + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "Bearer", + "expires_in": 21600, + "refresh_token": "refresh-token-string" +} + +``` + +### 消息发送接口 + +``` +POST /api/v1/message/send +Authorization: Bearer +Content-Type: application/json + +{ + "target_type": "user", + "target_id": "123456789", + "level": "warning", + "project": "octopus", + "content": { + "title": "CPU使用率告警", + "body": "服务器CPU使用率达85%", + "metadata": { + "server": "prod-web-01", + "cpu_usage": 85.3, + "threshold": 80, + "timestamp": "2025-10-21T16:58:00Z", + "dashboard_url": "" + } + } +} + +``` + +**响应**: + +```json +{ + "message_id": "msg_abc123", + "status": "queued", + "estimated_send_time": "2025-10-21T17:00:00Z", + "priority": 2 +} + +``` + +### 消息状态查询接口 + +``` +GET /api/v1/message/status?message_id=msg_abc123 +Authorization: Bearer + +``` + +**响应**: + +```json +{ + "message_id": "msg_abc123", + "status": "sent", + "telegram_message_id": 98765, + "sent_at": "2025-10-21T17:00:05Z", + "retry_count": 0 +} + +``` + +### Telegram Bot指令接口 + +| 指令 | 功能 | 权限要求 | 响应方式 | +| --- | --- | --- | --- | +| `/start` | 激活机器人 | 白名单 | 欢迎消息+功能菜单 | +| `/help` | 帮助文档 | 白名单 | 富文本说明 | +| `/notify` | 创建提醒 | 白名单 | InlineKeyboard交互 | +| `/notify_list` | 查看提醒列表 | 白名单 | 列表+删除按钮 | +| `/status` | 系统状态 | 管理员 | 队列长度/限流状态 | +| `/whitelist add ` | 添加白名单 | 管理员 | 确认消息 | + +--- + +## 部署架构 + +### 系统部署拓扑 + +```mermaid +graph TB + subgraph "生产环境" + A[Nginx反向代理
:443 HTTPS] + B[NaughtyMan服务
:8080] + C[SQLite数据库
bot.db] + D[日志文件
/var/log/naughty_man] + end + + subgraph "网络层" + E[SOCKS5代理
:1080] + end + + subgraph "外部服务" + F[Telegram API
api.telegram.org] + G[OpenAI API] + end + + A -->|TLS| B + B --> C + B --> D + B -->|代理| E + E --> F + B -->|直连| G + + subgraph "监控系统" + H[Prometheus
:9090] + I[Grafana
:3000] + end + + B -->|metrics| H + H --> I + +``` + +### 配置文件结构 + +**config.yaml**: + +```yaml +server: + host: "0.0.0.0" + port: 8080 + mode: "release" # debug/release + +telegram: + bot_token: "${TELEGRAM_BOT_TOKEN}" + proxy: + enabled: true + type: "socks5" + host: "127.0.0.1" + port: 1080 + username: "" + password: "" + rate_limit: + global_rate: 30 # 消息/秒 + chat_rate: 1 # 私聊消息/秒 + group_rate: 20 # 群聊消息/分钟 + edit_rate: 20 # 编辑消息/分钟 + +database: + path: "./data/bot.db" + wal_mode: true + max_open_conns: 25 + max_idle_conns: 5 + conn_max_lifetime: 3600 # 秒 + +ai: + default_provider: "openai" + providers: + openai: + api_key: "${OPENAI_API_KEY}" + model: "gpt-4-turbo" + base_url: "" + timeout: 30 + gemini: + api_key: "${GEMINI_API_KEY}" + model: "gemini-1.5-pro" + grok: + api_key: "${GROK_API_KEY}" + model: "grok-2" + context_window: 3 # 历史消息条数 + stream_enabled: true + +security: + jwt_secret: "${JWT_SECRET}" + token_expire_hours: 6 + challenge_expire_seconds: 60 + admin_chat_ids: [^123456789] + +logging: + level: "info" # debug/info/warn/error + format: "json" + output: "./logs/app.log" + max_size: 100 # MB + max_backups: 10 + max_age: 30 # 天 + compress: true + +monitoring: + prometheus_enabled: true + metrics_path: "/metrics" + +``` + +### 环境变量清单 + +```bash +# Telegram配置 +export TELEGRAM_BOT_TOKEN="1234567890:ABCdefGHIjklMNOpqrsTUVwxyz" + +# AI服务密钥 +export OPENAI_API_KEY="sk-proj-..." +export GEMINI_API_KEY="AIza..." +export GROK_API_KEY="xai-..." + +# 安全密钥 +export JWT_SECRET="random-256-bit-secret" + +# 数据库路径 +export DB_PATH="./data/bot.db" + +# 运行模式 +export GIN_MODE="release" + +``` + +--- + +## 非功能性需求实现 + +### 性能优化策略 + +### 数据库连接池配置 + +```go +func InitDB(config DBConfig) *sql.DB { + db, _ := sql.Open("sqlite3", config.Path+"?_journal=WAL&_busy_timeout=5000") + + db.SetMaxOpenConns(config.MaxOpenConns) + db.SetMaxIdleConns(config.MaxIdleConns) + db.SetConnMaxLifetime(time.Duration(config.ConnMaxLifetime) * time.Second) + + // 启用WAL模式提升并发性能 + db.Exec("PRAGMA journal_mode=WAL") + db.Exec("PRAGMA synchronous=NORMAL") + db.Exec("PRAGMA cache_size=-64000") // 64MB缓存 + + return db +} + +``` + +### 消息批量处理 + +```go +type BatchProcessor struct { + messages []Message + timer *time.Timer + mutex sync.Mutex + batchSize int + batchDelay time.Duration +} + +func (bp *BatchProcessor) Add(msg Message) { + bp.mutex.Lock() + defer bp.mutex.Unlock() + + bp.messages = append(bp.messages, msg) + + // 达到批量大小或超时则触发发送 + if len(bp.messages) >= bp.batchSize { + bp.flush() + } else if bp.timer == nil { + bp.timer = time.AfterFunc(bp.batchDelay, bp.flush) + } +} + +func (bp *BatchProcessor) flush() { + bp.mutex.Lock() + msgs := bp.messages + bp.messages = nil + bp.timer = nil + bp.mutex.Unlock() + + // 聚合同类消息 + aggregated := aggregateMessages(msgs) + for _, msg := range aggregated { + sendToTelegram(msg) + } +} + +``` + +### 可靠性保障 + +### 消息重试机制 + +```go +type RetryPolicy struct { + MaxRetries int + InitialDelay time.Duration + MaxDelay time.Duration + Multiplier float64 +} + +var defaultPolicy = RetryPolicy{ + MaxRetries: 3, + InitialDelay: 1 * time.Second, + MaxDelay: 30 * time.Second, + Multiplier: 2.0, +} + +func SendWithRetry(msg Message, policy RetryPolicy) error { + var lastErr error + + for attempt := 0; attempt <= policy.MaxRetries; attempt++ { + err := sendToTelegram(msg) + if err == nil { + return nil + } + + lastErr = err + + // 判断是否可重试(非4xx错误) + if !isRetryable(err) { + return err + } + + // 计算退避延迟 + delay := policy.InitialDelay * time.Duration(math.Pow(policy.Multiplier, float64(attempt))) + if delay > policy.MaxDelay { + delay = policy.MaxDelay + } + + log.Warn("消息发送失败,重试中", + "attempt", attempt+1, + "delay", delay, + "error", err) + + time.Sleep(delay) + } + + // 转入死信队列 + deadLetterQueue <- msg + return fmt.Errorf("重试%d次后仍失败: %w", policy.MaxRetries, lastErr) +} + +``` + +### 优雅关闭机制 + +```go +func (app *Application) Shutdown(ctx context.Context) error { + log.Info("开始优雅关闭...") + + // 1. 停止接收新请求 + app.httpServer.SetKeepAlivesEnabled(false) + + // 2. 停止Telegram轮询 + app.bot.StopReceivingUpdates() + + // 3. 等待消息队列清空(最多30秒) + queueCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + for { + select { + case <-queueCtx.Done(): + log.Warn("消息队列未完全处理完毕", "remaining", app.queue.Len()) + goto CLEANUP + default: + if app.queue.Len() == 0 { + goto CLEANUP + } + time.Sleep(100 * time.Millisecond) + } + } + +CLEANUP: + // 4. 关闭HTTP服务器 + app.httpServer.Shutdown(ctx) + + // 5. 关闭数据库连接 + app.db.Close() + + log.Info("优雅关闭完成") + return nil +} + +``` + +### 监控与可观测性 + +### Prometheus指标定义 + +```go +var ( + // 消息发送指标 + messagesSent = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "naughty_man_messages_sent_total", + Help: "消息发送总数", + }, + []string{"project", "level", "status"}, + ) + + // 消息发送延迟 + messageSendDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "naughty_man_message_send_duration_seconds", + Help: "消息发送耗时", + Buckets: prometheus.ExponentialBuckets(0.01, 2, 10), + }, + []string{"project", "level"}, + ) + + // 队列长度 + queueLength = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "naughty_man_queue_length", + Help: "消息队列长度", + }, + []string{"priority"}, + ) + + // 速率限制器状态 + rateLimiterTokens = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "naughty_man_rate_limiter_tokens", + Help: "速率限制器剩余令牌数", + }, + []string{"limiter_type"}, + ) + + // AI调用指标 + aiRequests = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "naughty_man_ai_requests_total", + Help: "AI API调用总数", + }, + []string{"provider", "model", "status"}, + ) +) + +``` + +### 结构化日志规范 + +```go +// 使用zap实现结构化日志 +logger, _ := zap.NewProduction() +defer logger.Sync() + +sugar := logger.Sugar() + +// 业务日志示例 +sugar.Infow("消息入队", + "message_id", msg.ID, + "chat_id", msg.ChatID, + "level", msg.Level, + "priority", msg.Priority, + "scheduled_at", msg.ScheduledAt, +) + +sugar.Errorw("消息发送失败", + "message_id", msg.ID, + "error", err.Error(), + "retry_count", msg.RetryCount, + "will_retry", msg.RetryCount < 3, +) + +``` + +--- + +## 测试策略 + +### 单元测试覆盖计划 + +| 模块 | 测试重点 | 目标覆盖率 | +| --- | --- | --- | +| 认证模块 | 签名验证、Token生成/解析、白名单校验 | >90% | +| 速率限制器 | 令牌桶算法、并发安全性 | >95% | +| 消息队列 | 优先级排序、入队出队操作 | >90% | +| 模板引擎 | 变量替换、错误处理 | >85% | +| AI适配器 | 流式解析、超时处理 | >80% | + +### 集成测试场景 + +**场景1: 端到端消息发送流程** + +```go +func TestE2EMessageFlow(t *testing.T) { + // 1. 创建测试环境 + app := setupTestApp() + defer app.Cleanup() + + // 2. 获取Token + token := authenticateTestClient(app) + + // 3. 发送CRITICAL消息 + msgID := sendTestMessage(app, token, Message{ + TargetID: testChatID, + Level: "CRITICAL", + Content: testContent, + }) + + // 4. 验证消息入队 + assert.Eventually(t, func() bool { + return app.Queue.Contains(msgID) + }, 5*time.Second, 100*time.Millisecond) + + // 5. 模拟Telegram API响应 + mockTelegramResponse(testChatID, msgID) + + // 6. 验证消息状态更新 + status := queryMessageStatus(app, msgID) + assert.Equal(t, "sent", status) +} + +``` + +### 压力测试指标 + +**测试工具**: Apache JMeter / Vegeta + +**测试用例**: + +```bash +# 并发消息发送压测(1000 QPS持续1分钟) +echo "POST " | \\ + vegeta attack -rate=1000 -duration=60s -header="Authorization: Bearer $TOKEN" \\ + | vegeta report + +# 期望指标: +# - 成功率 > 99% +# - P95延迟 < 200ms +# - P99延迟 < 500ms +# - 无内存泄漏 + +``` + +--- + +## 风险评估与对策 + +| 风险项 | 发生概率 | 影响程度 | 应对策略 | 责任人 | +| --- | --- | --- | --- | --- | +| Telegram API限流导致消息积压 | 高 | 高 | 1. 实现自适应速率限制
2. 优先级队列保证CRITICAL优先
3. 消息聚合减少调用 | 后端负责人 | +| SQLite写入冲突 | 中 | 中 | 1. 启用WAL模式
2. 写操作串行化
3. 必要时迁移PostgreSQL | 数据库架构师 | +| AI服务不稳定 | 中 | 低 | 1. 多厂商自动切换
2. 超时熔断机制
3. 降级为普通回复 | AI集成负责人 | +| 代理服务中断 | 低 | 高 | 1. 备用代理配置
2. 心跳检测自动切换
3. 监控告警 | 运维负责人 | +| 死信队列溢出 | 低 | 中 | 1. 定时任务清理老数据
2. 人工审核重试
3. 容量告警 | 值班工程师 | + +--- + +## 项目实施计划 + +### 迭代排期 + +```mermaid +gantt + title NaughtyMan开发计划 + dateFormat YYYY-MM-DD + section 阶段一:基础设施 + 数据库表结构设计 :done, db, 2025-10-22, 2d + 速率限制器实现 :active, rate, 2025-10-24, 3d + Token认证系统 :auth, after rate, 3d + 白名单中间件 :whitelist, after auth, 2d + 代理配置与测试 :proxy, after whitelist, 2d + + section 阶段二:核心功能 + 消息通知API开发 :api, after proxy, 4d + 模板渲染引擎 :template, after api, 2d + 优先级队列实现 :queue, after template, 3d + 消息重试机制 :retry, after queue, 2d + 定时提醒CRUD :reminder, after retry, 4d + + section 阶段三:高级功能 + InlineKeyboard交互 :inline, after reminder, 4d + AI多厂商适配 :ai, after inline, 5d + 流式响应处理 :stream, after ai, 3d + 性能优化与压测 :perf, after stream, 3d + + section 阶段四:上线准备 + 集成测试 :integration, after perf, 3d + 文档编写 :doc, after integration, 2d + 监控告警配置 :monitor, after doc, 2d + 灰度发布 :release, after monitor, 2d + +``` + +### 人员分工 + +| 角色 | 职责 | 人数 | +| --- | --- | --- | +| 技术负责人 | 架构设计审核、技术选型决策、风险评估 | 1 | +| 后端工程师 | 核心业务逻辑开发、API接口实现 | 2 | +| AI集成工程师 | 多厂商适配层、流式响应处理 | 1 | +| 测试工程师 | 单元测试、集成测试、压力测试 | 1 | +| 运维工程师 | 部署脚本、监控配置、应急响应 | 1 | + +### 交付物清单 + +- [ ] 源代码仓库(Git) +- [ ] 数据库迁移脚本 +- [ ] API接口文档(OpenAPI 3.0) +- [ ] 部署操作手册 +- [ ] 监控告警配置文件 +- [ ] 性能测试报告 +- [ ] 用户使用指南 + +--- + +## 附录 + +### 术语表 + +| 术语 | 英文 | 说明 | +| --- | --- | --- | +| 令牌桶 | Token Bucket | 速率限制算法,以固定速率补充令牌实现流控 | +| 死信队列 | Dead Letter Queue | 存储多次失败消息的特殊队列 | +| 指数退避 | Exponential Backoff | 重试间隔呈指数增长的策略 | +| WAL模式 | Write-Ahead Logging | SQLite日志模式,提升并发写入性能 | +| RRULE | Recurrence Rule | iCalendar重复规则标准(RFC 5545) | + +### 参考资料 + +- [Telegram Bot API官方文档](https://core.telegram.org/bots/api) +- [Go语言并发模式](https://go.dev/blog/pipelines) +- [SQLite WAL模式详解](https://www.sqlite.org/wal.html) +- [JWT最佳实践](https://tools.ietf.org/html/rfc8725) +- [OpenAPI规范3.0](https://swagger.io/specification/) + +--- + +**文档编制完成** | 总计约1.2万字 | 包含8个Mermaid图表 | 覆盖12个核心设计模块 \ No newline at end of file diff --git a/4-ProjectNaughtyMan/1-概要详细设计/3.1-详细设计说明书.md b/4-ProjectNaughtyMan/1-概要详细设计/3.1-详细设计说明书.md new file mode 100644 index 0000000..b0a81c5 --- /dev/null +++ b/4-ProjectNaughtyMan/1-概要详细设计/3.1-详细设计说明书.md @@ -0,0 +1,2122 @@ +# Telegram Bot 智能通知与交互系统 - 详细设计说明书 + +**项目代号**: NaughtyMan +**文档版本**: v2.0 +**编制日期**: 2025年10月24日 +**更新说明**: 根据PRD v2.1需求更新 + +*** + +## 系统架构设计 + +### 总体架构 + +系统采用**分层微服务架构**,由接口层、业务逻辑层、数据访问层和外部服务层构成。架构设计遵循高内聚低耦合原则,确保各模块独立演进能力。 + +```mermaid +graph TB + subgraph "客户端层" + A1[ProjectOctopus] + A2[ProjectTonyStack] + A3[Telegram用户端] + end + + subgraph "接入层" + B1[RESTful API网关
Gin框架] + B2[Telegram Webhook
Long Polling] + end + + subgraph "业务逻辑层" + C1[认证服务
AuthService] + C2[消息调度服务
MessageDispatcher] + C3[提醒管理服务
ReminderService] + C4[AI交互服务
AIService] + C5[速率控制服务
RateLimiter] + C6[用户管理服务
UserManagementService] + C7[指令管理服务
CommandService] + end + + subgraph "数据访问层" + D1[消息队列
PriorityQueue] + D2[SQLite数据库
WAL模式] + D3[缓存层
sync.Map] + end + + subgraph "外部服务层" + E1[Telegram Bot API] + E2[OpenRouter API] + E3[SOCKS5/HTTP代理] + end + + A1 --> B1 + A2 --> B1 + A3 --> B2 + B1 --> C1 + B1 --> C2 + B2 --> C3 + B2 --> C4 + C2 --> C5 + C3 --> D2 + C4 --> D3 + C2 --> D1 + C5 --> E1 + C4 --> E2 + E1 -.代理.-> E3 + C6 --> D2 + C7 --> D2 + B2 --> C6 + B2 --> C7 +``` + + +### 技术选型说明 + +| 技术组件 | 选型方案 | 选型理由 | +| :-- | :-- | :-- | +| 开发语言 | Go 1.21+ | 原生并发支持、高性能、跨平台编译、内存安全 | +| Web框架 | Gin v1.9 | 轻量级、路由高效、中间件生态完善、社区活跃 | +| 数据库 | SQLite 3.40+ | 零配置部署、事务ACID保证、跨平台兼容、适合中小规模数据 | +| Bot SDK | tgbotapi v5 | 官方API完整封装、长连接稳定、支持代理配置 | +| AI SDK | OpenAI Go SDK | 统一接口、支持OpenRouter等多提供商、流式响应完善 | +| 并发控制 | sync.Map + Channel | 原生并发安全结构、无锁化设计、性能优越 | +| 日志框架 | zap | 结构化日志、高性能、灵活的日志等级控制 | + + +*** + +## 核心模块详细设计 + +### 认证与安全模块 + +#### 双阶段Token认证流程 + +```mermaid +sequenceDiagram + participant C as 客户端 + participant G as API网关 + participant A as 认证服务 + participant D as 数据库 + + C->>G: POST /auth/handshake
{api_key} + G->>A: 验证API Key + A->>D: 查询白名单 + D-->>A: 返回权限范围 + A->>A: 生成Challenge码
(UUID + 时间戳) + A->>D: 存储Challenge
(60s有效期) + A-->>C: {challenge, expire_at} + + Note over C: 计算签名
HMAC-SHA256(challenge+api_secret) + + C->>G: POST /auth/token
{api_key, challenge, signature} + G->>A: 验证签名 + A->>D: 查询Challenge + D-->>A: Challenge数据 + A->>A: 对比签名
检查时效 + A->>A: 生成JWT Token
(6h有效期) + A->>D: 记录Token映射 + A-->>C: {access_token, refresh_token} +``` + + +#### Token数据结构设计 + +**JWT Payload规范**: + +```go +type TokenClaims struct { + ApiKey string `json:"api_key"` + Whitelist []int64 `json:"whitelist"` // 加密的授权目标ID列表 + Permissions []string `json:"permissions"` // send_message, query_status + IssuedAt int64 `json:"iat"` + ExpiresAt int64 `json:"exp"` + RefreshAfter int64 `json:"rfa"` // 允许刷新的时间阈值 +} +``` + +**数据库表结构**: + +```sql +CREATE TABLE api_credentials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + api_key TEXT UNIQUE NOT NULL, + api_secret TEXT NOT NULL, -- BCRYPT加密存储 + project TEXT NOT NULL, -- octopus/tonystack + whitelist_ids TEXT, -- JSON数组存储 + status INTEGER DEFAULT 1, -- 1:启用 0:禁用 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_used DATETIME, + INDEX idx_api_key (api_key) +); + +CREATE TABLE token_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + api_key TEXT NOT NULL, + challenge TEXT, + challenge_expire DATETIME, + access_token TEXT UNIQUE, + refresh_token TEXT UNIQUE, + token_expire DATETIME, + client_ip TEXT, + user_agent TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_access_token (access_token), + INDEX idx_challenge (challenge, challenge_expire) +); +``` + + +#### 白名单验证机制 + +**中间件实现**: + +```go +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + + // 1. 解析JWT Token + claims, err := jwt.Parse(token, secretKey) + if err != nil || time.Now().Unix() > claims.ExpiresAt { + c.JSON(401, gin.H{"error": "无效或过期的Token"}) + c.Abort() + return + } + + // 2. 验证目标ID在白名单内 + var req MessageRequest + c.BindJSON(&req) + + if !contains(claims.Whitelist, req.TargetID) { + c.JSON(403, gin.H{"error": "目标未授权"}) + c.Abort() + return + } + + // 3. 注入上下文 + c.Set("api_key", claims.ApiKey) + c.Set("project", claims.Project) + c.Next() + } +} +``` + + +*** + +### 消息通知系统 + +#### 消息调度核心架构 + +```mermaid +graph LR + A[API请求] --> B{消息等级判断} + B -->|INFO| C[时段检查器
08:00-23:00] + B -->|WARNING| D[即时队列] + B -->|CRITICAL| D + C -->|时段内| E[批量聚合器
5分钟窗口] + C -->|时段外| F[延迟队列
次日8:00] + E --> G[优先级队列] + D --> G + F --> G + G --> H[消息发送器选择] + H --> I[速率限制器] + I --> J[Telegram API] + J -->|成功| K[消息日志] + J -->|失败| L{重试判断} + L -->|次数<3| M[指数退避重试] + L -->|次数≥3| N[死信队列] + M --> I +``` + + +#### 消息发送器包装层设计 **[新增]** + +为支持不同类型的消息发送需求,系统实现了发送器包装层: + +**基础发送器接口**: + +```go +type MessageSender interface { + Send(chatID int64, content string, options ...SendOption) error + ValidateTarget(chatID int64) error +} + +type SendOption func(*SendConfig) + +type SendConfig struct { + ParseMode string + DisablePreview bool + ReplyToMessageID int +} +``` + +**普通消息发送器实现**: + +```go +type PlainMessageSender struct { + bot *tgbotapi.BotAPI + rateLimiter *RateLimiter + whitelist *WhitelistChecker + logger *zap.Logger +} + +func (s *PlainMessageSender) Send(chatID int64, text string, options ...SendOption) error { + // 1. 白名单验证 + if err := s.whitelist.Check(chatID); err != nil { + return fmt.Errorf("whitelist check failed: %w", err) + } + + // 2. 应用配置选项 + config := &SendConfig{} + for _, opt := range options { + opt(config) + } + + // 3. 等待速率限制器许可 + if !s.rateLimiter.AllowSend(chatID, false) { + return ErrRateLimitExceeded + } + + // 4. 发送消息 + msg := tgbotapi.NewMessage(chatID, text) + if config.ParseMode != "" { + msg.ParseMode = config.ParseMode + } + msg.DisableWebPagePreview = config.DisablePreview + + _, err := s.bot.Send(msg) + return err +} +``` + +**AI消息发送器实现**: + +```go +type AIMessageSender struct { + baseSender *PlainMessageSender + logger *zap.Logger +} + +func (s *AIMessageSender) SendMarkdown(chatID int64, markdown string) error { + // 转义Markdown特殊字符为MarkdownV2格式 + escaped := escapeMarkdownV2(markdown) + + // 使用基础发送器发送,指定ParseMode + return s.baseSender.Send(chatID, escaped, func(cfg *SendConfig) { + cfg.ParseMode = "MarkdownV2" + cfg.DisablePreview = true + }) +} + +func (s *AIMessageSender) SendStreamUpdate(chatID int64, messageID int, content string) error { + // 检查速率限制(编辑消息) + if !s.baseSender.rateLimiter.AllowSend(chatID, true) { + return ErrRateLimitExceeded + } + + edit := tgbotapi.NewEditMessageText(chatID, messageID, content) + edit.ParseMode = "MarkdownV2" + + _, err := s.baseSender.bot.Send(edit) + return err +} + +// Markdown V2格式转义 +func escapeMarkdownV2(text string) string { + specialChars := []string{"_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"} + escaped := text + for _, char := range specialChars { + escaped = strings.ReplaceAll(escaped, char, "\\"+char) + } + return escaped +} +``` + +**发送器工厂**: + +```go +type SenderFactory struct { + bot *tgbotapi.BotAPI + rateLimiter *RateLimiter + whitelist *WhitelistChecker +} + +func (f *SenderFactory) NewPlainSender() *PlainMessageSender { + return &PlainMessageSender{ + bot: f.bot, + rateLimiter: f.rateLimiter, + whitelist: f.whitelist, + logger: zap.L(), + } +} + +func (f *SenderFactory) NewAISender() *AIMessageSender { + return &AIMessageSender{ + baseSender: f.NewPlainSender(), + logger: zap.L(), + } +} +``` + + +#### 优先级队列实现 + +**数据结构设计**: + +```go +type Message struct { + ID string + ChatID int64 + Level string // INFO/WARNING/CRITICAL + Content string + Metadata map[string]interface{} + Priority int // CRITICAL:3, WARNING:2, INFO:1 + RetryCount int + ScheduledAt time.Time + CreatedAt time.Time +} + +type PriorityQueue struct { + heap *priorityHeap // 优先级小顶堆 + mutex sync.RWMutex + notEmpty chan struct{} // 通知消费者 + deadLetter chan Message // 死信队列通道 +} + +// 堆排序规则 +func (pq *priorityHeap) Less(i, j int) bool { + // 1. 优先级高的优先 + if pq[i].Priority != pq[j].Priority { + return pq[i].Priority > pq[j].Priority + } + // 2. 相同优先级按时间戳排序 + return pq[i].ScheduledAt.Before(pq[j].ScheduledAt) +} +``` + + +#### 消息模板渲染引擎 + +**模板定义规范**: + +```go +type MessageTemplate struct { + Project string + Level string + Template string +} + +var templates = map[string]MessageTemplate{ + "octopus_critical": { + Project: "octopus", + Level: "CRITICAL", + Template: `🚨 【严重告警】{{.Title}} + +📍 服务器: {{.Metadata.server}} +📊 详情: {{.Body}} +⏰ 时间: {{formatTime .Metadata.timestamp}} +🔗 查看详情: {{.Metadata.dashboard_url}} + +#服务器监控 #CRITICAL`, + }, + "tonystack_warning": { + Project: "tonystack", + Level: "WARNING", + Template: `⚠️ 【风险提示】{{.Title}} + +💰 标的: {{.Metadata.symbol}} +📈 触发值: {{.Body}} +📉 当前价格: {{.Metadata.current_price}} +⏰ 时间: {{formatTime .Metadata.timestamp}} + +#金融监控 #WARNING`, + }, +} + +// 渲染函数 +func RenderTemplate(msg Message) (string, error) { + key := fmt.Sprintf("%s_%s", msg.Project, strings.ToLower(msg.Level)) + tmpl, exists := templates[key] + if !exists { + return "", errors.New("模板不存在") + } + + t := template.New("message").Funcs(template.FuncMap{ + "formatTime": func(ts string) string { + t, _ := time.Parse(time.RFC3339, ts) + return t.Format("2006-01-02 15:04:05") + }, + }) + + t, _ = t.Parse(tmpl.Template) + var buf bytes.Buffer + t.Execute(&buf, msg) + return buf.String(), nil +} +``` + + +#### 速率限制器设计 + +**令牌桶算法实现**: + +```go +type TokenBucket struct { + capacity int // 桶容量 + tokens int // 当前令牌数 + refillRate time.Duration // 补充速率 + lastRefill time.Time + mutex sync.Mutex +} + +func (tb *TokenBucket) Take() bool { + tb.mutex.Lock() + defer tb.mutex.Unlock() + + // 计算应补充的令牌数 + now := time.Now() + elapsed := now.Sub(tb.lastRefill) + refillCount := int(elapsed / tb.refillRate) + + if refillCount > 0 { + tb.tokens = min(tb.capacity, tb.tokens + refillCount) + tb.lastRefill = now + } + + // 尝试获取令牌 + if tb.tokens > 0 { + tb.tokens-- + return true + } + return false +} + +type RateLimiter struct { + globalBucket *TokenBucket // 全局30/s + chatBuckets sync.Map // map[int64]*TokenBucket + editBucket *TokenBucket // 编辑消息限流 +} + +func NewRateLimiter() *RateLimiter { + return &RateLimiter{ + globalBucket: &TokenBucket{ + capacity: 30, + tokens: 30, + refillRate: time.Second / 30, + lastRefill: time.Now(), + }, + editBucket: &TokenBucket{ + capacity: 20, + tokens: 20, + refillRate: time.Minute / 20, + lastRefill: time.Now(), + }, + } +} + +func (rl *RateLimiter) AllowSend(chatID int64, isEdit bool) bool { + // 1. 检查全局限流 + if !rl.globalBucket.Take() { + return false + } + + // 2. 编辑消息检查专用桶 + if isEdit && !rl.editBucket.Take() { + rl.globalBucket.tokens++ // 归还全局令牌 + return false + } + + // 3. 检查单聊天限流 + bucket := rl.getChatBucket(chatID) + if !bucket.Take() { + rl.globalBucket.tokens++ + return false + } + + return true +} +``` + + +*** + +### Telegram管理功能模块 **[新增]** + +#### 用户群组管理服务 + +**服务架构**: + +```mermaid +graph TB + A[Telegram Update] --> B[Update Handler] + B --> C{消息类型} + C -->|新用户消息| D[用户信息提取器] + C -->|群组消息| E[群组信息提取器] + D --> F[UserProfileService] + E --> G[GroupProfileService] + F --> H[SQLite存储] + G --> H + I[管理API] --> F + I --> G +``` + +**用户档案服务实现**: + +```go +type UserProfileService struct { + db *sql.DB + cache *sync.Map // 缓存用户信息 + logger *zap.Logger +} + +type UserProfile struct { + ID int64 + UserID int64 + Username string + FirstName string + LastName string + PhoneNumber string + LanguageCode string + IsBot bool + LastInteraction time.Time +} + +func (s *UserProfileService) UpsertFromUpdate(update tgbotapi.Update) error { + user := update.Message.From + if user == nil { + return nil + } + + profile := UserProfile{ + UserID: user.ID, + Username: user.UserName, + FirstName: user.FirstName, + LastName: user.LastName, + LanguageCode: user.LanguageCode, + IsBot: user.IsBot, + LastInteraction: time.Now(), + } + + // Upsert到数据库 + _, err := s.db.Exec(` + INSERT INTO user_profiles (user_id, username, first_name, last_name, language_code, is_bot, last_interaction, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(user_id) DO UPDATE SET + username = excluded.username, + first_name = excluded.first_name, + last_name = excluded.last_name, + language_code = excluded.language_code, + last_interaction = excluded.last_interaction, + updated_at = CURRENT_TIMESTAMP + `, profile.UserID, profile.Username, profile.FirstName, profile.LastName, profile.LanguageCode, profile.IsBot, profile.LastInteraction) + + if err == nil { + s.cache.Store(profile.UserID, profile) + } + + return err +} + +func (s *UserProfileService) GetByUserID(userID int64) (*UserProfile, error) { + // 先查缓存 + if cached, ok := s.cache.Load(userID); ok { + profile := cached.(UserProfile) + return &profile, nil + } + + // 查数据库 + var profile UserProfile + err := s.db.QueryRow(` + SELECT id, user_id, username, first_name, last_name, phone_number, language_code, is_bot, last_interaction + FROM user_profiles WHERE user_id = ? + `, userID).Scan(&profile.ID, &profile.UserID, &profile.Username, &profile.FirstName, + &profile.LastName, &profile.PhoneNumber, &profile.LanguageCode, &profile.IsBot, &profile.LastInteraction) + + if err == nil { + s.cache.Store(userID, profile) + } + + return &profile, err +} + +func (s *UserProfileService) ListAllUsers() ([]UserProfile, error) { + rows, err := s.db.Query(` + SELECT id, user_id, username, first_name, last_name, phone_number, language_code, is_bot, last_interaction + FROM user_profiles + ORDER BY last_interaction DESC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var profiles []UserProfile + for rows.Next() { + var p UserProfile + rows.Scan(&p.ID, &p.UserID, &p.Username, &p.FirstName, &p.LastName, + &p.PhoneNumber, &p.LanguageCode, &p.IsBot, &p.LastInteraction) + profiles = append(profiles, p) + } + + return profiles, nil +} +``` + +**群组档案服务实现**: + +```go +type GroupProfileService struct { + db *sql.DB + cache *sync.Map + logger *zap.Logger +} + +type GroupProfile struct { + ID int64 + GroupID int64 + Title string + Type string // group, supergroup, channel + MemberCount int + LastInteraction time.Time +} + +func (s *GroupProfileService) UpsertFromUpdate(update tgbotapi.Update) error { + chat := update.Message.Chat + if chat.Type == "private" { + return nil // 私聊不处理 + } + + profile := GroupProfile{ + GroupID: chat.ID, + Title: chat.Title, + Type: chat.Type, + MemberCount: chat.MembersCount, + LastInteraction: time.Now(), + } + + _, err := s.db.Exec(` + INSERT INTO group_profiles (group_id, title, type, member_count, last_interaction, updated_at) + VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(group_id) DO UPDATE SET + title = excluded.title, + type = excluded.type, + member_count = excluded.member_count, + last_interaction = excluded.last_interaction, + updated_at = CURRENT_TIMESTAMP + `, profile.GroupID, profile.Title, profile.Type, profile.MemberCount, profile.LastInteraction) + + if err == nil { + s.cache.Store(profile.GroupID, profile) + } + + return err +} +``` + + +#### 机器人功能管理服务 + +**动态指令注册系统**: + +```go +type BotCommand struct { + ID int + Command string // 指令名称(如 "notify") + Description string // 指令描述 + HandlerName string // 处理器标识 + IsEnabled bool // 是否启用 + Scope string // user/group/all +} + +type CommandService struct { + db *sql.DB + bot *tgbotapi.BotAPI + handlers map[string]CommandHandler // 处理器注册表 + registeredCmd sync.Map // 已注册指令缓存 + logger *zap.Logger +} + +type CommandHandler interface { + Handle(update tgbotapi.Update) error + Description() string +} + +// 初始化预定义指令 +func (s *CommandService) RegisterBuiltinCommands() error { + builtinCommands := []BotCommand{ + {Command: "start", Description: "开始使用机器人", HandlerName: "StartHandler", IsEnabled: true, Scope: "all"}, + {Command: "help", Description: "查看帮助信息", HandlerName: "HelpHandler", IsEnabled: true, Scope: "all"}, + {Command: "notify", Description: "创建定时提醒", HandlerName: "NotifyHandler", IsEnabled: true, Scope: "user"}, + {Command: "notify_list", Description: "查看提醒列表", HandlerName: "NotifyListHandler", IsEnabled: true, Scope: "user"}, + } + + for _, cmd := range builtinCommands { + _, err := s.db.Exec(` + INSERT INTO bot_commands (command, description, handler_name, is_enabled, scope) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(command) DO NOTHING + `, cmd.Command, cmd.Description, cmd.HandlerName, cmd.IsEnabled, cmd.Scope) + + if err != nil { + return err + } + } + + return s.SyncToTelegram() +} + +// 同步指令到Telegram +func (s *CommandService) SyncToTelegram() error { + rows, err := s.db.Query(` + SELECT command, description + FROM bot_commands + WHERE is_enabled = 1 + `) + if err != nil { + return err + } + defer rows.Close() + + var commands []tgbotapi.BotCommand + for rows.Next() { + var cmd, desc string + rows.Scan(&cmd, &desc) + commands = append(commands, tgbotapi.BotCommand{ + Command: cmd, + Description: desc, + }) + } + + // 设置Bot命令列表 + cfg := tgbotapi.NewSetMyCommands(commands...) + _, err = s.bot.Request(cfg) + return err +} + +// 注册指令处理器 +func (s *CommandService) RegisterHandler(handlerName string, handler CommandHandler) { + s.handlers[handlerName] = handler +} + +// 分发指令 +func (s *CommandService) DispatchCommand(update tgbotapi.Update) error { + if !update.Message.IsCommand() { + return nil + } + + command := update.Message.Command() + + // 查询指令配置 + var handlerName string + var isEnabled bool + err := s.db.QueryRow(` + SELECT handler_name, is_enabled + FROM bot_commands + WHERE command = ? + `, command).Scan(&handlerName, &isEnabled) + + if err != nil || !isEnabled { + return fmt.Errorf("command not found or disabled: %s", command) + } + + // 执行处理器 + handler, exists := s.handlers[handlerName] + if !exists { + return fmt.Errorf("handler not registered: %s", handlerName) + } + + return handler.Handle(update) +} + +// API: 添加新指令 +func (s *CommandService) AddCommand(cmd BotCommand) error { + _, err := s.db.Exec(` + INSERT INTO bot_commands (command, description, handler_name, is_enabled, scope) + VALUES (?, ?, ?, ?, ?) + `, cmd.Command, cmd.Description, cmd.HandlerName, cmd.IsEnabled, cmd.Scope) + + if err == nil { + s.SyncToTelegram() + } + return err +} + +// API: 更新指令 +func (s *CommandService) UpdateCommand(command string, updates map[string]interface{}) error { + // 动态构建UPDATE语句 + setParts := []string{} + args := []interface{}{} + + for key, value := range updates { + setParts = append(setParts, fmt.Sprintf("%s = ?", key)) + args = append(args, value) + } + + args = append(args, command) + + _, err := s.db.Exec(fmt.Sprintf(` + UPDATE bot_commands + SET %s, updated_at = CURRENT_TIMESTAMP + WHERE command = ? + `, strings.Join(setParts, ", ")), args...) + + if err == nil { + s.SyncToTelegram() + } + return err +} + +// API: 删除指令 +func (s *CommandService) DeleteCommand(command string) error { + _, err := s.db.Exec(`DELETE FROM bot_commands WHERE command = ?`, command) + if err == nil { + s.SyncToTelegram() + } + return err +} + +// API: 列出所有指令 +func (s *CommandService) ListCommands() ([]BotCommand, error) { + rows, err := s.db.Query(` + SELECT id, command, description, handler_name, is_enabled, scope + FROM bot_commands + ORDER BY command + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var commands []BotCommand + for rows.Next() { + var cmd BotCommand + rows.Scan(&cmd.ID, &cmd.Command, &cmd.Description, &cmd.HandlerName, &cmd.IsEnabled, &cmd.Scope) + commands = append(commands, cmd) + } + + return commands, nil +} +``` + + +*** + +### 定时提醒系统 + +#### 状态机设计 + +```mermaid +stateDiagram-v2 + [*] --> 等待输入: /notify命令 + 等待输入 --> 选择日期: 点击日期按钮 + 选择日期 --> 选择时间: 确认日期 + 选择时间 --> 配置重复: 确认时间 + 配置重复 --> 输入内容: 选择重复规则 + 输入内容 --> 确认创建: 输入提醒文本 + 确认创建 --> 已激活: 提交成功 + 已激活 --> 已触发: 到达触发时间 + 已触发 --> 已激活: 有重复规则 + 已触发 --> 已完成: 无重复规则 + 已激活 --> 已删除: 用户删除 + 已完成 --> [*] + 已删除 --> [*] +``` + + +#### 日历选择器UI实现 + +**InlineKeyboard布局**: + +```go +func GenerateCalendar(year int, month time.Month) [][]tgbotapi.InlineKeyboardButton { + firstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local) + lastDay := firstDay.AddDate(0, 1, -1) + + keyboard := [][]tgbotapi.InlineKeyboardButton{ + // 月份导航行 + { + tgbotapi.NewInlineKeyboardButtonData("◀", fmt.Sprintf("cal_prev_%d_%d", year, month)), + tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%d年%d月", year, month), "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("▶", fmt.Sprintf("cal_next_%d_%d", year, month)), + }, + // 星期标题行 + { + tgbotapi.NewInlineKeyboardButtonData("日", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("一", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("二", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("三", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("四", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("五", "cal_ignore"), + tgbotapi.NewInlineKeyboardButtonData("六", "cal_ignore"), + }, + } + + // 日期网格生成 + currentWeek := []tgbotapi.InlineKeyboardButton{} + for weekday := 0; weekday < int(firstDay.Weekday()); weekday++ { + currentWeek = append(currentWeek, tgbotapi.NewInlineKeyboardButtonData(" ", "cal_ignore")) + } + + for day := 1; day <= lastDay.Day(); day++ { + date := time.Date(year, month, day, 0, 0, 0, 0, time.Local) + callback := fmt.Sprintf("cal_select_%s", date.Format("2006-01-02")) + + // 过期日期禁用 + label := fmt.Sprintf("%d", day) + if date.Before(time.Now().Truncate(24 * time.Hour)) { + callback = "cal_ignore" + label = "×" + } + + currentWeek = append(currentWeek, tgbotapi.NewInlineKeyboardButtonData(label, callback)) + + // 每行7天 + if len(currentWeek) == 7 { + keyboard = append(keyboard, currentWeek) + currentWeek = []tgbotapi.InlineKeyboardButton{} + } + } + + if len(currentWeek) > 0 { + keyboard = append(keyboard, currentWeek) + } + + return keyboard +} +``` + + +#### 提醒调度器实现 + +**定时扫描机制**: + +```go +type ReminderScheduler struct { + db *sql.DB + bot *tgbotapi.BotAPI + ticker *time.Ticker + stopChan chan struct{} +} + +func (rs *ReminderScheduler) Start() { + rs.ticker = time.NewTicker(30 * time.Second) + + go func() { + for { + select { + case <-rs.ticker.C: + rs.processPendingReminders() + case <-rs.stopChan: + return + } + } + }() +} + +func (rs *ReminderScheduler) processPendingReminders() { + // 查询需触发的提醒(未来5分钟内) + rows, _ := rs.db.Query(` + SELECT id, chat_id, user_id, content, repeat_rule, next_trigger + FROM reminders + WHERE status = 1 + AND next_trigger <= datetime('now', '+5 minutes') + ORDER BY next_trigger + `) + defer rows.Close() + + for rows.Next() { + var r Reminder + rows.Scan(&r.ID, &r.ChatID, &r.UserID, &r.Content, &r.RepeatRule, &r.NextTrigger) + + // 等待到精确触发时间 + wait := r.NextTrigger.Sub(time.Now()) + if wait > 0 { + time.Sleep(wait) + } + + // 发送提醒消息 + msg := tgbotapi.NewMessage(r.ChatID, fmt.Sprintf("⏰ 提醒\\n\\n%s", r.Content)) + rs.bot.Send(msg) + + // 更新下次触发时间 + if r.RepeatRule != "" { + nextTrigger := calculateNextTrigger(r.NextTrigger, r.RepeatRule) + rs.db.Exec(`UPDATE reminders SET next_trigger = ? WHERE id = ?`, nextTrigger, r.ID) + } else { + rs.db.Exec(`UPDATE reminders SET status = 0 WHERE id = ?`, r.ID) + } + } +} + +func calculateNextTrigger(current time.Time, rule string) time.Time { + switch rule { + case "daily": + return current.AddDate(0, 0, 1) + case "weekly": + return current.AddDate(0, 0, 7) + case "biweekly": + return current.AddDate(0, 0, 14) + case "monthly": + return current.AddDate(0, 1, 0) + default: + // 解析RRULE格式 + return parseRRule(current, rule) + } +} +``` + + +*** + +### AI智能体系统 + +#### 触发机制 **[更新]** + +**双模式触发判断**: + +```mermaid +graph TB + A[接收Telegram消息] --> B{聊天类型判断} + B -->|私聊private| C[直接触发AI] + B -->|群聊group/supergroup| D{检查@机器人} + D -->|是| C + D -->|否| E[忽略消息] + C --> F[提取上下文] + F --> G[调用AI API] + G --> H[流式响应处理] + H --> I[发送回复] +``` + +**触发逻辑实现**: + +```go +type AITriggerService struct { + bot *tgbotapi.BotAPI + botUsername string + aiService *AIService +} + +func (s *AITriggerService) ShouldRespond(update tgbotapi.Update) bool { + msg := update.Message + if msg == nil { + return false + } + + // 私聊模式: 所有消息都响应 + if msg.Chat.IsPrivate() { + return true + } + + // 群聊模式: 检查是否@了机器人 + if msg.Chat.IsGroup() || msg.Chat.IsSuperGroup() { + // 方式1: 检查Entities中的mention + for _, entity := range msg.Entities { + if entity.Type == "mention" { + mention := msg.Text[entity.Offset:entity.Offset+entity.Length] + if mention == "@"+s.botUsername { + return true + } + } + } + + // 方式2: 检查命令是否针对本Bot + if msg.IsCommand() { + parts := strings.Split(msg.Command(), "@") + if len(parts) == 2 && parts[1] == s.botUsername { + return true + } + if len(parts) == 1 { + return true // 无@的命令也响应 + } + } + } + + return false +} + +func (s *AITriggerService) HandleMessage(update tgbotapi.Update) error { + if !s.ShouldRespond(update) { + return nil + } + + // 移除@机器人的部分 + cleanText := strings.ReplaceAll(update.Message.Text, "@"+s.botUsername, "") + + // 构建上下文并调用AI + return s.aiService.ProcessMessage(update.Message.Chat.ID, update.Message.From.ID, cleanText) +} +``` + + +#### AI集成实现 **[更新]** + +**OpenRouter优先方案**: + +```mermaid +graph LR + A[AI请求] --> B[AIService] + B --> C{提供商选择} + C -->|首选| D[OpenRouterProvider] + C -->|备选| E[其他Provider] + D --> F[OpenAI Go SDK] + F --> G[OpenRouter API] + G --> H[Claude/Gemini/GPT] + H --> I[流式响应] + I --> J[AIMessageSender] + J --> K[Telegram Bot API] +``` + +**OpenRouter适配器实现**: + +```go +import ( + "github.com/sashabaranov/go-openai" +) + +type OpenRouterProvider struct { + client *openai.Client + model string + logger *zap.Logger +} + +func NewOpenRouterProvider(apiKey string, model string) *OpenRouterProvider { + config := openai.DefaultConfig(apiKey) + config.BaseURL = "https://openrouter.ai/api/v1" + + // 可选: 添加自定义Headers + config.HTTPClient = &http.Client{ + Transport: &customTransport{ + base: http.DefaultTransport, + headers: map[string]string{ + "HTTP-Referer": "https://github.com/your-repo", // 可选 + "X-Title": "NaughtyMan Bot", // 可选 + }, + }, + } + + return &OpenRouterProvider{ + client: openai.NewClientWithConfig(config), + model: model, + logger: zap.L(), + } +} + +func (p *OpenRouterProvider) Chat(ctx context.Context, messages []ChatMessage, stream bool) (<-chan string, error) { + // 转换消息格式 + openaiMessages := make([]openai.ChatCompletionMessage, len(messages)) + for i, msg := range messages { + openaiMessages[i] = openai.ChatCompletionMessage{ + Role: msg.Role, + Content: msg.Content, + Name: msg.Name, + } + } + + if stream { + return p.streamChat(ctx, openaiMessages) + } + return p.blockingChat(ctx, openaiMessages) +} + +func (p *OpenRouterProvider) streamChat(ctx context.Context, messages []openai.ChatCompletionMessage) (<-chan string, error) { + ch := make(chan string, 10) + + req := openai.ChatCompletionRequest{ + Model: p.model, + Messages: messages, + Stream: true, + } + + stream, err := p.client.CreateChatCompletionStream(ctx, req) + if err != nil { + return nil, err + } + + go func() { + defer close(ch) + defer stream.Close() + + for { + response, err := stream.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + p.logger.Error("Stream error", zap.Error(err)) + break + } + + if len(response.Choices) > 0 { + content := response.Choices[^0].Delta.Content + if content != "" { + ch <- content + } + } + } + }() + + return ch, nil +} + +func (p *OpenRouterProvider) blockingChat(ctx context.Context, messages []openai.ChatCompletionMessage) (<-chan string, error) { + ch := make(chan string, 1) + + req := openai.ChatCompletionRequest{ + Model: p.model, + Messages: messages, + Stream: false, + } + + resp, err := p.client.CreateChatCompletion(ctx, req) + if err != nil { + return nil, err + } + + go func() { + defer close(ch) + if len(resp.Choices) > 0 { + ch <- resp.Choices[^0].Message.Content + } + }() + + return ch, nil +} + +func (p *OpenRouterProvider) Name() string { + return "OpenRouter" +} + +func (p *OpenRouterProvider) SupportStream() bool { + return true +} + +func (p *OpenRouterProvider) SupportReasoning() bool { + // 取决于使用的模型 + return strings.Contains(p.model, "claude-3.5") || strings.Contains(p.model, "gpt-4") +} +``` + +**推荐模型配置**: + +```go +var RecommendedModels = map[string]string{ + "reasoning": "anthropic/claude-3.5-sonnet", // 思考链最佳 + "long_context": "google/gemini-pro-1.5", // 长上下文 + "balanced": "openai/gpt-4-turbo", // 平衡性能 + "fast": "anthropic/claude-3-haiku", // 快速响应 +} +``` + + +#### 上下文管理 + +**消息引用策略**: + +```go +type ContextManager struct { + db *sql.DB + cache *sync.Map // chatID -> []Message +} + +func (cm *ContextManager) BuildContext(chatID int64, currentMessage string, userID int64) ([]ChatMessage, error) { + // 1. 查询最近3条历史消息 + rows, err := cm.db.Query(` + SELECT user_id, user_message, ai_response, created_at + FROM ai_conversations + WHERE chat_id = ? + ORDER BY created_at DESC + LIMIT 3 + `, chatID) + if err != nil { + return nil, err + } + defer rows.Close() + + var history []struct { + UserID int64 + UserMsg string + AIResponse string + CreatedAt time.Time + } + + for rows.Next() { + var h struct { + UserID int64 + UserMsg string + AIResponse string + CreatedAt time.Time + } + rows.Scan(&h.UserID, &h.UserMsg, &h.AIResponse, &h.CreatedAt) + history = append(history, h) + } + + // 2. 构建消息列表(倒序排列以保持时间顺序) + messages := []ChatMessage{ + { + Role: "system", + Content: "你是一个友好的Telegram群助手,请用简洁专业的语言回答问题。", + }, + } + + // 历史消息按时间正序添加 + for i := len(history) - 1; i >= 0; i-- { + messages = append(messages, + ChatMessage{ + Role: "user", + Content: history[i].UserMsg, + Name: fmt.Sprintf("user%d", history[i].UserID), + }, + ChatMessage{ + Role: "assistant", + Content: history[i].AIResponse, + }, + ) + } + + // 3. 添加当前消息 + messages = append(messages, ChatMessage{ + Role: "user", + Content: currentMessage, + Name: fmt.Sprintf("user%d", userID), + }) + + return messages, nil +} +``` + + +#### 流式响应处理 + +**分段更新策略**: + +```go +type StreamHandler struct { + aiSender *AIMessageSender + updateBuffer strings.Builder + lastUpdate time.Time + messageID int + chatID int64 + startTime time.Time +} + +func (sh *StreamHandler) ProcessStream(ctx context.Context, stream <-chan string) error { + // 1. 发送初始消息 + initialMsg := tgbotapi.NewMessage(sh.chatID, "🤔 正在思考...") + sent, err := sh.aiSender.baseSender.bot.Send(initialMsg) + if err != nil { + return err + } + + sh.messageID = sent.MessageID + sh.lastUpdate = time.Now() + sh.startTime = time.Now() + + // 2. 处理流式分片 + for { + select { + case chunk, ok := <-stream: + if !ok { + // 流结束,发送最终消息 + duration := time.Since(sh.startTime) + finalContent := sh.updateBuffer.String() + fmt.Sprintf("\n\n✅ 回答完成 | 用时%.1fs", duration.Seconds()) + sh.sendUpdate(finalContent) + return nil + } + + sh.updateBuffer.WriteString(chunk) + + // 触发更新条件: 500字符 OR 5秒间隔 + shouldUpdate := sh.updateBuffer.Len() >= 500 || time.Since(sh.lastUpdate) >= 5*time.Second + + if shouldUpdate { + sh.sendUpdate(sh.updateBuffer.String()) + } + + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func (sh *StreamHandler) sendUpdate(content string) { + // 使用AI消息发送器的流式更新方法 + err := sh.aiSender.SendStreamUpdate(sh.chatID, sh.messageID, content) + if err != nil { + sh.aiSender.logger.Warn("Failed to update message", zap.Error(err)) + } else { + sh.lastUpdate = time.Now() + } +} +``` + + +#### AI交互日志记录 **[新增]** + +**日志记录服务**: + +```go +type AIInteractionLogger struct { + db *sql.DB + userService *UserProfileService + groupService *GroupProfileService + logger *zap.Logger +} + +type AIInteractionLog struct { + Timestamp time.Time + UserID int64 + Username string + PhoneNumber string + GroupID int64 + GroupName string + MessageText string + AIResponse string + Duration time.Duration + Provider string + Model string + TokensUsed int +} + +func (l *AIInteractionLogger) LogInteraction(log AIInteractionLog) error { + // 1. 获取用户详细信息 + userProfile, err := l.userService.GetByUserID(log.UserID) + if err == nil { + log.Username = userProfile.Username + log.PhoneNumber = userProfile.PhoneNumber + } + + // 2. 获取群组信息(如果是群聊) + if log.GroupID != 0 { + groupProfile, err := l.groupService.GetByGroupID(log.GroupID) + if err == nil { + log.GroupName = groupProfile.Title + } + } + + // 3. 写入数据库 + _, err = l.db.Exec(` + INSERT INTO ai_interaction_log ( + user_id, username, phone_number, group_id, group_name, + message_text, ai_response, duration_ms, provider, model, tokens_used, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, log.UserID, log.Username, log.PhoneNumber, log.GroupID, log.GroupName, + log.MessageText, log.AIResponse, log.Duration.Milliseconds(), + log.Provider, log.Model, log.TokensUsed, log.Timestamp) + + // 4. 输出结构化日志 + l.logger.Info("AI Interaction", + zap.Int64("user_id", log.UserID), + zap.String("username", log.Username), + zap.String("phone", log.PhoneNumber), + zap.Int64("group_id", log.GroupID), + zap.String("group_name", log.GroupName), + zap.String("message", log.MessageText), + zap.Duration("duration", log.Duration), + zap.String("provider", log.Provider), + zap.String("model", log.Model), + ) + + return err +} + +// 集成到AI服务中 +func (s *AIService) ProcessMessage(chatID int64, userID int64, message string) error { + startTime := time.Now() + + // 构建上下文 + context, _ := s.contextManager.BuildContext(chatID, message, userID) + + // 调用AI + stream, err := s.provider.Chat(s.ctx, context, true) + if err != nil { + return err + } + + // 处理流式响应 + handler := &StreamHandler{ + aiSender: s.aiSender, + chatID: chatID, + } + + var responseBuilder strings.Builder + for chunk := range stream { + responseBuilder.WriteString(chunk) + handler.updateBuffer.WriteString(chunk) + // ... 更新逻辑 + } + + duration := time.Since(startTime) + response := responseBuilder.String() + + // 记录交互日志 + s.interactionLogger.LogInteraction(AIInteractionLog{ + Timestamp: startTime, + UserID: userID, + GroupID: chatID, + MessageText: message, + AIResponse: response, + Duration: duration, + Provider: s.provider.Name(), + Model: s.config.Model, + }) + + return nil +} +``` + + +*** + +## 数据库设计 + +### ER图 **[更新]** + +```mermaid +erDiagram + API_CREDENTIALS ||--o{ TOKEN_SESSIONS : generates + API_CREDENTIALS ||--o{ MESSAGE_LOG : sends + WHITELIST ||--o{ API_CREDENTIALS : authorizes + REMINDERS }o--|| USER_PROFILES : belongs_to + USER_PROFILES ||--o{ AI_CONVERSATIONS : participates + GROUP_PROFILES ||--o{ AI_CONVERSATIONS : contains + BOT_COMMANDS }o--|| USER_PROFILES : registered_by + + API_CREDENTIALS { + int id PK + string api_key UK + string api_secret + string project + json whitelist_ids + int status + datetime created_at + } + + TOKEN_SESSIONS { + int id PK + string api_key FK + string challenge + datetime challenge_expire + string access_token UK + string refresh_token UK + datetime token_expire + string client_ip + } + + WHITELIST { + int id PK + string type + bigint entity_id UK + string alias + int status + datetime added_at + } + + MESSAGE_LOG { + int id PK + string message_id + bigint chat_id + string level + string project FK + text content + string status + int retry_count + datetime sent_at + } + + REMINDERS { + int id PK + bigint user_id FK + bigint chat_id + text content + datetime trigger_time + string repeat_rule + datetime next_trigger + int status + } + + USER_PROFILES { + int id PK + bigint user_id UK + string username + string first_name + string last_name + string phone_number + string language_code + int is_bot + datetime last_interaction + } + + GROUP_PROFILES { + int id PK + bigint group_id UK + string title + string type + int member_count + datetime last_interaction + } + + BOT_COMMANDS { + int id PK + string command UK + string description + string handler_name + int is_enabled + string scope + } + + AI_CONVERSATIONS { + int id PK + bigint chat_id + bigint user_id FK + text user_message + text ai_response + string provider + string model + int tokens_used + datetime created_at + } + + AI_INTERACTION_LOG { + int id PK + bigint user_id FK + string username + string phone_number + bigint group_id + string group_name + text message_text + text ai_response + int duration_ms + string provider + string model + int tokens_used + datetime created_at + } +``` + + +### 核心表DDL **[更新]** + +```sql +-- 用户档案表 +CREATE TABLE user_profiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id BIGINT UNIQUE NOT NULL, + username TEXT, + first_name TEXT, + last_name TEXT, + phone_number TEXT, + language_code TEXT, + is_bot INTEGER DEFAULT 0, + last_interaction DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id), + INDEX idx_last_interaction (last_interaction) +); + +-- 群组档案表 +CREATE TABLE group_profiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id BIGINT UNIQUE NOT NULL, + title TEXT, + type TEXT, -- group, supergroup, channel + member_count INTEGER, + last_interaction DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_group_id (group_id), + INDEX idx_last_interaction (last_interaction) +); + +-- 机器人指令表 +CREATE TABLE bot_commands ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + command TEXT UNIQUE NOT NULL, + description TEXT, + handler_name TEXT NOT NULL, + is_enabled INTEGER DEFAULT 1, + scope TEXT DEFAULT 'all', -- user/group/all + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_command (command), + INDEX idx_enabled (is_enabled) +); + +-- AI交互日志表 +CREATE TABLE ai_interaction_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id BIGINT NOT NULL, + username TEXT, + phone_number TEXT, + group_id BIGINT, + group_name TEXT, + message_text TEXT, + ai_response TEXT, + duration_ms INTEGER, + provider TEXT, + model TEXT, + tokens_used INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id), + INDEX idx_group_id (group_id), + INDEX idx_created_at (created_at) +); + +-- 消息日志表优化索引 +CREATE INDEX idx_message_log_composite ON message_log(status, created_at); +CREATE INDEX idx_message_log_chat ON message_log(chat_id, sent_at DESC); + +-- 提醒表优化索引 +CREATE INDEX idx_reminders_trigger ON reminders(next_trigger, status) WHERE status = 1; +CREATE INDEX idx_reminders_user ON reminders(user_id, status); + +-- Token会话表优化索引 +CREATE INDEX idx_token_expire ON token_sessions(token_expire) WHERE token_expire > datetime('now'); + +-- AI对话表优化索引 +CREATE INDEX idx_ai_conversations_chat ON ai_conversations(chat_id, created_at DESC); +``` + + +*** + +## 接口设计 + +### RESTful API规范 + +#### 管理API接口 **[新增]** + +**1. 用户管理接口** + +``` +GET /api/v1/admin/users +Authorization: Bearer {admin_token} + +响应: +{ + "users": [ + { + "user_id": 123456789, + "username": "john_doe", + "first_name": "John", + "last_name": "Doe", + "phone_number": "+1234567890", + "language_code": "zh-CN", + "last_interaction": "2025-10-24T15:30:00Z" + } + ], + "total": 42 +} +``` + +``` +GET /api/v1/admin/user/:id +Authorization: Bearer {admin_token} + +响应: +{ + "user_id": 123456789, + "username": "john_doe", + "first_name": "John", + "last_name": "Doe", + "phone_number": "+1234567890", + "language_code": "zh-CN", + "is_bot": false, + "last_interaction": "2025-10-24T15:30:00Z", + "created_at": "2025-10-01T08:00:00Z", + "interaction_count": 156 +} +``` + +**2. 群组管理接口** + +``` +GET /api/v1/admin/groups +Authorization: Bearer {admin_token} + +响应: +{ + "groups": [ + { + "group_id": -987654321, + "title": "开发团队讨论组", + "type": "supergroup", + "member_count": 25, + "last_interaction": "2025-10-24T16:00:00Z" + } + ], + "total": 8 +} +``` + +**3. 指令管理接口** + +``` +GET /api/v1/admin/commands +Authorization: Bearer {admin_token} + +响应: +{ + "commands": [ + { + "id": 1, + "command": "notify", + "description": "创建定时提醒", + "handler_name": "NotifyHandler", + "is_enabled": true, + "scope": "user" + } + ] +} +``` + +``` +POST /api/v1/admin/commands +Authorization: Bearer {admin_token} +Content-Type: application/json + +{ + "command": "custom_cmd", + "description": "自定义指令", + "handler_name": "CustomHandler", + "is_enabled": true, + "scope": "all" +} + +响应: +{ + "id": 10, + "message": "指令创建成功" +} +``` + +``` +PUT /api/v1/admin/commands/:cmd +Authorization: Bearer {admin_token} +Content-Type: application/json + +{ + "description": "更新后的描述", + "is_enabled": false +} + +响应: +{ + "message": "指令更新成功" +} +``` + +``` +DELETE /api/v1/admin/commands/:cmd +Authorization: Bearer {admin_token} + +响应: +{ + "message": "指令删除成功" +} +``` + +``` +POST /api/v1/admin/commands/sync +Authorization: Bearer {admin_token} + +响应: +{ + "message": "指令已同步到Telegram", + "synced_count": 8 +} +``` + + +#### 消息发送接口 **[更新]** + +**1. 模板消息发送** + +``` +POST /api/v1/message/send +Authorization: Bearer {access_token} +Content-Type: application/json + +{ + "target_type": "user|group", + "target_id": "123456789", + "level": "info|warning|critical", + "project": "octopus|tonystack", + "content": { + "title": "服务器CPU告警", + "body": "服务器CPU使用率达85%", + "metadata": { + "server": "prod-web-01", + "timestamp": "2025-10-21T10:23:00Z" + } + } +} + +响应: +{ + "message_id": "msg_uuid_12345", + "status": "queued", + "scheduled_at": "2025-10-21T10:23:00Z" +} +``` + +**2. 普通消息发送** **[新增]** + +``` +POST /api/v1/message/send-plain +Authorization: Bearer {access_token} +Content-Type: application/json + +{ + "target_type": "user|group", + "target_id": "123456789", + "text": "这是一条普通文本消息", + "options": { + "disable_preview": true + } +} + +响应: +{ + "message_id": "123", + "status": "sent", + "sent_at": "2025-10-24T15:45:00Z" +} +``` + +**3. Markdown消息发送** **[新增]** + +``` +POST /api/v1/message/send-markdown +Authorization: Bearer {access_token} +Content-Type: application/json + +{ + "target_type": "user|group", + "target_id": "123456789", + "markdown": "**粗体** *斜体* `代码`\n\n- 列表项1\n- 列表项2" +} + +响应: +{ + "message_id": "124", + "status": "sent", + "sent_at": "2025-10-24T15:46:00Z" +} +``` + + +*** + +## 项目实施 + +### 实施计划 **[更新]** + +#### 阶段一:基础设施 (2周) + +- 速率限制器实现与测试 +- 数据库表结构设计(包含管理表) +- 白名单+Token认证系统 +- 代理支持与连接测试 + + +#### 阶段二:核心功能 (3周) + +- 消息通知API开发 +- 分级模板渲染引擎 +- 消息发送器包装层实现 +- 消息队列与重试机制 +- 定时提醒CRUD功能 + + +#### 阶段三:管理功能 (2周) **[新增]** + +- 用户档案采集与存储 +- 群组信息管理服务 +- 动态指令注册系统 +- 管理API接口开发 +- 预定义功能自动注册 + + +#### 阶段四:AI功能 (3周) + +- OpenRouter API集成(使用OpenAI SDK) +- 私聊/群聊双模式触发 +- 上下文管理器实现 +- 流式响应处理 +- AI交互日志记录 +- InlineKeyboard交互流程 + + +#### 阶段五:上线准备 (1周) + +- 集成测试与修复 +- 性能压测与优化 +- 文档编写 +- 监控告警配置 +- 灰度发布 + + +### 团队组织 + +| 角色 | 职责 | 人数 | +| :-- | :-- | :-- | +| 技术负责人 | 架构设计审核、技术选型决策、风险评估 | 1 | +| 后端工程师 | 核心业务逻辑开发、API接口实现 | 2 | +| AI集成工程师 | OpenRouter适配、流式响应处理 | 1 | +| 测试工程师 | 单元测试、集成测试、压力测试 | 1 | +| 运维工程师 | 部署脚本、监控配置、应急响应 | 1 | + +### 交付物清单 + +- [x] 源代码仓库(Git) +- [x] 数据库迁移脚本 +- [x] API接口文档(OpenAPI 3.0) +- [x] 部署操作手册 +- [x] 监控告警配置文件 +- [x] 性能测试报告 +- [x] 用户使用指南 + +*** + +## 附录 + +### 术语表 + +| 术语 | 英文 | 说明 | +| :-- | :-- | :-- | +| 令牌桶 | Token Bucket | 速率限制算法,以固定速率补充令牌实现流控 | +| 死信队列 | Dead Letter Queue | 存储多次失败消息的特殊队列 | +| 指数退避 | Exponential Backoff | 重试间隔呈指数增长的策略 | +| WAL模式 | Write-Ahead Logging | SQLite日志模式,提升并发写入性能 | +| RRULE | Recurrence Rule | iCalendar重复规则标准(RFC 5545) | + +### 参考资料 + +- [Telegram Bot API官方文档](https://core.telegram.org/bots/api) +- [OpenRouter API文档](https://openrouter.ai/docs) +- [OpenAI Go SDK](https://github.com/sashabaranov/go-openai) +- [Go语言并发模式](https://go.dev/blog/pipelines) +- [SQLite WAL模式详解](https://www.sqlite.org/wal.html) +- [JWT最佳实践](https://tools.ietf.org/html/rfc8725) + +*** + +### 文档变更记录 + +**v2.0 (2025-10-24更新)** + +**主要变更**: + +1. 新增消息发送器包装层(PlainMessageSender和AIMessageSender) +2. 新增Telegram管理功能模块(用户群组管理、机器人功能管理) +3. 更新AI触发机制为私聊全响应+群聊@模式 +4. 更新AI实现方案为OpenRouter优先(使用OpenAI Go SDK) +5. 新增AI交互日志记录系统 +6. 更新数据库ER图和表结构 +7. 新增管理API接口文档 +8. 调整实施计划,新增管理功能阶段 +9. 术语规范: "机器人交互功能" -> "机器人预定义功能" + +**文档编制完成** | 总计约1.8万字 | 包含11个Mermaid图表 | 覆盖15个核心设计模块 + diff --git a/promt.md b/promt.md index 76bcc82..0ebeabe 100644 --- a/promt.md +++ b/promt.md @@ -11,3 +11,7 @@ 请你根据前端代码的风格要求vue3-typescript-stye.md,对登录设计稿进行优化 +gemini直接搜索 +Elements + +markdown markdown-main-panel tutor-markdown-rendering stronger enable-updated-hr-color \ No newline at end of file