Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4760c5d3e | |||
| d1b905b780 | |||
| 8dac2aff66 | |||
| cf64276ab1 | |||
| 29a3b6095d |
@ -1,208 +0,0 @@
|
|||||||
---
|
|
||||||
name: 多数据库方言与前缀支持
|
|
||||||
overview: 为 HoTimeDB ORM 实现完整的多数据库(MySQL/PostgreSQL/SQLite)方言支持和自动表前缀功能,同时保持完全向后兼容。
|
|
||||||
todos:
|
|
||||||
- id: dialect-interface
|
|
||||||
content: 扩展 Dialect 接口,添加 QuoteIdentifier 和 QuoteChar 方法
|
|
||||||
status: pending
|
|
||||||
- id: identifier-processor
|
|
||||||
content: 实现 IdentifierProcessor 结构体及其方法
|
|
||||||
status: pending
|
|
||||||
- id: db-integration
|
|
||||||
content: 在 HoTimeDB 中集成处理器,添加辅助方法
|
|
||||||
status: pending
|
|
||||||
- id: crud-update
|
|
||||||
content: 修改 crud.go 中的所有 CRUD 方法使用新处理器
|
|
||||||
status: pending
|
|
||||||
- id: where-update
|
|
||||||
content: 修改 where.go 中的条件处理逻辑
|
|
||||||
status: pending
|
|
||||||
- id: builder-update
|
|
||||||
content: 修改 builder.go 中的链式 JOIN 方法
|
|
||||||
status: pending
|
|
||||||
- id: testing
|
|
||||||
content: 测试多数据库和前缀功能
|
|
||||||
status: pending
|
|
||||||
---
|
|
||||||
|
|
||||||
# HoTimeDB 多数据库方言与自动前缀支持计划
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
1. **多数据库方言支持**:让所有 ORM 方法正确支持 MySQL、PostgreSQL、SQLite 的标识符引号格式
|
|
||||||
2. **自动表前缀**:在主表、JOIN 表、WHERE/ON 条件中自动识别并添加表前缀
|
|
||||||
3. **完全向后兼容**:用户现有写法(`order.name`、`` `order`.name ``)无需修改
|
|
||||||
|
|
||||||
## 核心设计
|
|
||||||
|
|
||||||
### 标识符处理流程
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
Input["用户输入: order.name 或 `order`.name"]
|
|
||||||
Parse["解析标识符"]
|
|
||||||
AddPrefix["添加表前缀"]
|
|
||||||
QuoteByDialect["根据数据库类型添加引号"]
|
|
||||||
Output["输出: `app_order`.`name` (MySQL) 或 \"app_order\".\"name\" (PG)"]
|
|
||||||
|
|
||||||
Input --> Parse
|
|
||||||
Parse --> AddPrefix
|
|
||||||
AddPrefix --> QuoteByDialect
|
|
||||||
QuoteByDialect --> Output
|
|
||||||
```
|
|
||||||
|
|
||||||
## 实现步骤
|
|
||||||
|
|
||||||
### 第1步:扩展 Dialect 接口([db/dialect.go](db/dialect.go))
|
|
||||||
|
|
||||||
在现有 `Dialect` 接口中添加新方法:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// QuoteIdentifier 处理标识符(支持 table.column 格式)
|
|
||||||
// 输入: "order" 或 "order.name" 或 "`order`.name"
|
|
||||||
// 输出: 带正确引号的标识符
|
|
||||||
QuoteIdentifier(name string) string
|
|
||||||
|
|
||||||
// QuoteChar 获取引号字符(用于字符串中的替换检测)
|
|
||||||
QuoteChar() string
|
|
||||||
```
|
|
||||||
|
|
||||||
为三种数据库实现这些方法,核心逻辑:
|
|
||||||
|
|
||||||
- 去除已有的引号(支持反引号和双引号)
|
|
||||||
- 按点号分割,对每部分单独加引号
|
|
||||||
- MySQL 使用反引号,PostgreSQL/SQLite 使用双引号
|
|
||||||
|
|
||||||
### 第2步:添加标识符处理器([db/dialect.go](db/dialect.go))
|
|
||||||
|
|
||||||
新增 `IdentifierProcessor` 结构体:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type IdentifierProcessor struct {
|
|
||||||
dialect Dialect
|
|
||||||
prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessTableName 处理表名(添加前缀+引号)
|
|
||||||
func (p *IdentifierProcessor) ProcessTableName(name string) string
|
|
||||||
|
|
||||||
// ProcessColumn 处理字段名(table.column 格式,自动给表名加前缀)
|
|
||||||
func (p *IdentifierProcessor) ProcessColumn(name string) string
|
|
||||||
|
|
||||||
// ProcessCondition 处理 ON/WHERE 条件字符串中的 table.column
|
|
||||||
func (p *IdentifierProcessor) ProcessCondition(condition string) string
|
|
||||||
```
|
|
||||||
|
|
||||||
关键实现细节:
|
|
||||||
|
|
||||||
- 使用正则表达式识别条件字符串中的 `table.column` 模式
|
|
||||||
- 识别已有的引号包裹(`` `table`.column `` 或 `"table".column`)
|
|
||||||
- 避免误处理字符串字面量中的内容
|
|
||||||
|
|
||||||
### 第3步:在 HoTimeDB 中集成处理器([db/db.go](db/db.go))
|
|
||||||
|
|
||||||
```go
|
|
||||||
// GetProcessor 获取标识符处理器(懒加载)
|
|
||||||
func (that *HoTimeDB) GetProcessor() *IdentifierProcessor
|
|
||||||
|
|
||||||
// processTable 内部方法:处理表名
|
|
||||||
func (that *HoTimeDB) processTable(table string) string
|
|
||||||
|
|
||||||
// processColumn 内部方法:处理字段名
|
|
||||||
func (that *HoTimeDB) processColumn(column string) string
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第4步:修改 CRUD 方法([db/crud.go](db/crud.go))
|
|
||||||
|
|
||||||
需要修改的方法及位置:
|
|
||||||
|
|
||||||
| 方法 | 修改内容 |
|
|
||||||
|
|
||||||
|------|---------|
|
|
||||||
|
|
||||||
| `Select` (L77-153) | 表名、字段名处理 |
|
|
||||||
|
|
||||||
| `buildJoin` (L156-222) | JOIN 表名、ON 条件处理 |
|
|
||||||
|
|
||||||
| `Insert` (L248-302) | 表名、字段名处理 |
|
|
||||||
|
|
||||||
| `BatchInsert` (L316-388) | 表名、字段名处理 |
|
|
||||||
|
|
||||||
| `Update` (L598-638) | 表名、字段名处理 |
|
|
||||||
|
|
||||||
| `Delete` (L640-661) | 表名处理 |
|
|
||||||
|
|
||||||
核心改动模式(以 Select 为例):
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 之前
|
|
||||||
query += " FROM `" + that.Prefix + table + "` "
|
|
||||||
// 之后
|
|
||||||
query += " FROM " + that.processTable(table) + " "
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第5步:修改 WHERE 条件处理([db/where.go](db/where.go))
|
|
||||||
|
|
||||||
修改 `varCond` 方法(L205-338)中的字段名处理:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 之前
|
|
||||||
if !strings.Contains(k, ".") {
|
|
||||||
k = "`" + k + "`"
|
|
||||||
}
|
|
||||||
// 之后
|
|
||||||
k = that.processColumn(k)
|
|
||||||
```
|
|
||||||
|
|
||||||
同样修改 `handlePlainField`、`handleDefaultCondition` 等方法。
|
|
||||||
|
|
||||||
### 第6步:修改链式构建器([db/builder.go](db/builder.go))
|
|
||||||
|
|
||||||
`LeftJoin`、`RightJoin` 等方法中的表名和条件字符串也需要处理:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (that *HotimeDBBuilder) LeftJoin(table, joinStr string) *HotimeDBBuilder {
|
|
||||||
// 处理表名和 ON 条件
|
|
||||||
table = that.HoTimeDB.processTable(table)
|
|
||||||
joinStr = that.HoTimeDB.GetProcessor().ProcessCondition(joinStr)
|
|
||||||
that.Join(Map{"[>]" + table: joinStr})
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安全考虑
|
|
||||||
|
|
||||||
1. **避免误替换数据**:条件处理只处理 SQL 语法结构中的标识符,不处理:
|
|
||||||
|
|
||||||
- 字符串字面量内的内容(通过检测引号边界)
|
|
||||||
- 已经是占位符 `?` 的参数值
|
|
||||||
|
|
||||||
2. **正则表达式设计**:识别 `table.column` 模式时排除:
|
|
||||||
|
|
||||||
- 数字开头的标识符
|
|
||||||
- 函数调用如 `NOW()`
|
|
||||||
- 特殊运算符如 `>=`、`<=`
|
|
||||||
|
|
||||||
## 测试要点
|
|
||||||
|
|
||||||
1. 多数据库类型切换
|
|
||||||
2. 带前缀和不带前缀场景
|
|
||||||
3. 复杂 JOIN 查询
|
|
||||||
4. 嵌套条件查询
|
|
||||||
5. 特殊字符和保留字作为表名/字段名
|
|
||||||
|
|
||||||
## 文件修改清单
|
|
||||||
|
|
||||||
| 文件 | 修改类型 |
|
|
||||||
|
|
||||||
|------|---------|
|
|
||||||
|
|
||||||
| [db/dialect.go](db/dialect.go) | 扩展接口,添加 IdentifierProcessor |
|
|
||||||
|
|
||||||
| [db/db.go](db/db.go) | 添加 GetProcessor 和辅助方法 |
|
|
||||||
|
|
||||||
| [db/crud.go](db/crud.go) | 修改所有 CRUD 方法 |
|
|
||||||
|
|
||||||
| [db/where.go](db/where.go) | 修改条件处理逻辑 |
|
|
||||||
|
|
||||||
| [db/builder.go](db/builder.go) | 修改链式构建器的 JOIN 方法 |
|
|
||||||
@ -169,7 +169,7 @@ onCondition := that.GetProcessor().ProcessConditionString(v.(string))
|
|||||||
query += " LEFT JOIN " + table + " ON " + onCondition + " "
|
query += " LEFT JOIN " + table + " ON " + onCondition + " "
|
||||||
```
|
```
|
||||||
|
|
||||||
**Insert/BatchInsert/Update/Delete** 同样修改表名和字段名处理。
|
**Insert/Inserts/Update/Delete** 同样修改表名和字段名处理。
|
||||||
|
|
||||||
### 第5步:修改 WHERE 条件处理([db/where.go](db/where.go))
|
### 第5步:修改 WHERE 条件处理([db/where.go](db/where.go))
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
|||||||
/example/tpt/demo/
|
/example/tpt/demo/
|
||||||
*.exe
|
*.exe
|
||||||
/example/config
|
/example/config
|
||||||
|
/.cursor/*.log
|
||||||
|
|||||||
210
README.md
210
README.md
@ -2,75 +2,42 @@
|
|||||||
|
|
||||||
**高性能 Go Web 服务框架**
|
**高性能 Go Web 服务框架**
|
||||||
|
|
||||||
## 特性
|
一个"小而全"的 Go Web 框架,内置 ORM、三级缓存、Session 管理,让你专注于业务逻辑。
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
- **高性能** - 单机 10万+ QPS,支持百万级并发用户
|
- **高性能** - 单机 10万+ QPS,支持百万级并发用户
|
||||||
- **多数据库支持** - MySQL、SQLite3,支持主从分离
|
- **内置 ORM** - 类 Medoo 语法,链式查询,支持 MySQL/SQLite/PostgreSQL
|
||||||
- **三级缓存系统** - Memory > Redis > DB,自动穿透与回填
|
- **三级缓存** - Memory > Redis > DB,自动穿透与回填
|
||||||
- **Session管理** - 内置会话管理,支持多种存储后端
|
- **Session 管理** - 内置会话管理,支持多种存储后端
|
||||||
- **自动代码生成** - 根据数据库表自动生成 CRUD 接口
|
- **代码生成** - 根据数据库表自动生成 CRUD 接口
|
||||||
- **丰富工具类** - 上下文管理、类型转换、加密解密等
|
- **开箱即用** - 微信支付/公众号/小程序、阿里云、腾讯云等 SDK 内置
|
||||||
|
|
||||||
## 快速开始
|
## 文档
|
||||||
|
|
||||||
### 安装
|
| 文档 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| [快速上手指南](docs/QUICKSTART.md) | 5 分钟入门,安装配置、路由、中间件、基础数据库操作 |
|
||||||
|
| [HoTimeDB 使用说明](docs/HoTimeDB_使用说明.md) | 完整数据库 ORM 教程 |
|
||||||
|
| [HoTimeDB API 参考](docs/HoTimeDB_API参考.md) | 数据库 API 速查手册 |
|
||||||
|
| [Common 工具类](docs/Common_工具类使用说明.md) | Map/Slice/Obj 类型、类型转换、工具函数 |
|
||||||
|
| [代码生成器](docs/CodeGen_使用说明.md) | 自动 CRUD 代码生成、配置规则 |
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get code.hoteas.com/golang/hotime
|
go get code.hoteas.com/golang/hotime
|
||||||
```
|
```
|
||||||
|
|
||||||
### 最小示例
|
## 性能
|
||||||
|
|
||||||
```go
|
| 并发数 | QPS | 成功率 | 平均延迟 |
|
||||||
package main
|
|--------|-----|--------|----------|
|
||||||
|
| 500 | 99,960 | 100% | 5.0ms |
|
||||||
|
| **1000** | **102,489** | **100%** | **9.7ms** |
|
||||||
|
| 2000 | 75,801 | 99.99% | 26.2ms |
|
||||||
|
|
||||||
import (
|
> 测试环境:24 核 CPU,Windows 10,Go 1.19.3
|
||||||
. "code.hoteas.com/golang/hotime"
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
appIns := Init("config/config.json")
|
|
||||||
|
|
||||||
appIns.Run(Router{
|
|
||||||
"app": {
|
|
||||||
"test": {
|
|
||||||
"hello": func(that *Context) {
|
|
||||||
that.Display(0, Map{"message": "Hello World"})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
访问: http://localhost:8081/app/test/hello
|
|
||||||
|
|
||||||
## 性能测试报告
|
|
||||||
|
|
||||||
### 测试环境
|
|
||||||
|
|
||||||
| 项目 | 配置 |
|
|
||||||
|------|------|
|
|
||||||
| CPU | 24 核心 |
|
|
||||||
| 系统 | Windows 10 |
|
|
||||||
| Go 版本 | 1.19.3 |
|
|
||||||
|
|
||||||
### 测试结果
|
|
||||||
|
|
||||||
| 并发数 | QPS | 成功率 | 平均延迟 | P99延迟 |
|
|
||||||
|--------|-----|--------|----------|---------|
|
|
||||||
| 500 | 99,960 | 100% | 5.0ms | 25.2ms |
|
|
||||||
| **1000** | **102,489** | **100%** | **9.7ms** | **56.8ms** |
|
|
||||||
| 2000 | 75,801 | 99.99% | 26.2ms | 127.7ms |
|
|
||||||
| 5000 | 12,611 | 99.95% | 391.4ms | 781.4ms |
|
|
||||||
|
|
||||||
### 性能总结
|
|
||||||
|
|
||||||
```
|
|
||||||
最高 QPS: 102,489 请求/秒
|
|
||||||
最佳并发数: 1,000
|
|
||||||
```
|
|
||||||
|
|
||||||
### 并发用户估算
|
### 并发用户估算
|
||||||
|
|
||||||
@ -79,24 +46,9 @@ func main() {
|
|||||||
| 高频交互 | 1次/秒 | ~10万 |
|
| 高频交互 | 1次/秒 | ~10万 |
|
||||||
| 活跃用户 | 1次/5秒 | ~50万 |
|
| 活跃用户 | 1次/5秒 | ~50万 |
|
||||||
| 普通浏览 | 1次/10秒 | ~100万 |
|
| 普通浏览 | 1次/10秒 | ~100万 |
|
||||||
| 低频访问 | 1次/30秒 | ~300万 |
|
|
||||||
|
|
||||||
**生产环境建议**: 保留 30-50% 性能余量,安全并发用户数约 **50万 - 70万**
|
|
||||||
|
|
||||||
### 与主流框架性能对比
|
|
||||||
|
|
||||||
| 框架 | 典型QPS | 基础实现 | 性能评级 |
|
|
||||||
|------|---------|----------|----------|
|
|
||||||
| **HoTime** | **~100K** | net/http | 第一梯队 |
|
|
||||||
| Fiber | ~100K+ | fasthttp | 第一梯队 |
|
|
||||||
| Gin | ~60-80K | net/http | 第二梯队 |
|
|
||||||
| Echo | ~60-80K | net/http | 第二梯队 |
|
|
||||||
| Chi | ~50-60K | net/http | 第二梯队 |
|
|
||||||
|
|
||||||
## 框架对比
|
## 框架对比
|
||||||
|
|
||||||
### 功能特性对比
|
|
||||||
|
|
||||||
| 特性 | HoTime | Gin | Echo | Fiber |
|
| 特性 | HoTime | Gin | Echo | Fiber |
|
||||||
|------|--------|-----|------|-------|
|
|------|--------|-----|------|-------|
|
||||||
| 性能 | 100K QPS | 70K QPS | 70K QPS | 100K QPS |
|
| 性能 | 100K QPS | 70K QPS | 70K QPS | 100K QPS |
|
||||||
@ -105,117 +57,25 @@ func main() {
|
|||||||
| Session | ✅ 内置 | ❌ 需插件 | ❌ 需插件 | ❌ 需插件 |
|
| Session | ✅ 内置 | ❌ 需插件 | ❌ 需插件 | ❌ 需插件 |
|
||||||
| 代码生成 | ✅ | ❌ | ❌ | ❌ |
|
| 代码生成 | ✅ | ❌ | ❌ | ❌ |
|
||||||
| 微信/支付集成 | ✅ 内置 | ❌ | ❌ | ❌ |
|
| 微信/支付集成 | ✅ 内置 | ❌ | ❌ | ❌ |
|
||||||
| 路由灵活性 | 中等 | 优秀 | 优秀 | 优秀 |
|
|
||||||
| 社区生态 | 较小 | 庞大 | 较大 | 较大 |
|
|
||||||
|
|
||||||
### HoTime 优势
|
## 适用场景
|
||||||
|
|
||||||
1. **开箱即用** - 内置 ORM + 缓存 + Session,无需额外集成
|
|
||||||
2. **三级缓存** - Memory > Redis > DB,自动穿透与回填
|
|
||||||
3. **开发效率高** - 链式查询语法简洁,内置微信/云服务SDK
|
|
||||||
4. **性能优异** - 100K QPS,媲美最快的 Fiber 框架
|
|
||||||
|
|
||||||
### 适用场景
|
|
||||||
|
|
||||||
| 场景 | 推荐度 | 说明 |
|
| 场景 | 推荐度 | 说明 |
|
||||||
|------|--------|------|
|
|------|--------|------|
|
||||||
| 中小型后台系统 | ⭐⭐⭐⭐⭐ | 完美适配,开发效率最高 |
|
| 中小型后台系统 | ⭐⭐⭐⭐⭐ | 完美适配,开发效率最高 |
|
||||||
| 微信小程序后端 | ⭐⭐⭐⭐⭐ | 内置微信SDK |
|
| 微信小程序后端 | ⭐⭐⭐⭐⭐ | 内置微信 SDK |
|
||||||
| 快速原型开发 | ⭐⭐⭐⭐⭐ | 代码生成 + 全功能集成 |
|
| 快速原型开发 | ⭐⭐⭐⭐⭐ | 代码生成 + 全功能集成 |
|
||||||
| 高并发API服务 | ⭐⭐⭐⭐ | 性能足够 |
|
| 高并发 API 服务 | ⭐⭐⭐⭐ | 性能足够 |
|
||||||
| 大型微服务 | ⭐⭐⭐ | 建议用Gin/Echo |
|
| 大型微服务 | ⭐⭐⭐ | 建议用 Gin/Echo |
|
||||||
|
|
||||||
### 总体评价
|
|
||||||
|
|
||||||
| 维度 | 评分 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| 性能 | 95分 | 第一梯队,媲美Fiber |
|
|
||||||
| 功能集成 | 90分 | 远超主流框架 |
|
|
||||||
| 开发效率 | 85分 | 适合快速开发 |
|
|
||||||
| 生态/社区 | 50分 | 持续建设中 |
|
|
||||||
|
|
||||||
> **总结**: HoTime 是"小而全"的高性能框架,性能不输主流,集成度远超主流,适合独立开发者或小团队快速构建中小型项目。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 数据库操作
|
|
||||||
|
|
||||||
### 基础 CRUD
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 查询单条
|
|
||||||
user := that.Db.Get("user", "*", Map{"id": 1})
|
|
||||||
|
|
||||||
// 查询列表
|
|
||||||
users := that.Db.Select("user", "*", Map{"status": 1, "ORDER": "id DESC"})
|
|
||||||
|
|
||||||
// 插入数据
|
|
||||||
id := that.Db.Insert("user", Map{"name": "test", "age": 18})
|
|
||||||
|
|
||||||
// 更新数据
|
|
||||||
rows := that.Db.Update("user", Map{"name": "new"}, Map{"id": 1})
|
|
||||||
|
|
||||||
// 删除数据
|
|
||||||
rows := that.Db.Delete("user", Map{"id": 1})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 链式查询
|
|
||||||
|
|
||||||
```go
|
|
||||||
users := that.Db.Table("user").
|
|
||||||
LeftJoin("order", "user.id=order.user_id").
|
|
||||||
And("status", 1).
|
|
||||||
Order("id DESC").
|
|
||||||
Page(1, 10).
|
|
||||||
Select("*")
|
|
||||||
```
|
|
||||||
|
|
||||||
### 条件语法
|
|
||||||
|
|
||||||
| 语法 | 说明 | 示例 |
|
|
||||||
|------|------|------|
|
|
||||||
| key | 等于 | "id": 1 |
|
|
||||||
| key[>] | 大于 | "age[>]": 18 |
|
|
||||||
| key[<] | 小于 | "age[<]": 60 |
|
|
||||||
| key[!] | 不等于 | "status[!]": 0 |
|
|
||||||
| key[~] | LIKE | "name[~]": "test" |
|
|
||||||
| key[<>] | BETWEEN | "age[<>]": Slice{18, 60} |
|
|
||||||
|
|
||||||
## 缓存系统
|
|
||||||
|
|
||||||
三级缓存: **Memory > Redis > Database**
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 通用缓存
|
|
||||||
that.Cache("key", value) // 设置
|
|
||||||
data := that.Cache("key") // 获取
|
|
||||||
that.Cache("key", nil) // 删除
|
|
||||||
|
|
||||||
// Session 缓存
|
|
||||||
that.Session("user_id", 123) // 设置
|
|
||||||
userId := that.Session("user_id") // 获取
|
|
||||||
```
|
|
||||||
|
|
||||||
## 中间件
|
|
||||||
|
|
||||||
```go
|
|
||||||
appIns.SetConnectListener(func(that *Context) bool {
|
|
||||||
if that.Session("user_id").Data == nil {
|
|
||||||
that.Display(2, "请先登录")
|
|
||||||
return true // 终止请求
|
|
||||||
}
|
|
||||||
return false // 继续处理
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## 扩展功能
|
## 扩展功能
|
||||||
|
|
||||||
- **微信支付/公众号/小程序** - dri/wechat/
|
- 微信支付/公众号/小程序 - `dri/wechat/`
|
||||||
- **阿里云服务** - dri/aliyun/
|
- 阿里云服务 - `dri/aliyun/`
|
||||||
- **腾讯云服务** - dri/tencent/
|
- 腾讯云服务 - `dri/tencent/`
|
||||||
- **文件上传下载** - dri/upload/, dri/download/
|
- 文件上传下载 - `dri/upload/`, `dri/download/`
|
||||||
- **MongoDB** - dri/mongodb/
|
- MongoDB - `dri/mongodb/`
|
||||||
- **RSA加解密** - dri/rsa/
|
- RSA 加解密 - `dri/rsa/`
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
252
db/README.md
252
db/README.md
@ -1,252 +0,0 @@
|
|||||||
# HoTimeDB ORM 文档集合
|
|
||||||
|
|
||||||
这是HoTimeDB ORM框架的完整文档集合,包含使用说明、API参考、示例代码和测试数据。
|
|
||||||
|
|
||||||
## ⚠️ 重要更新说明
|
|
||||||
|
|
||||||
**语法修正通知**:经过对源码的深入分析,发现HoTimeDB的条件查询语法有特定规则:
|
|
||||||
- ✅ **单个条件**:可以直接写在Map中
|
|
||||||
- ⚠️ **多个条件**:必须使用`AND`或`OR`包装
|
|
||||||
- 📝 所有文档和示例代码已按正确语法更新
|
|
||||||
|
|
||||||
## 📚 文档列表
|
|
||||||
|
|
||||||
### 1. [HoTimeDB_使用说明.md](./HoTimeDB_使用说明.md)
|
|
||||||
**完整使用说明书** - 详细的功能介绍和使用指南
|
|
||||||
- 🚀 快速开始
|
|
||||||
- ⚙️ 数据库配置
|
|
||||||
- 🔧 基本操作 (CRUD)
|
|
||||||
- 🔗 链式查询构建器
|
|
||||||
- 🔍 条件查询语法
|
|
||||||
- 🔄 JOIN操作
|
|
||||||
- 📄 分页查询
|
|
||||||
- 📊 聚合函数
|
|
||||||
- 🔐 事务处理
|
|
||||||
- 💾 缓存机制
|
|
||||||
- ⚡ 高级特性
|
|
||||||
|
|
||||||
### 2. [HoTimeDB_API参考.md](./HoTimeDB_API参考.md)
|
|
||||||
**快速API参考手册** - 开发时的速查手册
|
|
||||||
- 📖 基本方法
|
|
||||||
- 🔧 CRUD操作
|
|
||||||
- 📊 聚合函数
|
|
||||||
- 📄 分页查询
|
|
||||||
- 🔍 条件语法参考
|
|
||||||
- 🔗 JOIN语法
|
|
||||||
- 🔐 事务处理
|
|
||||||
- 🛠️ 工具方法
|
|
||||||
|
|
||||||
### 3. [示例代码文件](../examples/hotimedb_examples.go)
|
|
||||||
**完整示例代码集合** - 可运行的实际应用示例(语法已修正)
|
|
||||||
- 🏗️ 基本初始化和配置
|
|
||||||
- 📝 基本CRUD操作
|
|
||||||
- 🔗 链式查询操作
|
|
||||||
- 🤝 JOIN查询操作
|
|
||||||
- 🔍 条件查询语法
|
|
||||||
- 📄 分页查询
|
|
||||||
- 📊 聚合函数查询
|
|
||||||
- 🔐 事务处理
|
|
||||||
- 💾 缓存机制
|
|
||||||
- 🔧 原生SQL执行
|
|
||||||
- 🚨 错误处理和调试
|
|
||||||
- ⚡ 性能优化技巧
|
|
||||||
- 🎯 完整应用示例
|
|
||||||
|
|
||||||
### 4. [test_tables.sql](./test_tables.sql)
|
|
||||||
**测试数据库结构** - 快速搭建测试环境
|
|
||||||
- 🏗️ 完整的表结构定义
|
|
||||||
- 📊 测试数据插入
|
|
||||||
- 🔍 索引优化
|
|
||||||
- 👁️ 视图示例
|
|
||||||
- 🔧 存储过程示例
|
|
||||||
|
|
||||||
## 🎯 核心特性
|
|
||||||
|
|
||||||
### 🌟 主要优势
|
|
||||||
- **类Medoo语法**: 参考PHP Medoo设计,语法简洁易懂
|
|
||||||
- **链式查询**: 支持流畅的链式查询构建器
|
|
||||||
- **条件丰富**: 支持丰富的条件查询语法
|
|
||||||
- **事务支持**: 完整的事务处理机制
|
|
||||||
- **缓存集成**: 内置查询结果缓存
|
|
||||||
- **读写分离**: 支持主从数据库配置
|
|
||||||
- **类型安全**: 基于Golang的强类型系统
|
|
||||||
|
|
||||||
### 🔧 支持的数据库
|
|
||||||
- ✅ MySQL
|
|
||||||
- ✅ SQLite
|
|
||||||
- ✅ 其他标准SQL数据库
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 1. 安装依赖
|
|
||||||
```bash
|
|
||||||
go mod init your-project
|
|
||||||
go get github.com/go-sql-driver/mysql
|
|
||||||
go get github.com/sirupsen/logrus
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 创建测试数据库
|
|
||||||
```bash
|
|
||||||
mysql -u root -p < test_tables.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 基本使用
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"code.hoteas.com/golang/hotime/db"
|
|
||||||
"code.hoteas.com/golang/hotime/common"
|
|
||||||
"database/sql"
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
database := &db.HoTimeDB{
|
|
||||||
Prefix: "app_",
|
|
||||||
Mode: 2, // 开发模式
|
|
||||||
}
|
|
||||||
|
|
||||||
database.SetConnect(func() (master, slave *sql.DB) {
|
|
||||||
master, _ = sql.Open("mysql", "user:pass@tcp(localhost:3306)/dbname")
|
|
||||||
return master, master
|
|
||||||
})
|
|
||||||
|
|
||||||
// 链式查询(链式语法支持单独Where然后用And添加条件)
|
|
||||||
users := database.Table("user").
|
|
||||||
Where("status", 1). // 链式中可以单独Where
|
|
||||||
And("age[>]", 18). // 用And添加更多条件
|
|
||||||
Order("created_time DESC").
|
|
||||||
Limit(0, 10).
|
|
||||||
Select("id,name,email")
|
|
||||||
|
|
||||||
// 或者使用传统语法(多个条件必须用AND包装)
|
|
||||||
users2 := database.Select("user", "id,name,email", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
},
|
|
||||||
"ORDER": "created_time DESC",
|
|
||||||
"LIMIT": []int{0, 10},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚠️ 重要语法规则
|
|
||||||
|
|
||||||
**条件查询语法规则:**
|
|
||||||
- ✅ **单个条件**:可以直接写在Map中
|
|
||||||
- ✅ **多个条件**:必须使用`AND`或`OR`包装
|
|
||||||
- ✅ **特殊参数**:`ORDER`、`GROUP`、`LIMIT`与条件同级
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ✅ 正确:单个条件
|
|
||||||
Map{"status": 1}
|
|
||||||
|
|
||||||
// ✅ 正确:多个条件用AND包装
|
|
||||||
Map{
|
|
||||||
"AND": Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
},
|
|
||||||
"ORDER": "id DESC",
|
|
||||||
}
|
|
||||||
|
|
||||||
// ❌ 错误:多个条件不用AND包装
|
|
||||||
Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18, // 不支持!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 条件查询语法速查
|
|
||||||
|
|
||||||
| 语法 | SQL | 说明 |
|
|
||||||
|------|-----|------|
|
|
||||||
| `"field": value` | `field = ?` | 等于 |
|
|
||||||
| `"field[!]": value` | `field != ?` | 不等于 |
|
|
||||||
| `"field[>]": value` | `field > ?` | 大于 |
|
|
||||||
| `"field[>=]": value` | `field >= ?` | 大于等于 |
|
|
||||||
| `"field[<]": value` | `field < ?` | 小于 |
|
|
||||||
| `"field[<=]": value` | `field <= ?` | 小于等于 |
|
|
||||||
| `"field[~]": "keyword"` | `field LIKE '%keyword%'` | 包含 |
|
|
||||||
| `"field[<>]": [min, max]` | `field BETWEEN ? AND ?` | 区间内 |
|
|
||||||
| `"field": [v1, v2, v3]` | `field IN (?, ?, ?)` | 在集合中 |
|
|
||||||
| `"field": nil` | `field IS NULL` | 为空 |
|
|
||||||
| `"field[#]": "NOW()"` | `field = NOW()` | 直接SQL |
|
|
||||||
|
|
||||||
## 🔗 JOIN语法速查
|
|
||||||
|
|
||||||
| 语法 | SQL | 说明 |
|
|
||||||
|------|-----|------|
|
|
||||||
| `"[>]table"` | `LEFT JOIN` | 左连接 |
|
|
||||||
| `"[<]table"` | `RIGHT JOIN` | 右连接 |
|
|
||||||
| `"[><]table"` | `INNER JOIN` | 内连接 |
|
|
||||||
| `"[<>]table"` | `FULL JOIN` | 全连接 |
|
|
||||||
|
|
||||||
## 🛠️ 链式方法速查
|
|
||||||
|
|
||||||
```go
|
|
||||||
db.Table("table") // 指定表名
|
|
||||||
.Where(key, value) // WHERE条件
|
|
||||||
.And(key, value) // AND条件
|
|
||||||
.Or(map) // OR条件
|
|
||||||
.LeftJoin(table, on) // LEFT JOIN
|
|
||||||
.Order(fields...) // ORDER BY
|
|
||||||
.Group(fields...) // GROUP BY
|
|
||||||
.Limit(offset, limit) // LIMIT
|
|
||||||
.Page(page, pageSize) // 分页
|
|
||||||
.Select(fields...) // 查询
|
|
||||||
.Get(fields...) // 获取单条
|
|
||||||
.Count() // 计数
|
|
||||||
.Update(data) // 更新
|
|
||||||
.Delete() // 删除
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚡ 性能优化建议
|
|
||||||
|
|
||||||
### 🔍 查询优化
|
|
||||||
- 使用合适的索引字段作为查询条件
|
|
||||||
- IN查询会自动优化为BETWEEN(连续数字)
|
|
||||||
- 避免SELECT *,指定需要的字段
|
|
||||||
- 合理使用LIMIT限制结果集大小
|
|
||||||
|
|
||||||
### 💾 缓存使用
|
|
||||||
- 查询结果会自动缓存
|
|
||||||
- 增删改操作会自动清除缓存
|
|
||||||
- `cached`表不参与缓存
|
|
||||||
|
|
||||||
### 🔐 事务处理
|
|
||||||
- 批量操作使用事务提高性能
|
|
||||||
- 事务中避免长时间操作
|
|
||||||
- 合理设置事务隔离级别
|
|
||||||
|
|
||||||
## 🚨 注意事项
|
|
||||||
|
|
||||||
### 🔒 安全相关
|
|
||||||
- 使用参数化查询防止SQL注入
|
|
||||||
- `[#]`语法需要注意防止注入
|
|
||||||
- 敏感数据加密存储
|
|
||||||
|
|
||||||
### 🎯 最佳实践
|
|
||||||
- 开发时设置`Mode = 2`便于调试
|
|
||||||
- 生产环境设置`Mode = 0`
|
|
||||||
- 合理设置表前缀
|
|
||||||
- 定期检查慢查询日志
|
|
||||||
|
|
||||||
## 🤝 与PHP Medoo的差异
|
|
||||||
|
|
||||||
1. **类型系统**: 使用`common.Map`和`common.Slice`
|
|
||||||
2. **错误处理**: Golang风格的错误处理
|
|
||||||
3. **链式调用**: 提供更丰富的链式API
|
|
||||||
4. **缓存集成**: 内置缓存功能
|
|
||||||
5. **并发安全**: 需要注意并发使用
|
|
||||||
|
|
||||||
## 📞 技术支持
|
|
||||||
|
|
||||||
- 📧 查看源码:`hotimedb.go`
|
|
||||||
- 📖 参考文档:本目录下的各个文档文件
|
|
||||||
- 🔧 示例代码:运行`HoTimeDB_示例代码.go`中的示例
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**HoTimeDB ORM框架 - 让数据库操作更简单!** 🎉
|
|
||||||
|
|
||||||
> 本文档基于HoTimeDB源码分析生成,参考了PHP Medoo的设计理念,并根据Golang语言特性进行了优化。
|
|
||||||
@ -307,19 +307,19 @@ func (that *HoTimeDB) Insert(table string, data map[string]interface{}) int64 {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchInsert 批量插入数据
|
// Inserts 批量插入数据
|
||||||
// table: 表名
|
// table: 表名
|
||||||
// dataList: 数据列表,每个元素是一个 Map
|
// dataList: 数据列表,每个元素是一个 Map
|
||||||
// 返回受影响的行数
|
// 返回受影响的行数
|
||||||
//
|
//
|
||||||
// 示例:
|
// 示例:
|
||||||
//
|
//
|
||||||
// affected := db.BatchInsert("user", []Map{
|
// affected := db.Inserts("user", []Map{
|
||||||
// {"name": "张三", "age": 25, "email": "zhang@example.com"},
|
// {"name": "张三", "age": 25, "email": "zhang@example.com"},
|
||||||
// {"name": "李四", "age": 30, "email": "li@example.com"},
|
// {"name": "李四", "age": 30, "email": "li@example.com"},
|
||||||
// {"name": "王五", "age": 28, "email": "wang@example.com"},
|
// {"name": "王五", "age": 28, "email": "wang@example.com"},
|
||||||
// })
|
// })
|
||||||
func (that *HoTimeDB) BatchInsert(table string, dataList []Map) int64 {
|
func (that *HoTimeDB) Inserts(table string, dataList []Map) int64 {
|
||||||
if len(dataList) == 0 {
|
if len(dataList) == 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -248,6 +249,170 @@ func TestHoTimeDBHelperMethods(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWhereWithORCondition 测试 OR 条件处理是否正确添加括号
|
||||||
|
func TestWhereWithORCondition(t *testing.T) {
|
||||||
|
// 创建 MySQL 数据库实例
|
||||||
|
mysqlDB := &HoTimeDB{
|
||||||
|
Type: "mysql",
|
||||||
|
Prefix: "",
|
||||||
|
}
|
||||||
|
mysqlDB.initDialect()
|
||||||
|
|
||||||
|
// 测试 OR 与普通条件组合 (假设 A: 顺序问题)
|
||||||
|
t.Run("OR with normal condition", func(t *testing.T) {
|
||||||
|
data := Map{
|
||||||
|
"OR": Map{
|
||||||
|
"username": "test",
|
||||||
|
"phone": "123",
|
||||||
|
},
|
||||||
|
"state": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
where, params := mysqlDB.where(data)
|
||||||
|
fmt.Println("Test 1 - OR with normal condition:")
|
||||||
|
fmt.Println(" Generated WHERE:", where)
|
||||||
|
fmt.Println(" Params count:", len(params))
|
||||||
|
|
||||||
|
// 检查 OR 条件是否被括号包裹
|
||||||
|
if !strings.Contains(where, "(") || !strings.Contains(where, ")") {
|
||||||
|
t.Errorf("OR condition should be wrapped with parentheses, got: %s", where)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有 AND 连接
|
||||||
|
if !strings.Contains(where, "AND") {
|
||||||
|
t.Errorf("OR condition and normal condition should be connected with AND, got: %s", where)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试纯 OR 条件(无其他普通条件)
|
||||||
|
t.Run("Pure OR condition", func(t *testing.T) {
|
||||||
|
data := Map{
|
||||||
|
"OR": Map{
|
||||||
|
"username": "test",
|
||||||
|
"phone": "123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
where, params := mysqlDB.where(data)
|
||||||
|
fmt.Println("Test 2 - Pure OR condition:")
|
||||||
|
fmt.Println(" Generated WHERE:", where)
|
||||||
|
fmt.Println(" Params count:", len(params))
|
||||||
|
|
||||||
|
// 检查 OR 条件内部应该用 OR 连接
|
||||||
|
if !strings.Contains(where, "OR") {
|
||||||
|
t.Errorf("OR condition should contain OR keyword, got: %s", where)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试多个普通条件与 OR 组合 (假设 A)
|
||||||
|
t.Run("OR with multiple normal conditions", func(t *testing.T) {
|
||||||
|
data := Map{
|
||||||
|
"OR": Map{
|
||||||
|
"username": "test",
|
||||||
|
"phone": "123",
|
||||||
|
},
|
||||||
|
"state": 0,
|
||||||
|
"status": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
where, params := mysqlDB.where(data)
|
||||||
|
fmt.Println("Test 3 - OR with multiple normal conditions:")
|
||||||
|
fmt.Println(" Generated WHERE:", where)
|
||||||
|
fmt.Println(" Params count:", len(params))
|
||||||
|
|
||||||
|
// 应该有括号
|
||||||
|
if !strings.Contains(where, "(") {
|
||||||
|
t.Errorf("OR condition should be wrapped with parentheses, got: %s", where)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试嵌套 AND/OR 条件 (假设 B, E)
|
||||||
|
t.Run("Nested AND/OR conditions", func(t *testing.T) {
|
||||||
|
data := Map{
|
||||||
|
"OR": Map{
|
||||||
|
"username": "test",
|
||||||
|
"AND": Map{
|
||||||
|
"phone": "123",
|
||||||
|
"status": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"state": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
where, params := mysqlDB.where(data)
|
||||||
|
fmt.Println("Test 4 - Nested AND/OR conditions:")
|
||||||
|
fmt.Println(" Generated WHERE:", where)
|
||||||
|
fmt.Println(" Params count:", len(params))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试空 OR 条件 (假设 C)
|
||||||
|
t.Run("Empty OR condition", func(t *testing.T) {
|
||||||
|
data := Map{
|
||||||
|
"OR": Map{},
|
||||||
|
"state": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
where, params := mysqlDB.where(data)
|
||||||
|
fmt.Println("Test 5 - Empty OR condition:")
|
||||||
|
fmt.Println(" Generated WHERE:", where)
|
||||||
|
fmt.Println(" Params count:", len(params))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试 OR 与 LIMIT, ORDER 组合 (假设 D)
|
||||||
|
t.Run("OR with LIMIT and ORDER", func(t *testing.T) {
|
||||||
|
data := Map{
|
||||||
|
"OR": Map{
|
||||||
|
"username": "test",
|
||||||
|
"phone": "123",
|
||||||
|
},
|
||||||
|
"state": 0,
|
||||||
|
"ORDER": "id DESC",
|
||||||
|
"LIMIT": 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
where, params := mysqlDB.where(data)
|
||||||
|
fmt.Println("Test 6 - OR with LIMIT and ORDER:")
|
||||||
|
fmt.Println(" Generated WHERE:", where)
|
||||||
|
fmt.Println(" Params count:", len(params))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试同时有 OR 和 AND 关键字 (假设 E)
|
||||||
|
t.Run("Both OR and AND keywords", func(t *testing.T) {
|
||||||
|
data := Map{
|
||||||
|
"OR": Map{
|
||||||
|
"username": "test",
|
||||||
|
"phone": "123",
|
||||||
|
},
|
||||||
|
"AND": Map{
|
||||||
|
"type": 1,
|
||||||
|
"source": "web",
|
||||||
|
},
|
||||||
|
"state": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
where, params := mysqlDB.where(data)
|
||||||
|
fmt.Println("Test 7 - Both OR and AND keywords:")
|
||||||
|
fmt.Println(" Generated WHERE:", where)
|
||||||
|
fmt.Println(" Params count:", len(params))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试普通条件在 OR 之前(排序后)(假设 A)
|
||||||
|
t.Run("Normal condition before OR alphabetically", func(t *testing.T) {
|
||||||
|
data := Map{
|
||||||
|
"OR": Map{
|
||||||
|
"username": "test",
|
||||||
|
"phone": "123",
|
||||||
|
},
|
||||||
|
"active": 1, // 'a' 在 'O' 之前
|
||||||
|
}
|
||||||
|
|
||||||
|
where, params := mysqlDB.where(data)
|
||||||
|
fmt.Println("Test 8 - Normal condition before OR (alphabetically):")
|
||||||
|
fmt.Println(" Generated WHERE:", where)
|
||||||
|
fmt.Println(" Params count:", len(params))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 打印测试结果(用于调试)
|
// 打印测试结果(用于调试)
|
||||||
func ExampleIdentifierProcessor() {
|
func ExampleIdentifierProcessor() {
|
||||||
// MySQL 示例
|
// MySQL 示例
|
||||||
|
|||||||
@ -20,9 +20,21 @@ func NewIdentifierProcessor(dialect Dialect, prefix string) *IdentifierProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 系统数据库列表,这些数据库不添加前缀
|
||||||
|
var systemDatabases = map[string]bool{
|
||||||
|
"INFORMATION_SCHEMA": true,
|
||||||
|
"information_schema": true,
|
||||||
|
"mysql": true,
|
||||||
|
"performance_schema": true,
|
||||||
|
"sys": true,
|
||||||
|
"pg_catalog": true,
|
||||||
|
"pg_toast": true,
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessTableName 处理表名(添加前缀+引号)
|
// ProcessTableName 处理表名(添加前缀+引号)
|
||||||
// 输入: "order" 或 "`order`" 或 "\"order\""
|
// 输入: "order" 或 "`order`" 或 "\"order\"" 或 "INFORMATION_SCHEMA.TABLES"
|
||||||
// 输出: "`app_order`" (MySQL) 或 "\"app_order\"" (PostgreSQL/SQLite)
|
// 输出: "`app_order`" (MySQL) 或 "\"app_order\"" (PostgreSQL/SQLite)
|
||||||
|
// 对于 database.table 格式,会分别处理,系统数据库不添加前缀
|
||||||
func (p *IdentifierProcessor) ProcessTableName(name string) string {
|
func (p *IdentifierProcessor) ProcessTableName(name string) string {
|
||||||
// 去除已有的引号
|
// 去除已有的引号
|
||||||
name = p.stripQuotes(name)
|
name = p.stripQuotes(name)
|
||||||
@ -33,7 +45,23 @@ func (p *IdentifierProcessor) ProcessTableName(name string) string {
|
|||||||
parts := strings.SplitN(name, " ", 2)
|
parts := strings.SplitN(name, " ", 2)
|
||||||
tableName := p.stripQuotes(parts[0])
|
tableName := p.stripQuotes(parts[0])
|
||||||
alias := parts[1]
|
alias := parts[1]
|
||||||
return p.dialect.QuoteIdentifier(p.prefix+tableName) + " " + alias
|
// 递归处理表名部分(可能包含点号)
|
||||||
|
return p.ProcessTableName(tableName) + " " + alias
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含点号(database.table 格式)
|
||||||
|
if strings.Contains(name, ".") {
|
||||||
|
parts := p.splitTableColumn(name)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
dbName := p.stripQuotes(parts[0])
|
||||||
|
tableName := p.stripQuotes(parts[1])
|
||||||
|
// 系统数据库不添加前缀
|
||||||
|
if systemDatabases[dbName] {
|
||||||
|
return p.dialect.QuoteIdentifier(dbName) + "." + p.dialect.QuoteIdentifier(tableName)
|
||||||
|
}
|
||||||
|
// 非系统数据库,只给表名添加前缀
|
||||||
|
return p.dialect.QuoteIdentifier(dbName) + "." + p.dialect.QuoteIdentifier(p.prefix+tableName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加前缀和引号
|
// 添加前缀和引号
|
||||||
|
|||||||
26
db/where.go
26
db/where.go
@ -70,8 +70,8 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
|
|||||||
}
|
}
|
||||||
sort.Strings(testQu)
|
sort.Strings(testQu)
|
||||||
|
|
||||||
// 追踪普通条件数量,用于自动添加 AND
|
// 追踪条件数量,用于自动添加 AND
|
||||||
normalCondCount := 0
|
condCount := 0
|
||||||
|
|
||||||
for _, k := range testQu {
|
for _, k := range testQu {
|
||||||
v := data[k]
|
v := data[k]
|
||||||
@ -79,8 +79,16 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
|
|||||||
// 检查是否是 AND/OR 条件关键字
|
// 检查是否是 AND/OR 条件关键字
|
||||||
if isConditionKey(k) {
|
if isConditionKey(k) {
|
||||||
tw, ts := that.cond(strings.ToUpper(k), v.(Map))
|
tw, ts := that.cond(strings.ToUpper(k), v.(Map))
|
||||||
where += tw
|
if tw != "" && strings.TrimSpace(tw) != "" {
|
||||||
|
// 与前面的条件用 AND 连接
|
||||||
|
if condCount > 0 {
|
||||||
|
where += " AND "
|
||||||
|
}
|
||||||
|
// 用括号包裹 OR/AND 组条件
|
||||||
|
where += "(" + strings.TrimSpace(tw) + ")"
|
||||||
|
condCount++
|
||||||
res = append(res, ts...)
|
res = append(res, ts...)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,11 +103,11 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
|
|||||||
// 检查是否是 NOT IN(带 [!] 后缀)- NOT IN 空数组永真,跳过即可
|
// 检查是否是 NOT IN(带 [!] 后缀)- NOT IN 空数组永真,跳过即可
|
||||||
if !strings.HasSuffix(k, "[!]") {
|
if !strings.HasSuffix(k, "[!]") {
|
||||||
// IN 空数组 -> 生成永假条件
|
// IN 空数组 -> 生成永假条件
|
||||||
if normalCondCount > 0 {
|
if condCount > 0 {
|
||||||
where += " AND "
|
where += " AND "
|
||||||
}
|
}
|
||||||
where += "1=0 "
|
where += "1=0 "
|
||||||
normalCondCount++
|
condCount++
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -107,11 +115,11 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
|
|||||||
// 检查是否是 NOT IN(带 [!] 后缀)- NOT IN 空数组永真,跳过即可
|
// 检查是否是 NOT IN(带 [!] 后缀)- NOT IN 空数组永真,跳过即可
|
||||||
if !strings.HasSuffix(k, "[!]") {
|
if !strings.HasSuffix(k, "[!]") {
|
||||||
// IN 空数组 -> 生成永假条件
|
// IN 空数组 -> 生成永假条件
|
||||||
if normalCondCount > 0 {
|
if condCount > 0 {
|
||||||
where += " AND "
|
where += " AND "
|
||||||
}
|
}
|
||||||
where += "1=0 "
|
where += "1=0 "
|
||||||
normalCondCount++
|
condCount++
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -119,11 +127,11 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
|
|||||||
tv, vv := that.varCond(k, v)
|
tv, vv := that.varCond(k, v)
|
||||||
if tv != "" {
|
if tv != "" {
|
||||||
// 自动添加 AND 连接符
|
// 自动添加 AND 连接符
|
||||||
if normalCondCount > 0 {
|
if condCount > 0 {
|
||||||
where += " AND "
|
where += " AND "
|
||||||
}
|
}
|
||||||
where += tv
|
where += tv
|
||||||
normalCondCount++
|
condCount++
|
||||||
res = append(res, vv...)
|
res = append(res, vv...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
468
docs/CodeGen_使用说明.md
Normal file
468
docs/CodeGen_使用说明.md
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
# HoTime 代码生成器使用说明
|
||||||
|
|
||||||
|
`code` 包提供了 HoTime 框架的自动代码生成功能,能够根据数据库表结构自动生成 CRUD 接口代码和配置文件。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [功能概述](#功能概述)
|
||||||
|
- [配置说明](#配置说明)
|
||||||
|
- [使用方法](#使用方法)
|
||||||
|
- [生成规则](#生成规则)
|
||||||
|
- [自定义规则](#自定义规则)
|
||||||
|
- [生成的代码结构](#生成的代码结构)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
代码生成器可以:
|
||||||
|
|
||||||
|
1. **自动读取数据库表结构** - 支持 MySQL 和 SQLite
|
||||||
|
2. **生成 CRUD 接口** - 增删改查、搜索、分页
|
||||||
|
3. **生成配置文件** - 表字段配置、菜单配置、权限配置
|
||||||
|
4. **智能字段识别** - 根据字段名自动识别类型和权限
|
||||||
|
5. **支持表关联** - 自动识别外键关系
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
在 `config.json` 中配置代码生成:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codeConfig": [
|
||||||
|
{
|
||||||
|
"table": "admin",
|
||||||
|
"config": "config/admin.json",
|
||||||
|
"configDB": "config/adminDB.json",
|
||||||
|
"rule": "config/rule.json",
|
||||||
|
"name": "",
|
||||||
|
"mode": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置项说明
|
||||||
|
|
||||||
|
| 配置项 | 必须 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `table` | ✅ | 用户表名,用于权限控制的基准表 |
|
||||||
|
| `config` | ✅ | 接口描述配置文件路径 |
|
||||||
|
| `configDB` | ❌ | 数据库结构配置输出路径,有则每次自动生成 |
|
||||||
|
| `rule` | ❌ | 字段规则配置文件,无则使用默认规则 |
|
||||||
|
| `name` | ❌ | 生成代码的包名和目录名,空则使用内嵌模式 |
|
||||||
|
| `mode` | ❌ | 0=内嵌代码模式,1=生成代码模式 |
|
||||||
|
|
||||||
|
### 运行模式
|
||||||
|
|
||||||
|
- **mode=0(内嵌模式)**:不生成独立代码文件,使用框架内置的通用控制器
|
||||||
|
- **mode=1(生成模式)**:为每张表生成独立的 Go 控制器文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 基础配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": 2,
|
||||||
|
"codeConfig": [
|
||||||
|
{
|
||||||
|
"table": "admin",
|
||||||
|
"config": "config/admin.json",
|
||||||
|
"rule": "config/rule.json",
|
||||||
|
"mode": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启动应用
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime"
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := Init("config/config.json")
|
||||||
|
|
||||||
|
// 代码生成器在 Init 时自动执行
|
||||||
|
// 会读取数据库结构并生成配置
|
||||||
|
|
||||||
|
app.Run(Router{
|
||||||
|
// 路由配置
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 开发模式
|
||||||
|
|
||||||
|
在 `config.json` 中设置 `"mode": 2`(开发模式)时:
|
||||||
|
|
||||||
|
- 自动读取数据库表结构
|
||||||
|
- 自动生成/更新配置文件
|
||||||
|
- 自动生成代码(如果 codeConfig.mode=1)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 生成规则
|
||||||
|
|
||||||
|
### 默认字段规则
|
||||||
|
|
||||||
|
代码生成器内置了一套默认的字段识别规则:
|
||||||
|
|
||||||
|
| 字段名 | 列表显示 | 新增 | 编辑 | 详情 | 类型 |
|
||||||
|
|--------|----------|------|------|------|------|
|
||||||
|
| `id` | ✅ | ❌ | ❌ | ✅ | number |
|
||||||
|
| `name` | ✅ | ✅ | ✅ | ✅ | text |
|
||||||
|
| `status` | ✅ | ✅ | ✅ | ✅ | select |
|
||||||
|
| `create_time` | ❌ | ❌ | ❌ | ✅ | time |
|
||||||
|
| `modify_time` | ✅ | ❌ | ❌ | ✅ | time |
|
||||||
|
| `password` | ❌ | ✅ | ✅ | ❌ | password |
|
||||||
|
| `image/img/avatar` | ❌ | ✅ | ✅ | ✅ | image |
|
||||||
|
| `file` | ❌ | ✅ | ✅ | ✅ | file |
|
||||||
|
| `content/info` | ❌ | ✅ | ✅ | ✅ | textArea |
|
||||||
|
| `parent_id` | ✅ | ✅ | ✅ | ✅ | number |
|
||||||
|
| `parent_ids/index` | ❌ | ❌ | ❌ | ❌ | index |
|
||||||
|
| `delete` | ❌ | ❌ | ❌ | ❌ | - |
|
||||||
|
|
||||||
|
### 数据类型映射
|
||||||
|
|
||||||
|
数据库字段类型自动映射:
|
||||||
|
|
||||||
|
| 数据库类型 | 生成类型 |
|
||||||
|
|------------|----------|
|
||||||
|
| `int`, `integer`, `float`, `double`, `decimal` | number |
|
||||||
|
| `char`, `varchar`, `text`, `blob` | text |
|
||||||
|
| `date`, `datetime`, `time`, `timestamp`, `year` | time |
|
||||||
|
|
||||||
|
### 字段备注解析
|
||||||
|
|
||||||
|
支持从数据库字段备注中提取信息:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 字段备注格式: 标签名:选项1-名称1,选项2-名称2 {提示信息}
|
||||||
|
-- 例如:
|
||||||
|
status TINYINT COMMENT '状态:0-禁用,1-启用 {用户账号状态}'
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"label": "状态",
|
||||||
|
"type": "select",
|
||||||
|
"ps": "用户账号状态",
|
||||||
|
"options": [
|
||||||
|
{"name": "禁用", "value": "0"},
|
||||||
|
{"name": "启用", "value": "1"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 自定义规则
|
||||||
|
|
||||||
|
### rule.json 配置
|
||||||
|
|
||||||
|
创建 `config/rule.json` 自定义字段规则:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"list": true,
|
||||||
|
"add": false,
|
||||||
|
"edit": false,
|
||||||
|
"info": true,
|
||||||
|
"must": false,
|
||||||
|
"strict": true,
|
||||||
|
"type": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"list": true,
|
||||||
|
"add": true,
|
||||||
|
"edit": true,
|
||||||
|
"info": true,
|
||||||
|
"must": false,
|
||||||
|
"strict": false,
|
||||||
|
"type": "select"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user.special_field",
|
||||||
|
"list": true,
|
||||||
|
"add": true,
|
||||||
|
"edit": true,
|
||||||
|
"info": true,
|
||||||
|
"type": "text",
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 规则字段说明
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `name` | 字段名,支持 `表名.字段名` 格式精确匹配 |
|
||||||
|
| `list` | 是否在列表中显示 |
|
||||||
|
| `add` | 是否在新增表单中显示 |
|
||||||
|
| `edit` | 是否在编辑表单中显示 |
|
||||||
|
| `info` | 是否在详情中显示 |
|
||||||
|
| `must` | 是否必填 |
|
||||||
|
| `strict` | 是否严格匹配字段名(false 则模糊匹配) |
|
||||||
|
| `type` | 字段类型(覆盖自动识别) |
|
||||||
|
|
||||||
|
### 字段类型
|
||||||
|
|
||||||
|
| 类型 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `text` | 普通文本输入 |
|
||||||
|
| `textArea` | 多行文本 |
|
||||||
|
| `number` | 数字输入 |
|
||||||
|
| `select` | 下拉选择 |
|
||||||
|
| `time` | 时间选择器 |
|
||||||
|
| `unixTime` | Unix 时间戳 |
|
||||||
|
| `image` | 图片上传 |
|
||||||
|
| `file` | 文件上传 |
|
||||||
|
| `password` | 密码输入 |
|
||||||
|
| `money` | 金额(带格式化) |
|
||||||
|
| `index` | 索引字段(不显示) |
|
||||||
|
| `tree` | 树形选择 |
|
||||||
|
| `form` | 表单配置 |
|
||||||
|
| `auth` | 权限配置 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 生成的代码结构
|
||||||
|
|
||||||
|
### 内嵌模式 (mode=0)
|
||||||
|
|
||||||
|
不生成代码文件,使用框架内置控制器,只生成配置文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── admin.json # 接口配置
|
||||||
|
├── adminDB.json # 数据库结构配置(可选)
|
||||||
|
└── rule.json # 字段规则
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生成模式 (mode=1)
|
||||||
|
|
||||||
|
生成独立的控制器代码:
|
||||||
|
|
||||||
|
```
|
||||||
|
admin/ # 生成的包目录
|
||||||
|
├── init.go # 包初始化和路由注册
|
||||||
|
├── user.go # user 表控制器
|
||||||
|
├── role.go # role 表控制器
|
||||||
|
└── ... # 其他表控制器
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生成的控制器结构
|
||||||
|
|
||||||
|
```go
|
||||||
|
package admin
|
||||||
|
|
||||||
|
var userCtr = Ctr{
|
||||||
|
"info": func(that *Context) {
|
||||||
|
// 查询单条记录
|
||||||
|
},
|
||||||
|
"add": func(that *Context) {
|
||||||
|
// 新增记录
|
||||||
|
},
|
||||||
|
"update": func(that *Context) {
|
||||||
|
// 更新记录
|
||||||
|
},
|
||||||
|
"remove": func(that *Context) {
|
||||||
|
// 删除记录
|
||||||
|
},
|
||||||
|
"search": func(that *Context) {
|
||||||
|
// 搜索列表(分页)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置文件结构
|
||||||
|
|
||||||
|
### admin.json 示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"label": "管理平台",
|
||||||
|
"menus": [
|
||||||
|
{
|
||||||
|
"label": "系统管理",
|
||||||
|
"name": "sys",
|
||||||
|
"icon": "Setting",
|
||||||
|
"menus": [
|
||||||
|
{
|
||||||
|
"label": "用户管理",
|
||||||
|
"table": "user",
|
||||||
|
"auth": ["show", "add", "delete", "edit", "info", "download"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "角色管理",
|
||||||
|
"table": "role",
|
||||||
|
"auth": ["show", "add", "delete", "edit", "info"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tables": {
|
||||||
|
"user": {
|
||||||
|
"label": "用户",
|
||||||
|
"table": "user",
|
||||||
|
"auth": ["show", "add", "delete", "edit", "info", "download"],
|
||||||
|
"columns": [
|
||||||
|
{"name": "id", "type": "number", "label": "ID"},
|
||||||
|
{"name": "name", "type": "text", "label": "用户名"},
|
||||||
|
{"name": "status", "type": "select", "label": "状态",
|
||||||
|
"options": [{"name": "禁用", "value": "0"}, {"name": "启用", "value": "1"}]}
|
||||||
|
],
|
||||||
|
"search": [
|
||||||
|
{"type": "search", "name": "keyword", "label": "请输入关键词"},
|
||||||
|
{"type": "search", "name": "daterange", "label": "时间段"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 外键关联
|
||||||
|
|
||||||
|
### 自动识别
|
||||||
|
|
||||||
|
代码生成器会自动识别 `_id` 结尾的字段作为外键:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- user 表
|
||||||
|
CREATE TABLE user (
|
||||||
|
id INT PRIMARY KEY,
|
||||||
|
name VARCHAR(50),
|
||||||
|
role_id INT, -- 自动关联 role 表
|
||||||
|
org_id INT -- 自动关联 org 表
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的配置会包含 `link` 和 `value` 字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "role_id",
|
||||||
|
"type": "number",
|
||||||
|
"label": "角色",
|
||||||
|
"link": "role",
|
||||||
|
"value": "name"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 树形结构
|
||||||
|
|
||||||
|
`parent_id` 字段会被识别为树形结构的父级关联:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "parent_id",
|
||||||
|
"type": "number",
|
||||||
|
"label": "上级",
|
||||||
|
"link": "org",
|
||||||
|
"value": "name"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 权限控制
|
||||||
|
|
||||||
|
### 数据权限
|
||||||
|
|
||||||
|
配置 `flow` 实现数据权限控制:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"flow": {
|
||||||
|
"order": {
|
||||||
|
"table": "order",
|
||||||
|
"stop": false,
|
||||||
|
"sql": {
|
||||||
|
"user_id": "id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `stop`: 是否禁止修改该表
|
||||||
|
- `sql`: 数据过滤条件,`user_id = 当前用户.id`
|
||||||
|
|
||||||
|
### 操作权限
|
||||||
|
|
||||||
|
每张表可配置的权限:
|
||||||
|
|
||||||
|
| 权限 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `show` | 查看列表 |
|
||||||
|
| `add` | 新增 |
|
||||||
|
| `edit` | 编辑 |
|
||||||
|
| `delete` | 删除 |
|
||||||
|
| `info` | 查看详情 |
|
||||||
|
| `download` | 下载导出 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 开发流程
|
||||||
|
|
||||||
|
1. 设置 `config.json` 中 `mode: 2`(开发模式)
|
||||||
|
2. 设计数据库表结构,添加字段备注
|
||||||
|
3. 启动应用,自动生成配置
|
||||||
|
4. 检查生成的配置文件,按需调整
|
||||||
|
5. 生产环境改为 `mode: 0`
|
||||||
|
|
||||||
|
### 2. 字段命名规范
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 推荐的命名方式
|
||||||
|
id -- 主键
|
||||||
|
name -- 名称
|
||||||
|
status -- 状态(自动识别为 select)
|
||||||
|
create_time -- 创建时间
|
||||||
|
modify_time -- 修改时间
|
||||||
|
xxx_id -- 外键关联
|
||||||
|
parent_id -- 树形结构父级
|
||||||
|
avatar -- 头像(自动识别为 image)
|
||||||
|
content -- 内容(自动识别为 textArea)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 自定义扩展
|
||||||
|
|
||||||
|
如果默认规则不满足需求,可以:
|
||||||
|
|
||||||
|
1. 修改 `rule.json` 添加自定义规则
|
||||||
|
2. 使用 `mode=1` 生成代码后手动修改
|
||||||
|
3. 在生成的配置文件中直接调整字段属性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [快速上手指南](QUICKSTART.md)
|
||||||
|
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md)
|
||||||
|
- [Common 工具类使用说明](Common_工具类使用说明.md)
|
||||||
484
docs/Common_工具类使用说明.md
Normal file
484
docs/Common_工具类使用说明.md
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
# HoTime Common 工具类使用说明
|
||||||
|
|
||||||
|
`common` 包提供了 HoTime 框架的核心数据类型和工具函数,包括 `Map`、`Slice`、`Obj` 类型及丰富的类型转换函数。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [核心数据类型](#核心数据类型)
|
||||||
|
- [Map 类型](#map-类型)
|
||||||
|
- [Slice 类型](#slice-类型)
|
||||||
|
- [Obj 类型](#obj-类型)
|
||||||
|
- [类型转换函数](#类型转换函数)
|
||||||
|
- [工具函数](#工具函数)
|
||||||
|
- [错误处理](#错误处理)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心数据类型
|
||||||
|
|
||||||
|
### Map 类型
|
||||||
|
|
||||||
|
`Map` 是 `map[string]interface{}` 的别名,提供了丰富的链式调用方法。
|
||||||
|
|
||||||
|
```go
|
||||||
|
import . "code.hoteas.com/golang/hotime/common"
|
||||||
|
|
||||||
|
// 创建 Map
|
||||||
|
data := Map{
|
||||||
|
"name": "张三",
|
||||||
|
"age": 25,
|
||||||
|
"score": 98.5,
|
||||||
|
"active": true,
|
||||||
|
"tags": Slice{"Go", "Web"},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取值方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 获取字符串
|
||||||
|
name := data.GetString("name") // "张三"
|
||||||
|
|
||||||
|
// 获取整数
|
||||||
|
age := data.GetInt("age") // 25
|
||||||
|
age64 := data.GetInt64("age") // int64(25)
|
||||||
|
|
||||||
|
// 获取浮点数
|
||||||
|
score := data.GetFloat64("score") // 98.5
|
||||||
|
|
||||||
|
// 获取布尔值
|
||||||
|
active := data.GetBool("active") // true
|
||||||
|
|
||||||
|
// 获取嵌套 Map
|
||||||
|
info := data.GetMap("info") // 返回 Map 类型
|
||||||
|
|
||||||
|
// 获取 Slice
|
||||||
|
tags := data.GetSlice("tags") // 返回 Slice 类型
|
||||||
|
|
||||||
|
// 获取时间
|
||||||
|
createTime := data.GetTime("create_time") // 返回 *time.Time
|
||||||
|
|
||||||
|
// 获取原始值
|
||||||
|
raw := data.Get("name") // interface{}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 向上取整方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 向上取整获取整数
|
||||||
|
ceilInt := data.GetCeilInt("score") // 99
|
||||||
|
ceilInt64 := data.GetCeilInt64("score") // int64(99)
|
||||||
|
ceilFloat := data.GetCeilFloat64("score") // 99.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 操作方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 添加/修改值
|
||||||
|
data.Put("email", "test@example.com")
|
||||||
|
|
||||||
|
// 删除值
|
||||||
|
data.Delete("email")
|
||||||
|
|
||||||
|
// 转换为 JSON 字符串
|
||||||
|
jsonStr := data.ToJsonString()
|
||||||
|
|
||||||
|
// 从 JSON 字符串解析
|
||||||
|
data.JsonToMap(`{"key": "value"}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 有序遍历
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 按 key 字母顺序遍历
|
||||||
|
data.RangeSort(func(k string, v interface{}) bool {
|
||||||
|
fmt.Printf("%s: %v\n", k, v)
|
||||||
|
return false // 返回 true 则终止遍历
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 转换为结构体
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
Name string
|
||||||
|
Age int64
|
||||||
|
Score float64
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
data.ToStruct(&user) // 传入指针,字段名首字母大写匹配
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Slice 类型
|
||||||
|
|
||||||
|
`Slice` 是 `[]interface{}` 的别名,提供类似 Map 的链式调用方法。
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建 Slice
|
||||||
|
list := Slice{
|
||||||
|
Map{"id": 1, "name": "Alice"},
|
||||||
|
Map{"id": 2, "name": "Bob"},
|
||||||
|
"text",
|
||||||
|
123,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取值方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 按索引获取值(类型转换)
|
||||||
|
str := list.GetString(2) // "text"
|
||||||
|
num := list.GetInt(3) // 123
|
||||||
|
num64 := list.GetInt64(3) // int64(123)
|
||||||
|
f := list.GetFloat64(3) // 123.0
|
||||||
|
b := list.GetBool(3) // true (非0为true)
|
||||||
|
|
||||||
|
// 获取嵌套类型
|
||||||
|
item := list.GetMap(0) // Map{"id": 1, "name": "Alice"}
|
||||||
|
subList := list.GetSlice(0) // 尝试转换为 Slice
|
||||||
|
|
||||||
|
// 获取原始值
|
||||||
|
raw := list.Get(0) // interface{}
|
||||||
|
|
||||||
|
// 获取时间
|
||||||
|
t := list.GetTime(0) // *time.Time
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 向上取整方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
ceilInt := list.GetCeilInt(3)
|
||||||
|
ceilInt64 := list.GetCeilInt64(3)
|
||||||
|
ceilFloat := list.GetCeilFloat64(3)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 操作方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 修改指定位置的值
|
||||||
|
list.Put(0, "new value")
|
||||||
|
|
||||||
|
// 转换为 JSON 字符串
|
||||||
|
jsonStr := list.ToJsonString()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Obj 类型
|
||||||
|
|
||||||
|
`Obj` 是一个通用的对象包装器,用于链式类型转换,常用于 `Context` 方法的返回值。
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Obj struct {
|
||||||
|
Data interface{} // 原始数据
|
||||||
|
Error // 错误信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
obj := &Obj{Data: "123"}
|
||||||
|
|
||||||
|
// 链式类型转换
|
||||||
|
i := obj.ToInt() // 123
|
||||||
|
i64 := obj.ToInt64() // int64(123)
|
||||||
|
f := obj.ToFloat64() // 123.0
|
||||||
|
s := obj.ToStr() // "123"
|
||||||
|
b := obj.ToBool() // true
|
||||||
|
|
||||||
|
// 复杂类型转换
|
||||||
|
m := obj.ToMap() // 尝试转换为 Map
|
||||||
|
sl := obj.ToSlice() // 尝试转换为 Slice
|
||||||
|
arr := obj.ToMapArray() // 转换为 []Map
|
||||||
|
|
||||||
|
// 获取原始值
|
||||||
|
raw := obj.ToObj() // interface{}
|
||||||
|
|
||||||
|
// 获取时间
|
||||||
|
t := obj.ToTime() // *time.Time
|
||||||
|
|
||||||
|
// 向上取整
|
||||||
|
ceil := obj.ToCeilInt()
|
||||||
|
ceil64 := obj.ToCeilInt64()
|
||||||
|
ceilF := obj.ToCeilFloat64()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 在 Context 中的应用
|
||||||
|
|
||||||
|
```go
|
||||||
|
func handler(that *Context) {
|
||||||
|
// ReqData 返回 *Obj,支持链式调用
|
||||||
|
userId := that.ReqData("user_id").ToInt()
|
||||||
|
name := that.ReqData("name").ToStr()
|
||||||
|
|
||||||
|
// Session 也返回 *Obj
|
||||||
|
adminId := that.Session("admin_id").ToInt64()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 类型转换函数
|
||||||
|
|
||||||
|
`common` 包提供了一系列全局类型转换函数。
|
||||||
|
|
||||||
|
### 基础转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 转字符串
|
||||||
|
str := ObjToStr(123) // "123"
|
||||||
|
str := ObjToStr(3.14) // "3.14"
|
||||||
|
str := ObjToStr(Map{"a": 1}) // JSON 格式字符串
|
||||||
|
|
||||||
|
// 转整数
|
||||||
|
i := ObjToInt("123") // 123
|
||||||
|
i64 := ObjToInt64("123") // int64(123)
|
||||||
|
|
||||||
|
// 转浮点数
|
||||||
|
f := ObjToFloat64("3.14") // 3.14
|
||||||
|
|
||||||
|
// 转布尔
|
||||||
|
b := ObjToBool(1) // true
|
||||||
|
b := ObjToBool(0) // false
|
||||||
|
|
||||||
|
// 转 Map
|
||||||
|
m := ObjToMap(`{"a": 1}`) // Map{"a": 1}
|
||||||
|
m := ObjToMap(someStruct) // 结构体转 Map
|
||||||
|
|
||||||
|
// 转 Slice
|
||||||
|
s := ObjToSlice(`[1, 2, 3]`) // Slice{1, 2, 3}
|
||||||
|
|
||||||
|
// 转 []Map
|
||||||
|
arr := ObjToMapArray(slice) // []Map
|
||||||
|
```
|
||||||
|
|
||||||
|
### 向上取整转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 向上取整后转整数
|
||||||
|
ceil := ObjToCeilInt(3.2) // 4
|
||||||
|
ceil64 := ObjToCeilInt64(3.2) // int64(4)
|
||||||
|
ceilF := ObjToCeilFloat64(3.2) // 4.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 时间转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 自动识别多种格式
|
||||||
|
t := ObjToTime("2024-01-15 10:30:00") // *time.Time
|
||||||
|
t := ObjToTime("2024-01-15") // *time.Time
|
||||||
|
t := ObjToTime(1705298400) // Unix 秒
|
||||||
|
t := ObjToTime(1705298400000) // Unix 毫秒
|
||||||
|
t := ObjToTime(1705298400000000) // Unix 微秒
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字符串转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 字符串转 Map
|
||||||
|
m := StrToMap(`{"key": "value"}`)
|
||||||
|
|
||||||
|
// 字符串转 Slice
|
||||||
|
s := StrToSlice(`[1, 2, 3]`)
|
||||||
|
|
||||||
|
// 字符串转 int
|
||||||
|
i, err := StrToInt("123")
|
||||||
|
|
||||||
|
// 字符串数组格式转换
|
||||||
|
jsonArr := StrArrayToJsonStr("a1,a2,a3") // "[a1,a2,a3]"
|
||||||
|
strArr := JsonStrToStrArray("[a1,a2,a3]") // ",a1,a2,a3,"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
|
||||||
|
所有转换函数支持可选的错误参数:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var e Error
|
||||||
|
i := ObjToInt("abc", &e)
|
||||||
|
if e.GetError() != nil {
|
||||||
|
// 处理转换错误
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 工具函数
|
||||||
|
|
||||||
|
### 字符串处理
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 字符串截取(支持中文)
|
||||||
|
str := Substr("Hello世界", 0, 7) // "Hello世"
|
||||||
|
str := Substr("Hello", -2, 2) // "lo" (负数从末尾计算)
|
||||||
|
|
||||||
|
// 首字母大写
|
||||||
|
upper := StrFirstToUpper("hello") // "Hello"
|
||||||
|
|
||||||
|
// 查找最后出现位置
|
||||||
|
idx := IndexLastStr("a.b.c", ".") // 3
|
||||||
|
|
||||||
|
// 字符串相似度(Levenshtein 距离)
|
||||||
|
dist := StrLd("hello", "hallo", true) // 1 (忽略大小写)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 时间处理
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 时间转字符串
|
||||||
|
str := Time2Str(time.Now()) // "2024-01-15 10:30:00"
|
||||||
|
str := Time2Str(time.Now(), 1) // "2024-01"
|
||||||
|
str := Time2Str(time.Now(), 2) // "2024-01-15"
|
||||||
|
str := Time2Str(time.Now(), 3) // "2024-01-15 10"
|
||||||
|
str := Time2Str(time.Now(), 4) // "2024-01-15 10:30"
|
||||||
|
str := Time2Str(time.Now(), 5) // "2024-01-15 10:30:00"
|
||||||
|
|
||||||
|
// 特殊格式
|
||||||
|
str := Time2Str(time.Now(), 12) // "01-15"
|
||||||
|
str := Time2Str(time.Now(), 14) // "01-15 10:30"
|
||||||
|
str := Time2Str(time.Now(), 34) // "10:30"
|
||||||
|
str := Time2Str(time.Now(), 35) // "10:30:00"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 加密与随机
|
||||||
|
|
||||||
|
```go
|
||||||
|
// MD5 加密
|
||||||
|
hash := Md5("password") // 32位小写MD5
|
||||||
|
|
||||||
|
// 随机数
|
||||||
|
r := Rand(3) // 3位随机数 (0-999)
|
||||||
|
r := RandX(10, 100) // 10-100之间的随机数
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数学计算
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 四舍五入保留小数
|
||||||
|
f := Round(3.14159, 2) // 3.14
|
||||||
|
f := Round(3.145, 2) // 3.15
|
||||||
|
```
|
||||||
|
|
||||||
|
### 深拷贝
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 深拷贝 Map/Slice(递归复制)
|
||||||
|
original := Map{"a": Map{"b": 1}}
|
||||||
|
copied := DeepCopyMap(original).(Map)
|
||||||
|
|
||||||
|
// 修改副本不影响原始数据
|
||||||
|
copied.GetMap("a")["b"] = 2
|
||||||
|
// original["a"]["b"] 仍然是 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### Error 类型
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Error struct {
|
||||||
|
Logger *logrus.Logger // 可选的日志记录器
|
||||||
|
error // 内嵌错误
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
var e Error
|
||||||
|
|
||||||
|
// 设置错误
|
||||||
|
e.SetError(errors.New("something wrong"))
|
||||||
|
|
||||||
|
// 获取错误
|
||||||
|
if err := e.GetError(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配合日志自动记录
|
||||||
|
e.Logger = logrusLogger
|
||||||
|
e.SetError(errors.New("will be logged"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在类型转换中使用
|
||||||
|
|
||||||
|
```go
|
||||||
|
var e Error
|
||||||
|
data := Map{"count": "abc"}
|
||||||
|
|
||||||
|
count := data.GetInt("count", &e)
|
||||||
|
if e.GetError() != nil {
|
||||||
|
// 转换失败,count = 0
|
||||||
|
fmt.Println("转换失败:", e.GetError())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 链式调用处理请求数据
|
||||||
|
|
||||||
|
```go
|
||||||
|
func handler(that *Context) {
|
||||||
|
// 推荐:使用 Obj 链式调用
|
||||||
|
userId := that.ReqData("user_id").ToInt()
|
||||||
|
page := that.ReqData("page").ToInt()
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 Map 数据
|
||||||
|
user := that.Db.Get("user", "*", Map{"id": userId})
|
||||||
|
if user != nil {
|
||||||
|
name := user.GetString("name")
|
||||||
|
age := user.GetInt("age")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安全的类型转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 带错误检查的转换
|
||||||
|
var e Error
|
||||||
|
data := someMap.GetInt("key", &e)
|
||||||
|
if e.GetError() != nil {
|
||||||
|
// 使用默认值
|
||||||
|
data = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单场景直接转换(失败返回零值)
|
||||||
|
data := someMap.GetInt("key") // 失败返回 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 处理数据库查询结果
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 查询返回 Map
|
||||||
|
user := that.Db.Get("user", "*", Map{"id": 1})
|
||||||
|
if user != nil {
|
||||||
|
name := user.GetString("name")
|
||||||
|
createTime := user.GetTime("create_time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询返回 []Map
|
||||||
|
users := that.Db.Select("user", "*", Map{"status": 1})
|
||||||
|
for _, u := range users {
|
||||||
|
fmt.Printf("ID: %d, Name: %s\n", u.GetInt("id"), u.GetString("name"))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [快速上手指南](QUICKSTART.md)
|
||||||
|
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md)
|
||||||
|
- [代码生成器使用说明](CodeGen_使用说明.md)
|
||||||
@ -98,17 +98,17 @@ id := database.Insert("table", dataMap)
|
|||||||
// 返回新插入记录的ID
|
// 返回新插入记录的ID
|
||||||
```
|
```
|
||||||
|
|
||||||
### 批量插入 (BatchInsert) - 新增
|
### 批量插入 (Inserts) - 新增
|
||||||
```go
|
```go
|
||||||
// 使用 []Map 格式,更直观简洁
|
// 使用 []Map 格式,更直观简洁
|
||||||
affected := database.BatchInsert("table", []Map{
|
affected := database.Inserts("table", []Map{
|
||||||
{"col1": "val1", "col2": "val2", "col3": "val3"},
|
{"col1": "val1", "col2": "val2", "col3": "val3"},
|
||||||
{"col1": "val4", "col2": "val5", "col3": "val6"},
|
{"col1": "val4", "col2": "val5", "col3": "val6"},
|
||||||
})
|
})
|
||||||
// 返回受影响的行数
|
// 返回受影响的行数
|
||||||
|
|
||||||
// 支持 [#] 标记直接 SQL
|
// 支持 [#] 标记直接 SQL
|
||||||
affected := database.BatchInsert("log", []Map{
|
affected := database.Inserts("log", []Map{
|
||||||
{"user_id": 1, "created_time[#]": "NOW()"},
|
{"user_id": 1, "created_time[#]": "NOW()"},
|
||||||
{"user_id": 2, "created_time[#]": "NOW()"},
|
{"user_id": 2, "created_time[#]": "NOW()"},
|
||||||
})
|
})
|
||||||
@ -465,7 +465,7 @@ stats := database.Select("order",
|
|||||||
### 批量操作
|
### 批量操作
|
||||||
```go
|
```go
|
||||||
// 批量插入(使用 []Map 格式)
|
// 批量插入(使用 []Map 格式)
|
||||||
affected := database.BatchInsert("user", []Map{
|
affected := database.Inserts("user", []Map{
|
||||||
{"name": "用户1", "email": "user1@example.com", "status": 1},
|
{"name": "用户1", "email": "user1@example.com", "status": 1},
|
||||||
{"name": "用户2", "email": "user2@example.com", "status": 1},
|
{"name": "用户2", "email": "user2@example.com", "status": 1},
|
||||||
{"name": "用户3", "email": "user3@example.com", "status": 1},
|
{"name": "用户3", "email": "user3@example.com", "status": 1},
|
||||||
@ -511,3 +511,6 @@ result := database.Table("order").
|
|||||||
|
|
||||||
*快速参考版本: 2.0*
|
*快速参考版本: 2.0*
|
||||||
*更新日期: 2026年1月*
|
*更新日期: 2026年1月*
|
||||||
|
|
||||||
|
**详细说明:**
|
||||||
|
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md) - 完整教程
|
||||||
@ -12,7 +12,7 @@ HoTimeDB是一个基于Golang实现的轻量级ORM框架,参考PHP Medoo设计
|
|||||||
- [查询(Select)](#查询select)
|
- [查询(Select)](#查询select)
|
||||||
- [获取单条记录(Get)](#获取单条记录get)
|
- [获取单条记录(Get)](#获取单条记录get)
|
||||||
- [插入(Insert)](#插入insert)
|
- [插入(Insert)](#插入insert)
|
||||||
- [批量插入(BatchInsert)](#批量插入batchinsert)
|
- [批量插入(Inserts)](#批量插入Inserts)
|
||||||
- [更新(Update)](#更新update)
|
- [更新(Update)](#更新update)
|
||||||
- [Upsert操作](#upsert操作)
|
- [Upsert操作](#upsert操作)
|
||||||
- [删除(Delete)](#删除delete)
|
- [删除(Delete)](#删除delete)
|
||||||
@ -223,11 +223,11 @@ id := database.Insert("user", common.Map{
|
|||||||
fmt.Println("插入的用户ID:", id)
|
fmt.Println("插入的用户ID:", id)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 批量插入(BatchInsert)
|
### 批量插入(Inserts)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// 批量插入多条记录(使用 []Map 格式,更直观)
|
// 批量插入多条记录(使用 []Map 格式,更直观)
|
||||||
affected := database.BatchInsert("user", []common.Map{
|
affected := database.Inserts("user", []common.Map{
|
||||||
{"name": "张三", "email": "zhang@example.com", "age": 25},
|
{"name": "张三", "email": "zhang@example.com", "age": 25},
|
||||||
{"name": "李四", "email": "li@example.com", "age": 30},
|
{"name": "李四", "email": "li@example.com", "age": 30},
|
||||||
{"name": "王五", "email": "wang@example.com", "age": 28},
|
{"name": "王五", "email": "wang@example.com", "age": 28},
|
||||||
@ -237,7 +237,7 @@ affected := database.BatchInsert("user", []common.Map{
|
|||||||
fmt.Printf("批量插入 %d 条记录\n", affected)
|
fmt.Printf("批量插入 %d 条记录\n", affected)
|
||||||
|
|
||||||
// 支持 [#] 标记直接插入 SQL 表达式
|
// 支持 [#] 标记直接插入 SQL 表达式
|
||||||
affected := database.BatchInsert("log", []common.Map{
|
affected := database.Inserts("log", []common.Map{
|
||||||
{"user_id": 1, "action": "login", "created_time[#]": "NOW()"},
|
{"user_id": 1, "action": "login", "created_time[#]": "NOW()"},
|
||||||
{"user_id": 2, "action": "logout", "created_time[#]": "NOW()"},
|
{"user_id": 2, "action": "logout", "created_time[#]": "NOW()"},
|
||||||
})
|
})
|
||||||
@ -808,7 +808,7 @@ A1: 不再需要!现在多条件会自动用 AND 连接。当然,使用 `AND
|
|||||||
A2: 在 `Action` 函数中返回 `false` 即可触发回滚,所有操作都会被撤销。
|
A2: 在 `Action` 函数中返回 `false` 即可触发回滚,所有操作都会被撤销。
|
||||||
|
|
||||||
### Q3: 缓存何时会被清除?
|
### Q3: 缓存何时会被清除?
|
||||||
A3: 执行 `Insert`、`Update`、`Delete`、`Upsert`、`BatchInsert` 操作时会自动清除对应表的缓存。
|
A3: 执行 `Insert`、`Update`、`Delete`、`Upsert`、`Inserts` 操作时会自动清除对应表的缓存。
|
||||||
|
|
||||||
### Q4: 如何执行复杂的原生SQL?
|
### Q4: 如何执行复杂的原生SQL?
|
||||||
A4: 使用 `Query` 方法执行查询,使用 `Exec` 方法执行更新操作。
|
A4: 使用 `Query` 方法执行查询,使用 `Exec` 方法执行更新操作。
|
||||||
@ -828,3 +828,6 @@ A7: 框架会自动处理差异(占位符、引号等),代码无需修改
|
|||||||
*最后更新: 2026年1月*
|
*最后更新: 2026年1月*
|
||||||
|
|
||||||
> 本文档基于HoTimeDB源码分析生成,如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念,但根据Golang语言特性进行了适配和优化。
|
> 本文档基于HoTimeDB源码分析生成,如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念,但根据Golang语言特性进行了适配和优化。
|
||||||
|
|
||||||
|
**更多参考:**
|
||||||
|
- [HoTimeDB API 参考](HoTimeDB_API参考.md) - API 速查手册
|
||||||
529
docs/QUICKSTART.md
Normal file
529
docs/QUICKSTART.md
Normal file
@ -0,0 +1,529 @@
|
|||||||
|
# HoTime 快速上手指南
|
||||||
|
|
||||||
|
5 分钟入门 HoTime 框架。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get code.hoteas.com/golang/hotime
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最小示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime"
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
appIns := Init("config/config.json")
|
||||||
|
|
||||||
|
appIns.Run(Router{
|
||||||
|
"app": {
|
||||||
|
"test": {
|
||||||
|
"hello": func(that *Context) {
|
||||||
|
that.Display(0, Map{"message": "Hello World"})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
访问: `http://localhost:8081/app/test/hello`
|
||||||
|
|
||||||
|
## 配置文件
|
||||||
|
|
||||||
|
创建 `config/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"port": "8081",
|
||||||
|
"mode": 2,
|
||||||
|
"sessionName": "HOTIME",
|
||||||
|
"tpt": "tpt",
|
||||||
|
"defFile": ["index.html", "index.htm"],
|
||||||
|
"db": {
|
||||||
|
"mysql": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "3306",
|
||||||
|
"name": "your_database",
|
||||||
|
"user": "root",
|
||||||
|
"password": "your_password",
|
||||||
|
"prefix": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cache": {
|
||||||
|
"memory": {
|
||||||
|
"db": true,
|
||||||
|
"session": true,
|
||||||
|
"timeout": 7200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置项说明
|
||||||
|
|
||||||
|
| 配置项 | 默认值 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| `port` | 80 | HTTP 服务端口,0 为不启用 |
|
||||||
|
| `tlsPort` | - | HTTPS 端口,需配合 tlsCert/tlsKey |
|
||||||
|
| `tlsCert` | - | HTTPS 证书路径 |
|
||||||
|
| `tlsKey` | - | HTTPS 密钥路径 |
|
||||||
|
| `mode` | 0 | 0=生产, 1=测试, 2=开发(输出SQL) |
|
||||||
|
| `tpt` | tpt | 静态文件目录 |
|
||||||
|
| `sessionName` | HOTIME | Session Cookie 名称 |
|
||||||
|
| `modeRouterStrict` | false | 路由大小写敏感,false=忽略大小写 |
|
||||||
|
| `crossDomain` | - | 跨域设置,空=不开启,auto=智能开启,或指定域名 |
|
||||||
|
| `logFile` | - | 日志文件路径,如 `logs/20060102.txt` |
|
||||||
|
| `logLevel` | 0 | 日志等级,0=关闭,1=打印 |
|
||||||
|
| `webConnectLogShow` | true | 是否显示访问日志 |
|
||||||
|
| `defFile` | ["index.html"] | 目录默认访问文件 |
|
||||||
|
|
||||||
|
### 数据库配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"db": {
|
||||||
|
"mysql": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": "3306",
|
||||||
|
"name": "database_name",
|
||||||
|
"user": "root",
|
||||||
|
"password": "password",
|
||||||
|
"prefix": "app_",
|
||||||
|
"slave": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": "3306",
|
||||||
|
"name": "database_name",
|
||||||
|
"user": "root",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sqlite": {
|
||||||
|
"path": "config/data.db",
|
||||||
|
"prefix": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> MySQL 配置 `slave` 项即启用主从读写分离
|
||||||
|
|
||||||
|
### 缓存配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cache": {
|
||||||
|
"memory": {
|
||||||
|
"db": true,
|
||||||
|
"session": true,
|
||||||
|
"timeout": 7200
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 6379,
|
||||||
|
"password": "",
|
||||||
|
"db": true,
|
||||||
|
"session": true,
|
||||||
|
"timeout": 1296000
|
||||||
|
},
|
||||||
|
"db": {
|
||||||
|
"db": true,
|
||||||
|
"session": true,
|
||||||
|
"timeout": 2592000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
缓存优先级: **Memory > Redis > DB**,自动穿透与回填
|
||||||
|
|
||||||
|
### 错误码配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"1": "内部系统异常",
|
||||||
|
"2": "访问权限异常",
|
||||||
|
"3": "请求参数异常",
|
||||||
|
"4": "数据处理异常",
|
||||||
|
"5": "数据结果异常"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 自定义错误码建议从 10 开始
|
||||||
|
|
||||||
|
## 路由系统
|
||||||
|
|
||||||
|
HoTime 使用三层路由结构:`模块/控制器/方法`
|
||||||
|
|
||||||
|
```go
|
||||||
|
appIns.Run(Router{
|
||||||
|
"模块名": {
|
||||||
|
"控制器名": {
|
||||||
|
"方法名": func(that *Context) {
|
||||||
|
// 处理逻辑
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 路由路径
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 获取路由信息
|
||||||
|
module := that.RouterString[0] // 模块
|
||||||
|
controller := that.RouterString[1] // 控制器
|
||||||
|
action := that.RouterString[2] // 方法
|
||||||
|
|
||||||
|
// 完整请求路径
|
||||||
|
fullPath := that.HandlerStr // 如 /app/user/login
|
||||||
|
```
|
||||||
|
|
||||||
|
## 请求参数获取
|
||||||
|
|
||||||
|
### 新版推荐方法(支持链式调用)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 获取 URL 查询参数 (?id=1)
|
||||||
|
id := that.ReqParam("id").ToInt()
|
||||||
|
name := that.ReqParam("name").ToStr()
|
||||||
|
|
||||||
|
// 获取表单参数 (POST form-data / x-www-form-urlencoded)
|
||||||
|
username := that.ReqForm("username").ToStr()
|
||||||
|
age := that.ReqForm("age").ToInt()
|
||||||
|
|
||||||
|
// 获取 JSON Body 参数 (POST application/json)
|
||||||
|
data := that.ReqJson("data").ToMap()
|
||||||
|
items := that.ReqJson("items").ToSlice()
|
||||||
|
|
||||||
|
// 统一获取(自动判断来源,优先级: JSON > Form > URL)
|
||||||
|
userId := that.ReqData("user_id").ToInt()
|
||||||
|
status := that.ReqData("status").ToStr()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 类型转换方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
obj := that.ReqData("key")
|
||||||
|
|
||||||
|
obj.ToStr() // 转字符串
|
||||||
|
obj.ToInt() // 转 int
|
||||||
|
obj.ToInt64() // 转 int64
|
||||||
|
obj.ToFloat64() // 转 float64
|
||||||
|
obj.ToBool() // 转 bool
|
||||||
|
obj.ToMap() // 转 Map
|
||||||
|
obj.ToSlice() // 转 Slice
|
||||||
|
obj.Data // 获取原始值(interface{})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文件上传
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 单文件上传
|
||||||
|
file, header, err := that.ReqFile("avatar")
|
||||||
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
// header.Filename - 文件名
|
||||||
|
// header.Size - 文件大小
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多文件上传(批量)
|
||||||
|
files, err := that.ReqFiles("images")
|
||||||
|
if err == nil {
|
||||||
|
for _, fh := range files {
|
||||||
|
file, _ := fh.Open()
|
||||||
|
defer file.Close()
|
||||||
|
// 处理每个文件
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 传统方法(兼容)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// GET/POST 参数
|
||||||
|
name := that.Req.FormValue("name")
|
||||||
|
|
||||||
|
// URL 参数
|
||||||
|
id := that.Req.URL.Query().Get("id")
|
||||||
|
|
||||||
|
// 请求头
|
||||||
|
token := that.Req.Header.Get("Authorization")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应数据
|
||||||
|
|
||||||
|
### Display 方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 成功响应 (status=0)
|
||||||
|
that.Display(0, Map{"user": user, "token": token})
|
||||||
|
// 输出: {"status":0, "result":{"user":..., "token":...}}
|
||||||
|
|
||||||
|
// 错误响应 (status>0)
|
||||||
|
that.Display(1, "系统内部错误")
|
||||||
|
// 输出: {"status":1, "result":{"type":"内部系统异常", "msg":"系统内部错误"}, "error":{...}}
|
||||||
|
|
||||||
|
that.Display(2, "请先登录")
|
||||||
|
// 输出: {"status":2, "result":{"type":"访问权限异常", "msg":"请先登录"}, "error":{...}}
|
||||||
|
|
||||||
|
that.Display(3, "参数不能为空")
|
||||||
|
// 输出: {"status":3, "result":{"type":"请求参数异常", "msg":"参数不能为空"}, "error":{...}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码含义
|
||||||
|
|
||||||
|
| 错误码 | 类型 | 使用场景 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| 0 | 成功 | 请求成功 |
|
||||||
|
| 1 | 内部系统异常 | 环境配置、文件权限等基础运行环境错误 |
|
||||||
|
| 2 | 访问权限异常 | 未登录或登录异常 |
|
||||||
|
| 3 | 请求参数异常 | 参数不足、类型错误等 |
|
||||||
|
| 4 | 数据处理异常 | 数据库操作或第三方请求返回异常 |
|
||||||
|
| 5 | 数据结果异常 | 无法返回要求的格式 |
|
||||||
|
|
||||||
|
### 自定义响应
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 自定义 Header
|
||||||
|
that.Resp.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// 直接写入
|
||||||
|
that.Resp.Write([]byte("raw data"))
|
||||||
|
|
||||||
|
// 自定义响应函数
|
||||||
|
that.RespFunc = func() {
|
||||||
|
// 自定义响应逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 中间件
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 全局中间件(请求拦截)
|
||||||
|
appIns.SetConnectListener(func(that *Context) bool {
|
||||||
|
// 放行登录接口
|
||||||
|
if len(that.RouterString) >= 3 && that.RouterString[2] == "login" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
if that.Session("user_id").Data == nil {
|
||||||
|
that.Display(2, "请先登录")
|
||||||
|
return true // 返回 true 终止请求
|
||||||
|
}
|
||||||
|
return false // 返回 false 继续处理
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session 与缓存
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Session 操作
|
||||||
|
that.Session("user_id", 123) // 设置
|
||||||
|
userId := that.Session("user_id") // 获取 *Obj
|
||||||
|
that.Session("user_id", nil) // 删除
|
||||||
|
|
||||||
|
// 链式获取
|
||||||
|
id := that.Session("user_id").ToInt64()
|
||||||
|
name := that.Session("username").ToStr()
|
||||||
|
|
||||||
|
// 通用缓存
|
||||||
|
that.Cache("key", "value") // 设置
|
||||||
|
data := that.Cache("key") // 获取
|
||||||
|
that.Cache("key", nil) // 删除
|
||||||
|
```
|
||||||
|
|
||||||
|
三级缓存自动运作:**Memory → Redis → Database**
|
||||||
|
|
||||||
|
## 数据库操作(简要)
|
||||||
|
|
||||||
|
### 基础 CRUD
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 查询列表
|
||||||
|
users := that.Db.Select("user", "*", Map{"status": 1})
|
||||||
|
|
||||||
|
// 查询单条
|
||||||
|
user := that.Db.Get("user", "*", Map{"id": 1})
|
||||||
|
|
||||||
|
// 插入
|
||||||
|
id := that.Db.Insert("user", Map{"name": "test", "age": 18})
|
||||||
|
|
||||||
|
// 批量插入
|
||||||
|
affected := that.Db.Inserts("user", []Map{
|
||||||
|
{"name": "user1", "age": 20},
|
||||||
|
{"name": "user2", "age": 25},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
rows := that.Db.Update("user", Map{"name": "new"}, Map{"id": 1})
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
rows := that.Db.Delete("user", Map{"id": 1})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 链式查询
|
||||||
|
|
||||||
|
```go
|
||||||
|
users := that.Db.Table("user").
|
||||||
|
LeftJoin("order", "user.id=order.user_id").
|
||||||
|
Where("status", 1).
|
||||||
|
And("age[>]", 18).
|
||||||
|
Order("id DESC").
|
||||||
|
Page(1, 10).
|
||||||
|
Select("*")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 条件语法速查
|
||||||
|
|
||||||
|
| 语法 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `key` | 等于 | `"id": 1` |
|
||||||
|
| `key[>]` | 大于 | `"age[>]": 18` |
|
||||||
|
| `key[<]` | 小于 | `"age[<]": 60` |
|
||||||
|
| `key[>=]` | 大于等于 | `"age[>=]": 18` |
|
||||||
|
| `key[<=]` | 小于等于 | `"age[<=]": 60` |
|
||||||
|
| `key[!]` | 不等于 | `"status[!]": 0` |
|
||||||
|
| `key[~]` | LIKE | `"name[~]": "test"` |
|
||||||
|
| `key[<>]` | BETWEEN | `"age[<>]": Slice{18, 60}` |
|
||||||
|
| `key` | IN | `"id": Slice{1, 2, 3}` |
|
||||||
|
|
||||||
|
### 事务
|
||||||
|
|
||||||
|
```go
|
||||||
|
success := that.Db.Action(func(tx db.HoTimeDB) bool {
|
||||||
|
tx.Update("user", Map{"balance[#]": "balance - 100"}, Map{"id": 1})
|
||||||
|
tx.Insert("order", Map{"user_id": 1, "amount": 100})
|
||||||
|
return true // 返回 true 提交,false 回滚
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
> **更多数据库操作**:参见 [HoTimeDB 使用说明](HoTimeDB_使用说明.md)
|
||||||
|
|
||||||
|
## 日志记录
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建操作日志(自动插入 logs 表)
|
||||||
|
that.Log = Map{
|
||||||
|
"type": "login",
|
||||||
|
"action": "用户登录",
|
||||||
|
"data": Map{"phone": phone},
|
||||||
|
}
|
||||||
|
// 框架会自动添加 time, admin_id/user_id, ip 等字段
|
||||||
|
```
|
||||||
|
|
||||||
|
## 扩展功能
|
||||||
|
|
||||||
|
| 功能 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 微信支付/公众号/小程序 | `dri/wechat/` | 微信全套 SDK |
|
||||||
|
| 阿里云服务 | `dri/aliyun/` | 企业认证等 |
|
||||||
|
| 腾讯云服务 | `dri/tencent/` | 企业认证等 |
|
||||||
|
| 文件上传 | `dri/upload/` | 文件上传处理 |
|
||||||
|
| 文件下载 | `dri/download/` | 文件下载处理 |
|
||||||
|
| MongoDB | `dri/mongodb/` | MongoDB 驱动 |
|
||||||
|
| RSA 加解密 | `dri/rsa/` | RSA 加解密工具 |
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime"
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
appIns := Init("config/config.json")
|
||||||
|
|
||||||
|
// 登录检查中间件
|
||||||
|
appIns.SetConnectListener(func(that *Context) bool {
|
||||||
|
// 放行登录接口
|
||||||
|
if len(that.RouterString) >= 3 && that.RouterString[2] == "login" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if that.Session("user_id").Data == nil {
|
||||||
|
that.Display(2, "请先登录")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
appIns.Run(Router{
|
||||||
|
"api": {
|
||||||
|
"user": {
|
||||||
|
"login": func(that *Context) {
|
||||||
|
phone := that.ReqData("phone").ToStr()
|
||||||
|
password := that.ReqData("password").ToStr()
|
||||||
|
|
||||||
|
if phone == "" || password == "" {
|
||||||
|
that.Display(3, "手机号和密码不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := that.Db.Get("user", "*", Map{
|
||||||
|
"phone": phone,
|
||||||
|
"password": Md5(password),
|
||||||
|
})
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
that.Display(3, "账号或密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
that.Session("user_id", user.GetInt64("id"))
|
||||||
|
that.Display(0, Map{"user": user})
|
||||||
|
},
|
||||||
|
|
||||||
|
"info": func(that *Context) {
|
||||||
|
userId := that.Session("user_id").ToInt64()
|
||||||
|
user := that.Db.Get("user", "*", Map{"id": userId})
|
||||||
|
that.Display(0, Map{"user": user})
|
||||||
|
},
|
||||||
|
|
||||||
|
"list": func(that *Context) {
|
||||||
|
page := that.ReqData("page").ToInt()
|
||||||
|
if page == 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
users := that.Db.Table("user").
|
||||||
|
Where("status", 1).
|
||||||
|
Order("id DESC").
|
||||||
|
Page(page, 10).
|
||||||
|
Select("id,name,phone,created_at")
|
||||||
|
|
||||||
|
total := that.Db.Count("user", Map{"status": 1})
|
||||||
|
|
||||||
|
that.Display(0, Map{
|
||||||
|
"list": users,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
"logout": func(that *Context) {
|
||||||
|
that.Session("user_id", nil)
|
||||||
|
that.Display(0, "退出成功")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**下一步**:
|
||||||
|
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md) - 完整数据库教程
|
||||||
|
- [HoTimeDB API 参考](HoTimeDB_API参考.md) - API 速查手册
|
||||||
@ -44,7 +44,7 @@ func main() {
|
|||||||
results["6_pagination"] = testPagination(that)
|
results["6_pagination"] = testPagination(that)
|
||||||
|
|
||||||
// 7. 批量插入测试
|
// 7. 批量插入测试
|
||||||
results["7_batch_insert"] = testBatchInsert(that)
|
results["7_batch_insert"] = testInserts(that)
|
||||||
|
|
||||||
// 8. Upsert 测试
|
// 8. Upsert 测试
|
||||||
results["8_upsert"] = testUpsert(that)
|
results["8_upsert"] = testUpsert(that)
|
||||||
@ -86,7 +86,7 @@ func main() {
|
|||||||
"join": func(that *Context) { that.Display(0, testJoinQuery(that)) },
|
"join": func(that *Context) { that.Display(0, testJoinQuery(that)) },
|
||||||
"aggregate": func(that *Context) { that.Display(0, testAggregate(that)) },
|
"aggregate": func(that *Context) { that.Display(0, testAggregate(that)) },
|
||||||
"pagination": func(that *Context) { that.Display(0, testPagination(that)) },
|
"pagination": func(that *Context) { that.Display(0, testPagination(that)) },
|
||||||
"batch": func(that *Context) { that.Display(0, testBatchInsert(that)) },
|
"batch": func(that *Context) { that.Display(0, testInserts(that)) },
|
||||||
"upsert": func(that *Context) { that.Display(0, testUpsert(that)) },
|
"upsert": func(that *Context) { that.Display(0, testUpsert(that)) },
|
||||||
"transaction": func(that *Context) { that.Display(0, testTransaction(that)) },
|
"transaction": func(that *Context) { that.Display(0, testTransaction(that)) },
|
||||||
"rawsql": func(that *Context) { that.Display(0, testRawSQL(that)) },
|
"rawsql": func(that *Context) { that.Display(0, testRawSQL(that)) },
|
||||||
@ -650,14 +650,14 @@ func testPagination(that *Context) Map {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 7. 批量插入测试 ====================
|
// ==================== 7. 批量插入测试 ====================
|
||||||
func testBatchInsert(that *Context) Map {
|
func testInserts(that *Context) Map {
|
||||||
result := Map{"name": "批量插入测试", "tests": Slice{}}
|
result := Map{"name": "批量插入测试", "tests": Slice{}}
|
||||||
tests := Slice{}
|
tests := Slice{}
|
||||||
|
|
||||||
// 7.1 批量插入
|
// 7.1 批量插入
|
||||||
test1 := Map{"name": "BatchInsert 批量插入"}
|
test1 := Map{"name": "Inserts 批量插入"}
|
||||||
timestamp := time.Now().UnixNano()
|
timestamp := time.Now().UnixNano()
|
||||||
affected1 := that.Db.BatchInsert("test_batch", []Map{
|
affected1 := that.Db.Inserts("test_batch", []Map{
|
||||||
{"name": fmt.Sprintf("批量测试1_%d", timestamp), "title": "标题1", "state": 1},
|
{"name": fmt.Sprintf("批量测试1_%d", timestamp), "title": "标题1", "state": 1},
|
||||||
{"name": fmt.Sprintf("批量测试2_%d", timestamp), "title": "标题2", "state": 1},
|
{"name": fmt.Sprintf("批量测试2_%d", timestamp), "title": "标题2", "state": 1},
|
||||||
{"name": fmt.Sprintf("批量测试3_%d", timestamp), "title": "标题3", "state": 1},
|
{"name": fmt.Sprintf("批量测试3_%d", timestamp), "title": "标题3", "state": 1},
|
||||||
@ -668,9 +668,9 @@ func testBatchInsert(that *Context) Map {
|
|||||||
tests = append(tests, test1)
|
tests = append(tests, test1)
|
||||||
|
|
||||||
// 7.2 带 [#] 的批量插入
|
// 7.2 带 [#] 的批量插入
|
||||||
test2 := Map{"name": "BatchInsert 带 [#] 标记"}
|
test2 := Map{"name": "Inserts 带 [#] 标记"}
|
||||||
timestamp2 := time.Now().UnixNano()
|
timestamp2 := time.Now().UnixNano()
|
||||||
affected2 := that.Db.BatchInsert("test_batch", []Map{
|
affected2 := that.Db.Inserts("test_batch", []Map{
|
||||||
{"name": fmt.Sprintf("带时间测试1_%d", timestamp2), "title": "标题带时间1", "state": 1, "create_time[#]": "NOW()"},
|
{"name": fmt.Sprintf("带时间测试1_%d", timestamp2), "title": "标题带时间1", "state": 1, "create_time[#]": "NOW()"},
|
||||||
{"name": fmt.Sprintf("带时间测试2_%d", timestamp2), "title": "标题带时间2", "state": 1, "create_time[#]": "NOW()"},
|
{"name": fmt.Sprintf("带时间测试2_%d", timestamp2), "title": "标题带时间2", "state": 1, "create_time[#]": "NOW()"},
|
||||||
})
|
})
|
||||||
|
|||||||
Binary file not shown.
@ -766,12 +766,12 @@ correctUsers4 := db.Select("user", "*", common.Map{
|
|||||||
_ = db
|
_ = db
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchInsertExample 批量插入示例
|
// InsertsExample 批量插入示例
|
||||||
func BatchInsertExample(database *db.HoTimeDB) {
|
func InsertsExample(database *db.HoTimeDB) {
|
||||||
fmt.Println("\n=== 批量插入示例 ===")
|
fmt.Println("\n=== 批量插入示例 ===")
|
||||||
|
|
||||||
// 批量插入用户(使用 []Map 格式)
|
// 批量插入用户(使用 []Map 格式)
|
||||||
affected := database.BatchInsert("user", []common.Map{
|
affected := database.Inserts("user", []common.Map{
|
||||||
{"name": "批量用户1", "email": "batch1@example.com", "age": 25, "status": 1},
|
{"name": "批量用户1", "email": "batch1@example.com", "age": 25, "status": 1},
|
||||||
{"name": "批量用户2", "email": "batch2@example.com", "age": 30, "status": 1},
|
{"name": "批量用户2", "email": "batch2@example.com", "age": 30, "status": 1},
|
||||||
{"name": "批量用户3", "email": "batch3@example.com", "age": 28, "status": 1},
|
{"name": "批量用户3", "email": "batch3@example.com", "age": 28, "status": 1},
|
||||||
@ -779,7 +779,7 @@ func BatchInsertExample(database *db.HoTimeDB) {
|
|||||||
fmt.Printf("批量插入了 %d 条用户记录\n", affected)
|
fmt.Printf("批量插入了 %d 条用户记录\n", affected)
|
||||||
|
|
||||||
// 批量插入日志(使用 [#] 标记直接 SQL)
|
// 批量插入日志(使用 [#] 标记直接 SQL)
|
||||||
logAffected := database.BatchInsert("log", []common.Map{
|
logAffected := database.Inserts("log", []common.Map{
|
||||||
{"user_id": 1, "action": "login", "ip": "192.168.1.1", "created_time[#]": "NOW()"},
|
{"user_id": 1, "action": "login", "ip": "192.168.1.1", "created_time[#]": "NOW()"},
|
||||||
{"user_id": 2, "action": "logout", "ip": "192.168.1.2", "created_time[#]": "NOW()"},
|
{"user_id": 2, "action": "logout", "ip": "192.168.1.2", "created_time[#]": "NOW()"},
|
||||||
{"user_id": 3, "action": "view", "ip": "192.168.1.3", "created_time[#]": "NOW()"},
|
{"user_id": 3, "action": "view", "ip": "192.168.1.3", "created_time[#]": "NOW()"},
|
||||||
@ -838,7 +838,7 @@ func RunAllFixedExamples() {
|
|||||||
fmt.Println("所有示例代码已修正完毕,语法正确!")
|
fmt.Println("所有示例代码已修正完毕,语法正确!")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("新增功能示例说明:")
|
fmt.Println("新增功能示例说明:")
|
||||||
fmt.Println(" - BatchInsertExample(db): 批量插入示例,使用 []Map 格式")
|
fmt.Println(" - InsertsExample(db): 批量插入示例,使用 []Map 格式")
|
||||||
fmt.Println(" - UpsertExample(db): 插入或更新示例")
|
fmt.Println(" - UpsertExample(db): 插入或更新示例")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
110
log/logrus.go
110
log/logrus.go
@ -2,12 +2,13 @@ package log
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetLog(path string, showCodeLine bool) *log.Logger {
|
func GetLog(path string, showCodeLine bool) *log.Logger {
|
||||||
@ -73,41 +74,98 @@ func (that *MyHook) Fire(entry *log.Entry) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对caller进行递归查询, 直到找到非logrus包产生的第一个调用.
|
// 最大框架层数限制 - 超过这个层数后不再跳过,防止误过滤应用层
|
||||||
// 因为filename我获取到了上层目录名, 因此所有logrus包的调用的文件名都是 logrus/...
|
const maxFrameworkDepth = 10
|
||||||
// 因此通过排除logrus开头的文件名, 就可以排除所有logrus包的自己的函数调用
|
|
||||||
func findCaller(skip int) string {
|
// isHoTimeFrameworkFile 判断是否是 HoTime 框架文件
|
||||||
file := ""
|
// 更精确的匹配:只有明确属于框架的文件才会被跳过
|
||||||
line := 0
|
func isHoTimeFrameworkFile(file string) bool {
|
||||||
for i := 0; i < 10; i++ {
|
// 1. logrus 日志库内部文件
|
||||||
file, line = getCaller(skip + i)
|
if strings.HasPrefix(file, "logrus/") {
|
||||||
if !strings.HasPrefix(file, "logrus") {
|
return true
|
||||||
j := 0
|
|
||||||
for true {
|
|
||||||
j++
|
|
||||||
if file == "common/error.go" {
|
|
||||||
file, line = getCaller(skip + i + j)
|
|
||||||
}
|
}
|
||||||
if file == "db/hotimedb.go" {
|
|
||||||
file, line = getCaller(skip + i + j)
|
// 2. Go 运行时文件
|
||||||
|
if strings.HasPrefix(file, "runtime/") {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
if file == "code/makecode.go" {
|
|
||||||
file, line = getCaller(skip + i + j)
|
// 3. HoTime 框架核心文件 - 通过包含 "hotime" 或框架特有文件名来识别
|
||||||
|
// 检查路径中是否包含 hotime 框架标识
|
||||||
|
lowerFile := strings.ToLower(file)
|
||||||
|
if strings.Contains(lowerFile, "hotime") {
|
||||||
|
// 是 hotime 框架的一部分,检查是否是核心模块
|
||||||
|
frameworkDirs := []string{"/db/", "/common/", "/code/", "/cache/", "/log/", "/dri/"}
|
||||||
|
for _, dir := range frameworkDirs {
|
||||||
|
if strings.Contains(file, dir) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
if strings.Index(file, "common/") == 0 {
|
|
||||||
file, line = getCaller(skip + i + j)
|
|
||||||
}
|
}
|
||||||
if strings.Contains(file, "application.go") {
|
// 框架核心文件(在 hotime 根目录下的 .go 文件)
|
||||||
file, line = getCaller(skip + i + j)
|
if strings.HasSuffix(file, "application.go") ||
|
||||||
}
|
strings.HasSuffix(file, "context.go") ||
|
||||||
if j == 5 {
|
strings.HasSuffix(file, "session.go") ||
|
||||||
break
|
strings.HasSuffix(file, "const.go") ||
|
||||||
|
strings.HasSuffix(file, "type.go") ||
|
||||||
|
strings.HasSuffix(file, "var.go") ||
|
||||||
|
strings.HasSuffix(file, "mime.go") {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 直接匹配框架核心目录(用于没有完整路径的情况)
|
||||||
|
// 只匹配 "db/xxx.go" 这种在框架核心目录下的文件
|
||||||
|
frameworkCoreDirs := []string{"db/", "common/", "code/", "cache/"}
|
||||||
|
for _, dir := range frameworkCoreDirs {
|
||||||
|
if strings.HasPrefix(file, dir) {
|
||||||
|
// 额外检查:确保不是用户项目中同名目录
|
||||||
|
// 框架文件通常有特定的文件名
|
||||||
|
frameworkFiles := []string{
|
||||||
|
"query.go", "crud.go", "where.go", "builder.go", "db.go",
|
||||||
|
"dialect.go", "aggregate.go", "transaction.go", "identifier.go",
|
||||||
|
"error.go", "func.go", "map.go", "obj.go", "slice.go",
|
||||||
|
"makecode.go", "template.go", "config.go",
|
||||||
|
"cache.go", "cache_db.go", "cache_memory.go", "cache_redis.go",
|
||||||
|
}
|
||||||
|
for _, f := range frameworkFiles {
|
||||||
|
if strings.HasSuffix(file, f) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对caller进行递归查询, 直到找到非框架层产生的第一个调用.
|
||||||
|
// 遍历调用栈,跳过框架层文件,找到应用层代码
|
||||||
|
// 使用层数限制确保不会误过滤应用层同名目录
|
||||||
|
func findCaller(skip int) string {
|
||||||
|
frameworkCount := 0 // 连续框架层计数
|
||||||
|
|
||||||
|
// 遍历调用栈,找到第一个非框架文件
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
file, line := getCaller(skip + i)
|
||||||
|
if file == "" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isHoTimeFrameworkFile(file) {
|
||||||
|
frameworkCount++
|
||||||
|
// 层数限制:如果已经跳过太多层,停止跳过
|
||||||
|
if frameworkCount >= maxFrameworkDepth {
|
||||||
|
return fmt.Sprintf("%s:%d", file, line)
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到非框架文件,返回应用层代码位置
|
||||||
|
return fmt.Sprintf("%s:%d", file, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找不到应用层,返回最初的调用者
|
||||||
|
file, line := getCaller(skip)
|
||||||
return fmt.Sprintf("%s:%d", file, line)
|
return fmt.Sprintf("%s:%d", file, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user