Compare commits
No commits in common. "master" and "master_v2" have entirely different histories.
@ -1,249 +0,0 @@
|
|||||||
---
|
|
||||||
name: 多数据库方言与前缀支持
|
|
||||||
overview: 为 HoTimeDB ORM 实现完整的多数据库(MySQL/PostgreSQL/SQLite)方言支持和自动表前缀功能,采用智能解析+辅助方法兜底的混合策略,保持完全向后兼容。
|
|
||||||
todos:
|
|
||||||
- id: dialect-interface
|
|
||||||
content: 扩展 Dialect 接口,添加 QuoteIdentifier 和 QuoteChar 方法
|
|
||||||
status: completed
|
|
||||||
- id: identifier-processor
|
|
||||||
content: 新建 identifier.go,实现 IdentifierProcessor 及智能解析逻辑
|
|
||||||
status: completed
|
|
||||||
- id: db-integration
|
|
||||||
content: 在 db.go 中集成处理器,添加 T() 和 C() 辅助方法
|
|
||||||
status: completed
|
|
||||||
- id: crud-update
|
|
||||||
content: 修改 crud.go 中 Select/Insert/Update/Delete/buildJoin 等方法
|
|
||||||
status: completed
|
|
||||||
- id: where-update
|
|
||||||
content: 修改 where.go 中 varCond 等条件处理方法
|
|
||||||
status: completed
|
|
||||||
- id: builder-check
|
|
||||||
content: 检查 builder.go 是否需要额外修改
|
|
||||||
status: completed
|
|
||||||
- id: testing
|
|
||||||
content: 编写测试用例验证多数据库和前缀功能
|
|
||||||
status: completed
|
|
||||||
- id: todo-1769037903242-d7aip6nh1
|
|
||||||
content: ""
|
|
||||||
status: pending
|
|
||||||
---
|
|
||||||
|
|
||||||
# HoTimeDB 多数据库方言与自动前缀支持计划(更新版)
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
1. **多数据库方言支持**:MySQL、PostgreSQL、SQLite 标识符引号自动转换
|
|
||||||
2. **自动表前缀**:主表、JOIN 表、ON/WHERE 条件中的表名自动添加前缀
|
|
||||||
3. **完全向后兼容**:用户现有写法无需修改
|
|
||||||
4. **辅助方法兜底**:边缘情况可用 `T()` / `C()` 精确控制
|
|
||||||
|
|
||||||
## 混合策略设计
|
|
||||||
|
|
||||||
### 各部分处理方式
|
|
||||||
|
|
||||||
| 位置 | 处理方式 | 准确度 | 说明 |
|
|
||||||
|
|
||||||
|------|---------|--------|------|
|
|
||||||
|
|
||||||
| 主表名 | 自动 | 100% | `Select("order")` 自动处理 |
|
|
||||||
|
|
||||||
| JOIN 表名 | 自动 | 100% | `[><]order` 中提取表名处理 |
|
|
||||||
|
|
||||||
| ON 条件字符串 | 智能解析 | ~95% | 正则匹配 `table.column` 模式 |
|
|
||||||
|
|
||||||
| WHERE 条件 Map | 自动 | 100% | Map 的 key 是结构化的 |
|
|
||||||
|
|
||||||
| SELECT 字段 | 智能解析 | ~95% | 同 ON 条件 |
|
|
||||||
|
|
||||||
### 辅助方法(兜底)
|
|
||||||
|
|
||||||
```go
|
|
||||||
db.T("order") // 返回 "`app_order`" (MySQL) 或 "\"app_order\"" (PG)
|
|
||||||
db.C("order", "name") // 返回 "`app_order`.`name`"
|
|
||||||
db.C("order.name") // 同上,支持点号格式
|
|
||||||
```
|
|
||||||
|
|
||||||
## 实现步骤
|
|
||||||
|
|
||||||
### 第1步:扩展 Dialect 接口([db/dialect.go](db/dialect.go))
|
|
||||||
|
|
||||||
添加新方法到 `Dialect` 接口:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// QuoteIdentifier 处理单个标识符(去除已有引号,添加正确引号)
|
|
||||||
QuoteIdentifier(name string) string
|
|
||||||
|
|
||||||
// QuoteChar 返回引号字符
|
|
||||||
QuoteChar() string
|
|
||||||
```
|
|
||||||
|
|
||||||
三种方言实现:
|
|
||||||
|
|
||||||
- MySQL: 反引号 `` ` ``
|
|
||||||
- PostgreSQL/SQLite: 双引号 `"`
|
|
||||||
|
|
||||||
### 第2步:添加标识符处理器([db/identifier.go](db/identifier.go) 新文件)
|
|
||||||
|
|
||||||
```go
|
|
||||||
type IdentifierProcessor struct {
|
|
||||||
dialect Dialect
|
|
||||||
prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessTableName 处理表名(添加前缀+引号)
|
|
||||||
// "order" → "`app_order`"
|
|
||||||
func (p *IdentifierProcessor) ProcessTableName(name string) string
|
|
||||||
|
|
||||||
// ProcessColumn 处理 table.column 格式
|
|
||||||
// "order.name" → "`app_order`.`name`"
|
|
||||||
// "`order`.name" → "`app_order`.`name`"
|
|
||||||
func (p *IdentifierProcessor) ProcessColumn(name string) string
|
|
||||||
|
|
||||||
// ProcessConditionString 智能解析条件字符串
|
|
||||||
// "user.id = order.user_id" → "`app_user`.`id` = `app_order`.`user_id`"
|
|
||||||
func (p *IdentifierProcessor) ProcessConditionString(condition string) string
|
|
||||||
|
|
||||||
// ProcessFieldList 处理字段列表字符串
|
|
||||||
// "order.id, user.name AS uname" → "`app_order`.`id`, `app_user`.`name` AS uname"
|
|
||||||
func (p *IdentifierProcessor) ProcessFieldList(fields string) string
|
|
||||||
```
|
|
||||||
|
|
||||||
**智能解析正则**:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 匹配 table.column 模式,排除已有引号、函数调用等
|
|
||||||
// 模式: \b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b
|
|
||||||
// 排除: `table`.column, "table".column, FUNC(), 123.456
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第3步:在 HoTimeDB 中集成([db/db.go](db/db.go))
|
|
||||||
|
|
||||||
```go
|
|
||||||
// processor 缓存
|
|
||||||
var processorOnce sync.Once
|
|
||||||
var processor *IdentifierProcessor
|
|
||||||
|
|
||||||
// GetProcessor 获取标识符处理器
|
|
||||||
func (that *HoTimeDB) GetProcessor() *IdentifierProcessor
|
|
||||||
|
|
||||||
// T 辅助方法:获取带前缀和引号的表名
|
|
||||||
func (that *HoTimeDB) T(table string) string
|
|
||||||
|
|
||||||
// C 辅助方法:获取带前缀和引号的 table.column
|
|
||||||
func (that *HoTimeDB) C(args ...string) string
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第4步:修改 CRUD 方法([db/crud.go](db/crud.go))
|
|
||||||
|
|
||||||
**Select 方法改动**:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// L112-116 原代码
|
|
||||||
if !strings.Contains(table, ".") && !strings.Contains(table, " AS ") {
|
|
||||||
query += " FROM `" + that.Prefix + table + "` "
|
|
||||||
} else {
|
|
||||||
query += " FROM " + that.Prefix + table + " "
|
|
||||||
}
|
|
||||||
|
|
||||||
// 改为
|
|
||||||
query += " FROM " + that.GetProcessor().ProcessTableName(table) + " "
|
|
||||||
|
|
||||||
// 字段列表处理(L90-107)
|
|
||||||
// 如果是字符串,调用 ProcessFieldList 处理
|
|
||||||
```
|
|
||||||
|
|
||||||
**buildJoin 方法改动**(L156-222):
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 原代码 L186-190
|
|
||||||
table := Substr(k, 3, len(k)-3)
|
|
||||||
if !strings.Contains(table, " ") {
|
|
||||||
table = "`" + table + "`"
|
|
||||||
}
|
|
||||||
query += " LEFT JOIN " + table + " ON " + v.(string) + " "
|
|
||||||
|
|
||||||
// 改为
|
|
||||||
table := Substr(k, 3, len(k)-3)
|
|
||||||
table = that.GetProcessor().ProcessTableName(table)
|
|
||||||
onCondition := that.GetProcessor().ProcessConditionString(v.(string))
|
|
||||||
query += " LEFT JOIN " + table + " ON " + onCondition + " "
|
|
||||||
```
|
|
||||||
|
|
||||||
**Insert/Inserts/Update/Delete** 同样修改表名和字段名处理。
|
|
||||||
|
|
||||||
### 第5步:修改 WHERE 条件处理([db/where.go](db/where.go))
|
|
||||||
|
|
||||||
**varCond 方法改动**(多处):
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 原代码(多处出现)
|
|
||||||
if !strings.Contains(k, ".") {
|
|
||||||
k = "`" + k + "`"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 改为
|
|
||||||
k = that.GetProcessor().ProcessColumn(k)
|
|
||||||
```
|
|
||||||
|
|
||||||
需要修改的函数:
|
|
||||||
|
|
||||||
- `varCond` (L205-338)
|
|
||||||
- `handleDefaultCondition` (L340-368)
|
|
||||||
- `handlePlainField` (L370-400)
|
|
||||||
|
|
||||||
### 第6步:修改链式构建器([db/builder.go](db/builder.go))
|
|
||||||
|
|
||||||
**LeftJoin 等方法需要传递处理器**:
|
|
||||||
|
|
||||||
由于 builder 持有 HoTimeDB 引用,可以直接使用:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (that *HotimeDBBuilder) LeftJoin(table, joinStr string) *HotimeDBBuilder {
|
|
||||||
// 不在这里处理,让 buildJoin 统一处理
|
|
||||||
that.Join(Map{"[>]" + table: joinStr})
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
JOIN 的实际处理在 `crud.go` 的 `buildJoin` 中完成。
|
|
||||||
|
|
||||||
## 智能解析的边界处理
|
|
||||||
|
|
||||||
### 会自动处理的情况
|
|
||||||
|
|
||||||
- `user.id = order.user_id` → 正确处理
|
|
||||||
- `user.id=order.user_id` → 正确处理(无空格)
|
|
||||||
- `` `user`.id = order.user_id `` → 正确处理(混合格式)
|
|
||||||
- `user.id = order.user_id AND order.status = 1` → 正确处理
|
|
||||||
|
|
||||||
### 需要辅助方法的边缘情况
|
|
||||||
|
|
||||||
- 子查询中的表名
|
|
||||||
- 复杂 CASE WHEN 表达式
|
|
||||||
- 动态拼接的 SQL 片段
|
|
||||||
|
|
||||||
## 文件清单
|
|
||||||
|
|
||||||
| 文件 | 操作 | 说明 |
|
|
||||||
|
|
||||||
|------|------|------|
|
|
||||||
|
|
||||||
| [db/dialect.go](db/dialect.go) | 修改 | 扩展 Dialect 接口 |
|
|
||||||
|
|
||||||
| [db/identifier.go](db/identifier.go) | 新增 | IdentifierProcessor 实现 |
|
|
||||||
|
|
||||||
| [db/db.go](db/db.go) | 修改 | 集成处理器,添加 T()/C() 方法 |
|
|
||||||
|
|
||||||
| [db/crud.go](db/crud.go) | 修改 | 修改所有 CRUD 方法 |
|
|
||||||
|
|
||||||
| [db/where.go](db/where.go) | 修改 | 修改条件处理逻辑 |
|
|
||||||
|
|
||||||
| [db/builder.go](db/builder.go) | 检查 | 可能无需修改(buildJoin 统一处理)|
|
|
||||||
|
|
||||||
## 测试用例
|
|
||||||
|
|
||||||
1. **多数据库切换**:MySQL → PostgreSQL → SQLite
|
|
||||||
2. **前缀场景**:有前缀 vs 无前缀
|
|
||||||
3. **复杂 JOIN**:多表 JOIN + 复杂 ON 条件
|
|
||||||
4. **混合写法**:`order.name` + `` `user`.id `` 混用
|
|
||||||
5. **辅助方法**:`T()` 和 `C()` 正确性
|
|
||||||
@ -1,168 +0,0 @@
|
|||||||
---
|
|
||||||
name: 集成请求参数获取方法
|
|
||||||
overview: 在 context.go 中为 Context 结构体添加五个请求参数获取方法:ReqData(统一获取)、ReqDataParams(URL参数)、ReqDataJson(JSON Body)、ReqDataForm(表单数据)、ReqFile(获取上传文件)。
|
|
||||||
todos:
|
|
||||||
- id: add-imports
|
|
||||||
content: 在 context.go 中添加 bytes、io、mime/multipart 包的导入
|
|
||||||
status: completed
|
|
||||||
- id: impl-params
|
|
||||||
content: 实现 ReqParam/ReqParams 方法(获取 URL 参数,返回 *Obj)
|
|
||||||
status: completed
|
|
||||||
dependencies:
|
|
||||||
- add-imports
|
|
||||||
- id: impl-form
|
|
||||||
content: 实现 ReqForm/ReqForms 方法(获取表单数据,返回 *Obj)
|
|
||||||
status: completed
|
|
||||||
dependencies:
|
|
||||||
- add-imports
|
|
||||||
- id: impl-json
|
|
||||||
content: 实现 ReqJson/ReqJsons 方法(获取 JSON Body,返回 *Obj)
|
|
||||||
status: completed
|
|
||||||
dependencies:
|
|
||||||
- add-imports
|
|
||||||
- id: impl-file
|
|
||||||
content: 实现 ReqFile/ReqFiles 方法(获取上传文件)
|
|
||||||
status: completed
|
|
||||||
dependencies:
|
|
||||||
- add-imports
|
|
||||||
- id: impl-reqdata
|
|
||||||
content: 实现 ReqData/ReqDatas 方法(统一获取,返回 *Obj)
|
|
||||||
status: completed
|
|
||||||
dependencies:
|
|
||||||
- impl-params
|
|
||||||
- impl-form
|
|
||||||
- impl-json
|
|
||||||
---
|
|
||||||
|
|
||||||
# 集成请求参数获取方法
|
|
||||||
|
|
||||||
## 实现位置
|
|
||||||
|
|
||||||
在 [`context.go`](context.go) 中添加请求参数获取方法,**风格与 `Session("key")` 保持一致,返回 `*Obj` 支持链式调用**。
|
|
||||||
|
|
||||||
## 新增方法
|
|
||||||
|
|
||||||
### 1. ReqParam - 获取 URL 查询参数(返回 *Obj)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 获取单个参数,支持链式调用
|
|
||||||
func (that *Context) ReqParam(key string) *Obj {
|
|
||||||
// that.ReqParam("id").ToStr()
|
|
||||||
// that.ReqParam("id").ToInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有 URL 参数
|
|
||||||
func (that *Context) ReqParams() Map
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. ReqForm - 获取表单数据(返回 *Obj)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 获取单个表单字段
|
|
||||||
func (that *Context) ReqForm(key string) *Obj {
|
|
||||||
// that.ReqForm("name").ToStr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有表单数据
|
|
||||||
func (that *Context) ReqForms() Map
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. ReqJson - 获取 JSON Body(返回 *Obj)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 获取 JSON 中的单个字段
|
|
||||||
func (that *Context) ReqJson(key string) *Obj {
|
|
||||||
// that.ReqJson("data").ToMap()
|
|
||||||
// that.ReqJson("count").ToInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取完整 JSON Body
|
|
||||||
func (that *Context) ReqJsons() Map
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. ReqFile - 获取上传文件
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (that *Context) ReqFile(name string) (multipart.File, *multipart.FileHeader, error)
|
|
||||||
func (that *Context) ReqFiles(name string) ([]*multipart.FileHeader, error)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. ReqData - 统一获取参数(返回 *Obj)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 统一获取(JSON > Form > URL),支持链式调用
|
|
||||||
func (that *Context) ReqData(key string) *Obj {
|
|
||||||
// that.ReqData("id").ToStr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有合并后的参数
|
|
||||||
func (that *Context) ReqDatas() Map
|
|
||||||
```
|
|
||||||
|
|
||||||
## 需要的导入
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 关键实现细节
|
|
||||||
|
|
||||||
1. **Body 只能读取一次的问题**:读取 Body 后需要用 `io.NopCloser(bytes.NewBuffer(body))` 恢复,以便后续代码(如其他中间件)还能再次读取
|
|
||||||
|
|
||||||
2. **废弃 API 替换**:使用 `io.ReadAll` 替代已废弃的 `ioutil.ReadAll`
|
|
||||||
|
|
||||||
3. **多值参数处理**:当同一参数有多个值时(如 `?id=1&id=2`),存储为 `Slice`;单值则直接存储字符串
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
```go
|
|
||||||
appIns.Run(Router{
|
|
||||||
"app": {
|
|
||||||
"user": {
|
|
||||||
"info": func(that *Context) {
|
|
||||||
// 链式调用获取单个参数(类似 Session 风格)
|
|
||||||
id := that.ReqData("id").ToInt() // 统一获取
|
|
||||||
name := that.ReqParam("name").ToStr() // URL 参数
|
|
||||||
age := that.ReqForm("age").ToCeilInt() // 表单参数
|
|
||||||
data := that.ReqJson("profile").ToMap() // JSON 字段
|
|
||||||
|
|
||||||
// 获取所有参数(返回 Map)
|
|
||||||
allParams := that.ReqDatas() // 合并后的所有参数
|
|
||||||
urlParams := that.ReqParams() // 所有 URL 参数
|
|
||||||
formData := that.ReqForms() // 所有表单数据
|
|
||||||
jsonBody := that.ReqJsons() // 完整 JSON Body
|
|
||||||
|
|
||||||
that.Display(0, Map{"id": id, "name": name})
|
|
||||||
},
|
|
||||||
"upload": func(that *Context) {
|
|
||||||
// 获取单个上传文件
|
|
||||||
file, header, err := that.ReqFile("avatar")
|
|
||||||
if err == nil {
|
|
||||||
defer file.Close()
|
|
||||||
// header.Filename - 文件名
|
|
||||||
// header.Size - 文件大小
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取多个同名上传文件
|
|
||||||
files, err := that.ReqFiles("images")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## 风格对比
|
|
||||||
|
|
||||||
| 旧方式(需要类型断言) | 新方式(链式调用) |
|
|
||||||
|
|
||||||
|------------------------|-------------------|
|
|
||||||
|
|
||||||
| `req["id"].(string) `| `that.ReqData("id").ToStr()` |
|
|
||||||
|
|
||||||
| `ObjToInt(req["id"]) `| `that.ReqData("id").ToInt()` |
|
|
||||||
|
|
||||||
| 需要手动处理 nil | `*Obj` 自动处理空值 |
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"setup-worktree": [
|
|
||||||
"npm install"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,4 @@
|
|||||||
/.idea/*
|
/.idea/*
|
||||||
.idea
|
.idea
|
||||||
|
/example/config/app.json
|
||||||
/example/tpt/demo/
|
/example/tpt/demo/
|
||||||
*.exe
|
|
||||||
/example/config
|
|
||||||
/.cursor/*.log
|
|
||||||
|
|||||||
90
README.md
90
README.md
@ -1,86 +1,6 @@
|
|||||||
# HoTime
|
# hotime
|
||||||
|
golang web服务框架
|
||||||
|
支持数据库db:mysql、sqlite3
|
||||||
|
支持缓存cache:redis,memory,数据库
|
||||||
|
自带工具类,上下文,以及session等功能
|
||||||
|
|
||||||
**高性能 Go Web 服务框架**
|
|
||||||
|
|
||||||
一个"小而全"的 Go Web 框架,内置 ORM、三级缓存、Session 管理,让你专注于业务逻辑。
|
|
||||||
|
|
||||||
## 核心特性
|
|
||||||
|
|
||||||
- **高性能** - 单机 10万+ QPS,支持百万级并发用户
|
|
||||||
- **内置 ORM** - 类 Medoo 语法,链式查询,支持 MySQL/SQLite/PostgreSQL
|
|
||||||
- **三级缓存** - Memory > Redis > DB,自动穿透与回填
|
|
||||||
- **Session 管理** - 内置会话管理,支持多种存储后端
|
|
||||||
- **代码生成** - 根据数据库表自动生成 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
|
|
||||||
go get code.hoteas.com/golang/hotime
|
|
||||||
```
|
|
||||||
|
|
||||||
## 性能
|
|
||||||
|
|
||||||
| 并发数 | QPS | 成功率 | 平均延迟 |
|
|
||||||
|--------|-----|--------|----------|
|
|
||||||
| 500 | 99,960 | 100% | 5.0ms |
|
|
||||||
| **1000** | **102,489** | **100%** | **9.7ms** |
|
|
||||||
| 2000 | 75,801 | 99.99% | 26.2ms |
|
|
||||||
|
|
||||||
> 测试环境:24 核 CPU,Windows 10,Go 1.19.3
|
|
||||||
|
|
||||||
### 并发用户估算
|
|
||||||
|
|
||||||
| 使用场景 | 请求频率 | 可支持用户数 |
|
|
||||||
|----------|----------|--------------|
|
|
||||||
| 高频交互 | 1次/秒 | ~10万 |
|
|
||||||
| 活跃用户 | 1次/5秒 | ~50万 |
|
|
||||||
| 普通浏览 | 1次/10秒 | ~100万 |
|
|
||||||
|
|
||||||
## 框架对比
|
|
||||||
|
|
||||||
| 特性 | HoTime | Gin | Echo | Fiber |
|
|
||||||
|------|--------|-----|------|-------|
|
|
||||||
| 性能 | 100K QPS | 70K QPS | 70K QPS | 100K QPS |
|
|
||||||
| 内置ORM | ✅ | ❌ | ❌ | ❌ |
|
|
||||||
| 内置缓存 | ✅ 三级缓存 | ❌ | ❌ | ❌ |
|
|
||||||
| Session | ✅ 内置 | ❌ 需插件 | ❌ 需插件 | ❌ 需插件 |
|
|
||||||
| 代码生成 | ✅ | ❌ | ❌ | ❌ |
|
|
||||||
| 微信/支付集成 | ✅ 内置 | ❌ | ❌ | ❌ |
|
|
||||||
|
|
||||||
## 适用场景
|
|
||||||
|
|
||||||
| 场景 | 推荐度 | 说明 |
|
|
||||||
|------|--------|------|
|
|
||||||
| 中小型后台系统 | ⭐⭐⭐⭐⭐ | 完美适配,开发效率最高 |
|
|
||||||
| 微信小程序后端 | ⭐⭐⭐⭐⭐ | 内置微信 SDK |
|
|
||||||
| 快速原型开发 | ⭐⭐⭐⭐⭐ | 代码生成 + 全功能集成 |
|
|
||||||
| 高并发 API 服务 | ⭐⭐⭐⭐ | 性能足够 |
|
|
||||||
| 大型微服务 | ⭐⭐⭐ | 建议用 Gin/Echo |
|
|
||||||
|
|
||||||
## 扩展功能
|
|
||||||
|
|
||||||
- 微信支付/公众号/小程序 - `dri/wechat/`
|
|
||||||
- 阿里云服务 - `dri/aliyun/`
|
|
||||||
- 腾讯云服务 - `dri/tencent/`
|
|
||||||
- 文件上传下载 - `dri/upload/`, `dri/download/`
|
|
||||||
- MongoDB - `dri/mongodb/`
|
|
||||||
- RSA 加解密 - `dri/rsa/`
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**HoTime** - 让 Go Web 开发更简单、更高效
|
|
||||||
|
|||||||
100
application.go
100
application.go
@ -7,7 +7,6 @@ import (
|
|||||||
. "code.hoteas.com/golang/hotime/db"
|
. "code.hoteas.com/golang/hotime/db"
|
||||||
. "code.hoteas.com/golang/hotime/log"
|
. "code.hoteas.com/golang/hotime/log"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -23,6 +22,7 @@ type Application struct {
|
|||||||
MakeCodeRouter map[string]*code.MakeCode
|
MakeCodeRouter map[string]*code.MakeCode
|
||||||
MethodRouter
|
MethodRouter
|
||||||
Router
|
Router
|
||||||
|
ContextBase
|
||||||
Error
|
Error
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
WebConnectLog *logrus.Logger
|
WebConnectLog *logrus.Logger
|
||||||
@ -68,20 +68,18 @@ func (that *Application) Run(router Router) {
|
|||||||
if that.Router == nil {
|
if that.Router == nil {
|
||||||
that.Router = Router{}
|
that.Router = Router{}
|
||||||
}
|
}
|
||||||
for k, _ := range router {
|
for k, v := range router {
|
||||||
v := router[k]
|
|
||||||
if that.Router[k] == nil {
|
if that.Router[k] == nil {
|
||||||
that.Router[k] = v
|
that.Router[k] = v
|
||||||
}
|
}
|
||||||
//直达接口层复用
|
//直达接口层复用
|
||||||
for k1, _ := range v {
|
for k1, v1 := range v {
|
||||||
v1 := v[k1]
|
|
||||||
if that.Router[k][k1] == nil {
|
if that.Router[k][k1] == nil {
|
||||||
that.Router[k][k1] = v1
|
that.Router[k][k1] = v1
|
||||||
}
|
}
|
||||||
|
|
||||||
for k2, _ := range v1 {
|
for k2, v2 := range v1 {
|
||||||
v2 := v1[k2]
|
|
||||||
that.Router[k][k1][k2] = v2
|
that.Router[k][k1][k2] = v2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,8 +237,10 @@ func (that *Application) SetConfig(configPath ...string) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if that.Error.GetError() != nil {
|
that.Log = GetLog(that.Config.GetString("logFile"), true)
|
||||||
fmt.Println(that.Error.GetError().Error())
|
that.Error = Error{Logger: that.Log}
|
||||||
|
if that.Config.Get("webConnectLogShow") == nil || that.Config.GetBool("webConnectLogShow") {
|
||||||
|
that.WebConnectLog = GetLog(that.Config.GetString("webConnectLogFile"), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
//文件如果损坏则不写入配置防止配置文件数据丢失
|
//文件如果损坏则不写入配置防止配置文件数据丢失
|
||||||
@ -267,12 +267,6 @@ func (that *Application) SetConfig(configPath ...string) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
that.Log = GetLog(that.Config.GetString("logFile"), true)
|
|
||||||
that.Error = Error{Logger: that.Log}
|
|
||||||
if that.Config.Get("webConnectLogShow") == nil || that.Config.GetBool("webConnectLogShow") {
|
|
||||||
that.WebConnectLog = GetLog(that.Config.GetString("webConnectLogFile"), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConnectListener 连接判断,返回false继续传输至控制层,true则停止传输
|
// SetConnectListener 连接判断,返回false继续传输至控制层,true则停止传输
|
||||||
@ -331,9 +325,6 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
|
|||||||
if len(token) == 32 {
|
if len(token) == 32 {
|
||||||
sessionId = token
|
sessionId = token
|
||||||
//没有token,则查阅session
|
//没有token,则查阅session
|
||||||
if cookie == nil || cookie.Value != sessionId {
|
|
||||||
needSetCookie = sessionId
|
|
||||||
}
|
|
||||||
} else if err == nil && cookie.Value != "" {
|
} else if err == nil && cookie.Value != "" {
|
||||||
sessionId = cookie.Value
|
sessionId = cookie.Value
|
||||||
//session也没有则判断是否创建cookie
|
//session也没有则判断是否创建cookie
|
||||||
@ -362,18 +353,14 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
//是否展示日志
|
//是否展示日志
|
||||||
if that.WebConnectLog != nil {
|
if that.WebConnectLog != nil {
|
||||||
|
ipStr := Substr(context.Req.RemoteAddr, 0, strings.Index(context.Req.RemoteAddr, ":"))
|
||||||
//负载均衡优化
|
//负载均衡优化
|
||||||
ipStr := ""
|
if ipStr == "127.0.0.1" {
|
||||||
if req.Header.Get("X-Forwarded-For") != "" {
|
if req.Header.Get("X-Forwarded-For") != "" {
|
||||||
ipStr = req.Header.Get("X-Forwarded-For")
|
ipStr = req.Header.Get("X-Forwarded-For")
|
||||||
} else if req.Header.Get("X-Real-IP") != "" {
|
} else if req.Header.Get("X-Real-IP") != "" {
|
||||||
ipStr = req.Header.Get("X-Real-IP")
|
ipStr = req.Header.Get("X-Real-IP")
|
||||||
}
|
}
|
||||||
//负载均衡优化
|
|
||||||
if ipStr == "" {
|
|
||||||
//RemoteAddr := that.Req.RemoteAddr
|
|
||||||
ipStr = Substr(context.Req.RemoteAddr, 0, strings.Index(context.Req.RemoteAddr, ":"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
that.WebConnectLog.Infoln(ipStr, context.Req.Method,
|
that.WebConnectLog.Infoln(ipStr, context.Req.Method,
|
||||||
@ -383,17 +370,15 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
//访问拦截true继续false暂停
|
//访问拦截true继续false暂停
|
||||||
connectListenerLen := len(that.connectListener) - 1
|
connectListenerLen := len(that.connectListener)
|
||||||
|
|
||||||
|
for i := connectListenerLen - 1; i >= 0; i-- {
|
||||||
|
|
||||||
|
if that.connectListener[i](&context) {
|
||||||
|
|
||||||
for true {
|
|
||||||
if connectListenerLen < 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if that.connectListener[connectListenerLen](&context) {
|
|
||||||
context.View()
|
context.View()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
connectListenerLen--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//接口服务
|
//接口服务
|
||||||
@ -467,7 +452,6 @@ func (that *Application) crossDomain(context *Context, sessionId string) {
|
|||||||
if context.Config.GetString("crossDomain") == "" {
|
if context.Config.GetString("crossDomain") == "" {
|
||||||
if sessionId != "" {
|
if sessionId != "" {
|
||||||
http.SetCookie(context.Resp, &http.Cookie{Name: that.Config.GetString("sessionName"), Value: sessionId, Path: "/"})
|
http.SetCookie(context.Resp, &http.Cookie{Name: that.Config.GetString("sessionName"), Value: sessionId, Path: "/"})
|
||||||
//context.Resp.Header().Set("Set-Cookie", that.Config.GetString("sessionName")+"="+sessionId+"; Path=/; SameSite=None; Secure")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -497,7 +481,7 @@ func (that *Application) crossDomain(context *Context, sessionId string) {
|
|||||||
header.Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE")
|
header.Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE")
|
||||||
header.Set("Access-Control-Allow-Credentials", "true")
|
header.Set("Access-Control-Allow-Credentials", "true")
|
||||||
header.Set("Access-Control-Expose-Headers", "*")
|
header.Set("Access-Control-Expose-Headers", "*")
|
||||||
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token,Authorization,Cookie,Set-Cookie")
|
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token")
|
||||||
|
|
||||||
if sessionId != "" {
|
if sessionId != "" {
|
||||||
//跨域允许需要设置cookie的允许跨域https才有效果
|
//跨域允许需要设置cookie的允许跨域https才有效果
|
||||||
@ -512,8 +496,7 @@ func (that *Application) crossDomain(context *Context, sessionId string) {
|
|||||||
if (origin != "" && strings.Contains(origin, remoteHost)) || strings.Contains(refer, remoteHost) {
|
if (origin != "" && strings.Contains(origin, remoteHost)) || strings.Contains(refer, remoteHost) {
|
||||||
|
|
||||||
if sessionId != "" {
|
if sessionId != "" {
|
||||||
//http.SetCookie(context.Resp, &http.Cookie{Name: that.Config.GetString("sessionName"), Value: sessionId, Path: "/"})
|
http.SetCookie(context.Resp, &http.Cookie{Name: that.Config.GetString("sessionName"), Value: sessionId, Path: "/"})
|
||||||
context.Resp.Header().Set("Set-Cookie", that.Config.GetString("sessionName")+"="+sessionId+"; Path=/; SameSite=None; Secure")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -544,7 +527,7 @@ func (that *Application) crossDomain(context *Context, sessionId string) {
|
|||||||
header.Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE")
|
header.Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE")
|
||||||
header.Set("Access-Control-Allow-Credentials", "true")
|
header.Set("Access-Control-Allow-Credentials", "true")
|
||||||
header.Set("Access-Control-Expose-Headers", "*")
|
header.Set("Access-Control-Expose-Headers", "*")
|
||||||
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token,Authorization,Cookie,Set-Cookie")
|
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token")
|
||||||
|
|
||||||
if sessionId != "" {
|
if sessionId != "" {
|
||||||
//跨域允许需要设置cookie的允许跨域https才有效果
|
//跨域允许需要设置cookie的允许跨域https才有效果
|
||||||
@ -579,9 +562,14 @@ func Init(config string) *Application {
|
|||||||
codeMake["name"] = codeMake.GetString("table")
|
codeMake["name"] = codeMake.GetString("table")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if appIns.Config.GetInt("mode") > 0 {
|
||||||
appIns.MakeCodeRouter[codeMake.GetString("name")] = &code.MakeCode{Error: appIns.Error}
|
appIns.MakeCodeRouter[codeMake.GetString("name")] = &code.MakeCode{Error: appIns.Error}
|
||||||
appIns.MakeCodeRouter[codeMake.GetString("name")].Db2JSON(&appIns.Db, codeMake)
|
|
||||||
|
|
||||||
|
appIns.MakeCodeRouter[codeMake.GetString("name")].Db2JSON(&appIns.Db, codeMake)
|
||||||
|
} else {
|
||||||
|
appIns.MakeCodeRouter[codeMake.GetString("name")] = &code.MakeCode{Error: appIns.Error}
|
||||||
|
appIns.MakeCodeRouter[codeMake.GetString("name")].Db2JSON(nil, codeMake)
|
||||||
|
}
|
||||||
//接入动态代码层
|
//接入动态代码层
|
||||||
if appIns.Router == nil {
|
if appIns.Router == nil {
|
||||||
appIns.Router = Router{}
|
appIns.Router = Router{}
|
||||||
@ -589,30 +577,20 @@ func Init(config string) *Application {
|
|||||||
|
|
||||||
//appIns.Router[codeMake.GetString("name")] = TptProject
|
//appIns.Router[codeMake.GetString("name")] = TptProject
|
||||||
appIns.Router[codeMake.GetString("name")] = Proj{}
|
appIns.Router[codeMake.GetString("name")] = Proj{}
|
||||||
|
|
||||||
for k2, _ := range TptProject {
|
for k2, _ := range TptProject {
|
||||||
if appIns.Router[codeMake.GetString("name")][k2] == nil {
|
|
||||||
appIns.Router[codeMake.GetString("name")][k2] = Ctr{}
|
appIns.Router[codeMake.GetString("name")][k2] = Ctr{}
|
||||||
}
|
for k3, v3 := range TptProject[k2] {
|
||||||
for k3, _ := range TptProject[k2] {
|
|
||||||
v3 := TptProject[k2][k3]
|
|
||||||
appIns.Router[codeMake.GetString("name")][k2][k3] = v3
|
appIns.Router[codeMake.GetString("name")][k2][k3] = v3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for k1, _ := range appIns.MakeCodeRouter[codeMake.GetString("name")].TableColumns {
|
for k1, _ := range appIns.MakeCodeRouter[codeMake.GetString("name")].TableColumns {
|
||||||
if appIns.Router[codeMake.GetString("name")][k1] == nil {
|
appIns.Router[codeMake.GetString("name")][k1] = appIns.Router[codeMake.GetString("name")]["hotimeCommon"]
|
||||||
appIns.Router[codeMake.GetString("name")][k1] = Ctr{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for k2, _ := range appIns.Router[codeMake.GetString("name")]["hotimeCommon"] {
|
go func() {
|
||||||
//golang毛病
|
setMakeCodeLintener(codeMake.GetString("name"), &appIns)
|
||||||
v2 := appIns.Router[codeMake.GetString("name")]["hotimeCommon"][k2]
|
}()
|
||||||
appIns.Router[codeMake.GetString("name")][k1][k2] = v2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setMakeCodeListener(codeMake.GetString("name"), &appIns)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -682,7 +660,7 @@ func SetSqliteDB(appIns *Application, config Map) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMakeCodeListener(name string, appIns *Application) {
|
func setMakeCodeLintener(name string, appIns *Application) {
|
||||||
appIns.SetConnectListener(func(context *Context) (isFinished bool) {
|
appIns.SetConnectListener(func(context *Context) (isFinished bool) {
|
||||||
|
|
||||||
codeIns := appIns.MakeCodeRouter[name]
|
codeIns := appIns.MakeCodeRouter[name]
|
||||||
@ -702,9 +680,7 @@ func setMakeCodeListener(name string, appIns *Application) {
|
|||||||
if context.RouterString[1] == "hotime" && context.RouterString[2] == "config" {
|
if context.RouterString[1] == "hotime" && context.RouterString[2] == "config" {
|
||||||
return isFinished
|
return isFinished
|
||||||
}
|
}
|
||||||
if context.RouterString[1] == "hotime" && context.RouterString[2] == "wallpaper" {
|
|
||||||
return isFinished
|
|
||||||
}
|
|
||||||
if context.Session(codeIns.FileConfig.GetString("table")+"_id").Data == nil {
|
if context.Session(codeIns.FileConfig.GetString("table")+"_id").Data == nil {
|
||||||
context.Display(2, "你还没有登录")
|
context.Display(2, "你还没有登录")
|
||||||
return true
|
return true
|
||||||
@ -747,16 +723,6 @@ func setMakeCodeListener(name string, appIns *Application) {
|
|||||||
context.Req.Method == "POST" {
|
context.Req.Method == "POST" {
|
||||||
return isFinished
|
return isFinished
|
||||||
}
|
}
|
||||||
//分析
|
|
||||||
if len(context.RouterString) == 3 && context.RouterString[2] == "analyse" &&
|
|
||||||
context.Req.Method == "GET" {
|
|
||||||
|
|
||||||
if context.Router[context.RouterString[0]][context.RouterString[1]]["analyse"] == nil {
|
|
||||||
return isFinished
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Router[context.RouterString[0]][context.RouterString[1]]["analyse"](context)
|
|
||||||
}
|
|
||||||
//查询单条
|
//查询单条
|
||||||
if len(context.RouterString) == 3 &&
|
if len(context.RouterString) == 3 &&
|
||||||
context.Req.Method == "GET" {
|
context.Req.Method == "GET" {
|
||||||
@ -788,7 +754,7 @@ func setMakeCodeListener(name string, appIns *Application) {
|
|||||||
context.Router[context.RouterString[0]][context.RouterString[1]]["remove"](context)
|
context.Router[context.RouterString[0]][context.RouterString[1]]["remove"](context)
|
||||||
}
|
}
|
||||||
|
|
||||||
//context.View()
|
context.View()
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
18
cache/cache.go
vendored
18
cache/cache.go
vendored
@ -1,9 +1,8 @@
|
|||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HoTimeCache 可配置memory,db,redis,默认启用memory,默认优先级为memory>redis>db,memory与数据库缓存设置项一致,
|
// HoTimeCache 可配置memory,db,redis,默认启用memory,默认优先级为memory>redis>db,memory与数据库缓存设置项一致,
|
||||||
@ -93,7 +92,7 @@ func (that *HoTimeCache) Db(key string, data ...interface{}) *Obj {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//db缓存有
|
//redis缓存有
|
||||||
if that.dbCache != nil && that.dbCache.DbSet {
|
if that.dbCache != nil && that.dbCache.DbSet {
|
||||||
reData = that.dbCache.Cache(key, data...)
|
reData = that.dbCache.Cache(key, data...)
|
||||||
if reData.Data != nil {
|
if reData.Data != nil {
|
||||||
@ -120,7 +119,7 @@ func (that *HoTimeCache) Db(key string, data ...interface{}) *Obj {
|
|||||||
if that.redisCache != nil && that.redisCache.DbSet {
|
if that.redisCache != nil && that.redisCache.DbSet {
|
||||||
reData = that.redisCache.Cache(key, data...)
|
reData = that.redisCache.Cache(key, data...)
|
||||||
}
|
}
|
||||||
//db缓存有
|
//redis缓存有
|
||||||
if that.dbCache != nil && that.dbCache.DbSet {
|
if that.dbCache != nil && that.dbCache.DbSet {
|
||||||
reData = that.dbCache.Cache(key, data...)
|
reData = that.dbCache.Cache(key, data...)
|
||||||
}
|
}
|
||||||
@ -133,7 +132,7 @@ func (that *HoTimeCache) Cache(key string, data ...interface{}) *Obj {
|
|||||||
//内存缓存有
|
//内存缓存有
|
||||||
if that.memoryCache != nil {
|
if that.memoryCache != nil {
|
||||||
reData = that.memoryCache.Cache(key, data...)
|
reData = that.memoryCache.Cache(key, data...)
|
||||||
if reData != nil && reData.Data != nil {
|
if reData != nil {
|
||||||
return reData
|
return reData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,7 +140,8 @@ func (that *HoTimeCache) Cache(key string, data ...interface{}) *Obj {
|
|||||||
//redis缓存有
|
//redis缓存有
|
||||||
if that.redisCache != nil {
|
if that.redisCache != nil {
|
||||||
reData = that.redisCache.Cache(key, data...)
|
reData = that.redisCache.Cache(key, data...)
|
||||||
if reData != nil && reData.Data != nil {
|
if reData.Data != nil {
|
||||||
|
|
||||||
if that.memoryCache != nil {
|
if that.memoryCache != nil {
|
||||||
that.memoryCache.Cache(key, reData.Data)
|
that.memoryCache.Cache(key, reData.Data)
|
||||||
}
|
}
|
||||||
@ -149,10 +149,10 @@ func (that *HoTimeCache) Cache(key string, data ...interface{}) *Obj {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//db缓存有
|
//redis缓存有
|
||||||
if that.dbCache != nil {
|
if that.dbCache != nil {
|
||||||
reData = that.dbCache.Cache(key, data...)
|
reData = that.dbCache.Cache(key, data...)
|
||||||
if reData != nil && reData.Data != nil {
|
if reData.Data != nil {
|
||||||
if that.memoryCache != nil {
|
if that.memoryCache != nil {
|
||||||
that.memoryCache.Cache(key, reData.Data)
|
that.memoryCache.Cache(key, reData.Data)
|
||||||
}
|
}
|
||||||
@ -174,7 +174,7 @@ func (that *HoTimeCache) Cache(key string, data ...interface{}) *Obj {
|
|||||||
if that.redisCache != nil {
|
if that.redisCache != nil {
|
||||||
reData = that.redisCache.Cache(key, data...)
|
reData = that.redisCache.Cache(key, data...)
|
||||||
}
|
}
|
||||||
//db缓存有
|
//redis缓存有
|
||||||
if that.dbCache != nil {
|
if that.dbCache != nil {
|
||||||
reData = that.dbCache.Cache(key, data...)
|
reData = that.dbCache.Cache(key, data...)
|
||||||
}
|
}
|
||||||
|
|||||||
185
cache/cache_memory.go
vendored
185
cache/cache_memory.go
vendored
@ -7,13 +7,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CacheMemory 基于 sync.Map 的缓存实现
|
|
||||||
type CacheMemory struct {
|
type CacheMemory struct {
|
||||||
TimeOut int64
|
TimeOut int64
|
||||||
DbSet bool
|
DbSet bool
|
||||||
SessionSet bool
|
SessionSet bool
|
||||||
|
Map
|
||||||
*Error
|
*Error
|
||||||
cache sync.Map // 替代传统的 Map
|
ContextBase
|
||||||
|
mutex *sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (that *CacheMemory) GetError() *Error {
|
func (that *CacheMemory) GetError() *Error {
|
||||||
@ -25,91 +26,133 @@ func (that *CacheMemory) GetError() *Error {
|
|||||||
func (that *CacheMemory) SetError(err *Error) {
|
func (that *CacheMemory) SetError(err *Error) {
|
||||||
that.Error = err
|
that.Error = err
|
||||||
}
|
}
|
||||||
func (c *CacheMemory) get(key string) (res *Obj) {
|
|
||||||
|
|
||||||
res = &Obj{
|
//获取Cache键只能为string类型
|
||||||
Error: *c.Error,
|
func (that *CacheMemory) get(key string) interface{} {
|
||||||
}
|
that.Error.SetError(nil)
|
||||||
value, ok := c.cache.Load(key)
|
if that.Map == nil {
|
||||||
if !ok {
|
that.Map = Map{}
|
||||||
return res // 缓存不存在
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data := value.(cacheData)
|
if that.Map[key] == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data := that.Map.Get(key, that.Error).(cacheData)
|
||||||
|
if that.Error.GetError() != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否过期
|
|
||||||
if data.time < time.Now().Unix() {
|
if data.time < time.Now().Unix() {
|
||||||
c.cache.Delete(key) // 删除过期缓存
|
delete(that.Map, key)
|
||||||
return res
|
return nil
|
||||||
}
|
}
|
||||||
res.Data = data.data
|
return data.data
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
func (c *CacheMemory) set(key string, value interface{}, expireAt int64) {
|
|
||||||
data := cacheData{
|
|
||||||
data: value,
|
|
||||||
time: expireAt,
|
|
||||||
}
|
|
||||||
c.cache.Store(key, data)
|
|
||||||
}
|
|
||||||
func (c *CacheMemory) delete(key string) {
|
|
||||||
if strings.Contains(key, "*") {
|
|
||||||
// 通配符删除
|
|
||||||
prefix := strings.TrimSuffix(key, "*")
|
|
||||||
c.cache.Range(func(k, v interface{}) bool {
|
|
||||||
if strings.HasPrefix(k.(string), prefix) {
|
|
||||||
c.cache.Delete(k)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 精确删除
|
|
||||||
c.cache.Delete(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (c *CacheMemory) refreshMap() {
|
|
||||||
go func() {
|
|
||||||
now := time.Now().Unix()
|
|
||||||
c.cache.Range(func(key, value interface{}) bool {
|
|
||||||
data := value.(cacheData)
|
|
||||||
if data.time <= now {
|
|
||||||
c.cache.Delete(key) // 删除过期缓存
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
func (c *CacheMemory) Cache(key string, data ...interface{}) *Obj {
|
|
||||||
now := time.Now().Unix()
|
|
||||||
|
|
||||||
// 随机触发刷新
|
func (that *CacheMemory) refreshMap() {
|
||||||
if x := RandX(1, 100000); x > 99950 {
|
|
||||||
c.refreshMap()
|
go func() {
|
||||||
|
that.mutex.Lock()
|
||||||
|
defer that.mutex.Unlock()
|
||||||
|
for key, v := range that.Map {
|
||||||
|
data := v.(cacheData)
|
||||||
|
if data.time <= time.Now().Unix() {
|
||||||
|
delete(that.Map, key)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//key value ,时间为时间戳
|
||||||
|
func (that *CacheMemory) set(key string, value interface{}, time int64) {
|
||||||
|
that.Error.SetError(nil)
|
||||||
|
var data cacheData
|
||||||
|
|
||||||
|
if that.Map == nil {
|
||||||
|
that.Map = Map{}
|
||||||
|
}
|
||||||
|
|
||||||
|
dd := that.Map[key]
|
||||||
|
|
||||||
|
if dd == nil {
|
||||||
|
data = cacheData{}
|
||||||
|
} else {
|
||||||
|
data = dd.(cacheData)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.time = time
|
||||||
|
data.data = value
|
||||||
|
|
||||||
|
that.Map.Put(key, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (that *CacheMemory) delete(key string) {
|
||||||
|
del := strings.Index(key, "*")
|
||||||
|
//如果通配删除
|
||||||
|
if del != -1 {
|
||||||
|
key = Substr(key, 0, del)
|
||||||
|
for k, _ := range that.Map {
|
||||||
|
if strings.Index(k, key) != -1 {
|
||||||
|
delete(that.Map, k)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
delete(that.Map, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (that *CacheMemory) Cache(key string, data ...interface{}) *Obj {
|
||||||
|
|
||||||
|
x := RandX(1, 100000)
|
||||||
|
if x > 99950 {
|
||||||
|
that.refreshMap()
|
||||||
|
}
|
||||||
|
if that.mutex == nil {
|
||||||
|
that.mutex = &sync.RWMutex{}
|
||||||
|
}
|
||||||
|
|
||||||
|
reData := &Obj{Data: nil}
|
||||||
|
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
// 读操作
|
that.mutex.RLock()
|
||||||
return c.get(key)
|
reData.Data = that.get(key)
|
||||||
|
that.mutex.RUnlock()
|
||||||
|
return reData
|
||||||
}
|
}
|
||||||
|
tim := time.Now().Unix()
|
||||||
|
|
||||||
if len(data) == 1 && data[0] == nil {
|
if len(data) == 1 && data[0] == nil {
|
||||||
// 删除操作
|
that.mutex.Lock()
|
||||||
c.delete(key)
|
that.delete(key)
|
||||||
return nil
|
that.mutex.Unlock()
|
||||||
|
return reData
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写操作
|
if len(data) == 1 {
|
||||||
expireAt := now + c.TimeOut
|
|
||||||
|
tim = tim + that.TimeOut
|
||||||
|
|
||||||
|
}
|
||||||
if len(data) == 2 {
|
if len(data) == 2 {
|
||||||
if customExpire, ok := data[1].(int64); ok {
|
that.Error.SetError(nil)
|
||||||
if customExpire > now {
|
tempt := ObjToInt64(data[1], that.Error)
|
||||||
expireAt = customExpire
|
|
||||||
} else {
|
if tempt > tim {
|
||||||
expireAt = now + customExpire
|
|
||||||
}
|
tim = tempt
|
||||||
}
|
} else if that.Error.GetError() == nil {
|
||||||
}
|
|
||||||
|
tim = tim + tempt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
that.mutex.Lock()
|
||||||
|
that.set(key, data[0], tim)
|
||||||
|
that.mutex.Unlock()
|
||||||
|
return reData
|
||||||
|
|
||||||
c.set(key, data[0], expireAt)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
138
cache/cache_redis.go
vendored
138
cache/cache_redis.go
vendored
@ -4,7 +4,6 @@ import (
|
|||||||
. "code.hoteas.com/golang/hotime/common"
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
"github.com/garyburd/redigo/redis"
|
"github.com/garyburd/redigo/redis"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,11 +14,10 @@ type CacheRedis struct {
|
|||||||
Host string
|
Host string
|
||||||
Pwd string
|
Pwd string
|
||||||
Port int64
|
Port int64
|
||||||
pool *redis.Pool
|
conn redis.Conn
|
||||||
tag int64
|
tag int64
|
||||||
ContextBase
|
ContextBase
|
||||||
*Error
|
*Error
|
||||||
initOnce sync.Once
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (that *CacheRedis) GetError() *Error {
|
func (that *CacheRedis) GetError() *Error {
|
||||||
@ -41,112 +39,83 @@ func (that *CacheRedis) GetTag() int64 {
|
|||||||
return that.tag
|
return that.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
// initPool 初始化连接池(只执行一次)
|
func (that *CacheRedis) reCon() bool {
|
||||||
func (that *CacheRedis) initPool() {
|
var err error
|
||||||
that.initOnce.Do(func() {
|
that.conn, err = redis.Dial("tcp", that.Host+":"+ObjToStr(that.Port))
|
||||||
that.pool = &redis.Pool{
|
|
||||||
MaxIdle: 10, // 最大空闲连接数
|
|
||||||
MaxActive: 100, // 最大活跃连接数,0表示无限制
|
|
||||||
IdleTimeout: 5 * time.Minute, // 空闲连接超时时间
|
|
||||||
Wait: true, // 当连接池耗尽时是否等待
|
|
||||||
Dial: func() (redis.Conn, error) {
|
|
||||||
conn, err := redis.Dial("tcp", that.Host+":"+ObjToStr(that.Port),
|
|
||||||
redis.DialConnectTimeout(5*time.Second),
|
|
||||||
redis.DialReadTimeout(3*time.Second),
|
|
||||||
redis.DialWriteTimeout(3*time.Second),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
that.conn = nil
|
||||||
|
that.Error.SetError(err)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if that.Pwd != "" {
|
if that.Pwd != "" {
|
||||||
if _, err := conn.Do("AUTH", that.Pwd); err != nil {
|
_, err = that.conn.Do("AUTH", that.Pwd)
|
||||||
conn.Close()
|
if err != nil {
|
||||||
return nil, err
|
that.conn = nil
|
||||||
|
that.Error.SetError(err)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return conn, nil
|
|
||||||
},
|
|
||||||
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
|
||||||
if time.Since(t) < time.Minute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
_, err := c.Do("PING")
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// getConn 从连接池获取连接
|
return true
|
||||||
func (that *CacheRedis) getConn() redis.Conn {
|
|
||||||
that.initPool()
|
|
||||||
if that.pool == nil {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return that.pool.Get()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (that *CacheRedis) del(key string) {
|
func (that *CacheRedis) del(key string) {
|
||||||
conn := that.getConn()
|
|
||||||
if conn == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
del := strings.Index(key, "*")
|
del := strings.Index(key, "*")
|
||||||
if del != -1 {
|
if del != -1 {
|
||||||
val, err := redis.Strings(conn.Do("KEYS", key))
|
val, err := redis.Strings(that.conn.Do("KEYS", key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
that.Error.SetError(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(val) == 0 {
|
that.conn.Send("MULTI")
|
||||||
return
|
for i, _ := range val {
|
||||||
}
|
that.conn.Send("DEL", val[i])
|
||||||
conn.Send("MULTI")
|
|
||||||
for i := range val {
|
|
||||||
conn.Send("DEL", val[i])
|
|
||||||
}
|
|
||||||
_, err = conn.Do("EXEC")
|
|
||||||
if err != nil {
|
|
||||||
that.Error.SetError(err)
|
|
||||||
}
|
}
|
||||||
|
that.conn.Do("EXEC")
|
||||||
} else {
|
} else {
|
||||||
_, err := conn.Do("DEL", key)
|
_, err := that.conn.Do("DEL", key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
that.Error.SetError(err)
|
that.Error.SetError(err)
|
||||||
|
_, err = that.conn.Do("PING")
|
||||||
|
if err != nil {
|
||||||
|
if that.reCon() {
|
||||||
|
_, err = that.conn.Do("DEL", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//key value ,时间为时间戳
|
//key value ,时间为时间戳
|
||||||
func (that *CacheRedis) set(key string, value string, expireSeconds int64) {
|
func (that *CacheRedis) set(key string, value string, time int64) {
|
||||||
conn := that.getConn()
|
_, err := that.conn.Do("SET", key, value, "EX", ObjToStr(time))
|
||||||
if conn == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
_, err := conn.Do("SET", key, value, "EX", ObjToStr(expireSeconds))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
that.Error.SetError(err)
|
that.Error.SetError(err)
|
||||||
|
_, err = that.conn.Do("PING")
|
||||||
|
if err != nil {
|
||||||
|
if that.reCon() {
|
||||||
|
_, err = that.conn.Do("SET", key, value, "EX", ObjToStr(time))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (that *CacheRedis) get(key string) *Obj {
|
func (that *CacheRedis) get(key string) *Obj {
|
||||||
reData := &Obj{}
|
reData := &Obj{}
|
||||||
conn := that.getConn()
|
|
||||||
if conn == nil {
|
|
||||||
return reData
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
reData.Data, err = redis.String(conn.Do("GET", key))
|
reData.Data, err = redis.String(that.conn.Do("GET", key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reData.Data = nil
|
reData.Data = nil
|
||||||
if !strings.Contains(err.Error(), "nil returned") {
|
if !strings.Contains(err.Error(), "nil returned") {
|
||||||
that.Error.SetError(err)
|
that.Error.SetError(err)
|
||||||
|
_, err = that.conn.Do("PING")
|
||||||
|
if err != nil {
|
||||||
|
if that.reCon() {
|
||||||
|
reData.Data, err = redis.String(that.conn.Do("GET", key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reData
|
return reData
|
||||||
@ -154,13 +123,19 @@ func (that *CacheRedis) get(key string) *Obj {
|
|||||||
|
|
||||||
func (that *CacheRedis) Cache(key string, data ...interface{}) *Obj {
|
func (that *CacheRedis) Cache(key string, data ...interface{}) *Obj {
|
||||||
reData := &Obj{}
|
reData := &Obj{}
|
||||||
|
if that.conn == nil {
|
||||||
//查询缓存
|
re := that.reCon()
|
||||||
if len(data) == 0 {
|
if !re {
|
||||||
reData = that.get(key)
|
|
||||||
return reData
|
return reData
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
//查询缓存
|
||||||
|
if len(data) == 0 {
|
||||||
|
|
||||||
|
reData = that.get(key)
|
||||||
|
return reData
|
||||||
|
|
||||||
|
}
|
||||||
tim := int64(0)
|
tim := int64(0)
|
||||||
//删除缓存
|
//删除缓存
|
||||||
if len(data) == 1 && data[0] == nil {
|
if len(data) == 1 && data[0] == nil {
|
||||||
@ -169,17 +144,21 @@ func (that *CacheRedis) Cache(key string, data ...interface{}) *Obj {
|
|||||||
}
|
}
|
||||||
//添加缓存
|
//添加缓存
|
||||||
if len(data) == 1 {
|
if len(data) == 1 {
|
||||||
|
|
||||||
if that.TimeOut == 0 {
|
if that.TimeOut == 0 {
|
||||||
//that.Time = Config.GetInt64("cacheShortTime")
|
//that.Time = Config.GetInt64("cacheShortTime")
|
||||||
}
|
}
|
||||||
|
|
||||||
tim += that.TimeOut
|
tim += that.TimeOut
|
||||||
}
|
}
|
||||||
if len(data) == 2 {
|
if len(data) == 2 {
|
||||||
that.Error.SetError(nil)
|
that.Error.SetError(nil)
|
||||||
tempt := ObjToInt64(data[1], that.Error)
|
tempt := ObjToInt64(data[1], that.Error)
|
||||||
if tempt > tim {
|
if tempt > tim {
|
||||||
|
|
||||||
tim = tempt
|
tim = tempt
|
||||||
} else if that.GetError() == nil {
|
} else if that.GetError() == nil {
|
||||||
|
|
||||||
tim = tim + tempt
|
tim = tim + tempt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,4 +166,5 @@ func (that *CacheRedis) Cache(key string, data ...interface{}) *Obj {
|
|||||||
that.set(key, ObjToStr(data[0]), tim)
|
that.set(key, ObjToStr(data[0]), tim)
|
||||||
|
|
||||||
return reData
|
return reData
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
177
code/config.go
177
code/config.go
@ -8,15 +8,6 @@ var Config = Map{
|
|||||||
"name": "HoTimeDashBoard",
|
"name": "HoTimeDashBoard",
|
||||||
//"id": "2f92h3herh23rh2y8",
|
//"id": "2f92h3herh23rh2y8",
|
||||||
"label": "HoTime管理平台",
|
"label": "HoTime管理平台",
|
||||||
"stop": Slice{"role", "org"}, //不更新的,同时不允许修改用户自身对应的表数据
|
|
||||||
"labelConfig": Map{
|
|
||||||
"show": "开启",
|
|
||||||
"add": "添加",
|
|
||||||
"delete": "删除",
|
|
||||||
"edit": "编辑",
|
|
||||||
"info": "查看详情",
|
|
||||||
"download": "下载清单",
|
|
||||||
},
|
|
||||||
"menus": []Map{
|
"menus": []Map{
|
||||||
//{"label": "平台首页", "name": "HelloWorld", "icon": "el-icon-s-home"},
|
//{"label": "平台首页", "name": "HelloWorld", "icon": "el-icon-s-home"},
|
||||||
//{"label": "测试表格", "table": "table", "icon": "el-icon-suitcase"},
|
//{"label": "测试表格", "table": "table", "icon": "el-icon-suitcase"},
|
||||||
@ -59,130 +50,62 @@ var ColumnDataType = map[string]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ColumnShow struct {
|
type ColumnShow struct {
|
||||||
Name string //名称
|
Name string
|
||||||
|
List bool
|
||||||
List bool //列表权限
|
Edit bool
|
||||||
Edit bool //新增和编辑权限
|
Info bool
|
||||||
Info bool //详情权限
|
Must bool
|
||||||
Must bool //字段全匹配
|
|
||||||
Type string //空字符串表示
|
Type string //空字符串表示
|
||||||
Strict bool //name严格匹配必须是这个词才行
|
Strict bool //name严格匹配必须是这个词才行
|
||||||
}
|
}
|
||||||
|
|
||||||
var RuleConfig = []Map{
|
var ColumnNameType = []ColumnShow{
|
||||||
{"name": "idcard", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
//通用
|
||||||
{"name": "id", "add": false, "list": true, "edit": false, "info": true, "must": false, "strict": true, "type": ""},
|
{"idcard", false, true, true, false, "", false},
|
||||||
{"name": "sn", "add": false, "list": true, "edit": false, "info": true, "must": false, "strict": false, "type": ""},
|
{"id", true, false, true, false, "", true},
|
||||||
{"name": "parent_ids", "add": false, "list": false, "edit": false, "info": false, "must": false, "strict": true, "type": "index"},
|
{"sn", true, false, true, false, "", false},
|
||||||
{"name": "index", "add": false, "list": false, "edit": false, "info": false, "must": false, "strict": true, "type": "index"},
|
{"parent_ids", false, false, false, false, "index", true},
|
||||||
|
{"parent_id", true, true, true, false, "", true},
|
||||||
|
{"amount", true, true, true, false, "money", true},
|
||||||
|
{"info", false, true, true, false, "textArea", false},
|
||||||
|
//"sn"{true,true,true,""},
|
||||||
|
{"status", true, true, true, false, "select", false},
|
||||||
|
{"state", true, true, true, false, "select", false},
|
||||||
|
{"sex", true, true, true, false, "select", false},
|
||||||
|
{"delete", false, false, false, false, "", false},
|
||||||
|
|
||||||
{"name": "parent_id", "add": true, "list": true, "edit": true, "info": true, "must": false, "true": false, "type": ""},
|
{"lat", false, true, true, false, "", false},
|
||||||
|
{"lng", false, true, true, false, "", false},
|
||||||
|
{"latitude", false, true, true, false, "", false},
|
||||||
|
{"longitude", false, true, true, false, "", false},
|
||||||
|
|
||||||
{"name": "amount", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": true, "type": "money"},
|
{"index", false, false, false, false, "index", false},
|
||||||
|
|
||||||
{"name": "info", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "textArea"},
|
{"password", false, true, false, false, "password", false},
|
||||||
|
{"pwd", false, true, false, false, "password", false},
|
||||||
|
|
||||||
{"name": "status", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": "select"},
|
{"version", false, false, false, false, "", false},
|
||||||
{"name": "state", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": "select"},
|
{"seq", false, true, true, false, "", false},
|
||||||
{"name": "sex", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": "select"},
|
{"sort", false, true, true, false, "", false},
|
||||||
|
{"note", false, true, true, false, "", false},
|
||||||
{"name": "delete", "add": false, "list": false, "edit": false, "info": false, "must": false, "strict": false, "type": ""},
|
{"description", false, true, true, false, "", false},
|
||||||
|
{"abstract", false, true, true, false, "", false},
|
||||||
{"name": "lat", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
{"content", false, true, true, false, "textArea", false},
|
||||||
{"name": "lng", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
{"address", true, true, true, false, "", false},
|
||||||
|
{"full_name", false, true, true, false, "", false},
|
||||||
{"name": "latitude", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
{"create_time", false, false, true, false, "time", true},
|
||||||
{"name": "longitude", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
{"modify_time", true, false, true, false, "time", true},
|
||||||
|
{"image", false, true, true, false, "image", false},
|
||||||
{"name": "password", "add": true, "list": false, "edit": true, "info": false, "must": false, "strict": false, "type": "password"},
|
{"img", false, true, true, false, "image", false},
|
||||||
{"name": "pwd", "add": true, "list": false, "edit": true, "info": false, "must": false, "strict": false, "type": "password"},
|
{"icon", false, true, true, false, "image", false},
|
||||||
|
{"avatar", false, true, true, false, "image", false},
|
||||||
{"name": "version", "add": false, "list": false, "edit": false, "info": false, "must": false, "strict": false, "type": ""},
|
{"file", false, true, true, false, "file", false},
|
||||||
{"name": "seq", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
{"age", false, true, true, false, "", false},
|
||||||
|
{"email", false, true, true, false, "", false},
|
||||||
{"name": "sort", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
{"time", true, true, true, false, "time", false},
|
||||||
{"name": "note", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
{"level", false, false, true, false, "", false},
|
||||||
|
{"rule", true, true, true, false, "form", false},
|
||||||
{"name": "description", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
{"auth", false, true, true, false, "auth", true},
|
||||||
|
{"table", true, false, true, false, "table", false},
|
||||||
{"name": "abstract", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
{"table_id", true, false, true, false, "table_id", false},
|
||||||
|
|
||||||
{"name": "content", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "textArea"},
|
|
||||||
|
|
||||||
{"name": "address", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
|
||||||
{"name": "full_name", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
|
||||||
|
|
||||||
{"name": "create_time", "add": false, "list": false, "edit": false, "info": true, "must": false, "strict": true, "type": "time"},
|
|
||||||
{"name": "modify_time", "add": false, "list": true, "edit": false, "info": true, "must": false, "strict": true, "type": "time"},
|
|
||||||
|
|
||||||
{"name": "image", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "image"},
|
|
||||||
|
|
||||||
{"name": "img", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "image"},
|
|
||||||
{"name": "avatar", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "image"},
|
|
||||||
{"name": "icon", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "image"},
|
|
||||||
|
|
||||||
{"name": "file", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "file"},
|
|
||||||
|
|
||||||
{"name": "age", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
|
||||||
{"name": "email", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
|
|
||||||
{"name": "time", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": "time"},
|
|
||||||
|
|
||||||
{"name": "level", "add": false, "list": false, "edit": false, "info": true, "must": false, "strict": false, "type": ""},
|
|
||||||
{"name": "rule", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": "form"},
|
|
||||||
|
|
||||||
{"name": "auth", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "auth"},
|
|
||||||
|
|
||||||
{"name": "table", "add": false, "list": true, "edit": false, "info": true, "must": false, "strict": false, "type": "table"},
|
|
||||||
{"name": "table_id", "add": false, "list": true, "edit": false, "info": true, "must": false, "strict": false, "type": "table_id"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//var ColumnNameType = []ColumnShow{
|
|
||||||
// //通用
|
|
||||||
// {"idcard", false, true, true, false, "", false},
|
|
||||||
// {"id", true, false, true, false, "", true},
|
|
||||||
// {"sn", true, false, true, false, "", false},
|
|
||||||
// {"parent_ids", false, false, false, false, "index", true},
|
|
||||||
// {"parent_id", true, true, true, false, "", true},
|
|
||||||
// {"amount", true, true, true, false, "money", true},
|
|
||||||
// {"info", false, true, true, false, "textArea", false},
|
|
||||||
// //"sn"{true,true,true,""},
|
|
||||||
// {"status", true, true, true, false, "select", false},
|
|
||||||
// {"state", true, true, true, false, "select", false},
|
|
||||||
// {"sex", true, true, true, false, "select", false},
|
|
||||||
// {"delete", false, false, false, false, "", false},
|
|
||||||
//
|
|
||||||
// {"lat", false, true, true, false, "", false},
|
|
||||||
// {"lng", false, true, true, false, "", false},
|
|
||||||
// {"latitude", false, true, true, false, "", false},
|
|
||||||
// {"longitude", false, true, true, false, "", false},
|
|
||||||
//
|
|
||||||
// {"index", false, false, false, false, "index", false},
|
|
||||||
//
|
|
||||||
// {"password", false, true, false, false, "password", false},
|
|
||||||
// {"pwd", false, true, false, false, "password", false},
|
|
||||||
//
|
|
||||||
// {"version", false, false, false, false, "", false},
|
|
||||||
// {"seq", false, true, true, false, "", false},
|
|
||||||
// {"sort", false, true, true, false, "", false},
|
|
||||||
// {"note", false, true, true, false, "", false},
|
|
||||||
// {"description", false, true, true, false, "", false},
|
|
||||||
// {"abstract", false, true, true, false, "", false},
|
|
||||||
// {"content", false, true, true, false, "textArea", false},
|
|
||||||
// {"address", true, true, true, false, "", false},
|
|
||||||
// {"full_name", false, true, true, false, "", false},
|
|
||||||
// {"create_time", false, false, true, false, "time", true},
|
|
||||||
// {"modify_time", true, false, true, false, "time", true},
|
|
||||||
// {"image", false, true, true, false, "image", false},
|
|
||||||
// {"img", false, true, true, false, "image", false},
|
|
||||||
// {"icon", false, true, true, false, "image", false},
|
|
||||||
// {"avatar", false, true, true, false, "image", false},
|
|
||||||
// {"file", false, true, true, false, "file", false},
|
|
||||||
// {"age", false, true, true, false, "", false},
|
|
||||||
// {"email", false, true, true, false, "", false},
|
|
||||||
// {"time", true, true, true, false, "time", false},
|
|
||||||
// {"level", false, false, true, false, "", false},
|
|
||||||
// {"rule", true, true, true, false, "form", false},
|
|
||||||
// {"auth", false, true, true, false, "auth", true},
|
|
||||||
// {"table", true, false, true, false, "table", false},
|
|
||||||
// {"table_id", true, false, true, false, "table_id", false},
|
|
||||||
//}
|
|
||||||
|
|||||||
711
code/makecode.go
711
code/makecode.go
File diff suppressed because it is too large
Load Diff
@ -2,28 +2,26 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error 框架层处理错误
|
// Error 框架层处理错误
|
||||||
type Error struct {
|
type Error struct {
|
||||||
Logger *logrus.Logger
|
Logger *logrus.Logger
|
||||||
error
|
error
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (that *Error) GetError() error {
|
func (that *Error) GetError() error {
|
||||||
that.mu.RLock()
|
|
||||||
defer that.mu.RUnlock()
|
|
||||||
return that.error
|
return that.error
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (that *Error) SetError(err error) {
|
func (that *Error) SetError(err error) {
|
||||||
that.mu.Lock()
|
|
||||||
that.error = err
|
that.error = err
|
||||||
that.mu.Unlock()
|
|
||||||
|
|
||||||
if that.Logger != nil && err != nil {
|
if that.Logger != nil && err != nil {
|
||||||
|
//that.Logger=log.GetLog("",false)
|
||||||
that.Logger.Warn(err)
|
that.Logger.Warn(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//安全锁
|
//安全锁
|
||||||
@ -37,42 +36,6 @@ func StrFirstToUpper(str string) string {
|
|||||||
return strings.ToUpper(first) + other
|
return strings.ToUpper(first) + other
|
||||||
}
|
}
|
||||||
|
|
||||||
// 时间转字符串,第二个参数支持1-5对应显示年月日时分秒
|
|
||||||
func Time2Str(t time.Time, qu ...interface{}) string {
|
|
||||||
if t.Unix() < 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
tp := 5
|
|
||||||
if len(qu) != 0 {
|
|
||||||
tp = (qu[0]).(int)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tp {
|
|
||||||
case 1:
|
|
||||||
return t.Format("2006-01")
|
|
||||||
case 2:
|
|
||||||
return t.Format("2006-01-02")
|
|
||||||
case 3:
|
|
||||||
return t.Format("2006-01-02 15")
|
|
||||||
case 4:
|
|
||||||
return t.Format("2006-01-02 15:04")
|
|
||||||
case 5:
|
|
||||||
return t.Format("2006-01-02 15:04:05")
|
|
||||||
case 12:
|
|
||||||
return t.Format("01-02")
|
|
||||||
case 14:
|
|
||||||
return t.Format("01-02 15:04")
|
|
||||||
case 15:
|
|
||||||
return t.Format("01-02 15:04:05")
|
|
||||||
case 34:
|
|
||||||
return t.Format("15:04")
|
|
||||||
case 35:
|
|
||||||
return t.Format("15:04:05")
|
|
||||||
}
|
|
||||||
return t.Format("2006-01-02 15:04:05")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrLd 相似度计算 ld compares two strings and returns the levenshtein distance between them.
|
// StrLd 相似度计算 ld compares two strings and returns the levenshtein distance between them.
|
||||||
func StrLd(s, t string, ignoreCase bool) int {
|
func StrLd(s, t string, ignoreCase bool) int {
|
||||||
if ignoreCase {
|
if ignoreCase {
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -104,22 +103,7 @@ func ObjToTime(obj interface{}, e ...*Error) *time.Time {
|
|||||||
//字符串类型,只支持标准mysql datetime格式
|
//字符串类型,只支持标准mysql datetime格式
|
||||||
if tInt == 0 {
|
if tInt == 0 {
|
||||||
tStr := ObjToStr(obj)
|
tStr := ObjToStr(obj)
|
||||||
timeNewStr := ""
|
|
||||||
timeNewStrs := strings.Split(tStr, "-")
|
|
||||||
for _, v := range timeNewStrs {
|
|
||||||
if v == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(v) == 1 {
|
|
||||||
v = "0" + v
|
|
||||||
}
|
|
||||||
if timeNewStr == "" {
|
|
||||||
timeNewStr = v
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
timeNewStr = timeNewStr + "-" + v
|
|
||||||
}
|
|
||||||
tStr = timeNewStr
|
|
||||||
if len(tStr) > 18 {
|
if len(tStr) > 18 {
|
||||||
t, e := time.Parse("2006-01-02 15:04:05", tStr)
|
t, e := time.Parse("2006-01-02 15:04:05", tStr)
|
||||||
if e == nil {
|
if e == nil {
|
||||||
@ -151,23 +135,19 @@ func ObjToTime(obj interface{}, e ...*Error) *time.Time {
|
|||||||
|
|
||||||
//纳秒级别
|
//纳秒级别
|
||||||
if len(ObjToStr(tInt)) > 16 {
|
if len(ObjToStr(tInt)) > 16 {
|
||||||
//t := time.Time{}.Add(time.Nanosecond * time.Duration(tInt))
|
t := time.Time{}.Add(time.Nanosecond * time.Duration(tInt))
|
||||||
t := time.UnixMicro(tInt / 1000)
|
|
||||||
return &t
|
return &t
|
||||||
//微秒级别
|
//微秒级别
|
||||||
} else if len(ObjToStr(tInt)) > 13 {
|
} else if len(ObjToStr(tInt)) > 13 {
|
||||||
//t := time.Time{}.Add(time.Microsecond * time.Duration(tInt))
|
t := time.Time{}.Add(time.Microsecond * time.Duration(tInt))
|
||||||
t := time.UnixMicro(tInt)
|
|
||||||
return &t
|
return &t
|
||||||
//毫秒级别
|
//毫秒级别
|
||||||
} else if len(ObjToStr(tInt)) > 10 {
|
} else if len(ObjToStr(tInt)) > 10 {
|
||||||
//t := time.Time{}.Add(time.Millisecond * time.Duration(tInt))
|
t := time.Time{}.Add(time.Millisecond * time.Duration(tInt))
|
||||||
t := time.UnixMilli(tInt)
|
|
||||||
return &t
|
return &t
|
||||||
//秒级别
|
//秒级别
|
||||||
} else if len(ObjToStr(tInt)) > 9 {
|
} else if len(ObjToStr(tInt)) > 9 {
|
||||||
//t := time.Time{}.Add(time.Second * time.Duration(tInt))
|
t := time.Time{}.Add(time.Second * time.Duration(tInt))
|
||||||
t := time.Unix(tInt, 0)
|
|
||||||
return &t
|
return &t
|
||||||
} else if len(ObjToStr(tInt)) > 3 {
|
} else if len(ObjToStr(tInt)) > 3 {
|
||||||
t, e := time.Parse("2006", ObjToStr(tInt))
|
t, e := time.Parse("2006", ObjToStr(tInt))
|
||||||
|
|||||||
124
context.go
124
context.go
@ -1,17 +1,12 @@
|
|||||||
package hotime
|
package hotime
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
. "code.hoteas.com/golang/hotime/cache"
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
. "code.hoteas.com/golang/hotime/db"
|
. "code.hoteas.com/golang/hotime/db"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Context struct {
|
type Context struct {
|
||||||
@ -24,14 +19,10 @@ type Context struct {
|
|||||||
Db *HoTimeDB
|
Db *HoTimeDB
|
||||||
RespData Map
|
RespData Map
|
||||||
RespFunc func()
|
RespFunc func()
|
||||||
//CacheIns
|
CacheIns
|
||||||
SessionIns
|
SessionIns
|
||||||
DataSize int
|
DataSize int
|
||||||
HandlerStr string //复写请求url
|
HandlerStr string //复写请求url
|
||||||
|
|
||||||
// 请求参数缓存
|
|
||||||
reqJsonCache Map // JSON Body 缓存
|
|
||||||
reqMu sync.Once // 确保 JSON 只解析一次
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mtd 唯一标志
|
// Mtd 唯一标志
|
||||||
@ -84,19 +75,6 @@ func (that *Context) View() {
|
|||||||
if that.Session("user_id").Data != nil {
|
if that.Session("user_id").Data != nil {
|
||||||
that.Log["user_id"] = that.Session("user_id").ToCeilInt()
|
that.Log["user_id"] = that.Session("user_id").ToCeilInt()
|
||||||
}
|
}
|
||||||
//负载均衡优化
|
|
||||||
ipStr := ""
|
|
||||||
if that.Req.Header.Get("X-Forwarded-For") != "" {
|
|
||||||
ipStr = that.Req.Header.Get("X-Forwarded-For")
|
|
||||||
} else if that.Req.Header.Get("X-Real-IP") != "" {
|
|
||||||
ipStr = that.Req.Header.Get("X-Real-IP")
|
|
||||||
}
|
|
||||||
//负载均衡优化
|
|
||||||
if ipStr == "" {
|
|
||||||
//RemoteAddr := that.Req.RemoteAddr
|
|
||||||
ipStr = Substr(that.Req.RemoteAddr, 0, strings.Index(that.Req.RemoteAddr, ":"))
|
|
||||||
}
|
|
||||||
that.Log["ip"] = ipStr
|
|
||||||
that.Db.Insert("logs", that.Log)
|
that.Db.Insert("logs", that.Log)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,97 +87,5 @@ func (that *Context) View() {
|
|||||||
that.DataSize = len(d)
|
that.DataSize = len(d)
|
||||||
that.RespData = nil
|
that.RespData = nil
|
||||||
that.Resp.Write(d)
|
that.Resp.Write(d)
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 请求参数获取方法 ====================
|
|
||||||
|
|
||||||
// ReqParam 获取 URL 查询参数,返回 *Obj 支持链式调用
|
|
||||||
// 用法: that.ReqParam("id").ToInt()
|
|
||||||
func (that *Context) ReqParam(key string) *Obj {
|
|
||||||
v := that.Req.URL.Query().Get(key)
|
|
||||||
if v == "" {
|
|
||||||
return &Obj{Data: nil}
|
|
||||||
}
|
|
||||||
return &Obj{Data: v}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReqForm 获取表单参数,返回 *Obj 支持链式调用
|
|
||||||
// 用法: that.ReqForm("name").ToStr()
|
|
||||||
func (that *Context) ReqForm(key string) *Obj {
|
|
||||||
v := that.Req.FormValue(key)
|
|
||||||
if v == "" {
|
|
||||||
return &Obj{Data: nil}
|
|
||||||
}
|
|
||||||
return &Obj{Data: v}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReqJson 获取 JSON Body 中的字段,返回 *Obj 支持链式调用
|
|
||||||
// 用法: that.ReqJson("data").ToMap()
|
|
||||||
func (that *Context) ReqJson(key string) *Obj {
|
|
||||||
that.parseJsonBody()
|
|
||||||
if that.reqJsonCache == nil {
|
|
||||||
return &Obj{Data: nil}
|
|
||||||
}
|
|
||||||
return &Obj{Data: that.reqJsonCache.Get(key)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseJsonBody 解析 JSON Body(只解析一次,并发安全)
|
|
||||||
func (that *Context) parseJsonBody() {
|
|
||||||
that.reqMu.Do(func() {
|
|
||||||
if that.Req.Body == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
body, err := io.ReadAll(that.Req.Body)
|
|
||||||
if err != nil || len(body) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 恢复 Body 以便后续代码可以再次读取
|
|
||||||
that.Req.Body = io.NopCloser(bytes.NewBuffer(body))
|
|
||||||
that.reqJsonCache = ObjToMap(string(body))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReqData 统一获取请求参数,优先级: JSON > Form > URL
|
|
||||||
// 用法: that.ReqData("id").ToInt()
|
|
||||||
func (that *Context) ReqData(key string) *Obj {
|
|
||||||
// 1. 优先从 JSON Body 获取
|
|
||||||
that.parseJsonBody()
|
|
||||||
if that.reqJsonCache != nil {
|
|
||||||
if v := that.reqJsonCache.Get(key); v != nil {
|
|
||||||
return &Obj{Data: v}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 2. 其次从 Form 获取
|
|
||||||
if v := that.Req.FormValue(key); v != "" {
|
|
||||||
return &Obj{Data: v}
|
|
||||||
}
|
|
||||||
// 3. 最后从 URL 参数获取
|
|
||||||
if v := that.Req.URL.Query().Get(key); v != "" {
|
|
||||||
return &Obj{Data: v}
|
|
||||||
}
|
|
||||||
return &Obj{Data: nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReqFile 获取单个上传文件
|
|
||||||
// 用法: file, header, err := that.ReqFile("avatar")
|
|
||||||
func (that *Context) ReqFile(name string) (multipart.File, *multipart.FileHeader, error) {
|
|
||||||
return that.Req.FormFile(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReqFiles 获取多个同名上传文件(批量上传场景)
|
|
||||||
// 用法: files, err := that.ReqFiles("images")
|
|
||||||
func (that *Context) ReqFiles(name string) ([]*multipart.FileHeader, error) {
|
|
||||||
if that.Req.MultipartForm == nil {
|
|
||||||
if err := that.Req.ParseMultipartForm(32 << 20); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if that.Req.MultipartForm == nil || that.Req.MultipartForm.File == nil {
|
|
||||||
return nil, http.ErrMissingFile
|
|
||||||
}
|
|
||||||
files, ok := that.Req.MultipartForm.File[name]
|
|
||||||
if !ok {
|
|
||||||
return nil, http.ErrMissingFile
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|||||||
115
db/aggregate.go
115
db/aggregate.go
@ -1,115 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Count 计数
|
|
||||||
func (that *HoTimeDB) Count(table string, qu ...interface{}) int {
|
|
||||||
var req = []interface{}{}
|
|
||||||
if len(qu) == 2 {
|
|
||||||
req = append(req, qu[0])
|
|
||||||
req = append(req, "COUNT(*)")
|
|
||||||
req = append(req, qu[1])
|
|
||||||
} else {
|
|
||||||
req = append(req, "COUNT(*)")
|
|
||||||
req = append(req, qu...)
|
|
||||||
}
|
|
||||||
|
|
||||||
data := that.Select(table, req...)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
res := ObjToStr(data[0]["COUNT(*)"])
|
|
||||||
count, _ := StrToInt(res)
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sum 求和
|
|
||||||
func (that *HoTimeDB) Sum(table string, column string, qu ...interface{}) float64 {
|
|
||||||
var req = []interface{}{}
|
|
||||||
if len(qu) == 2 {
|
|
||||||
req = append(req, qu[0])
|
|
||||||
req = append(req, "SUM("+column+")")
|
|
||||||
req = append(req, qu[1])
|
|
||||||
} else {
|
|
||||||
req = append(req, "SUM("+column+")")
|
|
||||||
req = append(req, qu...)
|
|
||||||
}
|
|
||||||
|
|
||||||
data := that.Select(table, req...)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
res := ObjToStr(data[0]["SUM("+column+")"])
|
|
||||||
sum := ObjToFloat64(res)
|
|
||||||
return sum
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avg 平均值
|
|
||||||
func (that *HoTimeDB) Avg(table string, column string, qu ...interface{}) float64 {
|
|
||||||
var req = []interface{}{}
|
|
||||||
if len(qu) == 2 {
|
|
||||||
req = append(req, qu[0])
|
|
||||||
req = append(req, "AVG("+column+")")
|
|
||||||
req = append(req, qu[1])
|
|
||||||
} else {
|
|
||||||
req = append(req, "AVG("+column+")")
|
|
||||||
req = append(req, qu...)
|
|
||||||
}
|
|
||||||
|
|
||||||
data := that.Select(table, req...)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
res := ObjToStr(data[0]["AVG("+column+")"])
|
|
||||||
avg := ObjToFloat64(res)
|
|
||||||
return avg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max 最大值
|
|
||||||
func (that *HoTimeDB) Max(table string, column string, qu ...interface{}) float64 {
|
|
||||||
var req = []interface{}{}
|
|
||||||
if len(qu) == 2 {
|
|
||||||
req = append(req, qu[0])
|
|
||||||
req = append(req, "MAX("+column+")")
|
|
||||||
req = append(req, qu[1])
|
|
||||||
} else {
|
|
||||||
req = append(req, "MAX("+column+")")
|
|
||||||
req = append(req, qu...)
|
|
||||||
}
|
|
||||||
|
|
||||||
data := that.Select(table, req...)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
res := ObjToStr(data[0]["MAX("+column+")"])
|
|
||||||
max := ObjToFloat64(res)
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
|
|
||||||
// Min 最小值
|
|
||||||
func (that *HoTimeDB) Min(table string, column string, qu ...interface{}) float64 {
|
|
||||||
var req = []interface{}{}
|
|
||||||
if len(qu) == 2 {
|
|
||||||
req = append(req, qu[0])
|
|
||||||
req = append(req, "MIN("+column+")")
|
|
||||||
req = append(req, qu[1])
|
|
||||||
} else {
|
|
||||||
req = append(req, "MIN("+column+")")
|
|
||||||
req = append(req, qu...)
|
|
||||||
}
|
|
||||||
|
|
||||||
data := that.Select(table, req...)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
res := ObjToStr(data[0]["MIN("+column+")"])
|
|
||||||
min := ObjToFloat64(res)
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
93
db/backup.go
93
db/backup.go
@ -1,93 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// backupSave 保存备份
|
|
||||||
// path: 备份文件路径
|
|
||||||
// tt: 表名
|
|
||||||
// code: 备份类型 0=全部, 1=仅数据, 2=仅DDL
|
|
||||||
func (that *HoTimeDB) backupSave(path string, tt string, code int) {
|
|
||||||
fd, _ := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
|
||||||
defer fd.Close()
|
|
||||||
|
|
||||||
str := "\r\n"
|
|
||||||
if code == 0 || code == 2 {
|
|
||||||
str += that.backupDdl(tt)
|
|
||||||
}
|
|
||||||
|
|
||||||
if code == 0 || code == 1 {
|
|
||||||
str += "insert into `" + tt + "`\r\n\r\n("
|
|
||||||
str += that.backupCol(tt)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = fd.Write([]byte(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
// backupDdl 备份表结构(DDL)
|
|
||||||
func (that *HoTimeDB) backupDdl(tt string) string {
|
|
||||||
data := that.Query("show create table " + tt)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return ObjToStr(data[0]["Create Table"]) + ";\r\n\r\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
// backupCol 备份表数据
|
|
||||||
func (that *HoTimeDB) backupCol(tt string) string {
|
|
||||||
str := ""
|
|
||||||
data := that.Select(tt, "*")
|
|
||||||
|
|
||||||
lthData := len(data)
|
|
||||||
|
|
||||||
if lthData == 0 {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
lthCol := len(data[0])
|
|
||||||
col := make([]string, lthCol)
|
|
||||||
tempLthData := 0
|
|
||||||
|
|
||||||
for k := range data[0] {
|
|
||||||
if tempLthData == lthCol-1 {
|
|
||||||
str += "`" + k + "`) "
|
|
||||||
} else {
|
|
||||||
str += "`" + k + "`,"
|
|
||||||
}
|
|
||||||
col[tempLthData] = k
|
|
||||||
tempLthData++
|
|
||||||
}
|
|
||||||
|
|
||||||
str += " values"
|
|
||||||
|
|
||||||
for j := 0; j < lthData; j++ {
|
|
||||||
for m := 0; m < lthCol; m++ {
|
|
||||||
if m == 0 {
|
|
||||||
str += "("
|
|
||||||
}
|
|
||||||
|
|
||||||
v := "NULL"
|
|
||||||
if data[j][col[m]] != nil {
|
|
||||||
v = "'" + strings.Replace(ObjToStr(data[j][col[m]]), "'", `\'`, -1) + "'"
|
|
||||||
}
|
|
||||||
|
|
||||||
if m == lthCol-1 {
|
|
||||||
str += v + ") "
|
|
||||||
} else {
|
|
||||||
str += v + ","
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if j == lthData-1 {
|
|
||||||
str += ";\r\n\r\n"
|
|
||||||
} else {
|
|
||||||
str += ",\r\n\r\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
312
db/builder.go
312
db/builder.go
@ -1,312 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HotimeDBBuilder 链式查询构建器
|
|
||||||
type HotimeDBBuilder struct {
|
|
||||||
HoTimeDB *HoTimeDB
|
|
||||||
table string
|
|
||||||
selects []interface{}
|
|
||||||
join Slice
|
|
||||||
where Map
|
|
||||||
lastWhere Map
|
|
||||||
page int
|
|
||||||
pageRow int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Table 创建链式查询构建器
|
|
||||||
func (that *HoTimeDB) Table(table string) *HotimeDBBuilder {
|
|
||||||
return &HotimeDBBuilder{HoTimeDB: that, table: table, where: Map{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 获取单条记录
|
|
||||||
func (that *HotimeDBBuilder) Get(qu ...interface{}) Map {
|
|
||||||
// 构建参数:根据是否有 JOIN 来决定参数结构
|
|
||||||
var args []interface{}
|
|
||||||
if len(that.join) > 0 {
|
|
||||||
// 有 JOIN 时:join, fields, where
|
|
||||||
if len(qu) > 0 {
|
|
||||||
args = append(args, that.join, qu[0], that.where)
|
|
||||||
} else {
|
|
||||||
args = append(args, that.join, "*", that.where)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 无 JOIN 时:fields, where
|
|
||||||
if len(qu) > 0 {
|
|
||||||
args = append(args, qu[0], that.where)
|
|
||||||
} else {
|
|
||||||
args = append(args, "*", that.where)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return that.HoTimeDB.Get(that.table, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count 统计数量
|
|
||||||
func (that *HotimeDBBuilder) Count() int {
|
|
||||||
// 构建参数:根据是否有 JOIN 来决定参数结构
|
|
||||||
if len(that.join) > 0 {
|
|
||||||
return that.HoTimeDB.Count(that.table, that.join, that.where)
|
|
||||||
}
|
|
||||||
return that.HoTimeDB.Count(that.table, that.where)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page 设置分页
|
|
||||||
func (that *HotimeDBBuilder) Page(page, pageRow int) *HotimeDBBuilder {
|
|
||||||
that.page = page
|
|
||||||
that.pageRow = pageRow
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select 查询多条记录
|
|
||||||
func (that *HotimeDBBuilder) Select(qu ...interface{}) []Map {
|
|
||||||
// 构建参数:根据是否有 JOIN 来决定参数结构
|
|
||||||
var args []interface{}
|
|
||||||
if len(that.join) > 0 {
|
|
||||||
// 有 JOIN 时:join, fields, where
|
|
||||||
if len(qu) > 0 {
|
|
||||||
args = append(args, that.join, qu[0], that.where)
|
|
||||||
} else {
|
|
||||||
args = append(args, that.join, "*", that.where)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 无 JOIN 时:fields, where
|
|
||||||
if len(qu) > 0 {
|
|
||||||
args = append(args, qu[0], that.where)
|
|
||||||
} else {
|
|
||||||
args = append(args, "*", that.where)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if that.page != 0 {
|
|
||||||
return that.HoTimeDB.Page(that.page, that.pageRow).PageSelect(that.table, args...)
|
|
||||||
}
|
|
||||||
return that.HoTimeDB.Select(that.table, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update 更新记录
|
|
||||||
func (that *HotimeDBBuilder) Update(qu ...interface{}) int64 {
|
|
||||||
lth := len(qu)
|
|
||||||
if lth == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
data := Map{}
|
|
||||||
if lth == 1 {
|
|
||||||
data = ObjToMap(qu[0])
|
|
||||||
}
|
|
||||||
if lth > 1 {
|
|
||||||
for k := 1; k < lth; k++ {
|
|
||||||
data[ObjToStr(qu[k-1])] = qu[k]
|
|
||||||
k++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return that.HoTimeDB.Update(that.table, data, that.where)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete 删除记录
|
|
||||||
func (that *HotimeDBBuilder) Delete() int64 {
|
|
||||||
return that.HoTimeDB.Delete(that.table, that.where)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LeftJoin 左连接
|
|
||||||
func (that *HotimeDBBuilder) LeftJoin(table, joinStr string) *HotimeDBBuilder {
|
|
||||||
that.Join(Map{"[>]" + table: joinStr})
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// RightJoin 右连接
|
|
||||||
func (that *HotimeDBBuilder) RightJoin(table, joinStr string) *HotimeDBBuilder {
|
|
||||||
that.Join(Map{"[<]" + table: joinStr})
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// InnerJoin 内连接
|
|
||||||
func (that *HotimeDBBuilder) InnerJoin(table, joinStr string) *HotimeDBBuilder {
|
|
||||||
that.Join(Map{"[><]" + table: joinStr})
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// FullJoin 全连接
|
|
||||||
func (that *HotimeDBBuilder) FullJoin(table, joinStr string) *HotimeDBBuilder {
|
|
||||||
that.Join(Map{"[<>]" + table: joinStr})
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join 通用连接
|
|
||||||
func (that *HotimeDBBuilder) Join(qu ...interface{}) *HotimeDBBuilder {
|
|
||||||
lth := len(qu)
|
|
||||||
if lth == 0 {
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
data := Map{}
|
|
||||||
if lth == 1 {
|
|
||||||
data = ObjToMap(qu[0])
|
|
||||||
}
|
|
||||||
if lth > 1 {
|
|
||||||
for k := 1; k < lth; k++ {
|
|
||||||
data[ObjToStr(qu[k-1])] = qu[k]
|
|
||||||
k++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if that.join == nil {
|
|
||||||
that.join = Slice{}
|
|
||||||
}
|
|
||||||
if data == nil {
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
that.join = append(that.join, data)
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// And 添加 AND 条件
|
|
||||||
func (that *HotimeDBBuilder) And(qu ...interface{}) *HotimeDBBuilder {
|
|
||||||
lth := len(qu)
|
|
||||||
if lth == 0 {
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
var where Map
|
|
||||||
if lth == 1 {
|
|
||||||
where = ObjToMap(qu[0])
|
|
||||||
}
|
|
||||||
if lth > 1 {
|
|
||||||
where = Map{}
|
|
||||||
for k := 1; k < lth; k++ {
|
|
||||||
where[ObjToStr(qu[k-1])] = qu[k]
|
|
||||||
k++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if where == nil {
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
if that.lastWhere != nil {
|
|
||||||
that.lastWhere["AND"] = where
|
|
||||||
that.lastWhere = where
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
that.lastWhere = where
|
|
||||||
that.where = Map{"AND": where}
|
|
||||||
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// Or 添加 OR 条件
|
|
||||||
func (that *HotimeDBBuilder) Or(qu ...interface{}) *HotimeDBBuilder {
|
|
||||||
lth := len(qu)
|
|
||||||
if lth == 0 {
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
var where Map
|
|
||||||
if lth == 1 {
|
|
||||||
where = ObjToMap(qu[0])
|
|
||||||
}
|
|
||||||
if lth > 1 {
|
|
||||||
where = Map{}
|
|
||||||
for k := 1; k < lth; k++ {
|
|
||||||
where[ObjToStr(qu[k-1])] = qu[k]
|
|
||||||
k++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if where == nil {
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
if that.lastWhere != nil {
|
|
||||||
that.lastWhere["OR"] = where
|
|
||||||
that.lastWhere = where
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
that.lastWhere = where
|
|
||||||
that.where = Map{"Or": where}
|
|
||||||
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// Where 设置 WHERE 条件
|
|
||||||
func (that *HotimeDBBuilder) Where(qu ...interface{}) *HotimeDBBuilder {
|
|
||||||
lth := len(qu)
|
|
||||||
if lth == 0 {
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
var where Map
|
|
||||||
if lth == 1 {
|
|
||||||
where = ObjToMap(qu[0])
|
|
||||||
}
|
|
||||||
if lth > 1 {
|
|
||||||
where = Map{}
|
|
||||||
for k := 1; k < lth; k++ {
|
|
||||||
where[ObjToStr(qu[k-1])] = qu[k]
|
|
||||||
k++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if where == nil {
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
if that.lastWhere != nil {
|
|
||||||
that.lastWhere["AND"] = where
|
|
||||||
that.lastWhere = where
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
that.lastWhere = where
|
|
||||||
that.where = Map{"AND": that.lastWhere}
|
|
||||||
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// From 设置表名
|
|
||||||
func (that *HotimeDBBuilder) From(table string) *HotimeDBBuilder {
|
|
||||||
that.table = table
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order 设置排序
|
|
||||||
func (that *HotimeDBBuilder) Order(qu ...interface{}) *HotimeDBBuilder {
|
|
||||||
that.where["ORDER"] = ObjToSlice(qu)
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit 设置限制
|
|
||||||
func (that *HotimeDBBuilder) Limit(qu ...interface{}) *HotimeDBBuilder {
|
|
||||||
that.where["LIMIT"] = ObjToSlice(qu)
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group 设置分组
|
|
||||||
func (that *HotimeDBBuilder) Group(qu ...interface{}) *HotimeDBBuilder {
|
|
||||||
that.where["GROUP"] = ObjToSlice(qu)
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// Having 设置 HAVING 条件
|
|
||||||
func (that *HotimeDBBuilder) Having(qu ...interface{}) *HotimeDBBuilder {
|
|
||||||
lth := len(qu)
|
|
||||||
if lth == 0 {
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
var having Map
|
|
||||||
if lth == 1 {
|
|
||||||
having = ObjToMap(qu[0])
|
|
||||||
}
|
|
||||||
if lth > 1 {
|
|
||||||
having = Map{}
|
|
||||||
for k := 1; k < lth; k++ {
|
|
||||||
having[ObjToStr(qu[k-1])] = qu[k]
|
|
||||||
k++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
that.where["HAVING"] = having
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offset 设置偏移量
|
|
||||||
func (that *HotimeDBBuilder) Offset(offset int) *HotimeDBBuilder {
|
|
||||||
that.where["OFFSET"] = offset
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
679
db/crud.go
679
db/crud.go
@ -1,679 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Page 设置分页参数
|
|
||||||
// page: 页码(从1开始)
|
|
||||||
// pageRow: 每页数量
|
|
||||||
func (that *HoTimeDB) Page(page, pageRow int) *HoTimeDB {
|
|
||||||
if page < 1 {
|
|
||||||
page = 1
|
|
||||||
}
|
|
||||||
if pageRow < 1 {
|
|
||||||
pageRow = 10
|
|
||||||
}
|
|
||||||
offset := (page - 1) * pageRow
|
|
||||||
|
|
||||||
that.limitMu.Lock()
|
|
||||||
that.limit = Slice{offset, pageRow}
|
|
||||||
that.limitMu.Unlock()
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
|
|
||||||
// PageSelect 分页查询
|
|
||||||
func (that *HoTimeDB) PageSelect(table string, qu ...interface{}) []Map {
|
|
||||||
that.limitMu.Lock()
|
|
||||||
limit := that.limit
|
|
||||||
that.limit = nil // 使用后清空,避免影响下次调用
|
|
||||||
that.limitMu.Unlock()
|
|
||||||
|
|
||||||
if limit == nil {
|
|
||||||
return that.Select(table, qu...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据参数数量处理 LIMIT 注入
|
|
||||||
switch len(qu) {
|
|
||||||
case 0:
|
|
||||||
// PageSelect("user") -> 只有表名,添加 LIMIT
|
|
||||||
qu = append(qu, "*", Map{"LIMIT": limit})
|
|
||||||
case 1:
|
|
||||||
// PageSelect("user", "*") 或 PageSelect("user", Map{...})
|
|
||||||
if reflect.ValueOf(qu[0]).Kind() == reflect.Map {
|
|
||||||
// 是 where 条件
|
|
||||||
temp := DeepCopyMap(qu[0]).(Map)
|
|
||||||
temp["LIMIT"] = limit
|
|
||||||
qu[0] = temp
|
|
||||||
} else {
|
|
||||||
// 是字段选择
|
|
||||||
qu = append(qu, Map{"LIMIT": limit})
|
|
||||||
}
|
|
||||||
case 2:
|
|
||||||
// PageSelect("user", "*", Map{...}) 或 PageSelect("user", joinSlice, "*")
|
|
||||||
if reflect.ValueOf(qu[1]).Kind() == reflect.Map {
|
|
||||||
temp := DeepCopyMap(qu[1]).(Map)
|
|
||||||
temp["LIMIT"] = limit
|
|
||||||
qu[1] = temp
|
|
||||||
} else {
|
|
||||||
// join 模式,需要追加 where
|
|
||||||
qu = append(qu, Map{"LIMIT": limit})
|
|
||||||
}
|
|
||||||
case 3:
|
|
||||||
// PageSelect("user", joinSlice, "*", Map{...})
|
|
||||||
temp := DeepCopyMap(qu[2]).(Map)
|
|
||||||
temp["LIMIT"] = limit
|
|
||||||
qu[2] = temp
|
|
||||||
}
|
|
||||||
|
|
||||||
return that.Select(table, qu...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select 查询多条记录
|
|
||||||
func (that *HoTimeDB) Select(table string, qu ...interface{}) []Map {
|
|
||||||
query := "SELECT"
|
|
||||||
where := Map{}
|
|
||||||
qs := make([]interface{}, 0)
|
|
||||||
intQs, intWhere := 0, 1
|
|
||||||
join := false
|
|
||||||
|
|
||||||
if len(qu) == 3 {
|
|
||||||
intQs = 1
|
|
||||||
intWhere = 2
|
|
||||||
join = true
|
|
||||||
}
|
|
||||||
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
|
|
||||||
if len(qu) > 0 {
|
|
||||||
if reflect.ValueOf(qu[intQs]).Type().String() == "string" {
|
|
||||||
// 字段列表字符串,使用处理器处理 table.column 格式
|
|
||||||
fieldStr := qu[intQs].(string)
|
|
||||||
if fieldStr != "*" {
|
|
||||||
fieldStr = processor.ProcessFieldList(fieldStr)
|
|
||||||
}
|
|
||||||
query += " " + fieldStr
|
|
||||||
} else {
|
|
||||||
data := ObjToSlice(qu[intQs])
|
|
||||||
for i := 0; i < len(data); i++ {
|
|
||||||
k := data.GetString(i)
|
|
||||||
if strings.Contains(k, " AS ") || strings.Contains(k, ".") {
|
|
||||||
// 处理 table.column 格式
|
|
||||||
query += " " + processor.ProcessFieldList(k) + " "
|
|
||||||
} else {
|
|
||||||
// 单独的列名
|
|
||||||
query += " " + processor.ProcessColumnNoPrefix(k) + " "
|
|
||||||
}
|
|
||||||
|
|
||||||
if i+1 != len(data) {
|
|
||||||
query = query + ", "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query += " *"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理表名(添加前缀和正确的引号)
|
|
||||||
query += " FROM " + processor.ProcessTableName(table) + " "
|
|
||||||
|
|
||||||
if join {
|
|
||||||
query += that.buildJoin(qu[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(qu) > 1 {
|
|
||||||
where = qu[intWhere].(Map)
|
|
||||||
}
|
|
||||||
|
|
||||||
temp, resWhere := that.where(where)
|
|
||||||
|
|
||||||
query += temp + ";"
|
|
||||||
qs = append(qs, resWhere...)
|
|
||||||
md5 := that.md5(query, qs...)
|
|
||||||
|
|
||||||
if that.HoTimeCache != nil && table != "cached" {
|
|
||||||
// 如果缓存有则从缓存取
|
|
||||||
cacheData := that.HoTimeCache.Db(table + ":" + md5)
|
|
||||||
if cacheData != nil && cacheData.Data != nil {
|
|
||||||
return cacheData.ToMapArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无缓存则数据库取
|
|
||||||
res := that.Query(query, qs...)
|
|
||||||
|
|
||||||
if res == nil {
|
|
||||||
res = []Map{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存
|
|
||||||
if that.HoTimeCache != nil && table != "cached" {
|
|
||||||
_ = that.HoTimeCache.Db(table+":"+md5, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildJoin 构建 JOIN 语句
|
|
||||||
func (that *HoTimeDB) buildJoin(joinData interface{}) string {
|
|
||||||
query := ""
|
|
||||||
var testQu = []string{}
|
|
||||||
testQuData := Map{}
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
|
|
||||||
if reflect.ValueOf(joinData).Type().String() == "common.Map" {
|
|
||||||
testQuData = joinData.(Map)
|
|
||||||
for key := range testQuData {
|
|
||||||
testQu = append(testQu, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if reflect.ValueOf(joinData).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(joinData).Type().String(), "[]") {
|
|
||||||
qu0 := ObjToSlice(joinData)
|
|
||||||
for key := range qu0 {
|
|
||||||
v := qu0.GetMap(key)
|
|
||||||
for k1, v1 := range v {
|
|
||||||
testQu = append(testQu, k1)
|
|
||||||
testQuData[k1] = v1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(testQu)
|
|
||||||
|
|
||||||
for _, k := range testQu {
|
|
||||||
v := testQuData[k]
|
|
||||||
switch Substr(k, 0, 3) {
|
|
||||||
case "[>]":
|
|
||||||
func() {
|
|
||||||
table := Substr(k, 3, len(k)-3)
|
|
||||||
// 处理表名(添加前缀和正确的引号)
|
|
||||||
table = processor.ProcessTableName(table)
|
|
||||||
// 处理 ON 条件中的 table.column
|
|
||||||
onCondition := processor.ProcessConditionString(v.(string))
|
|
||||||
query += " LEFT JOIN " + table + " ON " + onCondition + " "
|
|
||||||
}()
|
|
||||||
case "[<]":
|
|
||||||
func() {
|
|
||||||
table := Substr(k, 3, len(k)-3)
|
|
||||||
table = processor.ProcessTableName(table)
|
|
||||||
onCondition := processor.ProcessConditionString(v.(string))
|
|
||||||
query += " RIGHT JOIN " + table + " ON " + onCondition + " "
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
switch Substr(k, 0, 4) {
|
|
||||||
case "[<>]":
|
|
||||||
func() {
|
|
||||||
table := Substr(k, 4, len(k)-4)
|
|
||||||
table = processor.ProcessTableName(table)
|
|
||||||
onCondition := processor.ProcessConditionString(v.(string))
|
|
||||||
query += " FULL JOIN " + table + " ON " + onCondition + " "
|
|
||||||
}()
|
|
||||||
case "[><]":
|
|
||||||
func() {
|
|
||||||
table := Substr(k, 4, len(k)-4)
|
|
||||||
table = processor.ProcessTableName(table)
|
|
||||||
onCondition := processor.ProcessConditionString(v.(string))
|
|
||||||
query += " INNER JOIN " + table + " ON " + onCondition + " "
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 获取单条记录
|
|
||||||
func (that *HoTimeDB) Get(table string, qu ...interface{}) Map {
|
|
||||||
if len(qu) == 0 {
|
|
||||||
// 没有参数时,添加默认字段和 LIMIT
|
|
||||||
qu = append(qu, "*", Map{"LIMIT": 1})
|
|
||||||
} else if len(qu) == 1 {
|
|
||||||
qu = append(qu, Map{"LIMIT": 1})
|
|
||||||
} else if len(qu) == 2 {
|
|
||||||
temp := qu[1].(Map)
|
|
||||||
temp["LIMIT"] = 1
|
|
||||||
qu[1] = temp
|
|
||||||
} else if len(qu) == 3 {
|
|
||||||
temp := qu[2].(Map)
|
|
||||||
temp["LIMIT"] = 1
|
|
||||||
qu[2] = temp
|
|
||||||
}
|
|
||||||
|
|
||||||
data := that.Select(table, qu...)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return data[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert 插入新数据
|
|
||||||
func (that *HoTimeDB) Insert(table string, data map[string]interface{}) int64 {
|
|
||||||
values := make([]interface{}, 0)
|
|
||||||
queryString := " ("
|
|
||||||
valueString := " ("
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
|
|
||||||
lens := len(data)
|
|
||||||
tempLen := 0
|
|
||||||
|
|
||||||
for k, v := range data {
|
|
||||||
tempLen++
|
|
||||||
|
|
||||||
vstr := "?"
|
|
||||||
if Substr(k, len(k)-3, 3) == "[#]" {
|
|
||||||
k = strings.Replace(k, "[#]", "", -1)
|
|
||||||
vstr = ObjToStr(v)
|
|
||||||
if tempLen < lens {
|
|
||||||
queryString += processor.ProcessColumnNoPrefix(k) + ","
|
|
||||||
valueString += vstr + ","
|
|
||||||
} else {
|
|
||||||
queryString += processor.ProcessColumnNoPrefix(k) + ") "
|
|
||||||
valueString += vstr + ");"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
values = append(values, v)
|
|
||||||
if tempLen < lens {
|
|
||||||
queryString += processor.ProcessColumnNoPrefix(k) + ","
|
|
||||||
valueString += "?,"
|
|
||||||
} else {
|
|
||||||
queryString += processor.ProcessColumnNoPrefix(k) + ") "
|
|
||||||
valueString += "?);"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query := "INSERT INTO " + processor.ProcessTableName(table) + " " + queryString + "VALUES" + valueString
|
|
||||||
|
|
||||||
res, err := that.Exec(query, values...)
|
|
||||||
|
|
||||||
id := int64(0)
|
|
||||||
if err.GetError() == nil && res != nil {
|
|
||||||
id1, err := res.LastInsertId()
|
|
||||||
that.LastErr.SetError(err)
|
|
||||||
id = id1
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果插入成功,删除缓存
|
|
||||||
if id != 0 {
|
|
||||||
if that.HoTimeCache != nil && table != "cached" {
|
|
||||||
_ = that.HoTimeCache.Db(table+"*", nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inserts 批量插入数据
|
|
||||||
// table: 表名
|
|
||||||
// dataList: 数据列表,每个元素是一个 Map
|
|
||||||
// 返回受影响的行数
|
|
||||||
//
|
|
||||||
// 示例:
|
|
||||||
//
|
|
||||||
// affected := db.Inserts("user", []Map{
|
|
||||||
// {"name": "张三", "age": 25, "email": "zhang@example.com"},
|
|
||||||
// {"name": "李四", "age": 30, "email": "li@example.com"},
|
|
||||||
// {"name": "王五", "age": 28, "email": "wang@example.com"},
|
|
||||||
// })
|
|
||||||
func (that *HoTimeDB) Inserts(table string, dataList []Map) int64 {
|
|
||||||
if len(dataList) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从第一条数据提取所有列名(确保顺序一致)
|
|
||||||
columns := make([]string, 0)
|
|
||||||
rawValues := make(map[string]string) // 存储 [#] 标记的直接 SQL 值
|
|
||||||
|
|
||||||
for k := range dataList[0] {
|
|
||||||
realKey := k
|
|
||||||
if Substr(k, len(k)-3, 3) == "[#]" {
|
|
||||||
realKey = strings.Replace(k, "[#]", "", -1)
|
|
||||||
rawValues[realKey] = ObjToStr(dataList[0][k])
|
|
||||||
}
|
|
||||||
columns = append(columns, realKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序列名以确保一致性
|
|
||||||
sort.Strings(columns)
|
|
||||||
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
|
|
||||||
// 构建列名部分
|
|
||||||
quotedCols := make([]string, len(columns))
|
|
||||||
for i, col := range columns {
|
|
||||||
quotedCols[i] = processor.ProcessColumnNoPrefix(col)
|
|
||||||
}
|
|
||||||
colStr := strings.Join(quotedCols, ", ")
|
|
||||||
|
|
||||||
// 构建每行的占位符和值
|
|
||||||
placeholders := make([]string, len(dataList))
|
|
||||||
values := make([]interface{}, 0, len(dataList)*len(columns))
|
|
||||||
|
|
||||||
for i, data := range dataList {
|
|
||||||
rowPlaceholders := make([]string, len(columns))
|
|
||||||
for j, col := range columns {
|
|
||||||
// 检查是否有 [#] 标记
|
|
||||||
rawKey := col + "[#]"
|
|
||||||
if rawVal, ok := data[rawKey]; ok {
|
|
||||||
// 直接 SQL 表达式
|
|
||||||
rowPlaceholders[j] = ObjToStr(rawVal)
|
|
||||||
} else if _, isRaw := rawValues[col]; isRaw && i == 0 {
|
|
||||||
// 第一条数据中的 [#] 标记
|
|
||||||
rowPlaceholders[j] = rawValues[col]
|
|
||||||
} else if val, ok := data[col]; ok {
|
|
||||||
// 普通值
|
|
||||||
rowPlaceholders[j] = "?"
|
|
||||||
values = append(values, val)
|
|
||||||
} else {
|
|
||||||
// 字段不存在,使用 NULL
|
|
||||||
rowPlaceholders[j] = "NULL"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
placeholders[i] = "(" + strings.Join(rowPlaceholders, ", ") + ")"
|
|
||||||
}
|
|
||||||
|
|
||||||
query := "INSERT INTO " + processor.ProcessTableName(table) + " (" + colStr + ") VALUES " + strings.Join(placeholders, ", ")
|
|
||||||
|
|
||||||
res, err := that.Exec(query, values...)
|
|
||||||
|
|
||||||
rows64 := int64(0)
|
|
||||||
if err.GetError() == nil && res != nil {
|
|
||||||
rows64, _ = res.RowsAffected()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果插入成功,删除缓存
|
|
||||||
if rows64 != 0 {
|
|
||||||
if that.HoTimeCache != nil && table != "cached" {
|
|
||||||
_ = that.HoTimeCache.Db(table+"*", nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upsert 插入或更新数据
|
|
||||||
// table: 表名
|
|
||||||
// data: 要插入的数据
|
|
||||||
// uniqueKeys: 唯一键字段(用于冲突检测),支持 Slice{"id"} 或 Slice{"col1", "col2"}
|
|
||||||
// updateColumns: 冲突时要更新的字段(如果为空,则更新所有非唯一键字段)
|
|
||||||
// 返回受影响的行数
|
|
||||||
//
|
|
||||||
// 示例:
|
|
||||||
//
|
|
||||||
// affected := db.Upsert("user",
|
|
||||||
// Map{"id": 1, "name": "张三", "email": "zhang@example.com"},
|
|
||||||
// Slice{"id"}, // 唯一键
|
|
||||||
// Slice{"name", "email"}, // 冲突时更新的字段
|
|
||||||
// )
|
|
||||||
func (that *HoTimeDB) Upsert(table string, data Map, uniqueKeys Slice, updateColumns ...interface{}) int64 {
|
|
||||||
if len(data) == 0 || len(uniqueKeys) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换 uniqueKeys 为 []string
|
|
||||||
uniqueKeyStrs := make([]string, len(uniqueKeys))
|
|
||||||
for i, uk := range uniqueKeys {
|
|
||||||
uniqueKeyStrs[i] = ObjToStr(uk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换 updateColumns 为 []string
|
|
||||||
var updateColumnStrs []string
|
|
||||||
if len(updateColumns) > 0 {
|
|
||||||
// 支持两种调用方式:Upsert(table, data, Slice{"id"}, Slice{"name"}) 或 Upsert(table, data, Slice{"id"}, "name", "email")
|
|
||||||
if slice, ok := updateColumns[0].(Slice); ok {
|
|
||||||
updateColumnStrs = make([]string, len(slice))
|
|
||||||
for i, col := range slice {
|
|
||||||
updateColumnStrs[i] = ObjToStr(col)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateColumnStrs = make([]string, len(updateColumns))
|
|
||||||
for i, col := range updateColumns {
|
|
||||||
updateColumnStrs[i] = ObjToStr(col)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收集列和值
|
|
||||||
columns := make([]string, 0, len(data))
|
|
||||||
values := make([]interface{}, 0, len(data))
|
|
||||||
rawValues := make(map[string]string) // 存储 [#] 标记的直接 SQL 值
|
|
||||||
|
|
||||||
for k, v := range data {
|
|
||||||
if Substr(k, len(k)-3, 3) == "[#]" {
|
|
||||||
realKey := strings.Replace(k, "[#]", "", -1)
|
|
||||||
columns = append(columns, realKey)
|
|
||||||
rawValues[realKey] = ObjToStr(v)
|
|
||||||
} else {
|
|
||||||
columns = append(columns, k)
|
|
||||||
values = append(values, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有指定更新字段,则更新所有非唯一键字段
|
|
||||||
if len(updateColumnStrs) == 0 {
|
|
||||||
uniqueKeySet := make(map[string]bool)
|
|
||||||
for _, uk := range uniqueKeyStrs {
|
|
||||||
uniqueKeySet[uk] = true
|
|
||||||
}
|
|
||||||
for _, col := range columns {
|
|
||||||
if !uniqueKeySet[col] {
|
|
||||||
updateColumnStrs = append(updateColumnStrs, col)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建 SQL
|
|
||||||
var query string
|
|
||||||
dbType := that.Type
|
|
||||||
if dbType == "" {
|
|
||||||
dbType = "mysql"
|
|
||||||
}
|
|
||||||
|
|
||||||
switch dbType {
|
|
||||||
case "postgres", "postgresql":
|
|
||||||
query = that.buildPostgresUpsert(table, columns, uniqueKeyStrs, updateColumnStrs, rawValues)
|
|
||||||
case "sqlite3", "sqlite":
|
|
||||||
query = that.buildSQLiteUpsert(table, columns, uniqueKeyStrs, updateColumnStrs, rawValues)
|
|
||||||
default: // mysql
|
|
||||||
query = that.buildMySQLUpsert(table, columns, uniqueKeyStrs, updateColumnStrs, rawValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := that.Exec(query, values...)
|
|
||||||
|
|
||||||
rows := int64(0)
|
|
||||||
if err.GetError() == nil && res != nil {
|
|
||||||
rows, _ = res.RowsAffected()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除缓存
|
|
||||||
if rows != 0 {
|
|
||||||
if that.HoTimeCache != nil && table != "cached" {
|
|
||||||
_ = that.HoTimeCache.Db(table+"*", nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildMySQLUpsert 构建 MySQL 的 Upsert 语句
|
|
||||||
func (that *HoTimeDB) buildMySQLUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string {
|
|
||||||
// INSERT INTO table (col1, col2) VALUES (?, ?)
|
|
||||||
// ON DUPLICATE KEY UPDATE col1 = VALUES(col1), col2 = VALUES(col2)
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
|
|
||||||
quotedCols := make([]string, len(columns))
|
|
||||||
valueParts := make([]string, len(columns))
|
|
||||||
for i, col := range columns {
|
|
||||||
quotedCols[i] = processor.ProcessColumnNoPrefix(col)
|
|
||||||
if raw, ok := rawValues[col]; ok {
|
|
||||||
valueParts[i] = raw
|
|
||||||
} else {
|
|
||||||
valueParts[i] = "?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateParts := make([]string, len(updateColumns))
|
|
||||||
for i, col := range updateColumns {
|
|
||||||
quotedCol := processor.ProcessColumnNoPrefix(col)
|
|
||||||
if raw, ok := rawValues[col]; ok {
|
|
||||||
updateParts[i] = quotedCol + " = " + raw
|
|
||||||
} else {
|
|
||||||
updateParts[i] = quotedCol + " = VALUES(" + quotedCol + ")"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "INSERT INTO " + processor.ProcessTableName(table) + " (" + strings.Join(quotedCols, ", ") +
|
|
||||||
") VALUES (" + strings.Join(valueParts, ", ") +
|
|
||||||
") ON DUPLICATE KEY UPDATE " + strings.Join(updateParts, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildPostgresUpsert 构建 PostgreSQL 的 Upsert 语句
|
|
||||||
func (that *HoTimeDB) buildPostgresUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string {
|
|
||||||
// INSERT INTO table (col1, col2) VALUES ($1, $2)
|
|
||||||
// ON CONFLICT (unique_key) DO UPDATE SET col1 = EXCLUDED.col1
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
dialect := that.GetDialect()
|
|
||||||
|
|
||||||
quotedCols := make([]string, len(columns))
|
|
||||||
valueParts := make([]string, len(columns))
|
|
||||||
paramIndex := 1
|
|
||||||
for i, col := range columns {
|
|
||||||
quotedCols[i] = dialect.QuoteIdentifier(col)
|
|
||||||
if raw, ok := rawValues[col]; ok {
|
|
||||||
valueParts[i] = raw
|
|
||||||
} else {
|
|
||||||
valueParts[i] = "$" + ObjToStr(paramIndex)
|
|
||||||
paramIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
quotedUniqueKeys := make([]string, len(uniqueKeys))
|
|
||||||
for i, key := range uniqueKeys {
|
|
||||||
quotedUniqueKeys[i] = dialect.QuoteIdentifier(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateParts := make([]string, len(updateColumns))
|
|
||||||
for i, col := range updateColumns {
|
|
||||||
quotedCol := dialect.QuoteIdentifier(col)
|
|
||||||
if raw, ok := rawValues[col]; ok {
|
|
||||||
updateParts[i] = quotedCol + " = " + raw
|
|
||||||
} else {
|
|
||||||
updateParts[i] = quotedCol + " = EXCLUDED." + quotedCol
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "INSERT INTO " + processor.ProcessTableName(table) + " (" + strings.Join(quotedCols, ", ") +
|
|
||||||
") VALUES (" + strings.Join(valueParts, ", ") +
|
|
||||||
") ON CONFLICT (" + strings.Join(quotedUniqueKeys, ", ") +
|
|
||||||
") DO UPDATE SET " + strings.Join(updateParts, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildSQLiteUpsert 构建 SQLite 的 Upsert 语句
|
|
||||||
func (that *HoTimeDB) buildSQLiteUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string {
|
|
||||||
// INSERT INTO table (col1, col2) VALUES (?, ?)
|
|
||||||
// ON CONFLICT (unique_key) DO UPDATE SET col1 = excluded.col1
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
dialect := that.GetDialect()
|
|
||||||
|
|
||||||
quotedCols := make([]string, len(columns))
|
|
||||||
valueParts := make([]string, len(columns))
|
|
||||||
for i, col := range columns {
|
|
||||||
quotedCols[i] = dialect.QuoteIdentifier(col)
|
|
||||||
if raw, ok := rawValues[col]; ok {
|
|
||||||
valueParts[i] = raw
|
|
||||||
} else {
|
|
||||||
valueParts[i] = "?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
quotedUniqueKeys := make([]string, len(uniqueKeys))
|
|
||||||
for i, key := range uniqueKeys {
|
|
||||||
quotedUniqueKeys[i] = dialect.QuoteIdentifier(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateParts := make([]string, len(updateColumns))
|
|
||||||
for i, col := range updateColumns {
|
|
||||||
quotedCol := dialect.QuoteIdentifier(col)
|
|
||||||
if raw, ok := rawValues[col]; ok {
|
|
||||||
updateParts[i] = quotedCol + " = " + raw
|
|
||||||
} else {
|
|
||||||
updateParts[i] = quotedCol + " = excluded." + quotedCol
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "INSERT INTO " + processor.ProcessTableName(table) + " (" + strings.Join(quotedCols, ", ") +
|
|
||||||
") VALUES (" + strings.Join(valueParts, ", ") +
|
|
||||||
") ON CONFLICT (" + strings.Join(quotedUniqueKeys, ", ") +
|
|
||||||
") DO UPDATE SET " + strings.Join(updateParts, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update 更新数据
|
|
||||||
func (that *HoTimeDB) Update(table string, data Map, where Map) int64 {
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
query := "UPDATE " + processor.ProcessTableName(table) + " SET "
|
|
||||||
qs := make([]interface{}, 0)
|
|
||||||
tp := len(data)
|
|
||||||
|
|
||||||
for k, v := range data {
|
|
||||||
vstr := "?"
|
|
||||||
if Substr(k, len(k)-3, 3) == "[#]" {
|
|
||||||
k = strings.Replace(k, "[#]", "", -1)
|
|
||||||
vstr = ObjToStr(v)
|
|
||||||
} else {
|
|
||||||
qs = append(qs, v)
|
|
||||||
}
|
|
||||||
query += processor.ProcessColumnNoPrefix(k) + "=" + vstr + " "
|
|
||||||
if tp--; tp != 0 {
|
|
||||||
query += ", "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
temp, resWhere := that.where(where)
|
|
||||||
|
|
||||||
query += temp + ";"
|
|
||||||
qs = append(qs, resWhere...)
|
|
||||||
|
|
||||||
res, err := that.Exec(query, qs...)
|
|
||||||
|
|
||||||
rows := int64(0)
|
|
||||||
if err.GetError() == nil && res != nil {
|
|
||||||
rows, _ = res.RowsAffected()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果更新成功,则删除缓存
|
|
||||||
if rows != 0 {
|
|
||||||
if that.HoTimeCache != nil && table != "cached" {
|
|
||||||
_ = that.HoTimeCache.Db(table+"*", nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete 删除数据
|
|
||||||
func (that *HoTimeDB) Delete(table string, data map[string]interface{}) int64 {
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
query := "DELETE FROM " + processor.ProcessTableName(table) + " "
|
|
||||||
|
|
||||||
temp, resWhere := that.where(data)
|
|
||||||
query += temp + ";"
|
|
||||||
|
|
||||||
res, err := that.Exec(query, resWhere...)
|
|
||||||
rows := int64(0)
|
|
||||||
if err.GetError() == nil && res != nil {
|
|
||||||
rows, _ = res.RowsAffected()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果删除成功,删除对应缓存
|
|
||||||
if rows != 0 {
|
|
||||||
if that.HoTimeCache != nil && table != "cached" {
|
|
||||||
_ = that.HoTimeCache.Db(table+"*", nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
147
db/db.go
147
db/db.go
@ -1,147 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"code.hoteas.com/golang/hotime/cache"
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
"database/sql"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HoTimeDB 数据库操作核心结构体
|
|
||||||
type HoTimeDB struct {
|
|
||||||
*sql.DB
|
|
||||||
ContextBase
|
|
||||||
DBName string
|
|
||||||
*cache.HoTimeCache
|
|
||||||
Log *logrus.Logger
|
|
||||||
Type string // 数据库类型: mysql, sqlite3, postgres
|
|
||||||
Prefix string
|
|
||||||
LastQuery string
|
|
||||||
LastData []interface{}
|
|
||||||
ConnectFunc func(err ...*Error) (*sql.DB, *sql.DB)
|
|
||||||
LastErr *Error
|
|
||||||
limit Slice
|
|
||||||
*sql.Tx //事务对象
|
|
||||||
SlaveDB *sql.DB // 从数据库
|
|
||||||
Mode int // mode为0生产模式,1为测试模式,2为开发模式
|
|
||||||
mu sync.RWMutex
|
|
||||||
limitMu sync.Mutex
|
|
||||||
Dialect Dialect // 数据库方言适配器
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetConnect 设置数据库配置连接
|
|
||||||
func (that *HoTimeDB) SetConnect(connect func(err ...*Error) (master, slave *sql.DB), err ...*Error) {
|
|
||||||
that.ConnectFunc = connect
|
|
||||||
_ = that.InitDb(err...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitDb 初始化数据库连接
|
|
||||||
func (that *HoTimeDB) InitDb(err ...*Error) *Error {
|
|
||||||
if len(err) != 0 {
|
|
||||||
that.LastErr = err[0]
|
|
||||||
}
|
|
||||||
that.DB, that.SlaveDB = that.ConnectFunc(that.LastErr)
|
|
||||||
if that.DB == nil {
|
|
||||||
return that.LastErr
|
|
||||||
}
|
|
||||||
e := that.DB.Ping()
|
|
||||||
|
|
||||||
that.LastErr.SetError(e)
|
|
||||||
|
|
||||||
if that.SlaveDB != nil {
|
|
||||||
e := that.SlaveDB.Ping()
|
|
||||||
that.LastErr.SetError(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据数据库类型初始化方言适配器
|
|
||||||
if that.Dialect == nil {
|
|
||||||
that.initDialect()
|
|
||||||
}
|
|
||||||
|
|
||||||
return that.LastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// initDialect 根据数据库类型初始化方言
|
|
||||||
func (that *HoTimeDB) initDialect() {
|
|
||||||
switch that.Type {
|
|
||||||
case "postgres", "postgresql":
|
|
||||||
that.Dialect = &PostgreSQLDialect{}
|
|
||||||
case "sqlite3", "sqlite":
|
|
||||||
that.Dialect = &SQLiteDialect{}
|
|
||||||
default:
|
|
||||||
that.Dialect = &MySQLDialect{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDialect 获取当前方言适配器
|
|
||||||
func (that *HoTimeDB) GetDialect() Dialect {
|
|
||||||
if that.Dialect == nil {
|
|
||||||
that.initDialect()
|
|
||||||
}
|
|
||||||
return that.Dialect
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDialect 设置方言适配器
|
|
||||||
func (that *HoTimeDB) SetDialect(dialect Dialect) {
|
|
||||||
that.Dialect = dialect
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetType 获取数据库类型
|
|
||||||
func (that *HoTimeDB) GetType() string {
|
|
||||||
return that.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPrefix 获取表前缀
|
|
||||||
func (that *HoTimeDB) GetPrefix() string {
|
|
||||||
return that.Prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProcessor 获取标识符处理器
|
|
||||||
// 用于处理表名、字段名的前缀添加和引号转换
|
|
||||||
func (that *HoTimeDB) GetProcessor() *IdentifierProcessor {
|
|
||||||
return NewIdentifierProcessor(that.GetDialect(), that.Prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// T 辅助方法:获取带前缀和引号的表名
|
|
||||||
// 用于手动构建 SQL 时使用
|
|
||||||
// 示例: db.T("order") 返回 "`app_order`" (MySQL) 或 "\"app_order\"" (PostgreSQL)
|
|
||||||
func (that *HoTimeDB) T(table string) string {
|
|
||||||
return that.GetProcessor().ProcessTableName(table)
|
|
||||||
}
|
|
||||||
|
|
||||||
// C 辅助方法:获取带前缀和引号的 table.column
|
|
||||||
// 支持两种调用方式:
|
|
||||||
// - db.C("order", "name") 返回 "`app_order`.`name`"
|
|
||||||
// - db.C("order.name") 返回 "`app_order`.`name`"
|
|
||||||
func (that *HoTimeDB) C(args ...string) string {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if len(args) == 1 {
|
|
||||||
return that.GetProcessor().ProcessColumn(args[0])
|
|
||||||
}
|
|
||||||
// 两个参数: table, column
|
|
||||||
dialect := that.GetDialect()
|
|
||||||
table := args[0]
|
|
||||||
column := args[1]
|
|
||||||
// 去除已有引号
|
|
||||||
table = trimQuotes(table)
|
|
||||||
column = trimQuotes(column)
|
|
||||||
return dialect.QuoteIdentifier(that.Prefix+table) + "." + dialect.QuoteIdentifier(column)
|
|
||||||
}
|
|
||||||
|
|
||||||
// trimQuotes 去除字符串两端的引号
|
|
||||||
func trimQuotes(s string) string {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
if len(s) >= 2 {
|
|
||||||
if (s[0] == '`' && s[len(s)-1] == '`') || (s[0] == '"' && s[len(s)-1] == '"') {
|
|
||||||
return s[1 : len(s)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
288
db/dialect.go
288
db/dialect.go
@ -1,288 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Dialect 数据库方言接口
|
|
||||||
// 用于处理不同数据库之间的语法差异
|
|
||||||
type Dialect interface {
|
|
||||||
// Quote 对表名/字段名添加引号
|
|
||||||
// MySQL 使用反引号 `name`
|
|
||||||
// PostgreSQL 使用双引号 "name"
|
|
||||||
// SQLite 使用双引号或方括号 "name" 或 [name]
|
|
||||||
Quote(name string) string
|
|
||||||
|
|
||||||
// QuoteIdentifier 处理单个标识符(去除已有引号,添加正确引号)
|
|
||||||
// 输入可能带有反引号或双引号,会先去除再添加正确格式
|
|
||||||
QuoteIdentifier(name string) string
|
|
||||||
|
|
||||||
// QuoteChar 返回引号字符
|
|
||||||
// MySQL: `
|
|
||||||
// PostgreSQL/SQLite: "
|
|
||||||
QuoteChar() string
|
|
||||||
|
|
||||||
// Placeholder 生成占位符
|
|
||||||
// MySQL/SQLite 使用 ?
|
|
||||||
// PostgreSQL 使用 $1, $2, $3...
|
|
||||||
Placeholder(index int) string
|
|
||||||
|
|
||||||
// Placeholders 生成多个占位符,用逗号分隔
|
|
||||||
Placeholders(count int, startIndex int) string
|
|
||||||
|
|
||||||
// SupportsLastInsertId 是否支持 LastInsertId
|
|
||||||
// PostgreSQL 不支持,需要使用 RETURNING
|
|
||||||
SupportsLastInsertId() bool
|
|
||||||
|
|
||||||
// ReturningClause 生成 RETURNING 子句(用于 PostgreSQL)
|
|
||||||
ReturningClause(column string) string
|
|
||||||
|
|
||||||
// UpsertSQL 生成 Upsert 语句
|
|
||||||
// MySQL: INSERT ... ON DUPLICATE KEY UPDATE ...
|
|
||||||
// PostgreSQL: INSERT ... ON CONFLICT ... DO UPDATE SET ...
|
|
||||||
// SQLite: INSERT OR REPLACE / INSERT ... ON CONFLICT ...
|
|
||||||
UpsertSQL(table string, columns []string, uniqueKeys []string, updateColumns []string) string
|
|
||||||
|
|
||||||
// GetName 获取方言名称
|
|
||||||
GetName() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// MySQLDialect MySQL 方言实现
|
|
||||||
type MySQLDialect struct{}
|
|
||||||
|
|
||||||
func (d *MySQLDialect) GetName() string {
|
|
||||||
return "mysql"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *MySQLDialect) Quote(name string) string {
|
|
||||||
// 如果已经包含点号(表.字段)或空格(别名),不添加引号
|
|
||||||
if strings.Contains(name, ".") || strings.Contains(name, " ") {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
return "`" + name + "`"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *MySQLDialect) QuoteIdentifier(name string) string {
|
|
||||||
// 去除已有的引号(反引号和双引号)
|
|
||||||
name = strings.Trim(name, "`\"")
|
|
||||||
return "`" + name + "`"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *MySQLDialect) QuoteChar() string {
|
|
||||||
return "`"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *MySQLDialect) Placeholder(index int) string {
|
|
||||||
return "?"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *MySQLDialect) Placeholders(count int, startIndex int) string {
|
|
||||||
if count <= 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
placeholders := make([]string, count)
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
placeholders[i] = "?"
|
|
||||||
}
|
|
||||||
return strings.Join(placeholders, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *MySQLDialect) SupportsLastInsertId() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *MySQLDialect) ReturningClause(column string) string {
|
|
||||||
return "" // MySQL 不支持 RETURNING
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *MySQLDialect) UpsertSQL(table string, columns []string, uniqueKeys []string, updateColumns []string) string {
|
|
||||||
// INSERT INTO table (col1, col2) VALUES (?, ?)
|
|
||||||
// ON DUPLICATE KEY UPDATE col1 = VALUES(col1), col2 = VALUES(col2)
|
|
||||||
quotedCols := make([]string, len(columns))
|
|
||||||
for i, col := range columns {
|
|
||||||
quotedCols[i] = d.Quote(col)
|
|
||||||
}
|
|
||||||
|
|
||||||
placeholders := d.Placeholders(len(columns), 1)
|
|
||||||
|
|
||||||
updateParts := make([]string, len(updateColumns))
|
|
||||||
for i, col := range updateColumns {
|
|
||||||
// 检查是否是 [#] 标记的直接 SQL
|
|
||||||
if strings.HasSuffix(col, "[#]") {
|
|
||||||
// 这种情况在调用处处理
|
|
||||||
updateParts[i] = col
|
|
||||||
} else {
|
|
||||||
quotedCol := d.Quote(col)
|
|
||||||
updateParts[i] = quotedCol + " = VALUES(" + quotedCol + ")"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s",
|
|
||||||
d.Quote(table),
|
|
||||||
strings.Join(quotedCols, ", "),
|
|
||||||
placeholders,
|
|
||||||
strings.Join(updateParts, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostgreSQLDialect PostgreSQL 方言实现
|
|
||||||
type PostgreSQLDialect struct{}
|
|
||||||
|
|
||||||
func (d *PostgreSQLDialect) GetName() string {
|
|
||||||
return "postgres"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PostgreSQLDialect) Quote(name string) string {
|
|
||||||
// 如果已经包含点号(表.字段)或空格(别名),不添加引号
|
|
||||||
if strings.Contains(name, ".") || strings.Contains(name, " ") {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
return "\"" + name + "\""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PostgreSQLDialect) QuoteIdentifier(name string) string {
|
|
||||||
// 去除已有的引号(反引号和双引号)
|
|
||||||
name = strings.Trim(name, "`\"")
|
|
||||||
return "\"" + name + "\""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PostgreSQLDialect) QuoteChar() string {
|
|
||||||
return "\""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PostgreSQLDialect) Placeholder(index int) string {
|
|
||||||
return fmt.Sprintf("$%d", index)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PostgreSQLDialect) Placeholders(count int, startIndex int) string {
|
|
||||||
if count <= 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
placeholders := make([]string, count)
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
placeholders[i] = fmt.Sprintf("$%d", startIndex+i)
|
|
||||||
}
|
|
||||||
return strings.Join(placeholders, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PostgreSQLDialect) SupportsLastInsertId() bool {
|
|
||||||
return false // PostgreSQL 需要使用 RETURNING
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PostgreSQLDialect) ReturningClause(column string) string {
|
|
||||||
return " RETURNING " + d.Quote(column)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *PostgreSQLDialect) UpsertSQL(table string, columns []string, uniqueKeys []string, updateColumns []string) string {
|
|
||||||
// INSERT INTO table (col1, col2) VALUES ($1, $2)
|
|
||||||
// ON CONFLICT (unique_key) DO UPDATE SET col1 = EXCLUDED.col1, col2 = EXCLUDED.col2
|
|
||||||
quotedCols := make([]string, len(columns))
|
|
||||||
for i, col := range columns {
|
|
||||||
quotedCols[i] = d.Quote(col)
|
|
||||||
}
|
|
||||||
|
|
||||||
placeholders := d.Placeholders(len(columns), 1)
|
|
||||||
|
|
||||||
quotedUniqueKeys := make([]string, len(uniqueKeys))
|
|
||||||
for i, key := range uniqueKeys {
|
|
||||||
quotedUniqueKeys[i] = d.Quote(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateParts := make([]string, len(updateColumns))
|
|
||||||
for i, col := range updateColumns {
|
|
||||||
if strings.HasSuffix(col, "[#]") {
|
|
||||||
updateParts[i] = col
|
|
||||||
} else {
|
|
||||||
quotedCol := d.Quote(col)
|
|
||||||
updateParts[i] = quotedCol + " = EXCLUDED." + quotedCol
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO UPDATE SET %s",
|
|
||||||
d.Quote(table),
|
|
||||||
strings.Join(quotedCols, ", "),
|
|
||||||
placeholders,
|
|
||||||
strings.Join(quotedUniqueKeys, ", "),
|
|
||||||
strings.Join(updateParts, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQLiteDialect SQLite 方言实现
|
|
||||||
type SQLiteDialect struct{}
|
|
||||||
|
|
||||||
func (d *SQLiteDialect) GetName() string {
|
|
||||||
return "sqlite3"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *SQLiteDialect) Quote(name string) string {
|
|
||||||
// 如果已经包含点号(表.字段)或空格(别名),不添加引号
|
|
||||||
if strings.Contains(name, ".") || strings.Contains(name, " ") {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
return "\"" + name + "\""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *SQLiteDialect) QuoteIdentifier(name string) string {
|
|
||||||
// 去除已有的引号(反引号和双引号)
|
|
||||||
name = strings.Trim(name, "`\"")
|
|
||||||
return "\"" + name + "\""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *SQLiteDialect) QuoteChar() string {
|
|
||||||
return "\""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *SQLiteDialect) Placeholder(index int) string {
|
|
||||||
return "?"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *SQLiteDialect) Placeholders(count int, startIndex int) string {
|
|
||||||
if count <= 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
placeholders := make([]string, count)
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
placeholders[i] = "?"
|
|
||||||
}
|
|
||||||
return strings.Join(placeholders, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *SQLiteDialect) SupportsLastInsertId() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *SQLiteDialect) ReturningClause(column string) string {
|
|
||||||
return "" // SQLite 3.35+ 支持 RETURNING,但为兼容性暂不使用
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *SQLiteDialect) UpsertSQL(table string, columns []string, uniqueKeys []string, updateColumns []string) string {
|
|
||||||
// INSERT INTO table (col1, col2) VALUES (?, ?)
|
|
||||||
// ON CONFLICT (unique_key) DO UPDATE SET col1 = excluded.col1, col2 = excluded.col2
|
|
||||||
quotedCols := make([]string, len(columns))
|
|
||||||
for i, col := range columns {
|
|
||||||
quotedCols[i] = d.Quote(col)
|
|
||||||
}
|
|
||||||
|
|
||||||
placeholders := d.Placeholders(len(columns), 1)
|
|
||||||
|
|
||||||
quotedUniqueKeys := make([]string, len(uniqueKeys))
|
|
||||||
for i, key := range uniqueKeys {
|
|
||||||
quotedUniqueKeys[i] = d.Quote(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateParts := make([]string, len(updateColumns))
|
|
||||||
for i, col := range updateColumns {
|
|
||||||
if strings.HasSuffix(col, "[#]") {
|
|
||||||
updateParts[i] = col
|
|
||||||
} else {
|
|
||||||
quotedCol := d.Quote(col)
|
|
||||||
updateParts[i] = quotedCol + " = excluded." + quotedCol
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO UPDATE SET %s",
|
|
||||||
d.Quote(table),
|
|
||||||
strings.Join(quotedCols, ", "),
|
|
||||||
placeholders,
|
|
||||||
strings.Join(quotedUniqueKeys, ", "),
|
|
||||||
strings.Join(updateParts, ", "))
|
|
||||||
}
|
|
||||||
@ -1,441 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestDialectQuoteIdentifier 测试方言的 QuoteIdentifier 方法
|
|
||||||
func TestDialectQuoteIdentifier(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
dialect Dialect
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
// MySQL 方言测试
|
|
||||||
{"MySQL simple", &MySQLDialect{}, "name", "`name`"},
|
|
||||||
{"MySQL with backticks", &MySQLDialect{}, "`name`", "`name`"},
|
|
||||||
{"MySQL with quotes", &MySQLDialect{}, "\"name\"", "`name`"},
|
|
||||||
|
|
||||||
// PostgreSQL 方言测试
|
|
||||||
{"PostgreSQL simple", &PostgreSQLDialect{}, "name", "\"name\""},
|
|
||||||
{"PostgreSQL with backticks", &PostgreSQLDialect{}, "`name`", "\"name\""},
|
|
||||||
{"PostgreSQL with quotes", &PostgreSQLDialect{}, "\"name\"", "\"name\""},
|
|
||||||
|
|
||||||
// SQLite 方言测试
|
|
||||||
{"SQLite simple", &SQLiteDialect{}, "name", "\"name\""},
|
|
||||||
{"SQLite with backticks", &SQLiteDialect{}, "`name`", "\"name\""},
|
|
||||||
{"SQLite with quotes", &SQLiteDialect{}, "\"name\"", "\"name\""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := tt.dialect.QuoteIdentifier(tt.input)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("QuoteIdentifier(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDialectQuoteChar 测试方言的 QuoteChar 方法
|
|
||||||
func TestDialectQuoteChar(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
dialect Dialect
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{"MySQL", &MySQLDialect{}, "`"},
|
|
||||||
{"PostgreSQL", &PostgreSQLDialect{}, "\""},
|
|
||||||
{"SQLite", &SQLiteDialect{}, "\""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := tt.dialect.QuoteChar()
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("QuoteChar() = %q, want %q", result, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIdentifierProcessorTableName 测试表名处理
|
|
||||||
func TestIdentifierProcessorTableName(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
dialect Dialect
|
|
||||||
prefix string
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
// MySQL 无前缀
|
|
||||||
{"MySQL no prefix", &MySQLDialect{}, "", "order", "`order`"},
|
|
||||||
{"MySQL no prefix with backticks", &MySQLDialect{}, "", "`order`", "`order`"},
|
|
||||||
|
|
||||||
// MySQL 有前缀
|
|
||||||
{"MySQL with prefix", &MySQLDialect{}, "app_", "order", "`app_order`"},
|
|
||||||
{"MySQL with prefix and backticks", &MySQLDialect{}, "app_", "`order`", "`app_order`"},
|
|
||||||
|
|
||||||
// PostgreSQL 无前缀
|
|
||||||
{"PostgreSQL no prefix", &PostgreSQLDialect{}, "", "order", "\"order\""},
|
|
||||||
|
|
||||||
// PostgreSQL 有前缀
|
|
||||||
{"PostgreSQL with prefix", &PostgreSQLDialect{}, "app_", "order", "\"app_order\""},
|
|
||||||
{"PostgreSQL with prefix and quotes", &PostgreSQLDialect{}, "app_", "\"order\"", "\"app_order\""},
|
|
||||||
|
|
||||||
// SQLite 有前缀
|
|
||||||
{"SQLite with prefix", &SQLiteDialect{}, "app_", "user", "\"app_user\""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
processor := NewIdentifierProcessor(tt.dialect, tt.prefix)
|
|
||||||
result := processor.ProcessTableName(tt.input)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("ProcessTableName(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIdentifierProcessorColumn 测试列名处理(包括 table.column 格式)
|
|
||||||
func TestIdentifierProcessorColumn(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
dialect Dialect
|
|
||||||
prefix string
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
// 单独列名
|
|
||||||
{"MySQL simple column", &MySQLDialect{}, "", "name", "`name`"},
|
|
||||||
{"MySQL simple column with prefix", &MySQLDialect{}, "app_", "name", "`name`"},
|
|
||||||
|
|
||||||
// table.column 格式
|
|
||||||
{"MySQL table.column no prefix", &MySQLDialect{}, "", "order.name", "`order`.`name`"},
|
|
||||||
{"MySQL table.column with prefix", &MySQLDialect{}, "app_", "order.name", "`app_order`.`name`"},
|
|
||||||
{"MySQL table.column with backticks", &MySQLDialect{}, "app_", "`order`.name", "`app_order`.`name`"},
|
|
||||||
|
|
||||||
// PostgreSQL
|
|
||||||
{"PostgreSQL table.column with prefix", &PostgreSQLDialect{}, "app_", "order.name", "\"app_order\".\"name\""},
|
|
||||||
{"PostgreSQL table.column with quotes", &PostgreSQLDialect{}, "app_", "\"order\".name", "\"app_order\".\"name\""},
|
|
||||||
|
|
||||||
// SQLite
|
|
||||||
{"SQLite table.column with prefix", &SQLiteDialect{}, "app_", "user.email", "\"app_user\".\"email\""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
processor := NewIdentifierProcessor(tt.dialect, tt.prefix)
|
|
||||||
result := processor.ProcessColumn(tt.input)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("ProcessColumn(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIdentifierProcessorConditionString 测试条件字符串处理
|
|
||||||
func TestIdentifierProcessorConditionString(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
dialect Dialect
|
|
||||||
prefix string
|
|
||||||
input string
|
|
||||||
contains []string // 结果应该包含这些字符串
|
|
||||||
}{
|
|
||||||
// MySQL 简单条件
|
|
||||||
{
|
|
||||||
"MySQL simple condition",
|
|
||||||
&MySQLDialect{},
|
|
||||||
"app_",
|
|
||||||
"user.id = order.user_id",
|
|
||||||
[]string{"`app_user`", "`app_order`"},
|
|
||||||
},
|
|
||||||
// MySQL 复杂条件
|
|
||||||
{
|
|
||||||
"MySQL complex condition",
|
|
||||||
&MySQLDialect{},
|
|
||||||
"app_",
|
|
||||||
"user.id = order.user_id AND order.status = 1",
|
|
||||||
[]string{"`app_user`", "`app_order`"},
|
|
||||||
},
|
|
||||||
// PostgreSQL
|
|
||||||
{
|
|
||||||
"PostgreSQL condition",
|
|
||||||
&PostgreSQLDialect{},
|
|
||||||
"app_",
|
|
||||||
"user.id = order.user_id",
|
|
||||||
[]string{"\"app_user\"", "\"app_order\""},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
processor := NewIdentifierProcessor(tt.dialect, tt.prefix)
|
|
||||||
result := processor.ProcessConditionString(tt.input)
|
|
||||||
for _, expected := range tt.contains {
|
|
||||||
if !strings.Contains(result, expected) {
|
|
||||||
t.Errorf("ProcessConditionString(%q) = %q, should contain %q", tt.input, result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHoTimeDBHelperMethods 测试 HoTimeDB 的辅助方法 T() 和 C()
|
|
||||||
func TestHoTimeDBHelperMethods(t *testing.T) {
|
|
||||||
// 创建 MySQL 数据库实例
|
|
||||||
mysqlDB := &HoTimeDB{
|
|
||||||
Type: "mysql",
|
|
||||||
Prefix: "app_",
|
|
||||||
}
|
|
||||||
mysqlDB.initDialect()
|
|
||||||
|
|
||||||
// 测试 T() 方法
|
|
||||||
t.Run("MySQL T() method", func(t *testing.T) {
|
|
||||||
result := mysqlDB.T("order")
|
|
||||||
expected := "`app_order`"
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("T(\"order\") = %q, want %q", result, expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 测试 C() 方法(两个参数)
|
|
||||||
t.Run("MySQL C() method with two args", func(t *testing.T) {
|
|
||||||
result := mysqlDB.C("order", "name")
|
|
||||||
expected := "`app_order`.`name`"
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("C(\"order\", \"name\") = %q, want %q", result, expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 测试 C() 方法(一个参数,点号格式)
|
|
||||||
t.Run("MySQL C() method with dot notation", func(t *testing.T) {
|
|
||||||
result := mysqlDB.C("order.name")
|
|
||||||
expected := "`app_order`.`name`"
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("C(\"order.name\") = %q, want %q", result, expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建 PostgreSQL 数据库实例
|
|
||||||
pgDB := &HoTimeDB{
|
|
||||||
Type: "postgres",
|
|
||||||
Prefix: "app_",
|
|
||||||
}
|
|
||||||
pgDB.initDialect()
|
|
||||||
|
|
||||||
// 测试 PostgreSQL 的 T() 方法
|
|
||||||
t.Run("PostgreSQL T() method", func(t *testing.T) {
|
|
||||||
result := pgDB.T("order")
|
|
||||||
expected := "\"app_order\""
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("T(\"order\") = %q, want %q", result, expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 测试 PostgreSQL 的 C() 方法
|
|
||||||
t.Run("PostgreSQL C() method", func(t *testing.T) {
|
|
||||||
result := pgDB.C("order", "name")
|
|
||||||
expected := "\"app_order\".\"name\""
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("C(\"order\", \"name\") = %q, want %q", result, expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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() {
|
|
||||||
// MySQL 示例
|
|
||||||
mysqlProcessor := NewIdentifierProcessor(&MySQLDialect{}, "app_")
|
|
||||||
fmt.Println("MySQL:")
|
|
||||||
fmt.Println(" Table:", mysqlProcessor.ProcessTableName("order"))
|
|
||||||
fmt.Println(" Column:", mysqlProcessor.ProcessColumn("order.name"))
|
|
||||||
fmt.Println(" Condition:", mysqlProcessor.ProcessConditionString("user.id = order.user_id"))
|
|
||||||
|
|
||||||
// PostgreSQL 示例
|
|
||||||
pgProcessor := NewIdentifierProcessor(&PostgreSQLDialect{}, "app_")
|
|
||||||
fmt.Println("PostgreSQL:")
|
|
||||||
fmt.Println(" Table:", pgProcessor.ProcessTableName("order"))
|
|
||||||
fmt.Println(" Column:", pgProcessor.ProcessColumn("order.name"))
|
|
||||||
fmt.Println(" Condition:", pgProcessor.ProcessConditionString("user.id = order.user_id"))
|
|
||||||
|
|
||||||
// Output:
|
|
||||||
// MySQL:
|
|
||||||
// Table: `app_order`
|
|
||||||
// Column: `app_order`.`name`
|
|
||||||
// Condition: `app_user`.`id` = `app_order`.`user_id`
|
|
||||||
// PostgreSQL:
|
|
||||||
// Table: "app_order"
|
|
||||||
// Column: "app_order"."name"
|
|
||||||
// Condition: "app_user"."id" = "app_order"."user_id"
|
|
||||||
}
|
|
||||||
1260
db/hotimedb.go
Normal file
1260
db/hotimedb.go
Normal file
File diff suppressed because it is too large
Load Diff
267
db/identifier.go
267
db/identifier.go
@ -1,267 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IdentifierProcessor 标识符处理器
|
|
||||||
// 用于处理表名、字段名的前缀添加和引号转换
|
|
||||||
type IdentifierProcessor struct {
|
|
||||||
dialect Dialect
|
|
||||||
prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewIdentifierProcessor 创建标识符处理器
|
|
||||||
func NewIdentifierProcessor(dialect Dialect, prefix string) *IdentifierProcessor {
|
|
||||||
return &IdentifierProcessor{
|
|
||||||
dialect: dialect,
|
|
||||||
prefix: prefix,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 系统数据库列表,这些数据库不添加前缀
|
|
||||||
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 处理表名(添加前缀+引号)
|
|
||||||
// 输入: "order" 或 "`order`" 或 "\"order\"" 或 "INFORMATION_SCHEMA.TABLES"
|
|
||||||
// 输出: "`app_order`" (MySQL) 或 "\"app_order\"" (PostgreSQL/SQLite)
|
|
||||||
// 对于 database.table 格式,会分别处理,系统数据库不添加前缀
|
|
||||||
func (p *IdentifierProcessor) ProcessTableName(name string) string {
|
|
||||||
// 去除已有的引号
|
|
||||||
name = p.stripQuotes(name)
|
|
||||||
|
|
||||||
// 检查是否包含空格(别名情况,如 "order AS o")
|
|
||||||
if strings.Contains(name, " ") {
|
|
||||||
// 处理别名情况
|
|
||||||
parts := strings.SplitN(name, " ", 2)
|
|
||||||
tableName := p.stripQuotes(parts[0])
|
|
||||||
alias := parts[1]
|
|
||||||
// 递归处理表名部分(可能包含点号)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加前缀和引号
|
|
||||||
return p.dialect.QuoteIdentifier(p.prefix + name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessTableNameNoPrefix 处理表名(只添加引号,不添加前缀)
|
|
||||||
// 用于已经包含前缀的情况
|
|
||||||
func (p *IdentifierProcessor) ProcessTableNameNoPrefix(name string) string {
|
|
||||||
name = p.stripQuotes(name)
|
|
||||||
if strings.Contains(name, " ") {
|
|
||||||
parts := strings.SplitN(name, " ", 2)
|
|
||||||
tableName := p.stripQuotes(parts[0])
|
|
||||||
alias := parts[1]
|
|
||||||
return p.dialect.QuoteIdentifier(tableName) + " " + alias
|
|
||||||
}
|
|
||||||
return p.dialect.QuoteIdentifier(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessColumn 处理 table.column 格式
|
|
||||||
// 输入: "name" 或 "order.name" 或 "`order`.name" 或 "`order`.`name`"
|
|
||||||
// 输出: "`name`" 或 "`app_order`.`name`" (MySQL)
|
|
||||||
func (p *IdentifierProcessor) ProcessColumn(name string) string {
|
|
||||||
// 检查是否包含点号
|
|
||||||
if !strings.Contains(name, ".") {
|
|
||||||
// 单独的列名,只加引号
|
|
||||||
return p.dialect.QuoteIdentifier(p.stripQuotes(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 table.column 格式
|
|
||||||
parts := p.splitTableColumn(name)
|
|
||||||
if len(parts) == 2 {
|
|
||||||
tableName := p.stripQuotes(parts[0])
|
|
||||||
columnName := p.stripQuotes(parts[1])
|
|
||||||
// 表名添加前缀
|
|
||||||
return p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(columnName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无法解析,返回原样但转换引号
|
|
||||||
return p.convertQuotes(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessColumnNoPrefix 处理 table.column 格式(不添加前缀)
|
|
||||||
func (p *IdentifierProcessor) ProcessColumnNoPrefix(name string) string {
|
|
||||||
if !strings.Contains(name, ".") {
|
|
||||||
return p.dialect.QuoteIdentifier(p.stripQuotes(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := p.splitTableColumn(name)
|
|
||||||
if len(parts) == 2 {
|
|
||||||
tableName := p.stripQuotes(parts[0])
|
|
||||||
columnName := p.stripQuotes(parts[1])
|
|
||||||
return p.dialect.QuoteIdentifier(tableName) + "." + p.dialect.QuoteIdentifier(columnName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.convertQuotes(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessConditionString 智能解析条件字符串(如 ON 条件)
|
|
||||||
// 输入: "user.id = order.user_id AND order.status = 1"
|
|
||||||
// 输出: "`app_user`.`id` = `app_order`.`user_id` AND `app_order`.`status` = 1" (MySQL)
|
|
||||||
func (p *IdentifierProcessor) ProcessConditionString(condition string) string {
|
|
||||||
if condition == "" {
|
|
||||||
return condition
|
|
||||||
}
|
|
||||||
|
|
||||||
result := condition
|
|
||||||
|
|
||||||
// 首先处理已有完整引号的情况 `table`.`column` 或 "table"."column"
|
|
||||||
// 这些需要先处理,因为它们的格式最明确
|
|
||||||
fullyQuotedPattern := regexp.MustCompile("[`\"]([a-zA-Z_][a-zA-Z0-9_]*)[`\"]\\.[`\"]([a-zA-Z_][a-zA-Z0-9_]*)[`\"]")
|
|
||||||
result = fullyQuotedPattern.ReplaceAllStringFunc(result, func(match string) string {
|
|
||||||
parts := fullyQuotedPattern.FindStringSubmatch(match)
|
|
||||||
if len(parts) == 3 {
|
|
||||||
tableName := parts[1]
|
|
||||||
colName := parts[2]
|
|
||||||
return p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(colName)
|
|
||||||
}
|
|
||||||
return match
|
|
||||||
})
|
|
||||||
|
|
||||||
// 然后处理部分引号的情况 `table`.column 或 "table".column
|
|
||||||
// 注意:需要避免匹配已处理的内容(已经是双引号包裹的)
|
|
||||||
quotedTablePattern := regexp.MustCompile("[`\"]([a-zA-Z_][a-zA-Z0-9_]*)[`\"]\\.([a-zA-Z_][a-zA-Z0-9_]*)(?:[^`\"]|$)")
|
|
||||||
result = quotedTablePattern.ReplaceAllStringFunc(result, func(match string) string {
|
|
||||||
parts := quotedTablePattern.FindStringSubmatch(match)
|
|
||||||
if len(parts) >= 3 {
|
|
||||||
tableName := parts[1]
|
|
||||||
colName := parts[2]
|
|
||||||
// 保留末尾字符(如果有)
|
|
||||||
suffix := ""
|
|
||||||
if len(match) > len(parts[0])-1 {
|
|
||||||
lastChar := match[len(match)-1]
|
|
||||||
if lastChar != '`' && lastChar != '"' && !isIdentChar(lastChar) {
|
|
||||||
suffix = string(lastChar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(colName) + suffix
|
|
||||||
}
|
|
||||||
return match
|
|
||||||
})
|
|
||||||
|
|
||||||
// 最后处理无引号的情况 table.column
|
|
||||||
// 使用更精确的正则,确保不匹配已处理的内容
|
|
||||||
unquotedPattern := regexp.MustCompile(`([^` + "`" + `"\w]|^)([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)([^` + "`" + `"\w(]|$)`)
|
|
||||||
result = unquotedPattern.ReplaceAllStringFunc(result, func(match string) string {
|
|
||||||
parts := unquotedPattern.FindStringSubmatch(match)
|
|
||||||
if len(parts) >= 5 {
|
|
||||||
prefix := parts[1] // 前面的边界字符
|
|
||||||
tableName := parts[2]
|
|
||||||
colName := parts[3]
|
|
||||||
suffix := parts[4] // 后面的边界字符
|
|
||||||
return prefix + p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(colName) + suffix
|
|
||||||
}
|
|
||||||
return match
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// isIdentChar 判断是否是标识符字符
|
|
||||||
func isIdentChar(c byte) bool {
|
|
||||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessFieldList 处理字段列表字符串
|
|
||||||
// 输入: "order.id, user.name AS uname, COUNT(*)"
|
|
||||||
// 输出: "`app_order`.`id`, `app_user`.`name` AS uname, COUNT(*)" (MySQL)
|
|
||||||
func (p *IdentifierProcessor) ProcessFieldList(fields string) string {
|
|
||||||
if fields == "" || fields == "*" {
|
|
||||||
return fields
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用与 ProcessConditionString 相同的逻辑
|
|
||||||
return p.ProcessConditionString(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stripQuotes 去除标识符两端的引号(反引号或双引号)
|
|
||||||
func (p *IdentifierProcessor) stripQuotes(name string) string {
|
|
||||||
name = strings.TrimSpace(name)
|
|
||||||
// 去除反引号
|
|
||||||
if strings.HasPrefix(name, "`") && strings.HasSuffix(name, "`") {
|
|
||||||
return name[1 : len(name)-1]
|
|
||||||
}
|
|
||||||
// 去除双引号
|
|
||||||
if strings.HasPrefix(name, "\"") && strings.HasSuffix(name, "\"") {
|
|
||||||
return name[1 : len(name)-1]
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitTableColumn 分割 table.column 格式
|
|
||||||
// 支持: table.column, `table`.column, `table`.`column`, "table".column 等
|
|
||||||
func (p *IdentifierProcessor) splitTableColumn(name string) []string {
|
|
||||||
// 先尝试按点号分割
|
|
||||||
dotIndex := -1
|
|
||||||
|
|
||||||
// 查找不在引号内的点号
|
|
||||||
inQuote := false
|
|
||||||
quoteChar := byte(0)
|
|
||||||
for i := 0; i < len(name); i++ {
|
|
||||||
c := name[i]
|
|
||||||
if c == '`' || c == '"' {
|
|
||||||
if !inQuote {
|
|
||||||
inQuote = true
|
|
||||||
quoteChar = c
|
|
||||||
} else if c == quoteChar {
|
|
||||||
inQuote = false
|
|
||||||
}
|
|
||||||
} else if c == '.' && !inQuote {
|
|
||||||
dotIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if dotIndex == -1 {
|
|
||||||
return []string{name}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{name[:dotIndex], name[dotIndex+1:]}
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertQuotes 将已有的引号转换为当前方言的引号格式
|
|
||||||
func (p *IdentifierProcessor) convertQuotes(name string) string {
|
|
||||||
quoteChar := p.dialect.QuoteChar()
|
|
||||||
// 替换反引号
|
|
||||||
name = strings.ReplaceAll(name, "`", quoteChar)
|
|
||||||
// 如果目标是反引号,需要替换双引号
|
|
||||||
if quoteChar == "`" {
|
|
||||||
name = strings.ReplaceAll(name, "\"", quoteChar)
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDialect 获取方言
|
|
||||||
func (p *IdentifierProcessor) GetDialect() Dialect {
|
|
||||||
return p.dialect
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPrefix 获取前缀
|
|
||||||
func (p *IdentifierProcessor) GetPrefix() string {
|
|
||||||
return p.prefix
|
|
||||||
}
|
|
||||||
391
db/query.go
391
db/query.go
@ -1,391 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
// md5 生成查询的 MD5 哈希(用于缓存)
|
|
||||||
func (that *HoTimeDB) md5(query string, args ...interface{}) string {
|
|
||||||
strByte, _ := json.Marshal(args)
|
|
||||||
str := Md5(query + ":" + string(strByte))
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query 执行查询 SQL
|
|
||||||
func (that *HoTimeDB) Query(query string, args ...interface{}) []Map {
|
|
||||||
return that.queryWithRetry(query, false, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// queryWithRetry 内部查询方法,支持重试标记
|
|
||||||
func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interface{}) []Map {
|
|
||||||
// 预处理数组占位符 ?[]
|
|
||||||
query, args = that.expandArrayPlaceholder(query, args)
|
|
||||||
|
|
||||||
// 保存调试信息(加锁保护)
|
|
||||||
that.mu.Lock()
|
|
||||||
that.LastQuery = query
|
|
||||||
that.LastData = args
|
|
||||||
that.mu.Unlock()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if that.Mode != 0 {
|
|
||||||
that.mu.RLock()
|
|
||||||
that.Log.Info("SQL:"+that.LastQuery, " DATA:", that.LastData, " ERROR:", that.LastErr.GetError())
|
|
||||||
that.mu.RUnlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var err error
|
|
||||||
var resl *sql.Rows
|
|
||||||
|
|
||||||
// 主从数据库切换,只有select语句有从数据库
|
|
||||||
db := that.DB
|
|
||||||
if that.SlaveDB != nil {
|
|
||||||
db = that.SlaveDB
|
|
||||||
}
|
|
||||||
|
|
||||||
if db == nil {
|
|
||||||
err = errors.New("没有初始化数据库")
|
|
||||||
that.LastErr.SetError(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理参数中的 slice 类型
|
|
||||||
processedArgs := that.processArgs(args)
|
|
||||||
|
|
||||||
if that.Tx != nil {
|
|
||||||
resl, err = that.Tx.Query(query, processedArgs...)
|
|
||||||
} else {
|
|
||||||
resl, err = db.Query(query, processedArgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
that.LastErr.SetError(err)
|
|
||||||
if err != nil {
|
|
||||||
// 如果还没重试过,尝试 Ping 后重试一次
|
|
||||||
if !retried {
|
|
||||||
if pingErr := db.Ping(); pingErr == nil {
|
|
||||||
return that.queryWithRetry(query, true, args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return that.Row(resl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec 执行非查询 SQL
|
|
||||||
func (that *HoTimeDB) Exec(query string, args ...interface{}) (sql.Result, *Error) {
|
|
||||||
return that.execWithRetry(query, false, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// execWithRetry 内部执行方法,支持重试标记
|
|
||||||
func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interface{}) (sql.Result, *Error) {
|
|
||||||
// 预处理数组占位符 ?[]
|
|
||||||
query, args = that.expandArrayPlaceholder(query, args)
|
|
||||||
|
|
||||||
// 保存调试信息(加锁保护)
|
|
||||||
that.mu.Lock()
|
|
||||||
that.LastQuery = query
|
|
||||||
that.LastData = args
|
|
||||||
that.mu.Unlock()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if that.Mode != 0 {
|
|
||||||
that.mu.RLock()
|
|
||||||
that.Log.Info("SQL: "+that.LastQuery, " DATA: ", that.LastData, " ERROR: ", that.LastErr.GetError())
|
|
||||||
that.mu.RUnlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var e error
|
|
||||||
var resl sql.Result
|
|
||||||
|
|
||||||
if that.DB == nil {
|
|
||||||
err := errors.New("没有初始化数据库")
|
|
||||||
that.LastErr.SetError(err)
|
|
||||||
return nil, that.LastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理参数中的 slice 类型
|
|
||||||
processedArgs := that.processArgs(args)
|
|
||||||
|
|
||||||
if that.Tx != nil {
|
|
||||||
resl, e = that.Tx.Exec(query, processedArgs...)
|
|
||||||
} else {
|
|
||||||
resl, e = that.DB.Exec(query, processedArgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
that.LastErr.SetError(e)
|
|
||||||
// 判断是否连接断开了,如果还没重试过,尝试重试一次
|
|
||||||
if e != nil {
|
|
||||||
if !retried {
|
|
||||||
if pingErr := that.DB.Ping(); pingErr == nil {
|
|
||||||
return that.execWithRetry(query, true, args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resl, that.LastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return resl, that.LastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// processArgs 处理参数中的 slice 类型
|
|
||||||
func (that *HoTimeDB) processArgs(args []interface{}) []interface{} {
|
|
||||||
processedArgs := make([]interface{}, len(args))
|
|
||||||
copy(processedArgs, args)
|
|
||||||
for key := range processedArgs {
|
|
||||||
arg := processedArgs[key]
|
|
||||||
if arg == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
argType := reflect.ValueOf(arg).Type().String()
|
|
||||||
if strings.Contains(argType, "[]") || strings.Contains(argType, "Slice") {
|
|
||||||
argLis := ObjToSlice(arg)
|
|
||||||
// 将slice转为逗号分割字符串
|
|
||||||
argStr := ""
|
|
||||||
for i := 0; i < len(argLis); i++ {
|
|
||||||
if i == len(argLis)-1 {
|
|
||||||
argStr += ObjToStr(argLis[i])
|
|
||||||
} else {
|
|
||||||
argStr += ObjToStr(argLis[i]) + ","
|
|
||||||
}
|
|
||||||
}
|
|
||||||
processedArgs[key] = argStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return processedArgs
|
|
||||||
}
|
|
||||||
|
|
||||||
// expandArrayPlaceholder 展开 IN (?) / NOT IN (?) 中的数组参数
|
|
||||||
// 自动识别 IN/NOT IN (?) 模式,当参数是数组时展开为多个 ?
|
|
||||||
//
|
|
||||||
// 示例:
|
|
||||||
//
|
|
||||||
// db.Query("SELECT * FROM user WHERE id IN (?)", []int{1, 2, 3})
|
|
||||||
// // 展开为: SELECT * FROM user WHERE id IN (?, ?, ?) 参数: [1, 2, 3]
|
|
||||||
//
|
|
||||||
// db.Query("SELECT * FROM user WHERE id IN (?)", []int{})
|
|
||||||
// // 展开为: SELECT * FROM user WHERE 1=0 参数: [] (空集合的IN永假)
|
|
||||||
//
|
|
||||||
// db.Query("SELECT * FROM user WHERE id NOT IN (?)", []int{})
|
|
||||||
// // 展开为: SELECT * FROM user WHERE 1=1 参数: [] (空集合的NOT IN永真)
|
|
||||||
//
|
|
||||||
// db.Query("SELECT * FROM user WHERE id = ?", 1)
|
|
||||||
// // 保持不变: SELECT * FROM user WHERE id = ? 参数: [1]
|
|
||||||
func (that *HoTimeDB) expandArrayPlaceholder(query string, args []interface{}) (string, []interface{}) {
|
|
||||||
if len(args) == 0 || !strings.Contains(query, "?") {
|
|
||||||
return query, args
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有数组参数
|
|
||||||
hasArray := false
|
|
||||||
for _, arg := range args {
|
|
||||||
if arg == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
argType := reflect.ValueOf(arg).Type().String()
|
|
||||||
if strings.Contains(argType, "[]") || strings.Contains(argType, "Slice") {
|
|
||||||
hasArray = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasArray {
|
|
||||||
return query, args
|
|
||||||
}
|
|
||||||
|
|
||||||
newArgs := make([]interface{}, 0, len(args))
|
|
||||||
result := strings.Builder{}
|
|
||||||
argIndex := 0
|
|
||||||
|
|
||||||
for i := 0; i < len(query); i++ {
|
|
||||||
if query[i] == '?' && argIndex < len(args) {
|
|
||||||
arg := args[argIndex]
|
|
||||||
argIndex++
|
|
||||||
|
|
||||||
if arg == nil {
|
|
||||||
result.WriteByte('?')
|
|
||||||
newArgs = append(newArgs, nil)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
argType := reflect.ValueOf(arg).Type().String()
|
|
||||||
if strings.Contains(argType, "[]") || strings.Contains(argType, "Slice") {
|
|
||||||
// 是数组参数,检查是否在 IN (...) 或 NOT IN (...) 中
|
|
||||||
prevPart := result.String()
|
|
||||||
prevUpper := strings.ToUpper(prevPart)
|
|
||||||
|
|
||||||
// 查找最近的 NOT IN ( 模式
|
|
||||||
notInIndex := strings.LastIndex(prevUpper, " NOT IN (")
|
|
||||||
notInIndex2 := strings.LastIndex(prevUpper, " NOT IN(")
|
|
||||||
if notInIndex2 > notInIndex {
|
|
||||||
notInIndex = notInIndex2
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找最近的 IN ( 模式(但要排除 NOT IN 的情况)
|
|
||||||
inIndex := strings.LastIndex(prevUpper, " IN (")
|
|
||||||
inIndex2 := strings.LastIndex(prevUpper, " IN(")
|
|
||||||
if inIndex2 > inIndex {
|
|
||||||
inIndex = inIndex2
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是 NOT IN 还是 IN
|
|
||||||
// 注意:" NOT IN (" 包含 " IN (",所以如果找到的 IN 位置在 NOT IN 范围内,应该优先判断为 NOT IN
|
|
||||||
isNotIn := false
|
|
||||||
matchIndex := -1
|
|
||||||
if notInIndex != -1 {
|
|
||||||
// 检查 inIndex 是否在 notInIndex 范围内(即 NOT IN 的 IN 部分)
|
|
||||||
// NOT IN ( 的 IN ( 部分从 notInIndex + 4 开始
|
|
||||||
if inIndex != -1 && inIndex >= notInIndex && inIndex <= notInIndex+5 {
|
|
||||||
// inIndex 是 NOT IN 的一部分,使用 NOT IN
|
|
||||||
isNotIn = true
|
|
||||||
matchIndex = notInIndex
|
|
||||||
} else if inIndex == -1 || notInIndex > inIndex {
|
|
||||||
// 没有独立的 IN,或 NOT IN 在 IN 之后
|
|
||||||
isNotIn = true
|
|
||||||
matchIndex = notInIndex
|
|
||||||
} else {
|
|
||||||
// 有独立的 IN 且在 NOT IN 之后
|
|
||||||
matchIndex = inIndex
|
|
||||||
}
|
|
||||||
} else if inIndex != -1 {
|
|
||||||
matchIndex = inIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 IN ( 后面是否只有空格(即当前 ? 紧跟在 IN ( 后面)
|
|
||||||
isInPattern := false
|
|
||||||
if matchIndex != -1 {
|
|
||||||
afterIn := prevPart[matchIndex:]
|
|
||||||
// 找到 ( 的位置
|
|
||||||
parenIdx := strings.Index(afterIn, "(")
|
|
||||||
if parenIdx != -1 {
|
|
||||||
afterParen := strings.TrimSpace(afterIn[parenIdx+1:])
|
|
||||||
if afterParen == "" {
|
|
||||||
isInPattern = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isInPattern {
|
|
||||||
// 在 IN (...) 或 NOT IN (...) 模式中
|
|
||||||
argList := ObjToSlice(arg)
|
|
||||||
if len(argList) == 0 {
|
|
||||||
// 空数组处理:需要找到字段名的开始位置
|
|
||||||
// 往前找最近的 AND/OR/WHERE/(,以确定条件的开始位置
|
|
||||||
truncateIndex := matchIndex
|
|
||||||
searchPart := prevUpper[:matchIndex]
|
|
||||||
|
|
||||||
// 找最近的分隔符位置
|
|
||||||
andIdx := strings.LastIndex(searchPart, " AND ")
|
|
||||||
orIdx := strings.LastIndex(searchPart, " OR ")
|
|
||||||
whereIdx := strings.LastIndex(searchPart, " WHERE ")
|
|
||||||
parenIdx := strings.LastIndex(searchPart, "(")
|
|
||||||
|
|
||||||
// 取最靠后的分隔符
|
|
||||||
sepIndex := -1
|
|
||||||
sepLen := 0
|
|
||||||
if andIdx > sepIndex {
|
|
||||||
sepIndex = andIdx
|
|
||||||
sepLen = 5 // " AND "
|
|
||||||
}
|
|
||||||
if orIdx > sepIndex {
|
|
||||||
sepIndex = orIdx
|
|
||||||
sepLen = 4 // " OR "
|
|
||||||
}
|
|
||||||
if whereIdx > sepIndex {
|
|
||||||
sepIndex = whereIdx
|
|
||||||
sepLen = 7 // " WHERE "
|
|
||||||
}
|
|
||||||
if parenIdx > sepIndex {
|
|
||||||
sepIndex = parenIdx
|
|
||||||
sepLen = 1 // "("
|
|
||||||
}
|
|
||||||
|
|
||||||
if sepIndex != -1 {
|
|
||||||
truncateIndex = sepIndex + sepLen
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Reset()
|
|
||||||
result.WriteString(prevPart[:truncateIndex])
|
|
||||||
if isNotIn {
|
|
||||||
// NOT IN 空集合 = 永真
|
|
||||||
result.WriteString(" 1=1 ")
|
|
||||||
} else {
|
|
||||||
// IN 空集合 = 永假
|
|
||||||
result.WriteString(" 1=0 ")
|
|
||||||
}
|
|
||||||
// 跳过后面的 )
|
|
||||||
for j := i + 1; j < len(query); j++ {
|
|
||||||
if query[j] == ')' {
|
|
||||||
i = j
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if len(argList) == 1 {
|
|
||||||
// 单元素数组
|
|
||||||
result.WriteByte('?')
|
|
||||||
newArgs = append(newArgs, argList[0])
|
|
||||||
} else {
|
|
||||||
// 多元素数组,展开为多个 ?
|
|
||||||
for j := 0; j < len(argList); j++ {
|
|
||||||
if j > 0 {
|
|
||||||
result.WriteString(", ")
|
|
||||||
}
|
|
||||||
result.WriteByte('?')
|
|
||||||
newArgs = append(newArgs, argList[j])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 不在 IN 模式中,保持原有行为(数组会被 processArgs 转为逗号字符串)
|
|
||||||
result.WriteByte('?')
|
|
||||||
newArgs = append(newArgs, arg)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 非数组参数
|
|
||||||
result.WriteByte('?')
|
|
||||||
newArgs = append(newArgs, arg)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.WriteByte(query[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.String(), newArgs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Row 数据库数据解析
|
|
||||||
func (that *HoTimeDB) Row(resl *sql.Rows) []Map {
|
|
||||||
dest := make([]Map, 0)
|
|
||||||
strs, _ := resl.Columns()
|
|
||||||
|
|
||||||
for i := 0; resl.Next(); i++ {
|
|
||||||
lis := make(Map, 0)
|
|
||||||
a := make([]interface{}, len(strs))
|
|
||||||
|
|
||||||
b := make([]interface{}, len(a))
|
|
||||||
for j := 0; j < len(a); j++ {
|
|
||||||
b[j] = &a[j]
|
|
||||||
}
|
|
||||||
err := resl.Scan(b...)
|
|
||||||
if err != nil {
|
|
||||||
that.LastErr.SetError(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for j := 0; j < len(a); j++ {
|
|
||||||
if a[j] != nil && reflect.ValueOf(a[j]).Type().String() == "[]uint8" {
|
|
||||||
lis[strs[j]] = string(a[j].([]byte))
|
|
||||||
} else {
|
|
||||||
lis[strs[j]] = a[j] // 取实际类型
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dest = append(dest, lis)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dest
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Action 事务操作
|
|
||||||
// 如果 action 返回 true 则提交事务;返回 false 则回滚
|
|
||||||
func (that *HoTimeDB) Action(action func(db HoTimeDB) (isSuccess bool)) (isSuccess bool) {
|
|
||||||
db := HoTimeDB{
|
|
||||||
DB: that.DB,
|
|
||||||
ContextBase: that.ContextBase,
|
|
||||||
DBName: that.DBName,
|
|
||||||
HoTimeCache: that.HoTimeCache,
|
|
||||||
Log: that.Log,
|
|
||||||
Type: that.Type,
|
|
||||||
Prefix: that.Prefix,
|
|
||||||
LastQuery: that.LastQuery,
|
|
||||||
LastData: that.LastData,
|
|
||||||
ConnectFunc: that.ConnectFunc,
|
|
||||||
LastErr: that.LastErr,
|
|
||||||
limit: that.limit,
|
|
||||||
Tx: that.Tx,
|
|
||||||
SlaveDB: that.SlaveDB,
|
|
||||||
Mode: that.Mode,
|
|
||||||
Dialect: that.Dialect,
|
|
||||||
mu: sync.RWMutex{},
|
|
||||||
limitMu: sync.Mutex{},
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := db.Begin()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
that.LastErr.SetError(err)
|
|
||||||
return isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
db.Tx = tx
|
|
||||||
|
|
||||||
isSuccess = action(db)
|
|
||||||
|
|
||||||
if !isSuccess {
|
|
||||||
err = db.Tx.Rollback()
|
|
||||||
if err != nil {
|
|
||||||
that.LastErr.SetError(err)
|
|
||||||
return isSuccess
|
|
||||||
}
|
|
||||||
return isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.Tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
that.LastErr.SetError(err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
555
db/where.go
555
db/where.go
@ -1,555 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 条件关键字
|
|
||||||
var condition = []string{"AND", "OR"}
|
|
||||||
|
|
||||||
// 特殊关键字(支持大小写)
|
|
||||||
var vcond = []string{"GROUP", "ORDER", "LIMIT", "DISTINCT", "HAVING", "OFFSET"}
|
|
||||||
|
|
||||||
// normalizeKey 标准化关键字(转大写)
|
|
||||||
func normalizeKey(k string) string {
|
|
||||||
upper := strings.ToUpper(k)
|
|
||||||
for _, v := range vcond {
|
|
||||||
if upper == v {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, v := range condition {
|
|
||||||
if upper == v {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return k
|
|
||||||
}
|
|
||||||
|
|
||||||
// isConditionKey 判断是否是条件关键字
|
|
||||||
func isConditionKey(k string) bool {
|
|
||||||
upper := strings.ToUpper(k)
|
|
||||||
for _, v := range condition {
|
|
||||||
if upper == v {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// isVcondKey 判断是否是特殊关键字
|
|
||||||
func isVcondKey(k string) bool {
|
|
||||||
upper := strings.ToUpper(k)
|
|
||||||
for _, v := range vcond {
|
|
||||||
if upper == v {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// where 语句解析
|
|
||||||
func (that *HoTimeDB) where(data Map) (string, []interface{}) {
|
|
||||||
where := ""
|
|
||||||
res := make([]interface{}, 0)
|
|
||||||
|
|
||||||
// 标准化 Map 的 key(大小写兼容)
|
|
||||||
normalizedData := Map{}
|
|
||||||
for k, v := range data {
|
|
||||||
normalizedData[normalizeKey(k)] = v
|
|
||||||
}
|
|
||||||
data = normalizedData
|
|
||||||
|
|
||||||
// 收集所有 key 并排序
|
|
||||||
testQu := []string{}
|
|
||||||
for key := range data {
|
|
||||||
testQu = append(testQu, key)
|
|
||||||
}
|
|
||||||
sort.Strings(testQu)
|
|
||||||
|
|
||||||
// 追踪条件数量,用于自动添加 AND
|
|
||||||
condCount := 0
|
|
||||||
|
|
||||||
for _, k := range testQu {
|
|
||||||
v := data[k]
|
|
||||||
|
|
||||||
// 检查是否是 AND/OR 条件关键字
|
|
||||||
if isConditionKey(k) {
|
|
||||||
tw, ts := that.cond(strings.ToUpper(k), v.(Map))
|
|
||||||
if tw != "" && strings.TrimSpace(tw) != "" {
|
|
||||||
// 与前面的条件用 AND 连接
|
|
||||||
if condCount > 0 {
|
|
||||||
where += " AND "
|
|
||||||
}
|
|
||||||
// 用括号包裹 OR/AND 组条件
|
|
||||||
where += "(" + strings.TrimSpace(tw) + ")"
|
|
||||||
condCount++
|
|
||||||
res = append(res, ts...)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否是特殊关键字(GROUP, ORDER, LIMIT 等)
|
|
||||||
if isVcondKey(k) {
|
|
||||||
continue // 特殊关键字在后面单独处理
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理普通条件字段
|
|
||||||
// 空切片的 IN 条件应该生成永假条件(1=0),而不是跳过
|
|
||||||
if v != nil && reflect.ValueOf(v).Type().String() == "common.Slice" && len(v.(Slice)) == 0 {
|
|
||||||
// 检查是否是 NOT IN(带 [!] 后缀)- NOT IN 空数组永真,跳过即可
|
|
||||||
if !strings.HasSuffix(k, "[!]") {
|
|
||||||
// IN 空数组 -> 生成永假条件
|
|
||||||
if condCount > 0 {
|
|
||||||
where += " AND "
|
|
||||||
}
|
|
||||||
where += "1=0 "
|
|
||||||
condCount++
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if v != nil && strings.Contains(reflect.ValueOf(v).Type().String(), "[]") && len(ObjToSlice(v)) == 0 {
|
|
||||||
// 检查是否是 NOT IN(带 [!] 后缀)- NOT IN 空数组永真,跳过即可
|
|
||||||
if !strings.HasSuffix(k, "[!]") {
|
|
||||||
// IN 空数组 -> 生成永假条件
|
|
||||||
if condCount > 0 {
|
|
||||||
where += " AND "
|
|
||||||
}
|
|
||||||
where += "1=0 "
|
|
||||||
condCount++
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tv, vv := that.varCond(k, v)
|
|
||||||
if tv != "" {
|
|
||||||
// 自动添加 AND 连接符
|
|
||||||
if condCount > 0 {
|
|
||||||
where += " AND "
|
|
||||||
}
|
|
||||||
where += tv
|
|
||||||
condCount++
|
|
||||||
res = append(res, vv...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加 WHERE 关键字
|
|
||||||
// 先去除首尾空格,检查是否有实际条件内容
|
|
||||||
trimmedWhere := strings.TrimSpace(where)
|
|
||||||
if len(trimmedWhere) != 0 {
|
|
||||||
hasWhere := true
|
|
||||||
for _, v := range vcond {
|
|
||||||
if strings.Index(trimmedWhere, v) == 0 {
|
|
||||||
hasWhere = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasWhere {
|
|
||||||
where = " WHERE " + trimmedWhere + " "
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 没有实际条件内容,重置 where
|
|
||||||
where = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理特殊字符(按固定顺序:GROUP, HAVING, ORDER, LIMIT, OFFSET)
|
|
||||||
specialOrder := []string{"GROUP", "HAVING", "ORDER", "LIMIT", "OFFSET", "DISTINCT"}
|
|
||||||
for _, vcondKey := range specialOrder {
|
|
||||||
v, exists := data[vcondKey]
|
|
||||||
if !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch vcondKey {
|
|
||||||
case "GROUP":
|
|
||||||
where += " GROUP BY "
|
|
||||||
where += that.formatVcondValue(v)
|
|
||||||
case "HAVING":
|
|
||||||
// HAVING 条件处理
|
|
||||||
if havingMap, ok := v.(Map); ok {
|
|
||||||
havingWhere, havingRes := that.cond("AND", havingMap)
|
|
||||||
if havingWhere != "" {
|
|
||||||
where += " HAVING " + strings.TrimSpace(havingWhere) + " "
|
|
||||||
res = append(res, havingRes...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "ORDER":
|
|
||||||
where += " ORDER BY "
|
|
||||||
where += that.formatVcondValue(v)
|
|
||||||
case "LIMIT":
|
|
||||||
where += " LIMIT "
|
|
||||||
where += that.formatVcondValue(v)
|
|
||||||
case "OFFSET":
|
|
||||||
where += " OFFSET " + ObjToStr(v) + " "
|
|
||||||
case "DISTINCT":
|
|
||||||
// DISTINCT 通常在 SELECT 中处理,这里暂时忽略
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatVcondValue 格式化特殊关键字的值
|
|
||||||
func (that *HoTimeDB) formatVcondValue(v interface{}) string {
|
|
||||||
result := ""
|
|
||||||
if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
|
|
||||||
vs := ObjToSlice(v)
|
|
||||||
for i := 0; i < len(vs); i++ {
|
|
||||||
result += " " + vs.GetString(i) + " "
|
|
||||||
if len(vs) != i+1 {
|
|
||||||
result += ", "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result += " " + ObjToStr(v) + " "
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// varCond 变量条件解析
|
|
||||||
func (that *HoTimeDB) varCond(k string, v interface{}) (string, []interface{}) {
|
|
||||||
where := ""
|
|
||||||
res := make([]interface{}, 0)
|
|
||||||
length := len(k)
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
|
|
||||||
if k == "[#]" {
|
|
||||||
k = strings.Replace(k, "[#]", "", -1)
|
|
||||||
where += " " + ObjToStr(v) + " "
|
|
||||||
} else if k == "[##]" {
|
|
||||||
// 直接添加 SQL 片段(key 为 [##] 时)
|
|
||||||
where += " " + ObjToStr(v) + " "
|
|
||||||
} else if length > 0 && strings.Contains(k, "[") && k[length-1] == ']' {
|
|
||||||
def := false
|
|
||||||
|
|
||||||
switch Substr(k, length-3, 3) {
|
|
||||||
case "[>]":
|
|
||||||
k = strings.Replace(k, "[>]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += k + ">? "
|
|
||||||
res = append(res, v)
|
|
||||||
case "[<]":
|
|
||||||
k = strings.Replace(k, "[<]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += k + "<? "
|
|
||||||
res = append(res, v)
|
|
||||||
case "[!]":
|
|
||||||
k = strings.Replace(k, "[!]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where, res = that.notIn(k, v, where, res)
|
|
||||||
case "[#]":
|
|
||||||
k = strings.Replace(k, "[#]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += " " + k + "=" + ObjToStr(v) + " "
|
|
||||||
case "[##]": // 直接添加value到sql,需要考虑防注入
|
|
||||||
where += " " + ObjToStr(v)
|
|
||||||
case "[#!]":
|
|
||||||
k = strings.Replace(k, "[#!]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += " " + k + "!=" + ObjToStr(v) + " "
|
|
||||||
case "[!#]":
|
|
||||||
k = strings.Replace(k, "[!#]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += " " + k + "!=" + ObjToStr(v) + " "
|
|
||||||
case "[~]":
|
|
||||||
k = strings.Replace(k, "[~]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += k + " LIKE ? "
|
|
||||||
v = "%" + ObjToStr(v) + "%"
|
|
||||||
res = append(res, v)
|
|
||||||
case "[!~]": // 左边任意
|
|
||||||
k = strings.Replace(k, "[!~]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += k + " LIKE ? "
|
|
||||||
v = "%" + ObjToStr(v) + ""
|
|
||||||
res = append(res, v)
|
|
||||||
case "[~!]": // 右边任意
|
|
||||||
k = strings.Replace(k, "[~!]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += k + " LIKE ? "
|
|
||||||
v = ObjToStr(v) + "%"
|
|
||||||
res = append(res, v)
|
|
||||||
case "[~~]": // 手动任意
|
|
||||||
k = strings.Replace(k, "[~~]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += k + " LIKE ? "
|
|
||||||
res = append(res, v)
|
|
||||||
default:
|
|
||||||
def = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if def {
|
|
||||||
switch Substr(k, length-4, 4) {
|
|
||||||
case "[>=]":
|
|
||||||
k = strings.Replace(k, "[>=]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += k + ">=? "
|
|
||||||
res = append(res, v)
|
|
||||||
case "[<=]":
|
|
||||||
k = strings.Replace(k, "[<=]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += k + "<=? "
|
|
||||||
res = append(res, v)
|
|
||||||
case "[><]":
|
|
||||||
k = strings.Replace(k, "[><]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += k + " NOT BETWEEN ? AND ? "
|
|
||||||
vs := ObjToSlice(v)
|
|
||||||
res = append(res, vs[0])
|
|
||||||
res = append(res, vs[1])
|
|
||||||
case "[<>]":
|
|
||||||
k = strings.Replace(k, "[<>]", "", -1)
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
where += k + " BETWEEN ? AND ? "
|
|
||||||
vs := ObjToSlice(v)
|
|
||||||
res = append(res, vs[0])
|
|
||||||
res = append(res, vs[1])
|
|
||||||
default:
|
|
||||||
where, res = that.handleDefaultCondition(k, v, where, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
where, res = that.handlePlainField(k, v, where, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleDefaultCondition 处理默认条件(带方括号但不是特殊操作符)
|
|
||||||
func (that *HoTimeDB) handleDefaultCondition(k string, v interface{}, where string, res []interface{}) (string, []interface{}) {
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
|
|
||||||
if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
|
|
||||||
vs := ObjToSlice(v)
|
|
||||||
if len(vs) == 0 {
|
|
||||||
// IN 空数组 -> 生成永假条件
|
|
||||||
where += "1=0 "
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(vs) == 1 {
|
|
||||||
where += k + "=? "
|
|
||||||
res = append(res, vs[0])
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
|
|
||||||
// IN 优化:连续整数转为 BETWEEN
|
|
||||||
where, res = that.optimizeInCondition(k, vs, where, res)
|
|
||||||
} else {
|
|
||||||
where += k + "=? "
|
|
||||||
res = append(res, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
|
|
||||||
// handlePlainField 处理普通字段(无方括号)
|
|
||||||
func (that *HoTimeDB) handlePlainField(k string, v interface{}, where string, res []interface{}) (string, []interface{}) {
|
|
||||||
processor := that.GetProcessor()
|
|
||||||
k = processor.ProcessColumn(k) + " "
|
|
||||||
|
|
||||||
if v == nil {
|
|
||||||
where += k + " IS NULL "
|
|
||||||
} else if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
|
|
||||||
vs := ObjToSlice(v)
|
|
||||||
if len(vs) == 0 {
|
|
||||||
// IN 空数组 -> 生成永假条件
|
|
||||||
where += "1=0 "
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(vs) == 1 {
|
|
||||||
where += k + "=? "
|
|
||||||
res = append(res, vs[0])
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
|
|
||||||
// IN 优化
|
|
||||||
where, res = that.optimizeInCondition(k, vs, where, res)
|
|
||||||
} else {
|
|
||||||
where += k + "=? "
|
|
||||||
res = append(res, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
|
|
||||||
// optimizeInCondition 优化 IN 条件(连续整数转为 BETWEEN)
|
|
||||||
func (that *HoTimeDB) optimizeInCondition(k string, vs Slice, where string, res []interface{}) (string, []interface{}) {
|
|
||||||
min := int64(0)
|
|
||||||
isMin := true
|
|
||||||
IsRange := true
|
|
||||||
num := int64(0)
|
|
||||||
isNum := true
|
|
||||||
|
|
||||||
where1 := ""
|
|
||||||
res1 := Slice{}
|
|
||||||
where2 := k + " IN ("
|
|
||||||
res2 := Slice{}
|
|
||||||
|
|
||||||
for kvs := 0; kvs <= len(vs); kvs++ {
|
|
||||||
vsv := int64(0)
|
|
||||||
if kvs < len(vs) {
|
|
||||||
vsv = vs.GetCeilInt64(kvs)
|
|
||||||
// 确保是全部是int类型
|
|
||||||
if ObjToStr(vsv) != vs.GetString(kvs) {
|
|
||||||
IsRange = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isNum {
|
|
||||||
isNum = false
|
|
||||||
num = vsv
|
|
||||||
} else {
|
|
||||||
num++
|
|
||||||
}
|
|
||||||
if isMin {
|
|
||||||
isMin = false
|
|
||||||
min = vsv
|
|
||||||
}
|
|
||||||
// 不等于则到了分路口
|
|
||||||
if num != vsv {
|
|
||||||
// between
|
|
||||||
if num-min > 1 {
|
|
||||||
if where1 != "" {
|
|
||||||
where1 += " OR " + k + " BETWEEN ? AND ? "
|
|
||||||
} else {
|
|
||||||
where1 += k + " BETWEEN ? AND ? "
|
|
||||||
}
|
|
||||||
res1 = append(res1, min)
|
|
||||||
res1 = append(res1, num-1)
|
|
||||||
} else {
|
|
||||||
where2 += "?,"
|
|
||||||
res2 = append(res2, min)
|
|
||||||
}
|
|
||||||
min = vsv
|
|
||||||
num = vsv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if IsRange {
|
|
||||||
where3 := ""
|
|
||||||
|
|
||||||
if where1 != "" {
|
|
||||||
where3 += where1
|
|
||||||
res = append(res, res1...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(res2) == 1 {
|
|
||||||
if where3 == "" {
|
|
||||||
where3 += k + " = ? "
|
|
||||||
} else {
|
|
||||||
where3 += " OR " + k + " = ? "
|
|
||||||
}
|
|
||||||
res = append(res, res2...)
|
|
||||||
} else if len(res2) > 1 {
|
|
||||||
where2 = where2[:len(where2)-1]
|
|
||||||
if where3 == "" {
|
|
||||||
where3 += where2 + ")"
|
|
||||||
} else {
|
|
||||||
where3 += " OR " + where2 + ")"
|
|
||||||
}
|
|
||||||
res = append(res, res2...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if where3 != "" {
|
|
||||||
where += "(" + where3 + ")"
|
|
||||||
}
|
|
||||||
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
|
|
||||||
// 非连续整数,使用普通 IN
|
|
||||||
where += k + " IN ("
|
|
||||||
res = append(res, vs...)
|
|
||||||
|
|
||||||
for i := 0; i < len(vs); i++ {
|
|
||||||
if i+1 != len(vs) {
|
|
||||||
where += "?,"
|
|
||||||
} else {
|
|
||||||
where += "?) "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
|
|
||||||
// notIn NOT IN 条件处理
|
|
||||||
func (that *HoTimeDB) notIn(k string, v interface{}, where string, res []interface{}) (string, []interface{}) {
|
|
||||||
if v == nil {
|
|
||||||
where += k + " IS NOT NULL "
|
|
||||||
} else if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
|
|
||||||
vs := ObjToSlice(v)
|
|
||||||
if len(vs) == 0 {
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
where += k + " NOT IN ("
|
|
||||||
res = append(res, vs...)
|
|
||||||
|
|
||||||
for i := 0; i < len(vs); i++ {
|
|
||||||
if i+1 != len(vs) {
|
|
||||||
where += "?,"
|
|
||||||
} else {
|
|
||||||
where += "?) "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
where += k + " !=? "
|
|
||||||
res = append(res, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
|
|
||||||
// cond 条件组合处理
|
|
||||||
func (that *HoTimeDB) cond(tag string, data Map) (string, []interface{}) {
|
|
||||||
where := " "
|
|
||||||
res := make([]interface{}, 0)
|
|
||||||
lens := len(data)
|
|
||||||
|
|
||||||
testQu := []string{}
|
|
||||||
for key := range data {
|
|
||||||
testQu = append(testQu, key)
|
|
||||||
}
|
|
||||||
sort.Strings(testQu)
|
|
||||||
|
|
||||||
for _, k := range testQu {
|
|
||||||
v := data[k]
|
|
||||||
x := 0
|
|
||||||
for i := 0; i < len(condition); i++ {
|
|
||||||
if condition[i] == strings.ToUpper(k) {
|
|
||||||
tw, ts := that.cond(strings.ToUpper(k), v.(Map))
|
|
||||||
if lens--; lens <= 0 {
|
|
||||||
where += "(" + tw + ") "
|
|
||||||
} else {
|
|
||||||
where += "(" + tw + ") " + tag + " "
|
|
||||||
}
|
|
||||||
|
|
||||||
res = append(res, ts...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
x++
|
|
||||||
}
|
|
||||||
|
|
||||||
if x == len(condition) {
|
|
||||||
tv, vv := that.varCond(k, v)
|
|
||||||
if tv == "" {
|
|
||||||
lens--
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
res = append(res, vv...)
|
|
||||||
if lens--; lens <= 0 {
|
|
||||||
where += tv + ""
|
|
||||||
} else {
|
|
||||||
where += tv + " " + tag + " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return where, res
|
|
||||||
}
|
|
||||||
@ -1,468 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,484 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@ -1,516 +0,0 @@
|
|||||||
# HoTimeDB API 快速参考
|
|
||||||
|
|
||||||
## 条件查询语法规则
|
|
||||||
|
|
||||||
**新版本改进:**
|
|
||||||
- 多条件自动用 AND 连接,无需手动包装
|
|
||||||
- 关键字支持大小写(如 `LIMIT` 和 `limit` 都有效)
|
|
||||||
- 新增 `HAVING` 和独立 `OFFSET` 支持
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ✅ 推荐:简化语法(多条件自动 AND)
|
|
||||||
Map{"status": 1, "age[>]": 18}
|
|
||||||
// 生成: WHERE `status`=? AND `age`>?
|
|
||||||
|
|
||||||
// ✅ 仍然支持:显式 AND 包装(向后兼容)
|
|
||||||
Map{
|
|
||||||
"AND": Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 混合条件和特殊关键字
|
|
||||||
Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
"ORDER": "id DESC", // 或 "order": "id DESC"
|
|
||||||
"LIMIT": 10, // 或 "limit": 10
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 基本方法
|
|
||||||
|
|
||||||
### 数据库连接
|
|
||||||
```go
|
|
||||||
database.SetConnect(func() (master, slave *sql.DB) { ... })
|
|
||||||
database.InitDb()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 链式查询构建器
|
|
||||||
```go
|
|
||||||
// 创建查询构建器
|
|
||||||
builder := database.Table("tablename")
|
|
||||||
|
|
||||||
// 设置条件
|
|
||||||
builder.Where(key, value)
|
|
||||||
builder.And(key, value) 或 builder.And(map)
|
|
||||||
builder.Or(key, value) 或 builder.Or(map)
|
|
||||||
|
|
||||||
// JOIN操作
|
|
||||||
builder.LeftJoin(table, condition)
|
|
||||||
builder.RightJoin(table, condition)
|
|
||||||
builder.InnerJoin(table, condition)
|
|
||||||
builder.FullJoin(table, condition)
|
|
||||||
builder.Join(map) // 通用JOIN
|
|
||||||
|
|
||||||
// 排序和分组
|
|
||||||
builder.Order(fields...)
|
|
||||||
builder.Group(fields...)
|
|
||||||
builder.Limit(args...)
|
|
||||||
builder.Having(map) // 新增
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
builder.Page(page, pageSize)
|
|
||||||
builder.Offset(offset) // 新增
|
|
||||||
|
|
||||||
// 执行查询
|
|
||||||
builder.Select(fields...) // 返回 []Map
|
|
||||||
builder.Get(fields...) // 返回 Map
|
|
||||||
builder.Count() // 返回 int
|
|
||||||
builder.Update(data) // 返回 int64
|
|
||||||
builder.Delete() // 返回 int64
|
|
||||||
```
|
|
||||||
|
|
||||||
## CRUD 操作
|
|
||||||
|
|
||||||
### 查询 (Select)
|
|
||||||
```go
|
|
||||||
// 基本查询
|
|
||||||
data := database.Select("table")
|
|
||||||
data := database.Select("table", "field1,field2")
|
|
||||||
data := database.Select("table", []string{"field1", "field2"})
|
|
||||||
data := database.Select("table", "*", whereMap)
|
|
||||||
|
|
||||||
// 带JOIN查询
|
|
||||||
data := database.Select("table", joinSlice, "fields", whereMap)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 获取单条 (Get)
|
|
||||||
```go
|
|
||||||
// 自动添加 LIMIT 1
|
|
||||||
row := database.Get("table", "fields", whereMap)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 插入 (Insert)
|
|
||||||
```go
|
|
||||||
id := database.Insert("table", dataMap)
|
|
||||||
// 返回新插入记录的ID
|
|
||||||
```
|
|
||||||
|
|
||||||
### 批量插入 (Inserts) - 新增
|
|
||||||
```go
|
|
||||||
// 使用 []Map 格式,更直观简洁
|
|
||||||
affected := database.Inserts("table", []Map{
|
|
||||||
{"col1": "val1", "col2": "val2", "col3": "val3"},
|
|
||||||
{"col1": "val4", "col2": "val5", "col3": "val6"},
|
|
||||||
})
|
|
||||||
// 返回受影响的行数
|
|
||||||
|
|
||||||
// 支持 [#] 标记直接 SQL
|
|
||||||
affected := database.Inserts("log", []Map{
|
|
||||||
{"user_id": 1, "created_time[#]": "NOW()"},
|
|
||||||
{"user_id": 2, "created_time[#]": "NOW()"},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 更新 (Update)
|
|
||||||
```go
|
|
||||||
affected := database.Update("table", dataMap, whereMap)
|
|
||||||
// 返回受影响的行数
|
|
||||||
```
|
|
||||||
|
|
||||||
### Upsert - 新增
|
|
||||||
```go
|
|
||||||
// 使用 Slice 格式
|
|
||||||
affected := database.Upsert("table",
|
|
||||||
dataMap, // 插入数据
|
|
||||||
Slice{"unique_key"}, // 唯一键
|
|
||||||
Slice{"col1", "col2"}, // 冲突时更新的字段
|
|
||||||
)
|
|
||||||
|
|
||||||
// 也支持可变参数
|
|
||||||
affected := database.Upsert("table", dataMap, Slice{"id"}, "col1", "col2")
|
|
||||||
// 返回受影响的行数
|
|
||||||
```
|
|
||||||
|
|
||||||
### 删除 (Delete)
|
|
||||||
```go
|
|
||||||
affected := database.Delete("table", whereMap)
|
|
||||||
// 返回删除的行数
|
|
||||||
```
|
|
||||||
|
|
||||||
## 聚合函数
|
|
||||||
|
|
||||||
### 计数
|
|
||||||
```go
|
|
||||||
count := database.Count("table")
|
|
||||||
count := database.Count("table", whereMap)
|
|
||||||
count := database.Count("table", joinSlice, whereMap)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 求和
|
|
||||||
```go
|
|
||||||
sum := database.Sum("table", "column")
|
|
||||||
sum := database.Sum("table", "column", whereMap)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 平均值 - 新增
|
|
||||||
```go
|
|
||||||
avg := database.Avg("table", "column")
|
|
||||||
avg := database.Avg("table", "column", whereMap)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 最大值 - 新增
|
|
||||||
```go
|
|
||||||
max := database.Max("table", "column")
|
|
||||||
max := database.Max("table", "column", whereMap)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 最小值 - 新增
|
|
||||||
```go
|
|
||||||
min := database.Min("table", "column")
|
|
||||||
min := database.Min("table", "column", whereMap)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 分页查询
|
|
||||||
```go
|
|
||||||
// 设置分页
|
|
||||||
database.Page(page, pageSize)
|
|
||||||
|
|
||||||
// 分页查询
|
|
||||||
data := database.Page(page, pageSize).PageSelect("table", "fields", whereMap)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 条件语法参考
|
|
||||||
|
|
||||||
### 比较操作符
|
|
||||||
| 写法 | SQL | 说明 |
|
|
||||||
|------|-----|------|
|
|
||||||
| `"field": value` | `field = ?` | 等于 |
|
|
||||||
| `"field[!]": value` | `field != ?` | 不等于 |
|
|
||||||
| `"field[>]": value` | `field > ?` | 大于 |
|
|
||||||
| `"field[>=]": value` | `field >= ?` | 大于等于 |
|
|
||||||
| `"field[<]": value` | `field < ?` | 小于 |
|
|
||||||
| `"field[<=]": value` | `field <= ?` | 小于等于 |
|
|
||||||
|
|
||||||
### 模糊查询
|
|
||||||
| 写法 | SQL | 说明 |
|
|
||||||
|------|-----|------|
|
|
||||||
| `"field[~]": "keyword"` | `field LIKE '%keyword%'` | 包含 |
|
|
||||||
| `"field[~!]": "keyword"` | `field LIKE 'keyword%'` | 以...开头 |
|
|
||||||
| `"field[!~]": "keyword"` | `field LIKE '%keyword'` | 以...结尾 |
|
|
||||||
| `"field[~~]": "%keyword%"` | `field LIKE '%keyword%'` | 手动LIKE |
|
|
||||||
|
|
||||||
### 范围查询
|
|
||||||
| 写法 | SQL | 说明 |
|
|
||||||
|------|-----|------|
|
|
||||||
| `"field[<>]": [min, max]` | `field BETWEEN ? AND ?` | 区间内 |
|
|
||||||
| `"field[><]": [min, max]` | `field NOT BETWEEN ? AND ?` | 区间外 |
|
|
||||||
|
|
||||||
### 集合查询
|
|
||||||
| 写法 | SQL | 说明 |
|
|
||||||
|------|-----|------|
|
|
||||||
| `"field": [v1, v2, v3]` | `field IN (?, ?, ?)` | 在集合中 |
|
|
||||||
| `"field[!]": [v1, v2, v3]` | `field NOT IN (?, ?, ?)` | 不在集合中 |
|
|
||||||
|
|
||||||
### NULL查询
|
|
||||||
| 写法 | SQL | 说明 |
|
|
||||||
|------|-----|------|
|
|
||||||
| `"field": nil` | `field IS NULL` | 为空 |
|
|
||||||
| `"field[!]": nil` | `field IS NOT NULL` | 不为空 |
|
|
||||||
|
|
||||||
### 直接SQL
|
|
||||||
| 写法 | SQL | 说明 |
|
|
||||||
|------|-----|------|
|
|
||||||
| `"field[#]": "NOW()"` | `field = NOW()` | 直接SQL函数 |
|
|
||||||
| `"[##]": "a > b"` | `a > b` | 直接SQL片段 |
|
|
||||||
| `"field[#!]": "1"` | `field != 1` | 不等于(不参数化) |
|
|
||||||
|
|
||||||
## 逻辑连接符
|
|
||||||
|
|
||||||
### AND 条件
|
|
||||||
```go
|
|
||||||
// 简化语法(推荐)
|
|
||||||
whereMap := Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
}
|
|
||||||
// 生成: WHERE `status`=? AND `age`>?
|
|
||||||
|
|
||||||
// 显式 AND(向后兼容)
|
|
||||||
whereMap := Map{
|
|
||||||
"AND": Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### OR 条件
|
|
||||||
```go
|
|
||||||
whereMap := Map{
|
|
||||||
"OR": Map{
|
|
||||||
"status": 1,
|
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 嵌套条件
|
|
||||||
```go
|
|
||||||
whereMap := Map{
|
|
||||||
"AND": Map{
|
|
||||||
"status": 1,
|
|
||||||
"OR": Map{
|
|
||||||
"age[<]": 30,
|
|
||||||
"level[>]": 5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## JOIN 语法
|
|
||||||
|
|
||||||
### 传统语法
|
|
||||||
```go
|
|
||||||
joinSlice := Slice{
|
|
||||||
Map{"[>]profile": "user.id = profile.user_id"}, // LEFT JOIN
|
|
||||||
Map{"[<]department": "user.dept_id = department.id"}, // RIGHT JOIN
|
|
||||||
Map{"[><]role": "user.role_id = role.id"}, // INNER JOIN
|
|
||||||
Map{"[<>]group": "user.group_id = group.id"}, // FULL JOIN
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 链式语法
|
|
||||||
```go
|
|
||||||
builder.LeftJoin("profile", "user.id = profile.user_id")
|
|
||||||
builder.RightJoin("department", "user.dept_id = department.id")
|
|
||||||
builder.InnerJoin("role", "user.role_id = role.id")
|
|
||||||
builder.FullJoin("group", "user.group_id = group.id")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 特殊字段语法
|
|
||||||
|
|
||||||
### ORDER BY
|
|
||||||
```go
|
|
||||||
Map{
|
|
||||||
"ORDER": []string{"created_time DESC", "id ASC"},
|
|
||||||
}
|
|
||||||
// 或
|
|
||||||
Map{
|
|
||||||
"order": "created_time DESC", // 支持小写
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GROUP BY
|
|
||||||
```go
|
|
||||||
Map{
|
|
||||||
"GROUP": []string{"department", "level"},
|
|
||||||
}
|
|
||||||
// 或
|
|
||||||
Map{
|
|
||||||
"group": "department", // 支持小写
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### HAVING - 新增
|
|
||||||
```go
|
|
||||||
Map{
|
|
||||||
"GROUP": "dept_id",
|
|
||||||
"HAVING": Map{
|
|
||||||
"COUNT(*).[>]": 5,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### LIMIT
|
|
||||||
```go
|
|
||||||
Map{
|
|
||||||
"LIMIT": []int{10, 20}, // offset 10, limit 20
|
|
||||||
}
|
|
||||||
// 或
|
|
||||||
Map{
|
|
||||||
"limit": 20, // limit 20,支持小写
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### OFFSET - 新增
|
|
||||||
```go
|
|
||||||
Map{
|
|
||||||
"LIMIT": 10,
|
|
||||||
"OFFSET": 20, // 独立的 OFFSET
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 事务处理
|
|
||||||
```go
|
|
||||||
success := database.Action(func(tx HoTimeDB) bool {
|
|
||||||
// 在这里执行数据库操作
|
|
||||||
// 返回 true 提交事务
|
|
||||||
// 返回 false 回滚事务
|
|
||||||
|
|
||||||
id := tx.Insert("table", data)
|
|
||||||
if id == 0 {
|
|
||||||
return false // 回滚
|
|
||||||
}
|
|
||||||
|
|
||||||
affected := tx.Update("table2", data2, where2)
|
|
||||||
if affected == 0 {
|
|
||||||
return false // 回滚
|
|
||||||
}
|
|
||||||
|
|
||||||
return true // 提交
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## 原生SQL执行
|
|
||||||
|
|
||||||
### 查询
|
|
||||||
```go
|
|
||||||
results := database.Query("SELECT * FROM user WHERE age > ?", 18)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 执行
|
|
||||||
```go
|
|
||||||
result, err := database.Exec("UPDATE user SET status = ? WHERE id = ?", 1, 100)
|
|
||||||
affected, _ := result.RowsAffected()
|
|
||||||
```
|
|
||||||
|
|
||||||
## PostgreSQL 支持 - 新增
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 配置 PostgreSQL
|
|
||||||
database := &db.HoTimeDB{
|
|
||||||
Type: "postgres", // 设置类型
|
|
||||||
}
|
|
||||||
|
|
||||||
// 框架自动处理差异:
|
|
||||||
// - 占位符: ? -> $1, $2, $3...
|
|
||||||
// - 引号: `name` -> "name"
|
|
||||||
// - Upsert: ON DUPLICATE KEY -> ON CONFLICT
|
|
||||||
```
|
|
||||||
|
|
||||||
## 错误处理
|
|
||||||
```go
|
|
||||||
// 检查最后的错误
|
|
||||||
if database.LastErr.GetError() != nil {
|
|
||||||
fmt.Println("错误:", database.LastErr.GetError())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看最后执行的SQL
|
|
||||||
fmt.Println("SQL:", database.LastQuery)
|
|
||||||
fmt.Println("参数:", database.LastData)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 工具方法
|
|
||||||
|
|
||||||
### 数据库信息
|
|
||||||
```go
|
|
||||||
prefix := database.GetPrefix() // 获取表前缀
|
|
||||||
dbType := database.GetType() // 获取数据库类型
|
|
||||||
dialect := database.GetDialect() // 获取方言适配器
|
|
||||||
```
|
|
||||||
|
|
||||||
### 设置模式
|
|
||||||
```go
|
|
||||||
database.Mode = 0 // 生产模式
|
|
||||||
database.Mode = 1 // 测试模式
|
|
||||||
database.Mode = 2 // 开发模式(输出SQL日志)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 常用查询模式
|
|
||||||
|
|
||||||
### 分页列表查询
|
|
||||||
```go
|
|
||||||
// 获取总数
|
|
||||||
total := database.Count("user", Map{"status": 1})
|
|
||||||
|
|
||||||
// 分页数据
|
|
||||||
users := database.Table("user").
|
|
||||||
Where("status", 1).
|
|
||||||
Order("created_time DESC").
|
|
||||||
Page(page, pageSize).
|
|
||||||
Select("id,name,email,created_time")
|
|
||||||
|
|
||||||
// 计算分页信息
|
|
||||||
totalPages := (total + pageSize - 1) / pageSize
|
|
||||||
```
|
|
||||||
|
|
||||||
### 关联查询
|
|
||||||
```go
|
|
||||||
orders := database.Table("order").
|
|
||||||
LeftJoin("user", "order.user_id = user.id").
|
|
||||||
LeftJoin("product", "order.product_id = product.id").
|
|
||||||
Where("order.status", "paid").
|
|
||||||
Select(`
|
|
||||||
order.*,
|
|
||||||
user.name as user_name,
|
|
||||||
product.title as product_title
|
|
||||||
`)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 统计查询
|
|
||||||
```go
|
|
||||||
stats := database.Select("order",
|
|
||||||
"user_id, COUNT(*) as order_count, SUM(amount) as total_amount",
|
|
||||||
Map{
|
|
||||||
"status": "paid",
|
|
||||||
"created_time[>]": "2023-01-01",
|
|
||||||
"GROUP": "user_id",
|
|
||||||
"ORDER": "total_amount DESC",
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 批量操作
|
|
||||||
```go
|
|
||||||
// 批量插入(使用 []Map 格式)
|
|
||||||
affected := database.Inserts("user", []Map{
|
|
||||||
{"name": "用户1", "email": "user1@example.com", "status": 1},
|
|
||||||
{"name": "用户2", "email": "user2@example.com", "status": 1},
|
|
||||||
{"name": "用户3", "email": "user3@example.com", "status": 1},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Upsert(插入或更新,使用 Slice 格式)
|
|
||||||
affected := database.Upsert("user",
|
|
||||||
Map{"id": 1, "name": "新名称", "email": "new@example.com"},
|
|
||||||
Slice{"id"},
|
|
||||||
Slice{"name", "email"},
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 链式调用完整示例
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 复杂查询链式调用
|
|
||||||
result := database.Table("order").
|
|
||||||
LeftJoin("user", "order.user_id = user.id").
|
|
||||||
LeftJoin("product", "order.product_id = product.id").
|
|
||||||
Where("order.status", "paid").
|
|
||||||
And("order.created_time[>]", "2023-01-01").
|
|
||||||
And(Map{
|
|
||||||
"OR": Map{
|
|
||||||
"user.level": "vip",
|
|
||||||
"order.amount[>]": 1000,
|
|
||||||
},
|
|
||||||
}).
|
|
||||||
Group("user.id").
|
|
||||||
Having(Map{"total_amount[>]": 500}).
|
|
||||||
Order("total_amount DESC").
|
|
||||||
Page(1, 20).
|
|
||||||
Select(`
|
|
||||||
user.id,
|
|
||||||
user.name,
|
|
||||||
user.email,
|
|
||||||
COUNT(order.id) as order_count,
|
|
||||||
SUM(order.amount) as total_amount
|
|
||||||
`)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*快速参考版本: 2.0*
|
|
||||||
*更新日期: 2026年1月*
|
|
||||||
|
|
||||||
**详细说明:**
|
|
||||||
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md) - 完整教程
|
|
||||||
@ -1,833 +0,0 @@
|
|||||||
# HoTimeDB ORM 使用说明书
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
HoTimeDB是一个基于Golang实现的轻量级ORM框架,参考PHP Medoo设计,提供简洁的数据库操作接口。支持MySQL、SQLite、PostgreSQL等数据库,并集成了缓存、事务、链式查询等功能。
|
|
||||||
|
|
||||||
## 目录
|
|
||||||
|
|
||||||
- [快速开始](#快速开始)
|
|
||||||
- [数据库配置](#数据库配置)
|
|
||||||
- [基本操作](#基本操作)
|
|
||||||
- [查询(Select)](#查询select)
|
|
||||||
- [获取单条记录(Get)](#获取单条记录get)
|
|
||||||
- [插入(Insert)](#插入insert)
|
|
||||||
- [批量插入(Inserts)](#批量插入Inserts)
|
|
||||||
- [更新(Update)](#更新update)
|
|
||||||
- [Upsert操作](#upsert操作)
|
|
||||||
- [删除(Delete)](#删除delete)
|
|
||||||
- [链式查询构建器](#链式查询构建器)
|
|
||||||
- [条件查询语法](#条件查询语法)
|
|
||||||
- [JOIN操作](#join操作)
|
|
||||||
- [分页查询](#分页查询)
|
|
||||||
- [聚合函数](#聚合函数)
|
|
||||||
- [事务处理](#事务处理)
|
|
||||||
- [缓存机制](#缓存机制)
|
|
||||||
- [PostgreSQL支持](#postgresql支持)
|
|
||||||
- [高级特性](#高级特性)
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
### 初始化数据库连接
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"code.hoteas.com/golang/hotime/db"
|
|
||||||
"code.hoteas.com/golang/hotime/common"
|
|
||||||
"database/sql"
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 创建连接函数
|
|
||||||
func createConnection() (master, slave *sql.DB) {
|
|
||||||
master, _ = sql.Open("mysql", "user:password@tcp(localhost:3306)/database")
|
|
||||||
// slave是可选的,用于读写分离
|
|
||||||
slave = master // 或者连接到从数据库
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化HoTimeDB
|
|
||||||
database := &db.HoTimeDB{
|
|
||||||
Type: "mysql", // 可选:mysql, sqlite3, postgres
|
|
||||||
}
|
|
||||||
database.SetConnect(createConnection)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 数据库配置
|
|
||||||
|
|
||||||
### 基本配置
|
|
||||||
|
|
||||||
```go
|
|
||||||
type HoTimeDB struct {
|
|
||||||
*sql.DB
|
|
||||||
ContextBase
|
|
||||||
DBName string
|
|
||||||
*cache.HoTimeCache
|
|
||||||
Log *logrus.Logger
|
|
||||||
Type string // 数据库类型:mysql, sqlite3, postgres
|
|
||||||
Prefix string // 表前缀
|
|
||||||
LastQuery string // 最后执行的SQL
|
|
||||||
LastData []interface{} // 最后的参数
|
|
||||||
ConnectFunc func(err ...*Error) (*sql.DB, *sql.DB)
|
|
||||||
LastErr *Error
|
|
||||||
limit Slice
|
|
||||||
*sql.Tx // 事务对象
|
|
||||||
SlaveDB *sql.DB // 从数据库
|
|
||||||
Mode int // 0生产模式,1测试模式,2开发模式
|
|
||||||
Dialect Dialect // 数据库方言适配器
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 设置表前缀
|
|
||||||
|
|
||||||
```go
|
|
||||||
database.Prefix = "app_"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 设置运行模式
|
|
||||||
|
|
||||||
```go
|
|
||||||
database.Mode = 2 // 开发模式,会输出SQL日志
|
|
||||||
```
|
|
||||||
|
|
||||||
## 基本操作
|
|
||||||
|
|
||||||
### 查询(Select)
|
|
||||||
|
|
||||||
#### 基本查询
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 查询所有字段
|
|
||||||
users := database.Select("user")
|
|
||||||
|
|
||||||
// 查询指定字段
|
|
||||||
users := database.Select("user", "id,name,email")
|
|
||||||
|
|
||||||
// 查询指定字段(数组形式)
|
|
||||||
users := database.Select("user", []string{"id", "name", "email"})
|
|
||||||
|
|
||||||
// 单条件查询
|
|
||||||
users := database.Select("user", "*", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 多条件查询(自动用 AND 连接)
|
|
||||||
users := database.Select("user", "*", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
})
|
|
||||||
// 生成: WHERE `status`=? AND `age`>?
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 复杂条件查询
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 简化语法:多条件自动用 AND 连接
|
|
||||||
users := database.Select("user", "*", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
"name[~]": "张",
|
|
||||||
})
|
|
||||||
// 生成: WHERE `status`=? AND `age`>? AND `name` LIKE ?
|
|
||||||
|
|
||||||
// 显式 AND 条件(与上面等效)
|
|
||||||
users := database.Select("user", "*", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
"name[~]": "张",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// OR 条件
|
|
||||||
users := database.Select("user", "*", common.Map{
|
|
||||||
"OR": common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 混合条件(嵌套 AND/OR)
|
|
||||||
users := database.Select("user", "*", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"OR": common.Map{
|
|
||||||
"age[<]": 30,
|
|
||||||
"level[>]": 5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 带 ORDER BY、LIMIT 等特殊条件(关键字支持大小写)
|
|
||||||
users := database.Select("user", "*", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
"ORDER": "id DESC", // 或 "order": "id DESC"
|
|
||||||
"LIMIT": 10, // 或 "limit": 10
|
|
||||||
})
|
|
||||||
|
|
||||||
// 带多个特殊条件
|
|
||||||
users := database.Select("user", "*", common.Map{
|
|
||||||
"OR": common.Map{
|
|
||||||
"level": "vip",
|
|
||||||
"balance[>]": 1000,
|
|
||||||
},
|
|
||||||
"ORDER": []string{"created_time DESC", "id ASC"},
|
|
||||||
"GROUP": "department",
|
|
||||||
"LIMIT": []int{0, 20}, // offset 0, limit 20
|
|
||||||
})
|
|
||||||
|
|
||||||
// 使用 HAVING 过滤分组结果
|
|
||||||
users := database.Select("user", "dept_id, COUNT(*) as cnt", common.Map{
|
|
||||||
"GROUP": "dept_id",
|
|
||||||
"HAVING": common.Map{
|
|
||||||
"cnt[>]": 5,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 使用独立 OFFSET
|
|
||||||
users := database.Select("user", "*", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"LIMIT": 10,
|
|
||||||
"OFFSET": 20,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 获取单条记录(Get)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 获取单个用户
|
|
||||||
user := database.Get("user", "*", common.Map{
|
|
||||||
"id": 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取指定字段
|
|
||||||
user := database.Get("user", "id,name,email", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 插入(Insert)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 基本插入
|
|
||||||
id := database.Insert("user", common.Map{
|
|
||||||
"name": "张三",
|
|
||||||
"email": "zhangsan@example.com",
|
|
||||||
"age": 25,
|
|
||||||
"status": 1,
|
|
||||||
"created_time[#]": "NOW()", // [#]表示直接插入SQL函数
|
|
||||||
})
|
|
||||||
|
|
||||||
// 返回插入的ID
|
|
||||||
fmt.Println("插入的用户ID:", id)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 批量插入(Inserts)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 批量插入多条记录(使用 []Map 格式,更直观)
|
|
||||||
affected := database.Inserts("user", []common.Map{
|
|
||||||
{"name": "张三", "email": "zhang@example.com", "age": 25},
|
|
||||||
{"name": "李四", "email": "li@example.com", "age": 30},
|
|
||||||
{"name": "王五", "email": "wang@example.com", "age": 28},
|
|
||||||
})
|
|
||||||
// 生成: INSERT INTO `user` (`age`, `email`, `name`) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
|
|
||||||
|
|
||||||
fmt.Printf("批量插入 %d 条记录\n", affected)
|
|
||||||
|
|
||||||
// 支持 [#] 标记直接插入 SQL 表达式
|
|
||||||
affected := database.Inserts("log", []common.Map{
|
|
||||||
{"user_id": 1, "action": "login", "created_time[#]": "NOW()"},
|
|
||||||
{"user_id": 2, "action": "logout", "created_time[#]": "NOW()"},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 更新(Update)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 基本更新
|
|
||||||
affected := database.Update("user", common.Map{
|
|
||||||
"name": "李四",
|
|
||||||
"email": "lisi@example.com",
|
|
||||||
"updated_time[#]": "NOW()",
|
|
||||||
}, common.Map{
|
|
||||||
"id": 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 条件更新(多条件自动 AND 连接)
|
|
||||||
affected := database.Update("user", common.Map{
|
|
||||||
"status": 0,
|
|
||||||
}, common.Map{
|
|
||||||
"age[<]": 18,
|
|
||||||
"status": 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Println("更新的记录数:", affected)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Upsert操作
|
|
||||||
|
|
||||||
Upsert(插入或更新):如果记录存在则更新,不存在则插入。
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Upsert 操作(使用 Slice 格式)
|
|
||||||
affected := database.Upsert("user",
|
|
||||||
common.Map{
|
|
||||||
"id": 1,
|
|
||||||
"name": "张三",
|
|
||||||
"email": "zhang@example.com",
|
|
||||||
"login_count": 1,
|
|
||||||
},
|
|
||||||
common.Slice{"id"}, // 唯一键(用于冲突检测)
|
|
||||||
common.Slice{"name", "email", "login_count"}, // 冲突时更新的字段
|
|
||||||
)
|
|
||||||
|
|
||||||
// MySQL 生成:
|
|
||||||
// INSERT INTO user (id,name,email,login_count) VALUES (?,?,?,?)
|
|
||||||
// ON DUPLICATE KEY UPDATE name=VALUES(name), email=VALUES(email), login_count=VALUES(login_count)
|
|
||||||
|
|
||||||
// PostgreSQL 生成:
|
|
||||||
// INSERT INTO "user" (id,name,email,login_count) VALUES ($1,$2,$3,$4)
|
|
||||||
// ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, email=EXCLUDED.email, login_count=EXCLUDED.login_count
|
|
||||||
|
|
||||||
// 使用 [#] 标记直接 SQL 更新
|
|
||||||
affected := database.Upsert("user",
|
|
||||||
common.Map{
|
|
||||||
"id": 1,
|
|
||||||
"name": "张三",
|
|
||||||
"login_count[#]": "login_count + 1", // 直接 SQL 表达式
|
|
||||||
},
|
|
||||||
common.Slice{"id"},
|
|
||||||
common.Slice{"name", "login_count"},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 也支持可变参数形式
|
|
||||||
affected := database.Upsert("user",
|
|
||||||
common.Map{"id": 1, "name": "张三"},
|
|
||||||
common.Slice{"id"},
|
|
||||||
"name", "email", // 可变参数
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 删除(Delete)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 根据ID删除
|
|
||||||
affected := database.Delete("user", common.Map{
|
|
||||||
"id": 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 条件删除(多条件自动 AND 连接)
|
|
||||||
affected := database.Delete("user", common.Map{
|
|
||||||
"status": 0,
|
|
||||||
"created_time[<]": "2023-01-01",
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Println("删除的记录数:", affected)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 链式查询构建器
|
|
||||||
|
|
||||||
HoTimeDB提供了链式查询构建器,让查询更加直观:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 基本链式查询
|
|
||||||
users := database.Table("user").
|
|
||||||
Where("status", 1).
|
|
||||||
And("age[>]", 18).
|
|
||||||
Order("created_time DESC").
|
|
||||||
Limit(10, 20). // offset, limit
|
|
||||||
Select()
|
|
||||||
|
|
||||||
// 链式获取单条记录
|
|
||||||
user := database.Table("user").
|
|
||||||
Where("id", 1).
|
|
||||||
Get()
|
|
||||||
|
|
||||||
// 链式更新
|
|
||||||
affected := database.Table("user").
|
|
||||||
Where("id", 1).
|
|
||||||
Update(common.Map{
|
|
||||||
"name": "新名称",
|
|
||||||
"updated_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
|
|
||||||
// 链式删除
|
|
||||||
affected := database.Table("user").
|
|
||||||
Where("status", 0).
|
|
||||||
Delete()
|
|
||||||
|
|
||||||
// 链式统计
|
|
||||||
count := database.Table("user").
|
|
||||||
Where("status", 1).
|
|
||||||
Count()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 链式条件组合
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 复杂条件组合
|
|
||||||
users := database.Table("user").
|
|
||||||
Where("status", 1).
|
|
||||||
And("age[>=]", 18).
|
|
||||||
Or(common.Map{
|
|
||||||
"level[>]": 5,
|
|
||||||
"vip": 1,
|
|
||||||
}).
|
|
||||||
Order("created_time DESC", "id ASC").
|
|
||||||
Group("department").
|
|
||||||
Having(common.Map{"COUNT(*).[>]": 5}). // 新增 HAVING 支持
|
|
||||||
Limit(0, 20).
|
|
||||||
Offset(10). // 新增独立 OFFSET 支持
|
|
||||||
Select("id,name,email,age")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 条件查询语法
|
|
||||||
|
|
||||||
HoTimeDB支持丰富的条件查询语法,类似于Medoo:
|
|
||||||
|
|
||||||
### 基本比较
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 等于
|
|
||||||
"id": 1
|
|
||||||
|
|
||||||
// 不等于
|
|
||||||
"id[!]": 1
|
|
||||||
|
|
||||||
// 大于
|
|
||||||
"age[>]": 18
|
|
||||||
|
|
||||||
// 大于等于
|
|
||||||
"age[>=]": 18
|
|
||||||
|
|
||||||
// 小于
|
|
||||||
"age[<]": 60
|
|
||||||
|
|
||||||
// 小于等于
|
|
||||||
"age[<=]": 60
|
|
||||||
```
|
|
||||||
|
|
||||||
### 模糊查询
|
|
||||||
|
|
||||||
```go
|
|
||||||
// LIKE %keyword%
|
|
||||||
"name[~]": "张"
|
|
||||||
|
|
||||||
// LIKE keyword% (右边任意)
|
|
||||||
"name[~!]": "张"
|
|
||||||
|
|
||||||
// LIKE %keyword (左边任意)
|
|
||||||
"name[!~]": "san"
|
|
||||||
|
|
||||||
// 手动LIKE(需要手动添加%)
|
|
||||||
"name[~~]": "%张%"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 区间查询
|
|
||||||
|
|
||||||
```go
|
|
||||||
// BETWEEN
|
|
||||||
"age[<>]": []int{18, 60}
|
|
||||||
|
|
||||||
// NOT BETWEEN
|
|
||||||
"age[><]": []int{18, 25}
|
|
||||||
```
|
|
||||||
|
|
||||||
### IN查询
|
|
||||||
|
|
||||||
```go
|
|
||||||
// IN
|
|
||||||
"id": []int{1, 2, 3, 4, 5}
|
|
||||||
|
|
||||||
// NOT IN
|
|
||||||
"id[!]": []int{1, 2, 3}
|
|
||||||
```
|
|
||||||
|
|
||||||
### NULL查询
|
|
||||||
|
|
||||||
```go
|
|
||||||
// IS NULL
|
|
||||||
"deleted_at": nil
|
|
||||||
|
|
||||||
// IS NOT NULL
|
|
||||||
"deleted_at[!]": nil
|
|
||||||
```
|
|
||||||
|
|
||||||
### 直接SQL
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 直接插入SQL表达式(注意防注入)
|
|
||||||
"created_time[#]": "> DATE_SUB(NOW(), INTERVAL 1 DAY)"
|
|
||||||
|
|
||||||
// 字段直接赋值(不使用参数化查询)
|
|
||||||
"update_time[#]": "NOW()"
|
|
||||||
|
|
||||||
// 直接SQL片段
|
|
||||||
"[##]": "user.status = 1 AND user.level > 0"
|
|
||||||
```
|
|
||||||
|
|
||||||
## JOIN操作
|
|
||||||
|
|
||||||
### 链式JOIN
|
|
||||||
|
|
||||||
```go
|
|
||||||
// LEFT JOIN
|
|
||||||
users := database.Table("user").
|
|
||||||
LeftJoin("profile", "user.id = profile.user_id").
|
|
||||||
LeftJoin("department", "user.dept_id = department.id").
|
|
||||||
Where("user.status", 1).
|
|
||||||
Select("user.*, profile.avatar, department.name AS dept_name")
|
|
||||||
|
|
||||||
// RIGHT JOIN
|
|
||||||
users := database.Table("user").
|
|
||||||
RightJoin("order", "user.id = order.user_id").
|
|
||||||
Select()
|
|
||||||
|
|
||||||
// INNER JOIN
|
|
||||||
users := database.Table("user").
|
|
||||||
InnerJoin("profile", "user.id = profile.user_id").
|
|
||||||
Select()
|
|
||||||
|
|
||||||
// FULL JOIN
|
|
||||||
users := database.Table("user").
|
|
||||||
FullJoin("profile", "user.id = profile.user_id").
|
|
||||||
Select()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 传统JOIN语法
|
|
||||||
|
|
||||||
```go
|
|
||||||
users := database.Select("user",
|
|
||||||
common.Slice{
|
|
||||||
common.Map{"[>]profile": "user.id = profile.user_id"},
|
|
||||||
common.Map{"[>]department": "user.dept_id = department.id"},
|
|
||||||
},
|
|
||||||
"user.*, profile.avatar, department.name AS dept_name",
|
|
||||||
common.Map{
|
|
||||||
"user.status": 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### JOIN类型说明
|
|
||||||
|
|
||||||
- `[>]`: LEFT JOIN
|
|
||||||
- `[<]`: RIGHT JOIN
|
|
||||||
- `[><]`: INNER JOIN
|
|
||||||
- `[<>]`: FULL JOIN
|
|
||||||
|
|
||||||
## 分页查询
|
|
||||||
|
|
||||||
### 基本分页
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 设置分页:页码3,每页20条
|
|
||||||
users := database.Page(3, 20).PageSelect("user", "*", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 链式分页
|
|
||||||
users := database.Table("user").
|
|
||||||
Where("status", 1).
|
|
||||||
Page(2, 15). // 第2页,每页15条
|
|
||||||
Select()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 分页信息获取
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 获取总数
|
|
||||||
total := database.Count("user", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算分页信息
|
|
||||||
page := 2
|
|
||||||
pageSize := 20
|
|
||||||
offset := (page - 1) * pageSize
|
|
||||||
totalPages := (total + pageSize - 1) / pageSize
|
|
||||||
|
|
||||||
fmt.Printf("总记录数: %d, 总页数: %d, 当前页: %d\n", total, totalPages, page)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 聚合函数
|
|
||||||
|
|
||||||
### 计数
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 总数统计
|
|
||||||
total := database.Count("user")
|
|
||||||
|
|
||||||
// 条件统计
|
|
||||||
activeUsers := database.Count("user", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// JOIN统计
|
|
||||||
count := database.Count("user",
|
|
||||||
common.Slice{
|
|
||||||
common.Map{"[>]profile": "user.id = profile.user_id"},
|
|
||||||
},
|
|
||||||
common.Map{
|
|
||||||
"user.status": 1,
|
|
||||||
"profile.verified": 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 求和
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 基本求和
|
|
||||||
totalAmount := database.Sum("order", "amount")
|
|
||||||
|
|
||||||
// 条件求和
|
|
||||||
paidAmount := database.Sum("order", "amount", common.Map{
|
|
||||||
"status": "paid",
|
|
||||||
"created_time[>]": "2023-01-01",
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 平均值
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 基本平均值
|
|
||||||
avgAge := database.Avg("user", "age")
|
|
||||||
|
|
||||||
// 条件平均值
|
|
||||||
avgAge := database.Avg("user", "age", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 最大值/最小值
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 最大值
|
|
||||||
maxAge := database.Max("user", "age")
|
|
||||||
|
|
||||||
// 最小值
|
|
||||||
minAge := database.Min("user", "age")
|
|
||||||
|
|
||||||
// 条件最大值
|
|
||||||
maxBalance := database.Max("user", "balance", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## 事务处理
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 事务操作
|
|
||||||
success := database.Action(func(tx db.HoTimeDB) bool {
|
|
||||||
// 在事务中执行多个操作
|
|
||||||
|
|
||||||
// 扣减用户余额
|
|
||||||
affected1 := tx.Update("user", common.Map{
|
|
||||||
"balance[#]": "balance - 100",
|
|
||||||
}, common.Map{
|
|
||||||
"id": 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
if affected1 == 0 {
|
|
||||||
return false // 回滚
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建订单
|
|
||||||
orderId := tx.Insert("order", common.Map{
|
|
||||||
"user_id": 1,
|
|
||||||
"amount": 100,
|
|
||||||
"status": "paid",
|
|
||||||
"created_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
|
|
||||||
if orderId == 0 {
|
|
||||||
return false // 回滚
|
|
||||||
}
|
|
||||||
|
|
||||||
return true // 提交
|
|
||||||
})
|
|
||||||
|
|
||||||
if success {
|
|
||||||
fmt.Println("事务执行成功")
|
|
||||||
} else {
|
|
||||||
fmt.Println("事务回滚")
|
|
||||||
fmt.Println("错误:", database.LastErr.GetError())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 缓存机制
|
|
||||||
|
|
||||||
HoTimeDB集成了缓存功能,可以自动缓存查询结果:
|
|
||||||
|
|
||||||
### 缓存配置
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "code.hoteas.com/golang/hotime/cache"
|
|
||||||
|
|
||||||
// 设置缓存
|
|
||||||
database.HoTimeCache = &cache.HoTimeCache{
|
|
||||||
// 缓存配置
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 缓存行为
|
|
||||||
|
|
||||||
- 查询操作会自动检查缓存
|
|
||||||
- 增删改操作会自动清除相关缓存
|
|
||||||
- 缓存键格式:`表名:查询MD5`
|
|
||||||
- `cached`表不会被缓存
|
|
||||||
|
|
||||||
### 缓存清理
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 手动清除表缓存
|
|
||||||
database.HoTimeCache.Db("user*", nil) // 清除user表所有缓存
|
|
||||||
```
|
|
||||||
|
|
||||||
## PostgreSQL支持
|
|
||||||
|
|
||||||
HoTimeDB 支持 PostgreSQL 数据库,自动处理语法差异。
|
|
||||||
|
|
||||||
### PostgreSQL 配置
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"code.hoteas.com/golang/hotime/db"
|
|
||||||
_ "github.com/lib/pq" // PostgreSQL 驱动
|
|
||||||
)
|
|
||||||
|
|
||||||
database := &db.HoTimeDB{
|
|
||||||
Type: "postgres", // 设置数据库类型为 postgres
|
|
||||||
Prefix: "app_",
|
|
||||||
}
|
|
||||||
|
|
||||||
database.SetConnect(func(err ...*common.Error) (master, slave *sql.DB) {
|
|
||||||
dsn := "host=localhost port=5432 user=postgres password=secret dbname=mydb sslmode=disable"
|
|
||||||
master, _ = sql.Open("postgres", dsn)
|
|
||||||
return master, master
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 主要差异
|
|
||||||
|
|
||||||
| 特性 | MySQL | PostgreSQL |
|
|
||||||
|------|-------|------------|
|
|
||||||
| 标识符引号 | \`name\` | "name" |
|
|
||||||
| 占位符 | ? | $1, $2, $3... |
|
|
||||||
| Upsert | ON DUPLICATE KEY UPDATE | ON CONFLICT DO UPDATE |
|
|
||||||
|
|
||||||
所有这些差异由框架自动处理,无需手动调整代码。
|
|
||||||
|
|
||||||
## 高级特性
|
|
||||||
|
|
||||||
### 调试模式
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 设置调试模式
|
|
||||||
database.Mode = 2
|
|
||||||
|
|
||||||
// 查看最后执行的SQL
|
|
||||||
fmt.Println("最后的SQL:", database.LastQuery)
|
|
||||||
fmt.Println("参数:", database.LastData)
|
|
||||||
fmt.Println("错误:", database.LastErr.GetError())
|
|
||||||
```
|
|
||||||
|
|
||||||
### 主从分离
|
|
||||||
|
|
||||||
```go
|
|
||||||
func createConnection() (master, slave *sql.DB) {
|
|
||||||
// 主库连接
|
|
||||||
master, _ = sql.Open("mysql", "user:password@tcp(master:3306)/database")
|
|
||||||
|
|
||||||
// 从库连接
|
|
||||||
slave, _ = sql.Open("mysql", "user:password@tcp(slave:3306)/database")
|
|
||||||
|
|
||||||
return master, slave
|
|
||||||
}
|
|
||||||
|
|
||||||
database.SetConnect(createConnection)
|
|
||||||
// 查询会自动使用从库,增删改使用主库
|
|
||||||
```
|
|
||||||
|
|
||||||
### 原生SQL执行
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 执行查询SQL
|
|
||||||
results := database.Query("SELECT * FROM user WHERE age > ? AND status = ?", 18, 1)
|
|
||||||
|
|
||||||
// 执行更新SQL
|
|
||||||
result, err := database.Exec("UPDATE user SET last_login = NOW() WHERE id = ?", 1)
|
|
||||||
if err.GetError() == nil {
|
|
||||||
affected, _ := result.RowsAffected()
|
|
||||||
fmt.Println("影响行数:", affected)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 特殊语法详解
|
|
||||||
|
|
||||||
### 条件标记符说明
|
|
||||||
|
|
||||||
| 标记符 | 功能 | 示例 | 生成SQL |
|
|
||||||
|--------|------|------|---------|
|
|
||||||
| `[>]` | 大于 | `"age[>]": 18` | `age > 18` |
|
|
||||||
| `[<]` | 小于 | `"age[<]": 60` | `age < 60` |
|
|
||||||
| `[>=]` | 大于等于 | `"age[>=]": 18` | `age >= 18` |
|
|
||||||
| `[<=]` | 小于等于 | `"age[<=]": 60` | `age <= 60` |
|
|
||||||
| `[!]` | 不等于/NOT IN | `"id[!]": 1` | `id != 1` |
|
|
||||||
| `[~]` | LIKE模糊查询 | `"name[~]": "张"` | `name LIKE '%张%'` |
|
|
||||||
| `[!~]` | 左模糊 | `"name[!~]": "张"` | `name LIKE '%张'` |
|
|
||||||
| `[~!]` | 右模糊 | `"name[~!]": "张"` | `name LIKE '张%'` |
|
|
||||||
| `[~~]` | 手动LIKE | `"name[~~]": "%张%"` | `name LIKE '%张%'` |
|
|
||||||
| `[<>]` | BETWEEN | `"age[<>]": [18,60]` | `age BETWEEN 18 AND 60` |
|
|
||||||
| `[><]` | NOT BETWEEN | `"age[><]": [18,25]` | `age NOT BETWEEN 18 AND 25` |
|
|
||||||
| `[#]` | 直接SQL | `"time[#]": "NOW()"` | `time = NOW()` |
|
|
||||||
| `[##]` | SQL片段 | `"[##]": "a > b"` | `a > b` |
|
|
||||||
| `[#!]` | 不等于直接SQL | `"status[#!]": "1"` | `status != 1` |
|
|
||||||
| `[!#]` | 不等于直接SQL | `"status[!#]": "1"` | `status != 1` |
|
|
||||||
|
|
||||||
### 特殊关键字(支持大小写)
|
|
||||||
|
|
||||||
| 关键字 | 功能 | 示例 |
|
|
||||||
|--------|------|------|
|
|
||||||
| `ORDER` / `order` | 排序 | `"ORDER": "id DESC"` |
|
|
||||||
| `GROUP` / `group` | 分组 | `"GROUP": "dept_id"` |
|
|
||||||
| `LIMIT` / `limit` | 限制 | `"LIMIT": 10` |
|
|
||||||
| `OFFSET` / `offset` | 偏移 | `"OFFSET": 20` |
|
|
||||||
| `HAVING` / `having` | 分组过滤 | `"HAVING": Map{"cnt[>]": 5}` |
|
|
||||||
| `DISTINCT` / `distinct` | 去重 | 在 SELECT 中使用 |
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Q1: 多条件查询需要用 AND 包装吗?
|
|
||||||
A1: 不再需要!现在多条件会自动用 AND 连接。当然,使用 `AND` 包装仍然有效(向后兼容)。
|
|
||||||
|
|
||||||
### Q2: 如何处理事务中的错误?
|
|
||||||
A2: 在 `Action` 函数中返回 `false` 即可触发回滚,所有操作都会被撤销。
|
|
||||||
|
|
||||||
### Q3: 缓存何时会被清除?
|
|
||||||
A3: 执行 `Insert`、`Update`、`Delete`、`Upsert`、`Inserts` 操作时会自动清除对应表的缓存。
|
|
||||||
|
|
||||||
### Q4: 如何执行复杂的原生SQL?
|
|
||||||
A4: 使用 `Query` 方法执行查询,使用 `Exec` 方法执行更新操作。
|
|
||||||
|
|
||||||
### Q5: 主从分离如何工作?
|
|
||||||
A5: 查询操作自动使用从库(如果配置了),增删改操作使用主库。
|
|
||||||
|
|
||||||
### Q6: 如何处理NULL值?
|
|
||||||
A6: 使用 `nil` 作为值,查询时使用 `"field": nil` 表示 `IS NULL`。
|
|
||||||
|
|
||||||
### Q7: PostgreSQL 和 MySQL 语法有区别吗?
|
|
||||||
A7: 框架会自动处理差异(占位符、引号等),代码无需修改。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*文档版本: 2.0*
|
|
||||||
*最后更新: 2026年1月*
|
|
||||||
|
|
||||||
> 本文档基于HoTimeDB源码分析生成,如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念,但根据Golang语言特性进行了适配和优化。
|
|
||||||
|
|
||||||
**更多参考:**
|
|
||||||
- [HoTimeDB API 参考](HoTimeDB_API参考.md) - API 速查手册
|
|
||||||
@ -1,529 +0,0 @@
|
|||||||
# 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 速查手册
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package baidu
|
package baidu
|
||||||
|
|
||||||
import (
|
import (
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -22,93 +21,6 @@ func (that *baiduMap) Init(Ak string) {
|
|||||||
//query
|
//query
|
||||||
}
|
}
|
||||||
|
|
||||||
// from 源坐标类型:
|
|
||||||
// 1:GPS标准坐标;
|
|
||||||
// 2:搜狗地图坐标;
|
|
||||||
// 3:火星坐标(gcj02),即高德地图、腾讯地图和MapABC等地图使用的坐标;
|
|
||||||
// 4:3中列举的地图坐标对应的墨卡托平面坐标;
|
|
||||||
// 5:百度地图采用的经纬度坐标(bd09ll);
|
|
||||||
// 6:百度地图采用的墨卡托平面坐标(bd09mc);
|
|
||||||
// 7:图吧地图坐标;
|
|
||||||
// 8:51地图坐标;
|
|
||||||
// int 1 1 否
|
|
||||||
// to
|
|
||||||
// 目标坐标类型:
|
|
||||||
// 3:火星坐标(gcj02),即高德地图、腾讯地图及MapABC等地图使用的坐标;
|
|
||||||
// 5:百度地图采用的经纬度坐标(bd09ll);
|
|
||||||
// 6:百度地图采用的墨卡托平面坐标(bd09mc);
|
|
||||||
func (that *baiduMap) Geoconv(latlngs []Map, from, to int) (Slice, error) {
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
latlngsStr := ""
|
|
||||||
for _, v := range latlngs {
|
|
||||||
if latlngsStr != "" {
|
|
||||||
latlngsStr = latlngsStr + ";" + v.GetString("lng") + "," + v.GetString("lat")
|
|
||||||
} else {
|
|
||||||
latlngsStr = v.GetString("lng") + "," + v.GetString("lat")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
url := "https://api.map.baidu.com/geoconv/v1/?from=" + ObjToStr(from) + "&to=" + ObjToStr(to) + "&ak=" + that.Ak + "&coords=" + latlngsStr
|
|
||||||
reqest, err := http.NewRequest("GET", url, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Fatal error ", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
response, err := client.Do(reqest)
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Fatal error ", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
//fmt.Println(string(body))
|
|
||||||
data := ObjToMap(string(body))
|
|
||||||
if data.GetCeilInt64("status") != 0 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.GetSlice("result"), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (that *baiduMap) GetAddress(lat string, lng string) (string, error) {
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
|
|
||||||
url := "https://api.map.baidu.com/reverse_geocoding/v3/?ak=" + that.Ak + "&output=json&location=" + lat + "," + lng
|
|
||||||
reqest, err := http.NewRequest("GET", url, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Fatal error ", err.Error())
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
response, err := client.Do(reqest)
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Fatal error ", err.Error())
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
//fmt.Println(string(body))
|
|
||||||
|
|
||||||
return string(body), err
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPosition 获取定位列表
|
// GetPosition 获取定位列表
|
||||||
func (that *baiduMap) GetPosition(name string, region string) (string, error) {
|
func (that *baiduMap) GetPosition(name string, region string) (string, error) {
|
||||||
|
|
||||||
|
|||||||
@ -18,13 +18,13 @@ func Down(url, path, name string, e ...*Error) bool {
|
|||||||
}
|
}
|
||||||
out, err := os.Create(path + name)
|
out, err := os.Create(path + name)
|
||||||
|
|
||||||
if err != nil && len(e) != 0 {
|
if err != nil && e[0] != nil {
|
||||||
e[0].SetError(err)
|
e[0].SetError(err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil && len(e) != 0 {
|
if err != nil && e[0] != nil {
|
||||||
e[0].SetError(err)
|
e[0].SetError(err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ func Down(url, path, name string, e ...*Error) bool {
|
|||||||
|
|
||||||
pix, err := ioutil.ReadAll(resp.Body)
|
pix, err := ioutil.ReadAll(resp.Body)
|
||||||
_, err = io.Copy(out, bytes.NewReader(pix))
|
_, err = io.Copy(out, bytes.NewReader(pix))
|
||||||
if err != nil && len(e) != 0 {
|
if err != nil && e[0] != nil {
|
||||||
e[0].SetError(err)
|
e[0].SetError(err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,149 +0,0 @@
|
|||||||
package mongodb
|
|
||||||
|
|
||||||
import (
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MongoDb struct {
|
|
||||||
Client *mongo.Client
|
|
||||||
Ctx context.Context
|
|
||||||
DataBase *mongo.Database
|
|
||||||
Connect *mongo.Collection
|
|
||||||
LastErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetMongoDb(database, url string) (*MongoDb, error) {
|
|
||||||
db := MongoDb{}
|
|
||||||
clientOptions := options.Client().ApplyURI(url)
|
|
||||||
|
|
||||||
db.Ctx = context.TODO()
|
|
||||||
// Connect to MongoDb
|
|
||||||
var err error
|
|
||||||
db.Client, err = mongo.Connect(db.Ctx, clientOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Check the connection
|
|
||||||
err = db.Client.Ping(db.Ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fmt.Println("Connected to MongoDb!")
|
|
||||||
//databases, err := db.Client.ListDatabaseNames(db.Ctx, bson.M{})
|
|
||||||
//if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
//}
|
|
||||||
//fmt.Println(databases)
|
|
||||||
db.DataBase = db.Client.Database(database)
|
|
||||||
return &db, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (that *MongoDb) Insert(table string, data interface{}) string {
|
|
||||||
collection := that.DataBase.Collection(table)
|
|
||||||
re, err := collection.InsertOne(that.Ctx, data)
|
|
||||||
if err != nil {
|
|
||||||
that.LastErr = err
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return ObjToStr(re.InsertedID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (that *MongoDb) InsertMany(table string, data ...interface{}) Slice {
|
|
||||||
collection := that.DataBase.Collection(table)
|
|
||||||
re, err := collection.InsertMany(that.Ctx, data)
|
|
||||||
if err != nil {
|
|
||||||
that.LastErr = err
|
|
||||||
return Slice{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ObjToSlice(re.InsertedIDs)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (that *MongoDb) Update(table string, data Map, where Map) int64 {
|
|
||||||
collection := that.DataBase.Collection(table)
|
|
||||||
re, err := collection.UpdateMany(that.Ctx, where, data)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
that.LastErr = err
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.ModifiedCount
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (that *MongoDb) Delete(table string, where Map) int64 {
|
|
||||||
collection := that.DataBase.Collection(table)
|
|
||||||
re, err := collection.DeleteMany(that.Ctx, where)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
that.LastErr = err
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return re.DeletedCount
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (that *MongoDb) Get(table string, where Map) Map {
|
|
||||||
results := []Map{}
|
|
||||||
var cursor *mongo.Cursor
|
|
||||||
var err error
|
|
||||||
collection := that.DataBase.Collection(table)
|
|
||||||
if cursor, err = collection.Find(that.Ctx, where, options.Find().SetSkip(0), options.Find().SetLimit(2)); err != nil {
|
|
||||||
that.LastErr = err
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
//延迟关闭游标
|
|
||||||
defer func() {
|
|
||||||
if err := cursor.Close(that.Ctx); err != nil {
|
|
||||||
that.LastErr = err
|
|
||||||
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
//这里的结果遍历可以使用另外一种更方便的方式:
|
|
||||||
if err = cursor.All(that.Ctx, &results); err != nil {
|
|
||||||
that.LastErr = err
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(results) > 0 {
|
|
||||||
|
|
||||||
return results[0]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (that MongoDb) Select(table string, where Map, page, pageRow int64) []Map {
|
|
||||||
page = (page - 1) * pageRow
|
|
||||||
if page < 0 {
|
|
||||||
page = 0
|
|
||||||
}
|
|
||||||
results := []Map{}
|
|
||||||
var cursor *mongo.Cursor
|
|
||||||
var err error
|
|
||||||
collection := that.DataBase.Collection(table)
|
|
||||||
if cursor, err = collection.Find(that.Ctx, where, options.Find().SetSkip(page), options.Find().SetLimit(pageRow)); err != nil {
|
|
||||||
that.LastErr = err
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
//延迟关闭游标
|
|
||||||
defer func() {
|
|
||||||
if err := cursor.Close(that.Ctx); err != nil {
|
|
||||||
that.LastErr = err
|
|
||||||
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
//这里的结果遍历可以使用另外一种更方便的方式:
|
|
||||||
|
|
||||||
if err = cursor.All(that.Ctx, &results); err != nil {
|
|
||||||
that.LastErr = err
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
104
example/app/article.go
Normal file
104
example/app/article.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime"
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ArticleCtr = Ctr{
|
||||||
|
"info": func(that *Context) {
|
||||||
|
sn := that.Req.FormValue("sn")
|
||||||
|
article := that.Db.Get("article", Map{"[><]ctg_article": "article.id=ctg_article.article_id"}, "article.*,ctg_article.ctg_id AS sctg_id", Map{"ctg_article.sn": sn})
|
||||||
|
if article == nil {
|
||||||
|
that.Display(4, "找不到对应数据")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctgId := article.GetCeilInt64("sctg_id")
|
||||||
|
ctg := that.Db.Get("ctg", "*", Map{"id": ctgId})
|
||||||
|
parents := []Map{}
|
||||||
|
parentId := ctg.GetCeilInt64("parent_id")
|
||||||
|
article["tongji"] = that.Db.Select("ctg", "sn,name,img,parent_id", Map{"parent_id": parentId})
|
||||||
|
for true {
|
||||||
|
|
||||||
|
if parentId == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := that.Db.Get("ctg", "sn,name,img,parent_id", Map{"id": parentId})
|
||||||
|
if parent == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
parents = append(parents, parent)
|
||||||
|
parentId = parent.GetCeilInt64("parent_id")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ctg["parents"] = parents
|
||||||
|
|
||||||
|
article["ctg"] = ctg
|
||||||
|
that.Display(0, article)
|
||||||
|
},
|
||||||
|
"list": func(that *Context) {
|
||||||
|
sn := that.Req.FormValue("ctg_sn") //ctgsn
|
||||||
|
page := ObjToInt(that.Req.FormValue("page"))
|
||||||
|
pageSize := ObjToInt(that.Req.FormValue("pageSize"))
|
||||||
|
|
||||||
|
if page == 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize == 0 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
keywords := that.Req.FormValue("keywords")
|
||||||
|
|
||||||
|
lunbo := ObjToInt(that.Req.FormValue("lunbo"))
|
||||||
|
|
||||||
|
sort := that.Req.FormValue("sort")
|
||||||
|
|
||||||
|
where := Map{"article.push_time[<]": time.Now().Format("2006-01-02 15:04"), "article.state": 0}
|
||||||
|
if sn != "" {
|
||||||
|
ctg := that.Db.Get("ctg", "id", Map{"sn": sn})
|
||||||
|
if ctg != nil {
|
||||||
|
where["ctg_article.ctg_id"] = ctg.GetCeilInt("id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := that.Req.FormValue("start_time") //ctgsn
|
||||||
|
finishTime := that.Req.FormValue("finish_time") //ctgsn
|
||||||
|
|
||||||
|
if lunbo != 0 {
|
||||||
|
where["article.lunbo"] = lunbo
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(startTime) > 5 {
|
||||||
|
where["article.push_time[>=]"] = startTime
|
||||||
|
}
|
||||||
|
if len(finishTime) > 5 {
|
||||||
|
where["article.push_time[<=]"] = finishTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if keywords != "" {
|
||||||
|
where["OR"] = Map{"article.title[~]": keywords, "article.description[~]": keywords, "article.author[~]": keywords, "article.sn[~]": keywords, "article.origin[~]": keywords, "article.url[~]": keywords}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(where) > 1 {
|
||||||
|
where = Map{"AND": where}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sort == "" {
|
||||||
|
where["ORDER"] = Slice{"article.sort DESC", "article.push_time DESC"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sort == "time" {
|
||||||
|
where["ORDER"] = "article.push_time DESC"
|
||||||
|
}
|
||||||
|
count := that.Db.Count("article", Map{"[><]ctg_article": "article.id=ctg_article.article_id"}, where)
|
||||||
|
article := that.Db.Page(page, pageSize).PageSelect("article", Map{"[><]ctg_article": "article.id=ctg_article.article_id"}, "ctg_article.sn,article.img,article.title,article.description,article.push_time,article.lunbo,article.author,article.origin,article.url", where)
|
||||||
|
|
||||||
|
that.Display(0, Map{"count": count, "data": article})
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
77
example/app/ctg.go
Normal file
77
example/app/ctg.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime"
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var CtgCtr = Ctr{
|
||||||
|
"info": func(that *Context) {
|
||||||
|
sn := that.Req.FormValue("sn")
|
||||||
|
ctg := that.Db.Get("ctg", "*", Map{"sn": sn})
|
||||||
|
parents := []Map{}
|
||||||
|
parentId := ctg.GetCeilInt64("parent_id")
|
||||||
|
ctg["tongji"] = that.Db.Select("ctg", "sn,name,img,parent_id", Map{"parent_id": parentId})
|
||||||
|
for true {
|
||||||
|
|
||||||
|
if parentId == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := that.Db.Get("ctg", "sn,name,img,parent_id", Map{"id": parentId})
|
||||||
|
if parent == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
parents = append(parents, parent)
|
||||||
|
parentId = parent.GetCeilInt64("parent_id")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctg.GetCeilInt64("article_id") != 0 {
|
||||||
|
ctg["article"] = that.Db.Get("article", "*", Map{"id": ctg.GetCeilInt64("article_id")})
|
||||||
|
}
|
||||||
|
|
||||||
|
ctg["parents"] = parents
|
||||||
|
that.Display(0, ctg)
|
||||||
|
},
|
||||||
|
"list": func(that *Context) {
|
||||||
|
sn := that.Req.FormValue("sn") //ctgsn
|
||||||
|
page := ObjToInt(that.Req.FormValue("page"))
|
||||||
|
pageSize := ObjToInt(that.Req.FormValue("pageSize"))
|
||||||
|
|
||||||
|
if page == 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize == 0 {
|
||||||
|
pageSize = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
keywords := that.Req.FormValue("keywords")
|
||||||
|
|
||||||
|
//sort:=that.Req.FormValue("sort")
|
||||||
|
|
||||||
|
where := Map{"state": 0}
|
||||||
|
if sn != "" {
|
||||||
|
ctg := that.Db.Get("ctg", "id", Map{"sn": sn})
|
||||||
|
if ctg != nil {
|
||||||
|
where["parent_id"] = ctg.GetCeilInt("id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if keywords != "" {
|
||||||
|
where["OR"] = Map{"name[~]": keywords, "url[~]": keywords, "sn[~]": keywords}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(where) > 1 {
|
||||||
|
where = Map{"AND": where}
|
||||||
|
}
|
||||||
|
|
||||||
|
where["ORDER"] = Slice{"sort DESC", "id DESC"}
|
||||||
|
|
||||||
|
article := that.Db.Page(page, pageSize).PageSelect("ctg", "name,sn,sort,url,img", where)
|
||||||
|
|
||||||
|
that.Display(0, article)
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
127
example/app/init.go
Normal file
127
example/app/init.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime"
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AppProj = Proj{
|
||||||
|
"article": ArticleCtr,
|
||||||
|
"org": OrgCtr,
|
||||||
|
"ctg": CtgCtr,
|
||||||
|
"mail": MailCtr,
|
||||||
|
"test": {
|
||||||
|
"test": func(that *Context) {
|
||||||
|
//data:=that.Db.Table("admin").Order("id DESC").Select("*")
|
||||||
|
//data1:=that.Db.Table("admin").Where(Map{"name[~]":"m"}).Order("id DESC").Select("*")
|
||||||
|
//
|
||||||
|
//data3:=that.Db.Select("admin","*",Map{"name[~]":"m"})
|
||||||
|
data2 := that.Db.Table("article").Where(Map{"title[~]": "m"}).Order("id DESC").Page(1, 10).Select("*")
|
||||||
|
c := that.Db.Table("article").Where(Map{"title[~]": "m"}).Order("id DESC").Group("title").Select("*")
|
||||||
|
//that.Display(0,Slice{data1,data,data3,data2})
|
||||||
|
that.Display(0, Slice{data2, c})
|
||||||
|
},
|
||||||
|
"res": func(that *Context) {
|
||||||
|
ebw_res := that.Db.Select("ebw_res", "*")
|
||||||
|
|
||||||
|
for _, v := range ebw_res {
|
||||||
|
data := Map{"id": v.GetCeilInt("id"), "name": v.GetString("name"),
|
||||||
|
"parent_id": v.GetCeilInt64("pid"),
|
||||||
|
"sn": v.GetString("url"), "create_time": time.Now().Format("2006-01-02 15:04"),
|
||||||
|
"modify_time": time.Now().Format("2006-01-02 15:04"), "admin_id": 1}
|
||||||
|
if data.GetCeilInt("parent_id") == 0 {
|
||||||
|
data["parent_id"] = nil
|
||||||
|
}
|
||||||
|
that.Db.Insert("ctg", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
that.Db.Exec("UPDATE ctg SET parent_id =NULL WHERE parent_id=id")
|
||||||
|
|
||||||
|
ss(0, that)
|
||||||
|
|
||||||
|
that.Display(0, len(ebw_res))
|
||||||
|
|
||||||
|
},
|
||||||
|
"news": func(that *Context) {
|
||||||
|
ebw_news := that.Db.Select("ebw_news", "*")
|
||||||
|
|
||||||
|
for _, v := range ebw_news {
|
||||||
|
ctg := that.Db.Get("ctg", "*", Map{"sn": v.GetString("type")})
|
||||||
|
data := Map{"sn": v.GetString("id"), "title": v.GetString("title"),
|
||||||
|
"content": v.GetString("content"), "push_time": v.GetString("timedate"),
|
||||||
|
"author": v.GetString("owner"), "origin": v.GetString("source"), "click_num": v.GetString("readtime"),
|
||||||
|
"sort": v.GetCeilInt("zhiding"), "create_time": time.Now().Format("2006-01-02 15:04"),
|
||||||
|
"modify_time": time.Now().Format("2006-01-02 15:04"), "admin_id": 1}
|
||||||
|
if ctg != nil {
|
||||||
|
data["ctg_id"] = ctg.GetCeilInt("id")
|
||||||
|
}
|
||||||
|
that.Db.Insert("article", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
that.Display(0, len(ebw_news))
|
||||||
|
|
||||||
|
},
|
||||||
|
"res2news": func(that *Context) {
|
||||||
|
ebw_news_addition_res := that.Db.Select("ebw_news_addition_res", "*")
|
||||||
|
|
||||||
|
for _, v := range ebw_news_addition_res {
|
||||||
|
ctg := that.Db.Get("ctg", "*", Map{"sn": v.GetString("fk_res")})
|
||||||
|
article := that.Db.Get("article", "*", Map{"sn": v.GetString("fk_newsid")})
|
||||||
|
data := Map{"sn": Md5(ObjToStr(time.Now().UnixNano()) + ObjToStr(RandX(10000, 100000))),
|
||||||
|
"create_time": time.Now().Format("2006-01-02 15:04"),
|
||||||
|
"modify_time": time.Now().Format("2006-01-02 15:04"), "admin_id": 1}
|
||||||
|
if ctg != nil {
|
||||||
|
data["ctg_id"] = ctg.GetCeilInt("id")
|
||||||
|
}
|
||||||
|
if article != nil {
|
||||||
|
data["article_id"] = article.GetCeilInt("id")
|
||||||
|
}
|
||||||
|
that.Db.Insert("ctg_article", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
that.Display(0, len(ebw_news_addition_res))
|
||||||
|
},
|
||||||
|
//将文章没有关联的ctg_article进行关联
|
||||||
|
"article": func(that *Context) {
|
||||||
|
articles := that.Db.Select("article", "id,ctg_id")
|
||||||
|
for _, v := range articles {
|
||||||
|
ctg_article := that.Db.Get("ctg_article", "id", Map{"article_id": v.GetCeilInt("id")})
|
||||||
|
if ctg_article == nil {
|
||||||
|
|
||||||
|
data := Map{"sn": Md5(ObjToStr(time.Now().UnixNano()) + ObjToStr(RandX(10000, 100000))),
|
||||||
|
"create_time": time.Now().Format("2006-01-02 15:04"),
|
||||||
|
"modify_time": time.Now().Format("2006-01-02 15:04"), "admin_id": 1}
|
||||||
|
if v.GetCeilInt("ctg_id") == 0 || v.GetCeilInt("id") == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data["ctg_id"] = v.GetCeilInt("ctg_id")
|
||||||
|
data["article_id"] = v.GetCeilInt("id")
|
||||||
|
that.Db.Insert("ctg_article", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
that.Display(0, len(articles))
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func ss(parent_id int, that *Context) {
|
||||||
|
var ctgs []Map
|
||||||
|
ctg := that.Db.Get("ctg", "*", Map{"id": parent_id})
|
||||||
|
if parent_id == 0 {
|
||||||
|
ctgs = that.Db.Select("ctg", "*", Map{"parent_id": nil})
|
||||||
|
} else {
|
||||||
|
ctgs = that.Db.Select("ctg", "*", Map{"parent_id": parent_id})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range ctgs {
|
||||||
|
if ctg == nil {
|
||||||
|
ctg = Map{"parent_ids": ","}
|
||||||
|
}
|
||||||
|
ids := ctg.GetString("parent_ids") + ObjToStr(v.GetCeilInt("id")) + ","
|
||||||
|
that.Db.Update("ctg", Map{"parent_ids": ids}, Map{"id": v.GetCeilInt("id")})
|
||||||
|
ss(v.GetCeilInt("id"), that)
|
||||||
|
}
|
||||||
|
}
|
||||||
96
example/app/mail.go
Normal file
96
example/app/mail.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime"
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var MailCtr = Ctr{
|
||||||
|
"add": func(that *Context) {
|
||||||
|
title := that.Req.FormValue("title")
|
||||||
|
name := that.Req.FormValue("name")
|
||||||
|
phone := that.Req.FormValue("phone")
|
||||||
|
content := that.Req.FormValue("content")
|
||||||
|
|
||||||
|
tp := ObjToInt(that.Req.FormValue("type"))
|
||||||
|
show := ObjToInt(that.Req.FormValue("show"))
|
||||||
|
|
||||||
|
if len(title) < 5 {
|
||||||
|
that.Display(3, "标题过短")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(name) < 2 {
|
||||||
|
that.Display(3, "姓名错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(phone) < 8 {
|
||||||
|
that.Display(3, "联系方式错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(content) < 10 {
|
||||||
|
that.Display(3, "内容过短")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Map{
|
||||||
|
"sn": Md5(ObjToStr(time.Now().UnixNano()) + ObjToStr(RandX(10000, 100000))),
|
||||||
|
"name": name, "title": title, "phone": phone, "content": content, "type": tp, "show": show,
|
||||||
|
"modify_time[#]": "NOW()", "create_time[#]": "NOW()",
|
||||||
|
}
|
||||||
|
id := that.Db.Insert("mail", data)
|
||||||
|
if id == 0 {
|
||||||
|
that.Display(4, "创建失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
that.Display(0, "成功")
|
||||||
|
return
|
||||||
|
|
||||||
|
},
|
||||||
|
"info": func(that *Context) {
|
||||||
|
sn := that.Req.FormValue("sn")
|
||||||
|
|
||||||
|
mail := that.Db.Get("mail", "*", Map{"sn": sn})
|
||||||
|
|
||||||
|
that.Display(0, mail)
|
||||||
|
},
|
||||||
|
"list": func(that *Context) {
|
||||||
|
page := ObjToInt(that.Req.FormValue("page"))
|
||||||
|
pageSize := ObjToInt(that.Req.FormValue("pageSize"))
|
||||||
|
|
||||||
|
if page == 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize == 0 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
//keywords:=that.Req.FormValue("keywords")
|
||||||
|
|
||||||
|
//sort:=that.Req.FormValue("sort")
|
||||||
|
|
||||||
|
where := Map{"state": 0, "show": 1}
|
||||||
|
|
||||||
|
//if keywords!=""{
|
||||||
|
// where["OR"]=Map{"title[~]":keywords,"description[~]":keywords,"author[~]":keywords,"sn[~]":keywords,"origin[~]":keywords,"url[~]":keywords}
|
||||||
|
//}
|
||||||
|
|
||||||
|
if len(where) > 1 {
|
||||||
|
where = Map{"AND": where}
|
||||||
|
}
|
||||||
|
|
||||||
|
//if sort==""{
|
||||||
|
// where["ORDER"]=Slice{"sort DESC","push_time DESC"}
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//if sort=="time"{
|
||||||
|
where["ORDER"] = "create_time DESC"
|
||||||
|
//}
|
||||||
|
count := that.Db.Count("mail", where)
|
||||||
|
mail := that.Db.Page(page, pageSize).PageSelect("mail", "*", where)
|
||||||
|
|
||||||
|
that.Display(0, Map{"count": count, "data": mail})
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
60
example/app/org.go
Normal file
60
example/app/org.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime"
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var OrgCtr = Ctr{
|
||||||
|
"info": func(that *Context) {
|
||||||
|
sn := that.Req.FormValue("sn")
|
||||||
|
article := that.Db.Get("article", "*", Map{"sn": sn})
|
||||||
|
that.Display(0, article)
|
||||||
|
},
|
||||||
|
"list": func(that *Context) {
|
||||||
|
sn := that.Req.FormValue("sn") //orgsn
|
||||||
|
page := ObjToInt(that.Req.FormValue("page"))
|
||||||
|
pageSize := ObjToInt(that.Req.FormValue("pageSize"))
|
||||||
|
|
||||||
|
if page == 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize == 0 {
|
||||||
|
pageSize = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
keywords := that.Req.FormValue("keywords")
|
||||||
|
|
||||||
|
sort := that.Req.FormValue("sort")
|
||||||
|
|
||||||
|
where := Map{"push_time[<=]": time.Now().Format("2006-01-02 15:04"), "state": 0}
|
||||||
|
if sn != "" {
|
||||||
|
org := that.Db.Get("org", "id", Map{"sn": sn})
|
||||||
|
if org != nil {
|
||||||
|
where["org_id"] = org.GetCeilInt("id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if keywords != "" {
|
||||||
|
where["OR"] = Map{"title[~]": keywords, "description[~]": keywords, "author[~]": keywords, "sn[~]": keywords, "origin[~]": keywords, "url[~]": keywords}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(where) > 1 {
|
||||||
|
where = Map{"AND": where}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sort == "" {
|
||||||
|
where["ORDER"] = Slice{"sort DESC", "id DESC"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sort == "time" {
|
||||||
|
where["ORDER"] = "push_time DESC"
|
||||||
|
}
|
||||||
|
|
||||||
|
article := that.Db.Page(page, pageSize).PageSelect("article", "sn,title,description,push_time,lunbo,author,origin,url", where)
|
||||||
|
|
||||||
|
that.Display(0, article)
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,414 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 压测配置
|
|
||||||
type BenchConfig struct {
|
|
||||||
URL string
|
|
||||||
Concurrency int
|
|
||||||
Duration time.Duration
|
|
||||||
Timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// 压测结果
|
|
||||||
type BenchResult struct {
|
|
||||||
TotalRequests int64
|
|
||||||
SuccessRequests int64
|
|
||||||
FailedRequests int64
|
|
||||||
TotalDuration time.Duration
|
|
||||||
MinLatency int64 // 纳秒
|
|
||||||
MaxLatency int64
|
|
||||||
AvgLatency int64
|
|
||||||
P50Latency int64 // 50分位
|
|
||||||
P90Latency int64 // 90分位
|
|
||||||
P99Latency int64 // 99分位
|
|
||||||
QPS float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// 延迟收集器
|
|
||||||
type LatencyCollector struct {
|
|
||||||
latencies []int64
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lc *LatencyCollector) Add(latency int64) {
|
|
||||||
lc.mu.Lock()
|
|
||||||
lc.latencies = append(lc.latencies, latency)
|
|
||||||
lc.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lc *LatencyCollector) GetPercentile(p float64) int64 {
|
|
||||||
lc.mu.Lock()
|
|
||||||
defer lc.mu.Unlock()
|
|
||||||
|
|
||||||
if len(lc.latencies) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 简单排序取百分位
|
|
||||||
n := len(lc.latencies)
|
|
||||||
idx := int(float64(n) * p)
|
|
||||||
if idx >= n {
|
|
||||||
idx = n - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// 部分排序找第idx个元素
|
|
||||||
return quickSelect(lc.latencies, idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func quickSelect(arr []int64, k int) int64 {
|
|
||||||
if len(arr) == 1 {
|
|
||||||
return arr[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
pivot := arr[len(arr)/2]
|
|
||||||
var left, right, equal []int64
|
|
||||||
|
|
||||||
for _, v := range arr {
|
|
||||||
if v < pivot {
|
|
||||||
left = append(left, v)
|
|
||||||
} else if v > pivot {
|
|
||||||
right = append(right, v)
|
|
||||||
} else {
|
|
||||||
equal = append(equal, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if k < len(left) {
|
|
||||||
return quickSelect(left, k)
|
|
||||||
} else if k < len(left)+len(equal) {
|
|
||||||
return pivot
|
|
||||||
}
|
|
||||||
return quickSelect(right, k-len(left)-len(equal))
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// 最大化利用CPU
|
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
|
||||||
|
|
||||||
fmt.Println("==========================================")
|
|
||||||
fmt.Println(" 🔥 HoTime 极限压力测试 🔥")
|
|
||||||
fmt.Println("==========================================")
|
|
||||||
fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// 极限测试配置
|
|
||||||
configs := []BenchConfig{
|
|
||||||
{URL: "http://127.0.0.1:8081/app/test/hello", Concurrency: 500, Duration: 15 * time.Second, Timeout: 10 * time.Second},
|
|
||||||
{URL: "http://127.0.0.1:8081/app/test/hello", Concurrency: 1000, Duration: 15 * time.Second, Timeout: 10 * time.Second},
|
|
||||||
{URL: "http://127.0.0.1:8081/app/test/hello", Concurrency: 2000, Duration: 15 * time.Second, Timeout: 10 * time.Second},
|
|
||||||
{URL: "http://127.0.0.1:8081/app/test/hello", Concurrency: 5000, Duration: 15 * time.Second, Timeout: 10 * time.Second},
|
|
||||||
{URL: "http://127.0.0.1:8081/app/test/hello", Concurrency: 10000, Duration: 15 * time.Second, Timeout: 10 * time.Second},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查服务
|
|
||||||
fmt.Println("正在检查服务是否可用...")
|
|
||||||
if !checkService(configs[0].URL) {
|
|
||||||
fmt.Println("❌ 服务不可用,请先启动示例应用")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println("✅ 服务已就绪")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// 预热
|
|
||||||
fmt.Println("🔄 预热中 (5秒)...")
|
|
||||||
warmup(configs[0].URL, 100, 5*time.Second)
|
|
||||||
fmt.Println("✅ 预热完成")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
var maxQPS float64
|
|
||||||
var maxConcurrency int
|
|
||||||
|
|
||||||
// 执行极限测试
|
|
||||||
for i, config := range configs {
|
|
||||||
fmt.Printf("══════════════════════════════════════════\n")
|
|
||||||
fmt.Printf("【极限测试 %d】并发数: %d, 持续时间: %v\n", i+1, config.Concurrency, config.Duration)
|
|
||||||
fmt.Printf("══════════════════════════════════════════\n")
|
|
||||||
|
|
||||||
result := runBenchmark(config)
|
|
||||||
printResult(result)
|
|
||||||
|
|
||||||
if result.QPS > maxQPS {
|
|
||||||
maxQPS = result.QPS
|
|
||||||
maxConcurrency = config.Concurrency
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否达到瓶颈
|
|
||||||
successRate := float64(result.SuccessRequests) / float64(result.TotalRequests) * 100
|
|
||||||
if successRate < 95 {
|
|
||||||
fmt.Println("\n⚠️ 成功率低于95%,已达到服务极限!")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.AvgLatency > int64(100*time.Millisecond) {
|
|
||||||
fmt.Println("\n⚠️ 平均延迟超过100ms,已达到服务极限!")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// 测试间隔
|
|
||||||
if i < len(configs)-1 {
|
|
||||||
fmt.Println("冷却 5 秒...")
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最终报告
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("══════════════════════════════════════════")
|
|
||||||
fmt.Println(" 📊 极限测试总结")
|
|
||||||
fmt.Println("══════════════════════════════════════════")
|
|
||||||
fmt.Printf("最高 QPS: %.2f 请求/秒\n", maxQPS)
|
|
||||||
fmt.Printf("最佳并发数: %d\n", maxConcurrency)
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// 并发用户估算
|
|
||||||
estimateUsers(maxQPS)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkService(url string) bool {
|
|
||||||
client := &http.Client{Timeout: 3 * time.Second}
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
return resp.StatusCode == 200
|
|
||||||
}
|
|
||||||
|
|
||||||
func warmup(url string, concurrency int, duration time.Duration) {
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 5 * time.Second,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
MaxIdleConns: concurrency * 2,
|
|
||||||
MaxIdleConnsPerHost: concurrency * 2,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for i := 0; i < concurrency; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
if err == nil {
|
|
||||||
io.Copy(io.Discard, resp.Body)
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(duration)
|
|
||||||
close(done)
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runBenchmark(config BenchConfig) BenchResult {
|
|
||||||
var (
|
|
||||||
totalRequests int64
|
|
||||||
successRequests int64
|
|
||||||
failedRequests int64
|
|
||||||
totalLatency int64
|
|
||||||
minLatency int64 = int64(time.Hour)
|
|
||||||
maxLatency int64
|
|
||||||
mu sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
collector := &LatencyCollector{
|
|
||||||
latencies: make([]int64, 0, 100000),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 高性能HTTP客户端
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: config.Timeout,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
MaxIdleConns: config.Concurrency * 2,
|
|
||||||
MaxIdleConnsPerHost: config.Concurrency * 2,
|
|
||||||
MaxConnsPerHost: config.Concurrency * 2,
|
|
||||||
IdleConnTimeout: 90 * time.Second,
|
|
||||||
DisableKeepAlives: false,
|
|
||||||
DisableCompression: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
|
|
||||||
// 启动并发
|
|
||||||
for i := 0; i < config.Concurrency; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
reqStart := time.Now()
|
|
||||||
success := makeRequest(client, config.URL)
|
|
||||||
latency := time.Since(reqStart).Nanoseconds()
|
|
||||||
|
|
||||||
atomic.AddInt64(&totalRequests, 1)
|
|
||||||
atomic.AddInt64(&totalLatency, latency)
|
|
||||||
|
|
||||||
if success {
|
|
||||||
atomic.AddInt64(&successRequests, 1)
|
|
||||||
} else {
|
|
||||||
atomic.AddInt64(&failedRequests, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 采样收集延迟(每100个请求采样1个,减少内存开销)
|
|
||||||
if atomic.LoadInt64(&totalRequests)%100 == 0 {
|
|
||||||
collector.Add(latency)
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
if latency < minLatency {
|
|
||||||
minLatency = latency
|
|
||||||
}
|
|
||||||
if latency > maxLatency {
|
|
||||||
maxLatency = latency
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(config.Duration)
|
|
||||||
close(done)
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
totalDuration := time.Since(startTime)
|
|
||||||
|
|
||||||
result := BenchResult{
|
|
||||||
TotalRequests: totalRequests,
|
|
||||||
SuccessRequests: successRequests,
|
|
||||||
FailedRequests: failedRequests,
|
|
||||||
TotalDuration: totalDuration,
|
|
||||||
MinLatency: minLatency,
|
|
||||||
MaxLatency: maxLatency,
|
|
||||||
P50Latency: collector.GetPercentile(0.50),
|
|
||||||
P90Latency: collector.GetPercentile(0.90),
|
|
||||||
P99Latency: collector.GetPercentile(0.99),
|
|
||||||
}
|
|
||||||
|
|
||||||
if totalRequests > 0 {
|
|
||||||
result.AvgLatency = totalLatency / totalRequests
|
|
||||||
result.QPS = float64(totalRequests) / totalDuration.Seconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeRequest(client *http.Client, url string) bool {
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var result map[string]interface{}
|
|
||||||
if err := json.Unmarshal(body, &result); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if status, ok := result["status"].(float64); !ok || status != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func printResult(result BenchResult) {
|
|
||||||
successRate := float64(result.SuccessRequests) / float64(result.TotalRequests) * 100
|
|
||||||
|
|
||||||
fmt.Printf("总请求数: %d\n", result.TotalRequests)
|
|
||||||
fmt.Printf("成功请求: %d\n", result.SuccessRequests)
|
|
||||||
fmt.Printf("失败请求: %d\n", result.FailedRequests)
|
|
||||||
fmt.Printf("成功率: %.2f%%\n", successRate)
|
|
||||||
fmt.Printf("总耗时: %v\n", result.TotalDuration.Round(time.Millisecond))
|
|
||||||
fmt.Printf("QPS: %.2f 请求/秒\n", result.QPS)
|
|
||||||
fmt.Println("------------------------------------------")
|
|
||||||
fmt.Printf("最小延迟: %v\n", time.Duration(result.MinLatency).Round(time.Microsecond))
|
|
||||||
fmt.Printf("平均延迟: %v\n", time.Duration(result.AvgLatency).Round(time.Microsecond))
|
|
||||||
fmt.Printf("P50延迟: %v\n", time.Duration(result.P50Latency).Round(time.Microsecond))
|
|
||||||
fmt.Printf("P90延迟: %v\n", time.Duration(result.P90Latency).Round(time.Microsecond))
|
|
||||||
fmt.Printf("P99延迟: %v\n", time.Duration(result.P99Latency).Round(time.Microsecond))
|
|
||||||
fmt.Printf("最大延迟: %v\n", time.Duration(result.MaxLatency).Round(time.Microsecond))
|
|
||||||
|
|
||||||
// 性能评级
|
|
||||||
fmt.Print("\n性能评级: ")
|
|
||||||
switch {
|
|
||||||
case result.QPS >= 200000:
|
|
||||||
fmt.Println("🏆 卓越 (QPS >= 200K)")
|
|
||||||
case result.QPS >= 100000:
|
|
||||||
fmt.Println("🚀 优秀 (QPS >= 100K)")
|
|
||||||
case result.QPS >= 50000:
|
|
||||||
fmt.Println("⭐ 良好 (QPS >= 50K)")
|
|
||||||
case result.QPS >= 20000:
|
|
||||||
fmt.Println("👍 中上 (QPS >= 20K)")
|
|
||||||
case result.QPS >= 10000:
|
|
||||||
fmt.Println("📊 中等 (QPS >= 10K)")
|
|
||||||
default:
|
|
||||||
fmt.Println("⚠️ 一般 (QPS < 10K)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func estimateUsers(maxQPS float64) {
|
|
||||||
fmt.Println("📈 并发用户数估算(基于不同使用场景):")
|
|
||||||
fmt.Println("------------------------------------------")
|
|
||||||
|
|
||||||
scenarios := []struct {
|
|
||||||
name string
|
|
||||||
requestInterval float64 // 用户平均请求间隔(秒)
|
|
||||||
}{
|
|
||||||
{"高频交互(每秒1次请求)", 1},
|
|
||||||
{"活跃用户(每5秒1次请求)", 5},
|
|
||||||
{"普通浏览(每10秒1次请求)", 10},
|
|
||||||
{"低频访问(每30秒1次请求)", 30},
|
|
||||||
{"偶尔访问(每60秒1次请求)", 60},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range scenarios {
|
|
||||||
users := maxQPS * s.requestInterval
|
|
||||||
fmt.Printf("%-30s ~%d 用户\n", s.name, int(users))
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("💡 实际生产环境建议保留 30-50% 性能余量")
|
|
||||||
fmt.Printf(" 安全并发用户数: %d - %d (普通浏览场景)\n",
|
|
||||||
int(maxQPS*10*0.5), int(maxQPS*10*0.7))
|
|
||||||
}
|
|
||||||
@ -1,327 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
|
||||||
|
|
||||||
fmt.Println("==========================================")
|
|
||||||
fmt.Println(" 🔥 HoTime 极限压力测试 🔥")
|
|
||||||
fmt.Println("==========================================")
|
|
||||||
fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
baseURL := "http://127.0.0.1:8081/app/test/hello"
|
|
||||||
|
|
||||||
// 检查服务
|
|
||||||
fmt.Println("正在检查服务是否可用...")
|
|
||||||
client := &http.Client{Timeout: 3 * time.Second}
|
|
||||||
resp, err := client.Get(baseURL)
|
|
||||||
if err != nil || resp.StatusCode != 200 {
|
|
||||||
fmt.Println("❌ 服务不可用,请先启动示例应用")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
fmt.Println("✅ 服务已就绪")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// 预热
|
|
||||||
fmt.Println("🔄 预热中 (5秒)...")
|
|
||||||
doWarmup(baseURL, 100, 5*time.Second)
|
|
||||||
fmt.Println("✅ 预热完成")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// 极限测试配置
|
|
||||||
concurrencyLevels := []int{500, 1000, 2000, 5000, 10000}
|
|
||||||
testDuration := 15 * time.Second
|
|
||||||
|
|
||||||
var maxQPS float64
|
|
||||||
var maxConcurrency int
|
|
||||||
|
|
||||||
for i, concurrency := range concurrencyLevels {
|
|
||||||
fmt.Printf("══════════════════════════════════════════\n")
|
|
||||||
fmt.Printf("【极限测试 %d】并发数: %d, 持续时间: %v\n", i+1, concurrency, testDuration)
|
|
||||||
fmt.Printf("══════════════════════════════════════════\n")
|
|
||||||
|
|
||||||
total, success, failed, qps, minLat, avgLat, maxLat, p50, p90, p99 := doBenchmark(baseURL, concurrency, testDuration)
|
|
||||||
|
|
||||||
successRate := float64(success) / float64(total) * 100
|
|
||||||
|
|
||||||
fmt.Printf("总请求数: %d\n", total)
|
|
||||||
fmt.Printf("成功请求: %d\n", success)
|
|
||||||
fmt.Printf("失败请求: %d\n", failed)
|
|
||||||
fmt.Printf("成功率: %.2f%%\n", successRate)
|
|
||||||
fmt.Printf("QPS: %.2f 请求/秒\n", qps)
|
|
||||||
fmt.Println("------------------------------------------")
|
|
||||||
fmt.Printf("最小延迟: %v\n", time.Duration(minLat))
|
|
||||||
fmt.Printf("平均延迟: %v\n", time.Duration(avgLat))
|
|
||||||
fmt.Printf("P50延迟: %v\n", time.Duration(p50))
|
|
||||||
fmt.Printf("P90延迟: %v\n", time.Duration(p90))
|
|
||||||
fmt.Printf("P99延迟: %v\n", time.Duration(p99))
|
|
||||||
fmt.Printf("最大延迟: %v\n", time.Duration(maxLat))
|
|
||||||
|
|
||||||
// 性能评级
|
|
||||||
fmt.Print("\n性能评级: ")
|
|
||||||
switch {
|
|
||||||
case qps >= 200000:
|
|
||||||
fmt.Println("🏆 卓越 (QPS >= 200K)")
|
|
||||||
case qps >= 100000:
|
|
||||||
fmt.Println("🚀 优秀 (QPS >= 100K)")
|
|
||||||
case qps >= 50000:
|
|
||||||
fmt.Println("⭐ 良好 (QPS >= 50K)")
|
|
||||||
case qps >= 20000:
|
|
||||||
fmt.Println("👍 中上 (QPS >= 20K)")
|
|
||||||
case qps >= 10000:
|
|
||||||
fmt.Println("📊 中等 (QPS >= 10K)")
|
|
||||||
default:
|
|
||||||
fmt.Println("⚠️ 一般 (QPS < 10K)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if qps > maxQPS {
|
|
||||||
maxQPS = qps
|
|
||||||
maxConcurrency = concurrency
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否达到瓶颈
|
|
||||||
if successRate < 95 {
|
|
||||||
fmt.Println("\n⚠️ 成功率低于95%,已达到服务极限!")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if avgLat > int64(100*time.Millisecond) {
|
|
||||||
fmt.Println("\n⚠️ 平均延迟超过100ms,已达到服务极限!")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
if i < len(concurrencyLevels)-1 {
|
|
||||||
fmt.Println("冷却 5 秒...")
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最终报告
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("══════════════════════════════════════════")
|
|
||||||
fmt.Println(" 📊 极限测试总结")
|
|
||||||
fmt.Println("══════════════════════════════════════════")
|
|
||||||
fmt.Printf("最高 QPS: %.2f 请求/秒\n", maxQPS)
|
|
||||||
fmt.Printf("最佳并发数: %d\n", maxConcurrency)
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// 并发用户估算
|
|
||||||
fmt.Println("📈 并发用户数估算:")
|
|
||||||
fmt.Println("------------------------------------------")
|
|
||||||
fmt.Printf("高频交互(1秒/次): ~%d 用户\n", int(maxQPS*1))
|
|
||||||
fmt.Printf("活跃用户(5秒/次): ~%d 用户\n", int(maxQPS*5))
|
|
||||||
fmt.Printf("普通浏览(10秒/次): ~%d 用户\n", int(maxQPS*10))
|
|
||||||
fmt.Printf("低频访问(30秒/次): ~%d 用户\n", int(maxQPS*30))
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("💡 生产环境建议保留 30-50% 性能余量")
|
|
||||||
fmt.Printf(" 安全并发用户数: %d - %d (普通浏览场景)\n",
|
|
||||||
int(maxQPS*10*0.5), int(maxQPS*10*0.7))
|
|
||||||
}
|
|
||||||
|
|
||||||
func doWarmup(url string, concurrency int, duration time.Duration) {
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 5 * time.Second,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
MaxIdleConns: concurrency * 2,
|
|
||||||
MaxIdleConnsPerHost: concurrency * 2,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for i := 0; i < concurrency; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
if err == nil {
|
|
||||||
io.Copy(io.Discard, resp.Body)
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(duration)
|
|
||||||
close(done)
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func doBenchmark(url string, concurrency int, duration time.Duration) (total, success, failed int64, qps float64, minLat, avgLat, maxLat, p50, p90, p99 int64) {
|
|
||||||
var totalLatency int64
|
|
||||||
minLat = int64(time.Hour)
|
|
||||||
var mu sync.Mutex
|
|
||||||
|
|
||||||
// 采样收集器
|
|
||||||
latencies := make([]int64, 0, 50000)
|
|
||||||
var latMu sync.Mutex
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
MaxIdleConns: concurrency * 2,
|
|
||||||
MaxIdleConnsPerHost: concurrency * 2,
|
|
||||||
MaxConnsPerHost: concurrency * 2,
|
|
||||||
IdleConnTimeout: 90 * time.Second,
|
|
||||||
DisableCompression: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
startTime := time.Now()
|
|
||||||
|
|
||||||
for i := 0; i < concurrency; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
reqStart := time.Now()
|
|
||||||
ok := doRequest(client, url)
|
|
||||||
lat := time.Since(reqStart).Nanoseconds()
|
|
||||||
|
|
||||||
atomic.AddInt64(&total, 1)
|
|
||||||
atomic.AddInt64(&totalLatency, lat)
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
atomic.AddInt64(&success, 1)
|
|
||||||
} else {
|
|
||||||
atomic.AddInt64(&failed, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 采样 (每50个采1个)
|
|
||||||
if atomic.LoadInt64(&total)%50 == 0 {
|
|
||||||
latMu.Lock()
|
|
||||||
latencies = append(latencies, lat)
|
|
||||||
latMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
if lat < minLat {
|
|
||||||
minLat = lat
|
|
||||||
}
|
|
||||||
if lat > maxLat {
|
|
||||||
maxLat = lat
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(duration)
|
|
||||||
close(done)
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
totalDuration := time.Since(startTime)
|
|
||||||
|
|
||||||
if total > 0 {
|
|
||||||
avgLat = totalLatency / total
|
|
||||||
qps = float64(total) / totalDuration.Seconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算百分位
|
|
||||||
if len(latencies) > 0 {
|
|
||||||
p50 = getPercentile(latencies, 0.50)
|
|
||||||
p90 = getPercentile(latencies, 0.90)
|
|
||||||
p99 = getPercentile(latencies, 0.99)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func doRequest(client *http.Client, url string) bool {
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var result map[string]interface{}
|
|
||||||
if err := json.Unmarshal(body, &result); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if status, ok := result["status"].(float64); !ok || status != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPercentile(arr []int64, p float64) int64 {
|
|
||||||
n := len(arr)
|
|
||||||
if n == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制一份避免修改原数组
|
|
||||||
tmp := make([]int64, n)
|
|
||||||
copy(tmp, arr)
|
|
||||||
|
|
||||||
idx := int(float64(n) * p)
|
|
||||||
if idx >= n {
|
|
||||||
idx = n - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return quickSelect(tmp, idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func quickSelect(arr []int64, k int) int64 {
|
|
||||||
if len(arr) == 1 {
|
|
||||||
return arr[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
pivot := arr[len(arr)/2]
|
|
||||||
var left, right, equal []int64
|
|
||||||
|
|
||||||
for _, v := range arr {
|
|
||||||
if v < pivot {
|
|
||||||
left = append(left, v)
|
|
||||||
} else if v > pivot {
|
|
||||||
right = append(right, v)
|
|
||||||
} else {
|
|
||||||
equal = append(equal, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if k < len(left) {
|
|
||||||
return quickSelect(left, k)
|
|
||||||
} else if k < len(left)+len(equal) {
|
|
||||||
return pivot
|
|
||||||
}
|
|
||||||
return quickSelect(right, k-len(left)-len(equal))
|
|
||||||
}
|
|
||||||
48
example/config/config.json
Normal file
48
example/config/config.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"cache": {
|
||||||
|
"db": {
|
||||||
|
"db": false,
|
||||||
|
"session": true,
|
||||||
|
"timeout": 7200
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"db": true,
|
||||||
|
"session": true,
|
||||||
|
"timeout": 7200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codeConfig": [
|
||||||
|
{
|
||||||
|
"config": "config/admin.json",
|
||||||
|
"configDB": "config/adminDB.json",
|
||||||
|
"mode": 0,
|
||||||
|
"name": "",
|
||||||
|
"rule": "config/rule.json",
|
||||||
|
"table": "admin"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"db": {
|
||||||
|
"mysql": {
|
||||||
|
"host": "192.168.6.253",
|
||||||
|
"name": "dgs-cms",
|
||||||
|
"password": "dasda8454456",
|
||||||
|
"port": "3306",
|
||||||
|
"user": "root"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defFile": [
|
||||||
|
"index.html",
|
||||||
|
"index.htm"
|
||||||
|
],
|
||||||
|
"error": {
|
||||||
|
"1": "内部系统异常",
|
||||||
|
"2": "访问权限异常",
|
||||||
|
"3": "请求参数异常",
|
||||||
|
"4": "数据处理异常",
|
||||||
|
"5": "数据结果异常"
|
||||||
|
},
|
||||||
|
"mode": 2,
|
||||||
|
"port": "8081",
|
||||||
|
"sessionName": "HOTIME",
|
||||||
|
"tpt": "tpt"
|
||||||
|
}
|
||||||
79
example/config/configNote.json
Normal file
79
example/config/configNote.json
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"cache": {
|
||||||
|
"db": {
|
||||||
|
"db": "默认false,非必须,缓存数据库,启用后能减少数据库的读写压力",
|
||||||
|
"session": "默认true,非必须,缓存web session,同时缓存session保持的用户缓存",
|
||||||
|
"timeout": "默认60 * 60 * 24 * 30,非必须,过期时间,超时自动删除"
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"db": "默认true,非必须,缓存数据库,启用后能减少数据库的读写压力",
|
||||||
|
"session": "默认true,非必须,缓存web session,同时缓存session保持的用户缓存",
|
||||||
|
"timeout": "默认60 * 60 * 2,非必须,过期时间,超时自动删除"
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"db": "默认true,非必须,缓存数据库,启用后能减少数据库的读写压力",
|
||||||
|
"host": "默认服务ip:127.0.0.1,必须,如果需要使用redis服务时配置,",
|
||||||
|
"password": "默认密码空,必须,如果需要使用redis服务时配置,默认密码空",
|
||||||
|
"port": "默认服务端口:6379,必须,如果需要使用redis服务时配置,",
|
||||||
|
"session": "默认true,非必须,缓存web session,同时缓存session保持的用户缓存",
|
||||||
|
"timeout": "默认60 * 60 * 24 * 15,非必须,过期时间,超时自动删除"
|
||||||
|
},
|
||||||
|
"注释": "可配置memory,db,redis,默认启用memory,默认优先级为memory\u003eredis\u003edb,memory与数据库缓存设置项一致,缓存数据填充会自动反方向反哺,加入memory缓存过期将自动从redis更新,但memory永远不会更新redis,如果是集群建议不要开启memory,配置即启用"
|
||||||
|
},
|
||||||
|
"codeConfig": [
|
||||||
|
"注释:配置即启用,非必须,默认无",
|
||||||
|
{
|
||||||
|
"config": "默认config/app.json,必须,接口描述配置文件",
|
||||||
|
"configDB": "默认无,非必须,有则每次将数据库数据生成到此目录用于配置读写,无则不生成",
|
||||||
|
"mode": "默认0,非必须,0为内嵌代码模式,1为生成代码模式",
|
||||||
|
"name": "默认无,非必须,有则生成代码到此目录,无则采用缺省模式使用表名,如设置为:admin,将在admin目录生成包名为admin的代码",
|
||||||
|
"rule": "默认config/rule.json,非必须,有则按改规则生成接口,无则按系统内嵌方式生成",
|
||||||
|
"table": "默认admin,必须,根据数据库内当前表名做为用户生成数据"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"crossDomain": "默认空 非必须,空字符串为不开启,如果需要跨域设置,auto为智能开启所有网站允许跨域,http://www.baidu.com为指定域允许跨域",
|
||||||
|
"db": {
|
||||||
|
"mysql": {
|
||||||
|
"host": "默认127.0.0.1,必须,数据库ip地址",
|
||||||
|
"name": "默认test,必须,数据库名称",
|
||||||
|
"password": "默认root,必须,数据库密码",
|
||||||
|
"port": "默认3306,必须,数据库端口",
|
||||||
|
"prefix": "默认空,非必须,数据表前缀",
|
||||||
|
"slave": {
|
||||||
|
"host": "默认127.0.0.1,必须,数据库ip地址",
|
||||||
|
"name": "默认test,必须,数据库名称",
|
||||||
|
"password": "默认root,必须,数据库密码",
|
||||||
|
"port": "默认3306,必须,数据库端口",
|
||||||
|
"user": "默认root,必须,数据库用户名",
|
||||||
|
"注释": "从数据库配置,mysql里配置slave项即启用主从读写,减少数据库压力"
|
||||||
|
},
|
||||||
|
"user": "默认root,必须,数据库用户名",
|
||||||
|
"注释": "除prefix及主从数据库slave项,其他全部必须"
|
||||||
|
},
|
||||||
|
"sqlite": {
|
||||||
|
"path": "默认config/data.db,必须,数据库位置"
|
||||||
|
},
|
||||||
|
"注释": "配置即启用,非必须,默认使用sqlite数据库"
|
||||||
|
},
|
||||||
|
"defFile": "默认访问index.html或者index.htm文件,必须,默认访问文件类型",
|
||||||
|
"error": {
|
||||||
|
"1": "内部系统异常,在环境配置,文件访问权限等基础运行环境条件不足造成严重错误时使用",
|
||||||
|
"2": "访问权限异常,没有登录或者登录异常等时候使用",
|
||||||
|
"3": "请求参数异常,request参数不满足要求,比如参数不足,参数类型错误,参数不满足要求等时候使用",
|
||||||
|
"4": "数据处理异常,数据库操作或者三方请求返回的结果非正常结果,比如数据库突然中断等时候使用",
|
||||||
|
"5": "数据结果异常,一般用于无法给出response要求的格式要求下使用,比如response需要的是string格式但你只能提供int数据时",
|
||||||
|
"注释": "web服务内置错误提示,自定义异常建议10开始"
|
||||||
|
},
|
||||||
|
"logFile": "无默认,非必须,如果需要存储日志文件时使用,保存格式为:a/b/c/20060102150405.txt,将生成:a/b/c/年月日时分秒.txt,按需设置",
|
||||||
|
"logLevel": "默认0,必须,0关闭,1打印,日志等级",
|
||||||
|
"mode": "默认0,非必须,0生产模式,1,测试模式,2开发模式,3内嵌代码模式,在开发模式下会显示更多的数据用于开发测试,并能够辅助研发,自动生成配置文件、代码等功能,web无缓存,数据库不启用缓存",
|
||||||
|
"modeRouterStrict": "默认false,必须,路由严格模式false,为大小写忽略必须匹配,true必须大小写匹配",
|
||||||
|
"port": "默认80,必须,web服务开启Http端口,0为不启用http服务,默认80",
|
||||||
|
"sessionName": "默认HOTIME,必须,设置session的cookie名",
|
||||||
|
"tlsCert": "默认空,非必须,https证书",
|
||||||
|
"tlsKey": "默认空,非必须,https密钥",
|
||||||
|
"tlsPort": "默认空,非必须,web服务https端口,0为不启用https服务",
|
||||||
|
"tpt": "默认tpt,必须,web静态文件目录,默认为程序目录下tpt目录",
|
||||||
|
"webConnectLogFile": "无默认,非必须,webConnectLogShow开启之后才能使用,如果需要存储日志文件时使用,保存格式为:a/b/c/20060102150405.txt,将生成:a/b/c/年月日时分秒.txt,按需设置",
|
||||||
|
"webConnectLogShow": "默认true,非必须,访问日志如果需要web访问链接、访问ip、访问时间打印,false为关闭true开启此功能"
|
||||||
|
}
|
||||||
BIN
example/config/data.db
Normal file
BIN
example/config/data.db
Normal file
Binary file not shown.
895
example/main.go
895
example/main.go
@ -2,904 +2,17 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
. "code.hoteas.com/golang/hotime"
|
. "code.hoteas.com/golang/hotime"
|
||||||
"fmt"
|
"code.hoteas.com/golang/hotime/example/app"
|
||||||
"time"
|
|
||||||
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
. "code.hoteas.com/golang/hotime/db"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
appIns := Init("config/config.json")
|
appIns := Init("config/config.json")
|
||||||
appIns.SetConnectListener(func(that *Context) (isFinished bool) {
|
appIns.SetConnectListener(func(that *Context) (isFinished bool) {
|
||||||
|
|
||||||
return isFinished
|
return isFinished
|
||||||
})
|
})
|
||||||
|
|
||||||
appIns.Run(Router{
|
appIns.Run(Router{"app": app.AppProj})
|
||||||
"app": {
|
|
||||||
"test": {
|
|
||||||
// 测试入口 - 运行所有测试
|
|
||||||
"all": func(that *Context) {
|
|
||||||
results := Map{}
|
|
||||||
|
|
||||||
// 初始化测试表
|
|
||||||
initTestTables(that)
|
|
||||||
|
|
||||||
// 1. 基础 CRUD 测试
|
|
||||||
results["1_basic_crud"] = testBasicCRUD(that)
|
|
||||||
|
|
||||||
// 2. 条件查询语法测试
|
|
||||||
results["2_condition_syntax"] = testConditionSyntax(that)
|
|
||||||
|
|
||||||
// 3. 链式查询测试
|
|
||||||
results["3_chain_query"] = testChainQuery(that)
|
|
||||||
|
|
||||||
// 4. JOIN 查询测试
|
|
||||||
results["4_join_query"] = testJoinQuery(that)
|
|
||||||
|
|
||||||
// 5. 聚合函数测试
|
|
||||||
results["5_aggregate"] = testAggregate(that)
|
|
||||||
|
|
||||||
// 6. 分页查询测试
|
|
||||||
results["6_pagination"] = testPagination(that)
|
|
||||||
|
|
||||||
// 7. 批量插入测试
|
|
||||||
results["7_batch_insert"] = testInserts(that)
|
|
||||||
|
|
||||||
// 8. Upsert 测试
|
|
||||||
results["8_upsert"] = testUpsert(that)
|
|
||||||
|
|
||||||
// 9. 事务测试
|
|
||||||
results["9_transaction"] = testTransaction(that)
|
|
||||||
|
|
||||||
// 10. 原生 SQL 测试
|
|
||||||
results["10_raw_sql"] = testRawSQL(that)
|
|
||||||
|
|
||||||
that.Display(0, results)
|
|
||||||
},
|
|
||||||
|
|
||||||
// 查询数据库表结构
|
|
||||||
"tables": func(that *Context) {
|
|
||||||
// 查询所有表
|
|
||||||
tables := that.Db.Query("SHOW TABLES")
|
|
||||||
that.Display(0, Map{"tables": tables})
|
|
||||||
},
|
|
||||||
|
|
||||||
// 查询指定表的结构
|
|
||||||
"describe": func(that *Context) {
|
|
||||||
tableName := that.Req.FormValue("table")
|
|
||||||
if tableName == "" {
|
|
||||||
that.Display(1, "请提供 table 参数")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 查询表结构
|
|
||||||
columns := that.Db.Query("DESCRIBE " + tableName)
|
|
||||||
// 查询表数据(前10条)
|
|
||||||
data := that.Db.Select(tableName, Map{"LIMIT": 10})
|
|
||||||
that.Display(0, Map{"table": tableName, "columns": columns, "sample_data": data})
|
|
||||||
},
|
|
||||||
|
|
||||||
// 单独测试入口
|
|
||||||
"crud": func(that *Context) { that.Display(0, testBasicCRUD(that)) },
|
|
||||||
"condition": func(that *Context) { that.Display(0, testConditionSyntax(that)) },
|
|
||||||
"chain": func(that *Context) { that.Display(0, testChainQuery(that)) },
|
|
||||||
"join": func(that *Context) { that.Display(0, testJoinQuery(that)) },
|
|
||||||
"aggregate": func(that *Context) { that.Display(0, testAggregate(that)) },
|
|
||||||
"pagination": func(that *Context) { that.Display(0, testPagination(that)) },
|
|
||||||
"batch": func(that *Context) { that.Display(0, testInserts(that)) },
|
|
||||||
"upsert": func(that *Context) { that.Display(0, testUpsert(that)) },
|
|
||||||
"transaction": func(that *Context) { that.Display(0, testTransaction(that)) },
|
|
||||||
"rawsql": func(that *Context) { that.Display(0, testRawSQL(that)) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// initTestTables 初始化测试表(MySQL真实数据库)
|
|
||||||
func initTestTables(that *Context) {
|
|
||||||
// MySQL 真实数据库已有表,创建测试用的临时表
|
|
||||||
// 创建测试用的批量插入表
|
|
||||||
that.Db.Exec(`CREATE TABLE IF NOT EXISTS test_batch (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
name VARCHAR(100),
|
|
||||||
title VARCHAR(200),
|
|
||||||
state INT DEFAULT 0,
|
|
||||||
create_time DATETIME
|
|
||||||
)`)
|
|
||||||
|
|
||||||
// 检查 admin 表数据(确认表存在)
|
|
||||||
_ = that.Db.Count("admin")
|
|
||||||
_ = that.Db.Count("article")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 1. 基础 CRUD 测试 ====================
|
|
||||||
func testBasicCRUD(that *Context) Map {
|
|
||||||
result := Map{"name": "基础CRUD测试", "tests": Slice{}}
|
|
||||||
tests := Slice{}
|
|
||||||
|
|
||||||
// 1.1 Insert 测试 - 使用 admin 表
|
|
||||||
insertTest := Map{"name": "Insert 插入测试 (admin表)"}
|
|
||||||
adminId := that.Db.Insert("admin", Map{
|
|
||||||
"name": "测试管理员_" + fmt.Sprintf("%d", time.Now().Unix()),
|
|
||||||
"phone": fmt.Sprintf("138%d", time.Now().Unix()%100000000),
|
|
||||||
"state": 1,
|
|
||||||
"password": "test123456",
|
|
||||||
"role_id": 1,
|
|
||||||
"title": "测试职位",
|
|
||||||
"create_time[#]": "NOW()",
|
|
||||||
"modify_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
insertTest["result"] = adminId > 0
|
|
||||||
insertTest["adminId"] = adminId
|
|
||||||
insertTest["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, insertTest)
|
|
||||||
|
|
||||||
// 1.2 Get 测试
|
|
||||||
getTest := Map{"name": "Get 获取单条记录测试"}
|
|
||||||
admin := that.Db.Get("admin", "*", Map{"id": adminId})
|
|
||||||
getTest["result"] = admin != nil && admin.GetInt64("id") == adminId
|
|
||||||
getTest["admin"] = admin
|
|
||||||
tests = append(tests, getTest)
|
|
||||||
|
|
||||||
// 1.3 Select 测试 - 单条件
|
|
||||||
selectTest1 := Map{"name": "Select 单条件查询测试"}
|
|
||||||
admins1 := that.Db.Select("admin", "*", Map{"state": 1, "LIMIT": 5})
|
|
||||||
selectTest1["result"] = len(admins1) >= 0 // 可能表中没有数据
|
|
||||||
selectTest1["count"] = len(admins1)
|
|
||||||
tests = append(tests, selectTest1)
|
|
||||||
|
|
||||||
// 1.4 Select 测试 - 多条件(自动 AND)
|
|
||||||
selectTest2 := Map{"name": "Select 多条件自动AND测试"}
|
|
||||||
admins2 := that.Db.Select("admin", "*", Map{
|
|
||||||
"state": 1,
|
|
||||||
"role_id[>]": 0,
|
|
||||||
"ORDER": "id DESC",
|
|
||||||
"LIMIT": 5,
|
|
||||||
})
|
|
||||||
selectTest2["result"] = true // 只要不报错就算成功
|
|
||||||
selectTest2["count"] = len(admins2)
|
|
||||||
selectTest2["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, selectTest2)
|
|
||||||
|
|
||||||
// 1.5 Update 测试
|
|
||||||
updateTest := Map{"name": "Update 更新测试"}
|
|
||||||
affected := that.Db.Update("admin", Map{
|
|
||||||
"title": "更新后的职位",
|
|
||||||
"modify_time[#]": "NOW()",
|
|
||||||
}, Map{"id": adminId})
|
|
||||||
updateTest["result"] = affected > 0
|
|
||||||
updateTest["affected"] = affected
|
|
||||||
tests = append(tests, updateTest)
|
|
||||||
|
|
||||||
// 1.6 Delete 测试 - 使用 test_batch 表
|
|
||||||
deleteTest := Map{"name": "Delete 删除测试"}
|
|
||||||
// 先创建一个临时记录用于删除
|
|
||||||
tempId := that.Db.Insert("test_batch", Map{
|
|
||||||
"name": "临时删除测试",
|
|
||||||
"title": "测试标题",
|
|
||||||
"state": 1,
|
|
||||||
"create_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
deleteAffected := that.Db.Delete("test_batch", Map{"id": tempId})
|
|
||||||
deleteTest["result"] = deleteAffected > 0
|
|
||||||
deleteTest["affected"] = deleteAffected
|
|
||||||
tests = append(tests, deleteTest)
|
|
||||||
|
|
||||||
result["tests"] = tests
|
|
||||||
result["success"] = true
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 2. 条件查询语法测试 ====================
|
|
||||||
func testConditionSyntax(that *Context) Map {
|
|
||||||
result := Map{"name": "条件查询语法测试", "tests": Slice{}}
|
|
||||||
tests := Slice{}
|
|
||||||
|
|
||||||
// 2.1 等于 (=) - 使用 article 表
|
|
||||||
test1 := Map{"name": "等于条件 (=)"}
|
|
||||||
articles1 := that.Db.Select("article", "id,title", Map{"state": 0, "LIMIT": 3})
|
|
||||||
test1["result"] = true
|
|
||||||
test1["count"] = len(articles1)
|
|
||||||
tests = append(tests, test1)
|
|
||||||
|
|
||||||
// 2.2 不等于 ([!])
|
|
||||||
test2 := Map{"name": "不等于条件 ([!])"}
|
|
||||||
articles2 := that.Db.Select("article", "id,title,state", Map{"state[!]": -1, "LIMIT": 3})
|
|
||||||
test2["result"] = true
|
|
||||||
test2["count"] = len(articles2)
|
|
||||||
test2["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test2)
|
|
||||||
|
|
||||||
// 2.3 大于 ([>]) 和 小于 ([<])
|
|
||||||
test3 := Map{"name": "大于小于条件 ([>], [<])"}
|
|
||||||
articles3 := that.Db.Select("article", "id,title,click_num", Map{
|
|
||||||
"click_num[>]": 0,
|
|
||||||
"click_num[<]": 100000,
|
|
||||||
"LIMIT": 3,
|
|
||||||
})
|
|
||||||
test3["result"] = true
|
|
||||||
test3["count"] = len(articles3)
|
|
||||||
tests = append(tests, test3)
|
|
||||||
|
|
||||||
// 2.4 大于等于 ([>=]) 和 小于等于 ([<=])
|
|
||||||
test4 := Map{"name": "大于等于小于等于条件 ([>=], [<=])"}
|
|
||||||
articles4 := that.Db.Select("article", "id,title,sort", Map{
|
|
||||||
"sort[>=]": 0,
|
|
||||||
"sort[<=]": 100,
|
|
||||||
"LIMIT": 3,
|
|
||||||
})
|
|
||||||
test4["result"] = true
|
|
||||||
test4["count"] = len(articles4)
|
|
||||||
tests = append(tests, test4)
|
|
||||||
|
|
||||||
// 2.5 LIKE 模糊查询 ([~])
|
|
||||||
test5 := Map{"name": "LIKE 模糊查询 ([~])"}
|
|
||||||
articles5 := that.Db.Select("article", "id,title", Map{"title[~]": "新闻", "LIMIT": 3})
|
|
||||||
test5["result"] = true
|
|
||||||
test5["count"] = len(articles5)
|
|
||||||
test5["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test5)
|
|
||||||
|
|
||||||
// 2.6 右模糊 ([~!])
|
|
||||||
test6 := Map{"name": "右模糊查询 ([~!])"}
|
|
||||||
articles6 := that.Db.Select("admin", "id,name", Map{"name[~!]": "管理", "LIMIT": 3})
|
|
||||||
test6["result"] = true
|
|
||||||
test6["count"] = len(articles6)
|
|
||||||
tests = append(tests, test6)
|
|
||||||
|
|
||||||
// 2.7 BETWEEN ([<>])
|
|
||||||
test7 := Map{"name": "BETWEEN 区间查询 ([<>])"}
|
|
||||||
articles7 := that.Db.Select("article", "id,title,click_num", Map{"click_num[<>]": Slice{0, 1000}, "LIMIT": 3})
|
|
||||||
test7["result"] = true
|
|
||||||
test7["count"] = len(articles7)
|
|
||||||
test7["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test7)
|
|
||||||
|
|
||||||
// 2.8 NOT BETWEEN ([><])
|
|
||||||
test8 := Map{"name": "NOT BETWEEN 查询 ([><])"}
|
|
||||||
articles8 := that.Db.Select("article", "id,title,sort", Map{"sort[><]": Slice{-10, 0}, "LIMIT": 3})
|
|
||||||
test8["result"] = true
|
|
||||||
test8["count"] = len(articles8)
|
|
||||||
tests = append(tests, test8)
|
|
||||||
|
|
||||||
// 2.9 IN 查询
|
|
||||||
test9 := Map{"name": "IN 查询"}
|
|
||||||
articles9 := that.Db.Select("article", "id,title", Map{"id": Slice{1, 2, 3, 4, 5}, "LIMIT": 5})
|
|
||||||
test9["result"] = true
|
|
||||||
test9["count"] = len(articles9)
|
|
||||||
test9["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test9)
|
|
||||||
|
|
||||||
// 2.10 NOT IN ([!])
|
|
||||||
test10 := Map{"name": "NOT IN 查询 ([!])"}
|
|
||||||
articles10 := that.Db.Select("article", "id,title", Map{"id[!]": Slice{1, 2, 3}, "LIMIT": 5})
|
|
||||||
test10["result"] = true
|
|
||||||
test10["count"] = len(articles10)
|
|
||||||
tests = append(tests, test10)
|
|
||||||
|
|
||||||
// 2.11 IS NULL
|
|
||||||
test11 := Map{"name": "IS NULL 查询"}
|
|
||||||
articles11 := that.Db.Select("article", "id,title,img", Map{"img": nil, "LIMIT": 3})
|
|
||||||
test11["result"] = true
|
|
||||||
test11["count"] = len(articles11)
|
|
||||||
tests = append(tests, test11)
|
|
||||||
|
|
||||||
// 2.12 IS NOT NULL ([!])
|
|
||||||
test12 := Map{"name": "IS NOT NULL 查询 ([!])"}
|
|
||||||
articles12 := that.Db.Select("article", "id,title,create_time", Map{"create_time[!]": nil, "LIMIT": 3})
|
|
||||||
test12["result"] = true
|
|
||||||
test12["count"] = len(articles12)
|
|
||||||
tests = append(tests, test12)
|
|
||||||
|
|
||||||
// 2.13 直接 SQL ([##] 用于 SQL 片段)
|
|
||||||
test13 := Map{"name": "直接 SQL 片段查询 ([##])"}
|
|
||||||
articles13 := that.Db.Select("article", "id,title,create_time", Map{
|
|
||||||
"[##]": "create_time > DATE_SUB(NOW(), INTERVAL 365 DAY)",
|
|
||||||
"LIMIT": 3,
|
|
||||||
})
|
|
||||||
test13["result"] = true
|
|
||||||
test13["count"] = len(articles13)
|
|
||||||
test13["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test13)
|
|
||||||
|
|
||||||
// 2.14 显式 AND 条件
|
|
||||||
test14 := Map{"name": "显式 AND 条件"}
|
|
||||||
articles14 := that.Db.Select("article", "id,title,state,click_num", Map{
|
|
||||||
"AND": Map{
|
|
||||||
"state": 0,
|
|
||||||
"click_num[>=]": 0,
|
|
||||||
},
|
|
||||||
"LIMIT": 3,
|
|
||||||
})
|
|
||||||
test14["result"] = true
|
|
||||||
test14["count"] = len(articles14)
|
|
||||||
tests = append(tests, test14)
|
|
||||||
|
|
||||||
// 2.15 OR 条件
|
|
||||||
test15 := Map{"name": "OR 条件"}
|
|
||||||
articles15 := that.Db.Select("article", "id,title,sort,click_num", Map{
|
|
||||||
"OR": Map{
|
|
||||||
"sort": 0,
|
|
||||||
"click_num[>]": 10,
|
|
||||||
},
|
|
||||||
"LIMIT": 5,
|
|
||||||
})
|
|
||||||
test15["result"] = true
|
|
||||||
test15["count"] = len(articles15)
|
|
||||||
tests = append(tests, test15)
|
|
||||||
|
|
||||||
// 2.16 嵌套 AND/OR 条件
|
|
||||||
test16 := Map{"name": "嵌套 AND/OR 条件"}
|
|
||||||
articles16 := that.Db.Select("article", "id,title,sort,state", Map{
|
|
||||||
"AND": Map{
|
|
||||||
"state": 0,
|
|
||||||
"OR": Map{
|
|
||||||
"sort[>=]": 0,
|
|
||||||
"click_num[>]": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"LIMIT": 5,
|
|
||||||
})
|
|
||||||
test16["result"] = true
|
|
||||||
test16["count"] = len(articles16)
|
|
||||||
test16["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test16)
|
|
||||||
|
|
||||||
result["tests"] = tests
|
|
||||||
result["success"] = true
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 3. 链式查询测试 ====================
|
|
||||||
func testChainQuery(that *Context) Map {
|
|
||||||
result := Map{"name": "链式查询测试", "tests": Slice{}}
|
|
||||||
tests := Slice{}
|
|
||||||
|
|
||||||
// 3.1 基本链式查询 - 使用 article 表
|
|
||||||
test1 := Map{"name": "基本链式查询 Table().Where().Select()"}
|
|
||||||
articles1 := that.Db.Table("article").
|
|
||||||
Where("state", 0).
|
|
||||||
Select("id,title,author")
|
|
||||||
test1["result"] = len(articles1) >= 0
|
|
||||||
test1["count"] = len(articles1)
|
|
||||||
tests = append(tests, test1)
|
|
||||||
|
|
||||||
// 3.2 链式 And 条件
|
|
||||||
test2 := Map{"name": "链式 And 条件"}
|
|
||||||
articles2 := that.Db.Table("article").
|
|
||||||
Where("state", 0).
|
|
||||||
And("click_num[>=]", 0).
|
|
||||||
And("sort[>=]", 0).
|
|
||||||
Select("id,title,click_num")
|
|
||||||
test2["result"] = len(articles2) >= 0
|
|
||||||
test2["count"] = len(articles2)
|
|
||||||
tests = append(tests, test2)
|
|
||||||
|
|
||||||
// 3.3 链式 Or 条件
|
|
||||||
test3 := Map{"name": "链式 Or 条件"}
|
|
||||||
articles3 := that.Db.Table("article").
|
|
||||||
Where("state", 0).
|
|
||||||
Or(Map{
|
|
||||||
"sort": 0,
|
|
||||||
"click_num[>]": 10,
|
|
||||||
}).
|
|
||||||
Select("id,title,sort,click_num")
|
|
||||||
test3["result"] = len(articles3) >= 0
|
|
||||||
test3["count"] = len(articles3)
|
|
||||||
tests = append(tests, test3)
|
|
||||||
|
|
||||||
// 3.4 链式 Order
|
|
||||||
test4 := Map{"name": "链式 Order 排序"}
|
|
||||||
articles4 := that.Db.Table("article").
|
|
||||||
Where("state", 0).
|
|
||||||
Order("create_time DESC", "id ASC").
|
|
||||||
Limit(0, 5).
|
|
||||||
Select("id,title,create_time")
|
|
||||||
test4["result"] = len(articles4) >= 0
|
|
||||||
test4["count"] = len(articles4)
|
|
||||||
tests = append(tests, test4)
|
|
||||||
|
|
||||||
// 3.5 链式 Limit
|
|
||||||
test5 := Map{"name": "链式 Limit 限制"}
|
|
||||||
articles5 := that.Db.Table("article").
|
|
||||||
Where("state", 0).
|
|
||||||
Limit(0, 3).
|
|
||||||
Select("id,title")
|
|
||||||
test5["result"] = len(articles5) <= 3
|
|
||||||
test5["count"] = len(articles5)
|
|
||||||
tests = append(tests, test5)
|
|
||||||
|
|
||||||
// 3.6 链式 Get 单条
|
|
||||||
test6 := Map{"name": "链式 Get 获取单条"}
|
|
||||||
article6 := that.Db.Table("article").
|
|
||||||
Where("state", 0).
|
|
||||||
Get("id,title,author")
|
|
||||||
test6["result"] = article6 != nil || true // 允许为空
|
|
||||||
test6["article"] = article6
|
|
||||||
tests = append(tests, test6)
|
|
||||||
|
|
||||||
// 3.7 链式 Count
|
|
||||||
test7 := Map{"name": "链式 Count 统计"}
|
|
||||||
count7 := that.Db.Table("article").
|
|
||||||
Where("state", 0).
|
|
||||||
Count()
|
|
||||||
test7["result"] = count7 >= 0
|
|
||||||
test7["count"] = count7
|
|
||||||
tests = append(tests, test7)
|
|
||||||
|
|
||||||
// 3.8 链式 Page 分页
|
|
||||||
test8 := Map{"name": "链式 Page 分页"}
|
|
||||||
articles8 := that.Db.Table("article").
|
|
||||||
Where("state", 0).
|
|
||||||
Page(1, 5).
|
|
||||||
Select("id,title")
|
|
||||||
test8["result"] = len(articles8) <= 5
|
|
||||||
test8["count"] = len(articles8)
|
|
||||||
tests = append(tests, test8)
|
|
||||||
|
|
||||||
// 3.9 链式 Group 分组
|
|
||||||
test9 := Map{"name": "链式 Group 分组"}
|
|
||||||
stats9 := that.Db.Table("article").
|
|
||||||
Where("state", 0).
|
|
||||||
Group("ctg_id").
|
|
||||||
Select("ctg_id, COUNT(*) as cnt")
|
|
||||||
test9["result"] = len(stats9) >= 0
|
|
||||||
test9["stats"] = stats9
|
|
||||||
tests = append(tests, test9)
|
|
||||||
|
|
||||||
// 3.10 链式 Update
|
|
||||||
test10 := Map{"name": "链式 Update 更新"}
|
|
||||||
// 先获取一个文章ID
|
|
||||||
testArticle := that.Db.Table("article").Where("state", 0).Get("id")
|
|
||||||
if testArticle != nil {
|
|
||||||
affected := that.Db.Table("article").
|
|
||||||
Where("id", testArticle.GetInt64("id")).
|
|
||||||
Update(Map{"modify_time[#]": "NOW()"})
|
|
||||||
test10["result"] = affected >= 0
|
|
||||||
test10["affected"] = affected
|
|
||||||
} else {
|
|
||||||
test10["result"] = true
|
|
||||||
test10["note"] = "无可用测试数据"
|
|
||||||
}
|
|
||||||
tests = append(tests, test10)
|
|
||||||
|
|
||||||
result["tests"] = tests
|
|
||||||
result["success"] = true
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 4. JOIN 查询测试 ====================
|
|
||||||
func testJoinQuery(that *Context) Map {
|
|
||||||
result := Map{"name": "JOIN查询测试", "tests": Slice{}}
|
|
||||||
tests := Slice{}
|
|
||||||
|
|
||||||
// 4.1 LEFT JOIN 链式 - article 关联 ctg
|
|
||||||
test1 := Map{"name": "LEFT JOIN 链式查询"}
|
|
||||||
articles1 := that.Db.Table("article").
|
|
||||||
LeftJoin("ctg", "article.ctg_id = ctg.id").
|
|
||||||
Where("article.state", 0).
|
|
||||||
Limit(0, 5).
|
|
||||||
Select("article.id, article.title, ctg.name as ctg_name")
|
|
||||||
test1["result"] = len(articles1) >= 0
|
|
||||||
test1["count"] = len(articles1)
|
|
||||||
test1["data"] = articles1
|
|
||||||
tests = append(tests, test1)
|
|
||||||
|
|
||||||
// 4.2 传统 JOIN 语法
|
|
||||||
test2 := Map{"name": "传统 JOIN 语法"}
|
|
||||||
articles2 := that.Db.Select("article",
|
|
||||||
Slice{
|
|
||||||
Map{"[>]ctg": "article.ctg_id = ctg.id"},
|
|
||||||
},
|
|
||||||
"article.id, article.title, ctg.name as ctg_name",
|
|
||||||
Map{
|
|
||||||
"article.state": 0,
|
|
||||||
"LIMIT": 5,
|
|
||||||
})
|
|
||||||
test2["result"] = len(articles2) >= 0
|
|
||||||
test2["count"] = len(articles2)
|
|
||||||
test2["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test2)
|
|
||||||
|
|
||||||
// 4.3 多表 JOIN - article 关联 ctg 和 admin
|
|
||||||
test3 := Map{"name": "多表 JOIN"}
|
|
||||||
articles3 := that.Db.Table("article").
|
|
||||||
LeftJoin("ctg", "article.ctg_id = ctg.id").
|
|
||||||
LeftJoin("admin", "article.admin_id = admin.id").
|
|
||||||
Where("article.state", 0).
|
|
||||||
Limit(0, 5).
|
|
||||||
Select("article.id, article.title, ctg.name as ctg_name, admin.name as admin_name")
|
|
||||||
test3["result"] = len(articles3) >= 0
|
|
||||||
test3["count"] = len(articles3)
|
|
||||||
test3["data"] = articles3
|
|
||||||
tests = append(tests, test3)
|
|
||||||
|
|
||||||
// 4.4 INNER JOIN
|
|
||||||
test4 := Map{"name": "INNER JOIN"}
|
|
||||||
articles4 := that.Db.Table("article").
|
|
||||||
InnerJoin("ctg", "article.ctg_id = ctg.id").
|
|
||||||
Where("ctg.state", 0).
|
|
||||||
Limit(0, 5).
|
|
||||||
Select("article.id, article.title, ctg.name as ctg_name")
|
|
||||||
test4["result"] = len(articles4) >= 0
|
|
||||||
test4["count"] = len(articles4)
|
|
||||||
tests = append(tests, test4)
|
|
||||||
|
|
||||||
result["tests"] = tests
|
|
||||||
result["success"] = true
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 5. 聚合函数测试 ====================
|
|
||||||
func testAggregate(that *Context) Map {
|
|
||||||
result := Map{"name": "聚合函数测试", "tests": Slice{}}
|
|
||||||
tests := Slice{}
|
|
||||||
|
|
||||||
// 5.1 Count 总数
|
|
||||||
test1 := Map{"name": "Count 总数统计"}
|
|
||||||
count1 := that.Db.Count("article")
|
|
||||||
test1["result"] = count1 >= 0
|
|
||||||
test1["count"] = count1
|
|
||||||
tests = append(tests, test1)
|
|
||||||
|
|
||||||
// 5.2 Count 带条件
|
|
||||||
test2 := Map{"name": "Count 条件统计"}
|
|
||||||
count2 := that.Db.Count("article", Map{"state": 0})
|
|
||||||
test2["result"] = count2 >= 0
|
|
||||||
test2["count"] = count2
|
|
||||||
tests = append(tests, test2)
|
|
||||||
|
|
||||||
// 5.3 Sum 求和
|
|
||||||
test3 := Map{"name": "Sum 求和"}
|
|
||||||
sum3 := that.Db.Sum("article", "click_num", Map{"state": 0})
|
|
||||||
test3["result"] = sum3 >= 0
|
|
||||||
test3["sum"] = sum3
|
|
||||||
tests = append(tests, test3)
|
|
||||||
|
|
||||||
// 5.4 Avg 平均值
|
|
||||||
test4 := Map{"name": "Avg 平均值"}
|
|
||||||
avg4 := that.Db.Avg("article", "click_num", Map{"state": 0})
|
|
||||||
test4["result"] = avg4 >= 0
|
|
||||||
test4["avg"] = avg4
|
|
||||||
tests = append(tests, test4)
|
|
||||||
|
|
||||||
// 5.5 Max 最大值
|
|
||||||
test5 := Map{"name": "Max 最大值"}
|
|
||||||
max5 := that.Db.Max("article", "click_num", Map{"state": 0})
|
|
||||||
test5["result"] = max5 >= 0
|
|
||||||
test5["max"] = max5
|
|
||||||
tests = append(tests, test5)
|
|
||||||
|
|
||||||
// 5.6 Min 最小值
|
|
||||||
test6 := Map{"name": "Min 最小值"}
|
|
||||||
min6 := that.Db.Min("article", "sort", Map{"state": 0})
|
|
||||||
test6["result"] = true // sort 可能为 0
|
|
||||||
test6["min"] = min6
|
|
||||||
tests = append(tests, test6)
|
|
||||||
|
|
||||||
// 5.7 GROUP BY 分组统计
|
|
||||||
test7 := Map{"name": "GROUP BY 分组统计"}
|
|
||||||
stats7 := that.Db.Select("article",
|
|
||||||
"ctg_id, COUNT(*) as article_count, AVG(click_num) as avg_clicks, SUM(click_num) as total_clicks",
|
|
||||||
Map{
|
|
||||||
"state": 0,
|
|
||||||
"GROUP": "ctg_id",
|
|
||||||
"ORDER": "article_count DESC",
|
|
||||||
"LIMIT": 10,
|
|
||||||
})
|
|
||||||
test7["result"] = len(stats7) >= 0
|
|
||||||
test7["stats"] = stats7
|
|
||||||
tests = append(tests, test7)
|
|
||||||
|
|
||||||
result["tests"] = tests
|
|
||||||
result["success"] = true
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 6. 分页查询测试 ====================
|
|
||||||
func testPagination(that *Context) Map {
|
|
||||||
result := Map{"name": "分页查询测试", "tests": Slice{}}
|
|
||||||
tests := Slice{}
|
|
||||||
|
|
||||||
// 6.1 PageSelect 分页查询
|
|
||||||
test1 := Map{"name": "PageSelect 分页查询"}
|
|
||||||
articles1 := that.Db.Page(1, 5).PageSelect("article", "*", Map{
|
|
||||||
"state": 0,
|
|
||||||
"ORDER": "id DESC",
|
|
||||||
})
|
|
||||||
test1["result"] = len(articles1) <= 5
|
|
||||||
test1["count"] = len(articles1)
|
|
||||||
tests = append(tests, test1)
|
|
||||||
|
|
||||||
// 6.2 第二页
|
|
||||||
test2 := Map{"name": "PageSelect 第二页"}
|
|
||||||
articles2 := that.Db.Page(2, 5).PageSelect("article", "*", Map{
|
|
||||||
"state": 0,
|
|
||||||
"ORDER": "id DESC",
|
|
||||||
})
|
|
||||||
test2["result"] = len(articles2) <= 5
|
|
||||||
test2["count"] = len(articles2)
|
|
||||||
tests = append(tests, test2)
|
|
||||||
|
|
||||||
// 6.3 链式分页
|
|
||||||
test3 := Map{"name": "链式 Page 分页"}
|
|
||||||
articles3 := that.Db.Table("article").
|
|
||||||
Where("state", 0).
|
|
||||||
Order("id DESC").
|
|
||||||
Page(1, 3).
|
|
||||||
Select("id,title,author")
|
|
||||||
test3["result"] = len(articles3) <= 3
|
|
||||||
test3["count"] = len(articles3)
|
|
||||||
tests = append(tests, test3)
|
|
||||||
|
|
||||||
// 6.4 Offset 偏移
|
|
||||||
test4 := Map{"name": "Offset 偏移查询"}
|
|
||||||
articles4 := that.Db.Table("article").
|
|
||||||
Where("state", 0).
|
|
||||||
Limit(3).
|
|
||||||
Offset(2).
|
|
||||||
Select("id,title")
|
|
||||||
test4["result"] = len(articles4) <= 3
|
|
||||||
test4["count"] = len(articles4)
|
|
||||||
test4["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test4)
|
|
||||||
|
|
||||||
result["tests"] = tests
|
|
||||||
result["success"] = true
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 7. 批量插入测试 ====================
|
|
||||||
func testInserts(that *Context) Map {
|
|
||||||
result := Map{"name": "批量插入测试", "tests": Slice{}}
|
|
||||||
tests := Slice{}
|
|
||||||
|
|
||||||
// 7.1 批量插入
|
|
||||||
test1 := Map{"name": "Inserts 批量插入"}
|
|
||||||
timestamp := time.Now().UnixNano()
|
|
||||||
affected1 := that.Db.Inserts("test_batch", []Map{
|
|
||||||
{"name": fmt.Sprintf("批量测试1_%d", timestamp), "title": "标题1", "state": 1},
|
|
||||||
{"name": fmt.Sprintf("批量测试2_%d", timestamp), "title": "标题2", "state": 1},
|
|
||||||
{"name": fmt.Sprintf("批量测试3_%d", timestamp), "title": "标题3", "state": 1},
|
|
||||||
})
|
|
||||||
test1["result"] = affected1 >= 0
|
|
||||||
test1["affected"] = affected1
|
|
||||||
test1["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test1)
|
|
||||||
|
|
||||||
// 7.2 带 [#] 的批量插入
|
|
||||||
test2 := Map{"name": "Inserts 带 [#] 标记"}
|
|
||||||
timestamp2 := time.Now().UnixNano()
|
|
||||||
affected2 := that.Db.Inserts("test_batch", []Map{
|
|
||||||
{"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()"},
|
|
||||||
})
|
|
||||||
test2["result"] = affected2 >= 0
|
|
||||||
test2["affected"] = affected2
|
|
||||||
tests = append(tests, test2)
|
|
||||||
|
|
||||||
// 清理测试数据
|
|
||||||
that.Db.Delete("test_batch", Map{"name[~]": fmt.Sprintf("_%d", timestamp)})
|
|
||||||
that.Db.Delete("test_batch", Map{"name[~]": fmt.Sprintf("_%d", timestamp2)})
|
|
||||||
|
|
||||||
result["tests"] = tests
|
|
||||||
result["success"] = true
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 8. Upsert 测试 ====================
|
|
||||||
func testUpsert(that *Context) Map {
|
|
||||||
result := Map{"name": "Upsert测试", "tests": Slice{}}
|
|
||||||
tests := Slice{}
|
|
||||||
|
|
||||||
// 使用 admin 表测试 Upsert(MySQL ON DUPLICATE KEY UPDATE)
|
|
||||||
timestamp := time.Now().Unix()
|
|
||||||
testPhone := fmt.Sprintf("199%08d", timestamp%100000000)
|
|
||||||
|
|
||||||
// 8.1 Upsert 插入新记录
|
|
||||||
test1 := Map{"name": "Upsert 插入新记录 (admin表)"}
|
|
||||||
affected1 := that.Db.Upsert("admin",
|
|
||||||
Map{
|
|
||||||
"name": "Upsert测试管理员",
|
|
||||||
"phone": testPhone,
|
|
||||||
"state": 1,
|
|
||||||
"password": "test123",
|
|
||||||
"role_id": 1,
|
|
||||||
"title": "测试职位",
|
|
||||||
"create_time[#]": "NOW()",
|
|
||||||
"modify_time[#]": "NOW()",
|
|
||||||
},
|
|
||||||
Slice{"phone"},
|
|
||||||
Slice{"name", "state", "title", "modify_time"},
|
|
||||||
)
|
|
||||||
test1["result"] = affected1 >= 0
|
|
||||||
test1["affected"] = affected1
|
|
||||||
test1["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test1)
|
|
||||||
|
|
||||||
// 8.2 Upsert 更新已存在记录
|
|
||||||
test2 := Map{"name": "Upsert 更新已存在记录"}
|
|
||||||
affected2 := that.Db.Upsert("admin",
|
|
||||||
Map{
|
|
||||||
"name": "Upsert更新后管理员",
|
|
||||||
"phone": testPhone,
|
|
||||||
"state": 1,
|
|
||||||
"password": "updated123",
|
|
||||||
"role_id": 2,
|
|
||||||
"title": "更新后职位",
|
|
||||||
"create_time[#]": "NOW()",
|
|
||||||
"modify_time[#]": "NOW()",
|
|
||||||
},
|
|
||||||
Slice{"phone"},
|
|
||||||
Slice{"name", "title", "role_id", "modify_time"},
|
|
||||||
)
|
|
||||||
test2["result"] = affected2 >= 0
|
|
||||||
test2["affected"] = affected2
|
|
||||||
tests = append(tests, test2)
|
|
||||||
|
|
||||||
// 验证更新结果
|
|
||||||
updatedAdmin := that.Db.Get("admin", "*", Map{"phone": testPhone})
|
|
||||||
test2["updatedAdmin"] = updatedAdmin
|
|
||||||
|
|
||||||
// 清理测试数据
|
|
||||||
that.Db.Delete("admin", Map{"phone": testPhone})
|
|
||||||
|
|
||||||
result["tests"] = tests
|
|
||||||
result["success"] = true
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 9. 事务测试 ====================
|
|
||||||
func testTransaction(that *Context) Map {
|
|
||||||
result := Map{"name": "事务测试", "tests": Slice{}}
|
|
||||||
tests := Slice{}
|
|
||||||
|
|
||||||
// 9.1 事务成功提交
|
|
||||||
test1 := Map{"name": "事务成功提交"}
|
|
||||||
timestamp := time.Now().Unix()
|
|
||||||
testName1 := fmt.Sprintf("事务测试_%d", timestamp)
|
|
||||||
|
|
||||||
success1 := that.Db.Action(func(tx HoTimeDB) bool {
|
|
||||||
// 插入记录到 test_batch 表
|
|
||||||
recordId := tx.Insert("test_batch", Map{
|
|
||||||
"name": testName1,
|
|
||||||
"title": "事务提交测试",
|
|
||||||
"state": 1,
|
|
||||||
"create_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
|
|
||||||
return recordId != 0
|
|
||||||
})
|
|
||||||
|
|
||||||
test1["result"] = success1
|
|
||||||
// 验证数据是否存在
|
|
||||||
checkRecord := that.Db.Get("test_batch", "*", Map{"name": testName1})
|
|
||||||
test1["recordExists"] = checkRecord != nil
|
|
||||||
tests = append(tests, test1)
|
|
||||||
|
|
||||||
// 9.2 事务回滚
|
|
||||||
test2 := Map{"name": "事务回滚"}
|
|
||||||
testName2 := fmt.Sprintf("事务回滚测试_%d", timestamp)
|
|
||||||
|
|
||||||
success2 := that.Db.Action(func(tx HoTimeDB) bool {
|
|
||||||
// 插入记录
|
|
||||||
_ = tx.Insert("test_batch", Map{
|
|
||||||
"name": testName2,
|
|
||||||
"title": "事务回滚测试",
|
|
||||||
"state": 1,
|
|
||||||
"create_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
|
|
||||||
// 返回 false 触发回滚
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
test2["result"] = !success2 // 期望回滚,所以 success2 应该为 false
|
|
||||||
// 验证数据是否不存在(已回滚)
|
|
||||||
checkRecord2 := that.Db.Get("test_batch", "*", Map{"name": testName2})
|
|
||||||
test2["recordRolledBack"] = checkRecord2 == nil
|
|
||||||
tests = append(tests, test2)
|
|
||||||
|
|
||||||
// 清理测试数据
|
|
||||||
that.Db.Delete("test_batch", Map{"name": testName1})
|
|
||||||
|
|
||||||
result["tests"] = tests
|
|
||||||
result["success"] = true
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 10. 原生 SQL 测试 ====================
|
|
||||||
func testRawSQL(that *Context) Map {
|
|
||||||
result := Map{"name": "原生SQL测试", "tests": Slice{}}
|
|
||||||
tests := Slice{}
|
|
||||||
|
|
||||||
// 10.1 Query 查询 - 使用真实的 article 表
|
|
||||||
test1 := Map{"name": "Query 原生查询"}
|
|
||||||
articles1 := that.Db.Query("SELECT id, title, author FROM `article` WHERE state = ? LIMIT ?", 0, 5)
|
|
||||||
test1["result"] = len(articles1) >= 0
|
|
||||||
test1["count"] = len(articles1)
|
|
||||||
tests = append(tests, test1)
|
|
||||||
|
|
||||||
// 10.2 Exec 执行 - 使用 article 表
|
|
||||||
test2 := Map{"name": "Exec 原生执行"}
|
|
||||||
// 更新一条记录
|
|
||||||
testArticle := that.Db.Get("article", "id", Map{"state": 0})
|
|
||||||
if testArticle != nil {
|
|
||||||
res, err := that.Db.Exec("UPDATE `article` SET modify_time = NOW() WHERE id = ?", testArticle.GetInt64("id"))
|
|
||||||
if err.GetError() == nil && res != nil {
|
|
||||||
affected, _ := res.RowsAffected()
|
|
||||||
test2["result"] = affected >= 0
|
|
||||||
test2["affected"] = affected
|
|
||||||
} else {
|
|
||||||
test2["result"] = false
|
|
||||||
test2["error"] = err.GetError()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
test2["result"] = true
|
|
||||||
test2["note"] = "无可用测试数据"
|
|
||||||
}
|
|
||||||
tests = append(tests, test2)
|
|
||||||
|
|
||||||
// ==================== IN/NOT IN 数组测试 ====================
|
|
||||||
// H1: IN (?) 配合非空数组能正确展开
|
|
||||||
test3 := Map{"name": "H1: IN (?) 非空数组展开"}
|
|
||||||
articles3 := that.Db.Query("SELECT id, title FROM `article` WHERE id IN (?) LIMIT 10", []int{1, 2, 3, 4, 5})
|
|
||||||
test3["result"] = len(articles3) >= 0
|
|
||||||
test3["count"] = len(articles3)
|
|
||||||
test3["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test3)
|
|
||||||
|
|
||||||
// H2: IN (?) 配合空数组替换为 1=0
|
|
||||||
test4 := Map{"name": "H2: IN (?) 空数组替换为1=0"}
|
|
||||||
articles4 := that.Db.Query("SELECT id, title FROM `article` WHERE id IN (?) LIMIT 10", []int{})
|
|
||||||
test4["result"] = len(articles4) == 0 // 空数组的IN应该返回0条
|
|
||||||
test4["count"] = len(articles4)
|
|
||||||
test4["lastQuery"] = that.Db.LastQuery
|
|
||||||
test4["expected"] = "count=0, SQL应包含1=0"
|
|
||||||
tests = append(tests, test4)
|
|
||||||
|
|
||||||
// H3: NOT IN (?) 配合空数组替换为 1=1
|
|
||||||
test5 := Map{"name": "H3: NOT IN (?) 空数组替换为1=1"}
|
|
||||||
articles5 := that.Db.Query("SELECT id, title FROM `article` WHERE id NOT IN (?) LIMIT 10", []int{})
|
|
||||||
test5["result"] = len(articles5) > 0 // NOT IN空数组应该返回记录
|
|
||||||
test5["count"] = len(articles5)
|
|
||||||
test5["lastQuery"] = that.Db.LastQuery
|
|
||||||
test5["expected"] = "count>0, SQL应包含1=1"
|
|
||||||
tests = append(tests, test5)
|
|
||||||
|
|
||||||
// H4: NOT IN (?) 配合非空数组正常展开
|
|
||||||
test6 := Map{"name": "H4: NOT IN (?) 非空数组展开"}
|
|
||||||
articles6 := that.Db.Query("SELECT id, title FROM `article` WHERE id NOT IN (?) LIMIT 10", []int{1, 2, 3})
|
|
||||||
test6["result"] = len(articles6) >= 0
|
|
||||||
test6["count"] = len(articles6)
|
|
||||||
test6["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test6)
|
|
||||||
|
|
||||||
// H5: 普通 ? 占位符保持原有行为
|
|
||||||
test7 := Map{"name": "H5: 普通?占位符不受影响"}
|
|
||||||
articles7 := that.Db.Query("SELECT id, title FROM `article` WHERE state = ? LIMIT ?", 0, 5)
|
|
||||||
test7["result"] = len(articles7) >= 0
|
|
||||||
test7["count"] = len(articles7)
|
|
||||||
test7["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test7)
|
|
||||||
|
|
||||||
// 额外测试: Select ORM方法的空数组处理
|
|
||||||
test8 := Map{"name": "ORM Select: IN空数组"}
|
|
||||||
articles8 := that.Db.Select("article", "id,title", Map{"id": []int{}, "LIMIT": 10})
|
|
||||||
test8["result"] = len(articles8) == 0
|
|
||||||
test8["count"] = len(articles8)
|
|
||||||
test8["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test8)
|
|
||||||
|
|
||||||
// 额外测试: Select ORM方法的NOT IN空数组处理
|
|
||||||
test9 := Map{"name": "ORM Select: NOT IN空数组"}
|
|
||||||
articles9 := that.Db.Select("article", "id,title", Map{"id[!]": []int{}, "LIMIT": 10})
|
|
||||||
test9["result"] = len(articles9) > 0 // NOT IN 空数组应返回记录
|
|
||||||
test9["count"] = len(articles9)
|
|
||||||
test9["lastQuery"] = that.Db.LastQuery
|
|
||||||
tests = append(tests, test9)
|
|
||||||
|
|
||||||
result["tests"] = tests
|
|
||||||
result["success"] = true
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
|||||||
.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.el-upload img[data-v-3b5105e6]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-3b5105e6]{font-size:40px;margin:30% 31%;display:block}.custom-tree-node[data-v-3b5105e6]{font-size:14px;padding-right:8px}.info-descriptions[data-v-4d4566fc] .el-descriptions__body .el-descriptions__label{min-width:90px;text-align:right;background:transparent;color:#606266;font-weight:400}.el-descriptions .is-bordered th[data-v-4d4566fc],.info-descriptions[data-v-4d4566fc] .el-descriptions .is-bordered td{border:transparent;max-width:25vw}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}.dialog-box .el-descriptions__header{margin:20px 0 5px}.dialog-box .el-descriptions__body{background:#fff}.textarea-box *{word-break:break-all;white-space:pre-wrap}.textarea-box table{width:100%!important}.textarea-box img{max-width:80%!important}.textarea-box::-webkit-scrollbar-thumb{height:5px;background-color:rgba(0,0,0,.2)!important}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
body[data-v-c08f3364],dd[data-v-c08f3364],dl[data-v-c08f3364],form[data-v-c08f3364],h1[data-v-c08f3364],h2[data-v-c08f3364],h3[data-v-c08f3364],h4[data-v-c08f3364],h5[data-v-c08f3364],h6[data-v-c08f3364],html[data-v-c08f3364],ol[data-v-c08f3364],p[data-v-c08f3364],pre[data-v-c08f3364],tbody[data-v-c08f3364],textarea[data-v-c08f3364],tfoot[data-v-c08f3364],thead[data-v-c08f3364],ul[data-v-c08f3364]{margin:0;font-size:14px;font-family:Microsoft YaHei}dl[data-v-c08f3364],ol[data-v-c08f3364],ul[data-v-c08f3364]{padding:0}li[data-v-c08f3364]{list-style:none}input[data-v-c08f3364]{border:none;outline:none;font-family:Microsoft YaHei;background-color:#fff}a[data-v-c08f3364]{font-family:Microsoft YaHei;text-decoration:none}[data-v-c08f3364]{margin:0;padding:0}.login[data-v-c08f3364]{position:relative;width:100%;height:100%;background-color:#353d56;background-repeat:no-repeat;background-attachment:fixed;background-size:cover}.login-item[data-v-c08f3364]{position:absolute;top:calc(50% - 30vh);left:60%;min-width:388px;width:18vw;max-width:588px;padding:8vh 30px;box-sizing:border-box;background-size:468px 468px;background:hsla(0,0%,100%,.85);border-radius:10px}.login-item .right-content[data-v-c08f3364]{box-sizing:border-box;width:100%}.login-item .right-content .login-title[data-v-c08f3364]{font-size:26px;font-weight:700;color:#4f619b;text-align:center}.errorMsg[data-v-c08f3364]{width:100%;height:34px;line-height:34px;color:red;font-size:14px;overflow:hidden}.login-item .right-content .inputWrap[data-v-c08f3364]{width:90%;height:32px;line-height:32px;color:#646464;font-size:16px;border:1px solid #b4b4b4;margin:0 auto 5%;padding:2% 10px;border-radius:10px}.login-item .right-content .inputWrap.inputFocus[data-v-c08f3364]{border:1px solid #4f619b;box-shadow:0 0 0 3px rgba(91,113,185,.4)}.login-item .right-content .inputWrap input[data-v-c08f3364]{background-color:transparent;color:#646464;display:inline-block;height:100%;width:80%}.login-btn[data-v-c08f3364]{width:97%;height:52px;text-align:center;line-height:52px;font-size:17px;color:#fff;background-color:#4f619b;border-radius:10px;margin:0 auto;margin-top:50px;cursor:pointer;font-weight:800}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.el-upload img[data-v-51dbed3c]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-51dbed3c]{font-size:40px;margin:30% 31%;display:block}.custom-tree-node[data-v-51dbed3c]{font-size:14px;padding-right:8px}.info-descriptions[data-v-4d4566fc] .el-descriptions__body .el-descriptions__label{min-width:90px;text-align:right;background:transparent;color:#606266;font-weight:400}.el-descriptions .is-bordered th[data-v-4d4566fc],.info-descriptions[data-v-4d4566fc] .el-descriptions .is-bordered td{border:transparent;max-width:25vw}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}.dialog-box .el-descriptions__header{margin:20px 0 5px}.dialog-box .el-descriptions__body{background:#fff}.textarea-box *{word-break:break-all;white-space:pre-wrap}.textarea-box table{width:100%!important}.textarea-box img{max-width:80%!important}.textarea-box::-webkit-scrollbar-thumb{height:5px;background-color:rgba(0,0,0,.2)!important}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
.full-screen-container[data-v-12e6b782]{z-index:10000}.file-upload .el-upload{background:transparent;width:100%;height:auto;text-align:left;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-upload .el-upload .el-button{margin-right:10px}.el-upload img[data-v-69e31561]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-69e31561]{font-size:40px;margin:30% 31%;display:block}.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
.left-nav-home-bar{background:#2c3759!important;overflow:hidden;text-overflow:ellipsis}.left-nav-home-bar,.left-nav-home-bar i{color:#fff!important}.el-submenu .el-menu-item{height:40px;line-height:40px;width:auto;min-width:60px;padding:0 10px 0 25px!important;text-overflow:ellipsis;overflow:hidden}.el-menu .el-submenu__title{height:46px;line-height:46px;padding-left:10px!important;text-overflow:ellipsis;overflow:hidden}.left-nav-home-bar i{margin-bottom:6px!important}.el-menu-item-group__title{padding:0 0 0 10px}.el-menu--collapse .el-menu-item-group__title,.el-menu--collapse .el-submenu__title{padding-left:20px!important}.el-menu-item i,.el-submenu__title i{margin-top:-4px;vertical-align:middle;margin:-3px 5px 0 0;right:1px}.head-left[data-v-b2941c10],.head-right[data-v-b2941c10]{display:flex;justify-content:center;flex-direction:column}.head-right[data-v-b2941c10]{align-items:flex-end}.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.el-upload img[data-v-51dbed3c]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-51dbed3c]{font-size:40px;margin:30% 31%;display:block}.custom-tree-node[data-v-51dbed3c]{font-size:14px;padding-right:8px}.info-descriptions[data-v-4d4566fc] .el-descriptions__body .el-descriptions__label{min-width:90px;text-align:right;background:transparent;color:#606266;font-weight:400}.el-descriptions .is-bordered th[data-v-4d4566fc],.info-descriptions[data-v-4d4566fc] .el-descriptions .is-bordered td{border:transparent;max-width:25vw}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}.dialog-box .el-descriptions__header{margin:20px 0 5px}.dialog-box .el-descriptions__body{background:#fff}.textarea-box *{word-break:break-all;white-space:pre-wrap}.textarea-box table{width:100%!important}.textarea-box img{max-width:80%!important}.textarea-box::-webkit-scrollbar-thumb{height:5px;background-color:rgba(0,0,0,.2)!important}.el-dialog{margin:auto!important;top:50%;transform:translateY(-50%)}.el-dialog-div{height:75vh;overflow:auto}.el-dialog__body{padding-top:15px;padding-bottom:45px}.el-dialog-div .el-tabs__header{position:absolute;left:1px;top:69px;width:calc(90vw - 42px);margin:0 20px;z-index:1}.el-dialog-div .el-tabs__content{padding-top:50px}.el-dialog-div .el-affix--fixed{bottom:20vh}.el-dialog-div .el-affix--fixed .el-form-item{padding:0!important;background:transparent!important}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
.full-screen-container[data-v-12e6b782]{z-index:10000}.file-upload .el-upload{background:transparent;width:100%;height:auto;text-align:left;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-upload .el-upload .el-button{margin-right:10px}.el-upload img[data-v-96cdfb38]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-96cdfb38]{font-size:40px;margin:30% 31%;display:block}.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
.not-show-tab-label .el-tabs__header{display:none}.el-descriptions__body{background:#f0f0f0}.not-show-tab-search{display:none}.el-table__body-wrapper{margin-bottom:4px;padding-bottom:2px}.el-table__body-wrapper::-webkit-scrollbar{width:8px;height:8px}.el-table__body-wrapper::-webkit-scrollbar-track{border-radius:10px;-webkit-box-shadow:inset 0 0 6px hsla(0,0%,93.3%,.3);background-color:#eee}.el-table__body-wrapper::-webkit-scrollbar-thumb{border-radius:10px;-webkit-box-shadow:inset 0 0 6px rgba(145,143,143,.3);background-color:#918f8f}.input-with-select .el-input-group__prepend{background-color:#fff}.daterange-box .select .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.daterange-box .daterange.el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0;vertical-align:bottom}[data-v-e9f58da4] .el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content{background-color:#409eff!important;color:#fff}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
.left-nav-home-bar{background:#2c3759!important;overflow:hidden;text-overflow:ellipsis}.left-nav-home-bar,.left-nav-home-bar i{color:#fff!important}.el-submenu .el-menu-item{height:40px;line-height:40px;width:auto;min-width:60px;padding:0 10px 0 25px!important;text-overflow:ellipsis;overflow:hidden}.el-menu .el-submenu__title{height:46px;line-height:46px;padding-left:10px!important;text-overflow:ellipsis;overflow:hidden}.left-nav-home-bar i{margin-bottom:6px!important}.el-menu-item-group__title{padding:0 0 0 10px}.el-menu--collapse .el-menu-item-group__title,.el-menu--collapse .el-submenu__title{padding-left:20px!important}.el-menu-item i,.el-submenu__title i{margin-top:-4px;vertical-align:middle;margin:-3px 5px 0 0;right:1px}.head-left[data-v-b2941c10],.head-right[data-v-b2941c10]{display:flex;justify-content:center;flex-direction:column}.head-right[data-v-b2941c10]{align-items:flex-end}.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.el-upload img[data-v-3b5105e6]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-3b5105e6]{font-size:40px;margin:30% 31%;display:block}.custom-tree-node[data-v-3b5105e6]{font-size:14px;padding-right:8px}.info-descriptions[data-v-4d4566fc] .el-descriptions__body .el-descriptions__label{min-width:90px;text-align:right;background:transparent;color:#606266;font-weight:400}.el-descriptions .is-bordered th[data-v-4d4566fc],.info-descriptions[data-v-4d4566fc] .el-descriptions .is-bordered td{border:transparent;max-width:25vw}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}.dialog-box .el-descriptions__header{margin:20px 0 5px}.dialog-box .el-descriptions__body{background:#fff}.textarea-box *{word-break:break-all;white-space:pre-wrap}.textarea-box table{width:100%!important}.textarea-box img{max-width:80%!important}.textarea-box::-webkit-scrollbar-thumb{height:5px;background-color:rgba(0,0,0,.2)!important}.el-dialog{margin:auto!important;top:50%;transform:translateY(-50%)}.el-dialog-div{height:75vh;overflow:auto}.el-dialog__body{padding-top:15px;padding-bottom:45px}.el-dialog-div .el-tabs__header{position:absolute;left:1px;top:69px;width:calc(90vw - 42px);margin:0 20px;z-index:1}.el-dialog-div .el-tabs__content{padding-top:50px}.el-dialog-div .el-affix--fixed{bottom:20vh}.el-dialog-div .el-affix--fixed .el-form-item{padding:0!important;background:transparent!important}
|
|
||||||
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
|||||||
.full-screen-container[data-v-12e6b782]{z-index:10000}.not-show-tab-label .el-tabs__header{display:none}.el-descriptions__body{background:#f0f0f0}.not-show-tab-search{display:none}.el-table__body-wrapper{margin-bottom:4px;padding-bottom:2px}.el-table__body-wrapper::-webkit-scrollbar{width:8px;height:8px}.el-table__body-wrapper::-webkit-scrollbar-track{border-radius:10px;-webkit-box-shadow:inset 0 0 6px hsla(0,0%,93.3%,.3);background-color:#eee}.el-table__body-wrapper::-webkit-scrollbar-thumb{border-radius:10px;-webkit-box-shadow:inset 0 0 6px rgba(145,143,143,.3);background-color:#918f8f}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><script src="js/manage.js"></script><script src="https://api.map.baidu.com/api?v=2.0&ak=bF4Y6tQg94hV2vesn2ZIaUIXO4aRxxRk"></script><title></title><style>body{
|
|
||||||
margin: 0px;
|
|
||||||
}</style><link href="css/chunk-0de923e9.c4e8272a.css" rel="prefetch"><link href="css/chunk-1a458102.466135f0.css" rel="prefetch"><link href="css/chunk-291edee4.508cb3c8.css" rel="prefetch"><link href="css/chunk-4aefa5ec.fcc75990.css" rel="prefetch"><link href="css/chunk-4f81d902.91f1ef17.css" rel="prefetch"><link href="css/chunk-856f3c38.9b9508b8.css" rel="prefetch"><link href="css/chunk-e3f8e5a6.7876554f.css" rel="prefetch"><link href="js/chunk-0de923e9.1f0fca7e.js" rel="prefetch"><link href="js/chunk-1a458102.9a54e9ae.js" rel="prefetch"><link href="js/chunk-25ddb50b.55ec750b.js" rel="prefetch"><link href="js/chunk-291edee4.e8dbe8c8.js" rel="prefetch"><link href="js/chunk-4aefa5ec.c237610d.js" rel="prefetch"><link href="js/chunk-4f81d902.c5fdb7dd.js" rel="prefetch"><link href="js/chunk-7ea17297.38d572ef.js" rel="prefetch"><link href="js/chunk-856f3c38.29c2fcc0.js" rel="prefetch"><link href="js/chunk-e3f8e5a6.ff58e6f8.js" rel="prefetch"><link href="js/chunk-e624c1ca.62513cbc.js" rel="prefetch"><link href="css/app.abfb5de2.css" rel="preload" as="style"><link href="js/app.25e49e88.js" rel="preload" as="script"><link href="css/app.abfb5de2.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but hotime doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/app.25e49e88.js"></script></body></html>
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
|||||||
(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-0de923e9"],{"578a":function(o,n,t){"use strict";t.r(n);t("b0c0");var i=t("f2bf"),s={class:"login-item"},r={class:"right-content"},u={class:"login-title"},l={style:{height:"60px"}},d=Object(i.r)(" 账号:"),b=Object(i.r)(" 密码:");var e=t("2934"),c={name:"Login",data:function(){return{showLog:!1,showLogInfo:"",label:"HoTime DashBoard",form:{name:"",password:""},backgroundImage:window.Hotime.data.name+"/hotime/wallpaper?random=1&type=1",focusName:!1,focusPassword:!1}},methods:{login:function(){var n=this;if(""==this.name||""==this.password)return this.showLogInfo="参数不足!",void(this.showLog=!0);Object(e.a)(window.Hotime.data.name+"/hotime/login",n.form).then(function(o){if(0!=o.status)return n.showLogInfo=o.error.msg,void(n.showLog=!0);location.hash="#/",location.reload()})},getBgImg:function(){var n=this;Object(e.e)(window.Hotime.data.name+"/hotime/wallpaper?random=1").then(function(o){0==o.status&&(n.backgroundImage=o.result.url)})},focusPrice:function(o){this["focus"+o]=!0},blurPrice:function(o){this["focus"+o]=!1}},mounted:function(){var n=this;this.label=window.Hotime.data.label,document.onkeydown=function(o){o=window.event||o;13==(o.keyCode||o.which||o.charCode)&&n.login()}}},a=(t("fad9"),t("6b0d")),t=t.n(a);n.default=t()(c,[["render",function(o,n,t,e,c,a){return Object(i.L)(),Object(i.n)("div",{class:"login",style:Object(i.C)({width:"100%",height:"100vh","background-image":"url("+c.backgroundImage+")"})},[Object(i.o)("div",s,[Object(i.o)("div",r,[Object(i.o)("p",u,Object(i.Y)(c.label),1),Object(i.o)("div",l,[Object(i.lb)(Object(i.o)("p",{class:"errorMsg"},Object(i.Y)(c.showLogInfo),513),[[i.hb,c.showLog]])]),Object(i.o)("p",{class:Object(i.B)(["inputWrap",{inputFocus:c.focusName}])},[d,Object(i.lb)(Object(i.o)("input",{type:"text","onUpdate:modelValue":n[0]||(n[0]=function(o){return c.form.name=o}),class:"accountVal",onKeyup:n[1]||(n[1]=Object(i.mb)(function(){return a.login&&a.login.apply(a,arguments)},["enter"])),onFocus:n[2]||(n[2]=function(o){return a.focusPrice("Name")}),onBlur:n[3]||(n[3]=function(o){return a.blurPrice("Name")})},null,544),[[i.gb,c.form.name]])],2),Object(i.o)("p",{class:Object(i.B)(["inputWrap",{inputFocus:c.focusPassword}])},[b,Object(i.lb)(Object(i.o)("input",{type:"password","onUpdate:modelValue":n[4]||(n[4]=function(o){return c.form.password=o}),class:"passwordVal",onKeyup:n[5]||(n[5]=Object(i.mb)(function(){return a.login&&a.login.apply(a,arguments)},["enter"])),onFocus:n[6]||(n[6]=function(o){return a.focusPrice("Password")}),onBlur:n[7]||(n[7]=function(o){return a.blurPrice("Password")})},null,544),[[i.gb,c.form.password]])],2),Object(i.o)("p",{class:"login-btn",onClick:n[8]||(n[8]=function(){return a.login&&a.login.apply(a,arguments)})},"登录")])])],4)}],["__scopeId","data-v-c08f3364"]])},dbeb:function(o,n,t){},fad9:function(o,n,t){"use strict";t("dbeb")}}]);
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
|||||||
(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-25ddb50b"],{"107c":function(t,e,n){var r=n("d039"),c=n("da84").RegExp;t.exports=r(function(){var t=c("(?<a>b)","g");return"b"!==t.exec("b").groups.a||"bc"!=="b".replace(t,"$<a>c")})},"129f":function(t,e){t.exports=Object.is||function(t,e){return t===e?0!==t||1/t==1/e:t!=t&&e!=e}},"14c3":function(t,e,n){var r=n("c6b6"),c=n("9263");t.exports=function(t,e){var n=t.exec;if("function"==typeof n){n=n.call(t,e);if("object"!=typeof n)throw TypeError("RegExp exec method returned something other than an Object or null");return n}if("RegExp"!==r(t))throw TypeError("RegExp#exec called on incompatible receiver");return c.call(t,e)}},"159b":function(t,e,n){var r,c=n("da84"),i=n("fdbc"),o=n("17c2"),a=n("9112");for(r in i){var l=c[r],l=l&&l.prototype;if(l&&l.forEach!==o)try{a(l,"forEach",o)}catch(t){l.forEach=o}}},"17c2":function(t,e,n){"use strict";var r=n("b727").forEach,n=n("a640")("forEach");t.exports=n?[].forEach:function(t){return r(this,t,1<arguments.length?arguments[1]:void 0)}},"83c5":function(t,e,n){"use strict";n("159b");e.a={list:{},constructor:function(){this.list={}},$on:function(t,e){this.list[t]=this.list[t]||[],this.list[t].push(e)},$emit:function(t,e){this.list[t]&&this.list[t].forEach(function(t){t(e)})},$off:function(t){this.list[t]&&delete this.list[t]}}},"841c":function(t,e,n){"use strict";var r=n("d784"),o=n("825a"),a=n("1d80"),l=n("129f"),s=n("577e"),u=n("14c3");r("search",function(r,c,i){return[function(t){var e=a(this),n=null==t?void 0:t[r];return void 0!==n?n.call(t,e):new RegExp(t)[r](s(e))},function(t){var e=o(this),t=s(t),n=i(c,e,t);if(n.done)return n.value;n=e.lastIndex,l(n,0)||(e.lastIndex=0),t=u(e,t);return l(e.lastIndex,n)||(e.lastIndex=n),null===t?-1:t.index}]})},9263:function(t,e,n){"use strict";var r,p=n("577e"),h=n("ad6d"),c=n("9f7f"),i=n("5692"),g=n("7c73"),v=n("69f3").get,o=n("fce3"),n=n("107c"),E=RegExp.prototype.exec,b=i("native-string-replace",String.prototype.replace),I=E,R=(i=/a/,r=/b*/g,E.call(i,"a"),E.call(r,"a"),0!==i.lastIndex||0!==r.lastIndex),y=c.UNSUPPORTED_Y||c.BROKEN_CARET,w=void 0!==/()??/.exec("")[1];(R||w||y||o||n)&&(I=function(t){var e,n,r,c,i,o,a=this,l=v(a),t=p(t),s=l.raw;if(s)return s.lastIndex=a.lastIndex,f=I.call(s,t),a.lastIndex=s.lastIndex,f;var u=l.groups,s=y&&a.sticky,f=h.call(a),l=a.source,d=0,x=t;if(s&&(-1===(f=f.replace("y","")).indexOf("g")&&(f+="g"),x=t.slice(a.lastIndex),0<a.lastIndex&&(!a.multiline||a.multiline&&"\n"!==t.charAt(a.lastIndex-1))&&(l="(?: "+l+")",x=" "+x,d++),e=new RegExp("^(?:"+l+")",f)),w&&(e=new RegExp("^"+l+"$(?!\\s)",f)),R&&(n=a.lastIndex),r=E.call(s?e:a,x),s?r?(r.input=r.input.slice(d),r[0]=r[0].slice(d),r.index=a.lastIndex,a.lastIndex+=r[0].length):a.lastIndex=0:R&&r&&(a.lastIndex=a.global?r.index+r[0].length:n),w&&r&&1<r.length&&b.call(r[0],e,function(){for(c=1;c<arguments.length-2;c++)void 0===arguments[c]&&(r[c]=void 0)}),r&&u)for(r.groups=i=g(null),c=0;c<u.length;c++)i[(o=u[c])[0]]=r[o[1]];return r}),t.exports=I},"9f7f":function(t,e,n){var r=n("d039"),c=n("da84").RegExp;e.UNSUPPORTED_Y=r(function(){var t=c("a","y");return t.lastIndex=2,null!=t.exec("abcd")}),e.BROKEN_CARET=r(function(){var t=c("^r","gy");return t.lastIndex=2,null!=t.exec("str")})},ac1f:function(t,e,n){"use strict";var r=n("23e7"),n=n("9263");r({target:"RegExp",proto:!0,forced:/./.exec!==n},{exec:n})},ad6d:function(t,e,n){"use strict";var r=n("825a");t.exports=function(){var t=r(this),e="";return t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.dotAll&&(e+="s"),t.unicode&&(e+="u"),t.sticky&&(e+="y"),e}},d784:function(t,e,n){"use strict";n("ac1f");var l=n("6eeb"),s=n("9263"),u=n("d039"),f=n("b622"),d=n("9112"),x=f("species"),p=RegExp.prototype;t.exports=function(n,t,e,r){var o,c=f(n),a=!u(function(){var t={};return t[c]=function(){return 7},7!=""[n](t)}),i=a&&!u(function(){var t=!1,e=/a/;return"split"===n&&((e={constructor:{}}).constructor[x]=function(){return e},e.flags="",e[c]=/./[c]),e.exec=function(){return t=!0,null},e[c](""),!t});a&&i&&!e||(o=/./[c],i=t(c,""[n],function(t,e,n,r,c){var i=e.exec;return i===s||i===p.exec?a&&!c?{done:!0,value:o.call(e,n,r)}:{done:!0,value:t.call(n,e,r)}:{done:!1}}),l(String.prototype,n,i[0]),l(p,c,i[1])),r&&d(p[c],"sham",!0)}},fce3:function(t,e,n){var r=n("d039"),c=n("da84").RegExp;t.exports=r(function(){var t=c(".","s");return!(t.dotAll&&t.exec("\n")&&"s"===t.flags)})}}]);
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
|||||||
(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-e624c1ca"],{"4de4":function(t,n,r){"use strict";var e=r("23e7"),o=r("b727").filter;e({target:"Array",proto:!0,forced:!r("1dde")("filter")},{filter:function(t){return o(this,t,1<arguments.length?arguments[1]:void 0)}})},a640:function(t,n,r){"use strict";var e=r("d039");t.exports=function(t,n){var r=[][t];return!!r&&e(function(){r.call(null,n||function(){throw 1},1)})}}}]);
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2022/7/23 下午7:22
|
|
||||||
* Author:HoTeas
|
|
||||||
*/
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
var Hotime = {
|
|
||||||
vueComponent: {},
|
|
||||||
mapData: {},
|
|
||||||
pageRow:20,
|
|
||||||
tableMapData: {},
|
|
||||||
tableName:"admin"
|
|
||||||
}
|
|
||||||
@ -1,848 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
// 实际使用时请替换为正确的导入路径
|
|
||||||
// "code.hoteas.com/golang/hotime/cache"
|
|
||||||
// "code.hoteas.com/golang/hotime/common"
|
|
||||||
// "code.hoteas.com/golang/hotime/db"
|
|
||||||
"code.hoteas.com/golang/hotime/cache"
|
|
||||||
"code.hoteas.com/golang/hotime/common"
|
|
||||||
"code.hoteas.com/golang/hotime/db"
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HoTimeDB使用示例代码集合 - 修正版
|
|
||||||
// 本文件包含了各种常见场景的完整示例代码,所有条件查询语法已修正
|
|
||||||
|
|
||||||
// 示例1: 基本初始化和配置
|
|
||||||
func Example1_BasicSetup() {
|
|
||||||
// 创建数据库实例
|
|
||||||
database := &db.HoTimeDB{
|
|
||||||
Prefix: "app_", // 设置表前缀
|
|
||||||
Mode: 2, // 开发模式,输出SQL日志
|
|
||||||
Type: "mysql", // 数据库类型
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置日志
|
|
||||||
logger := logrus.New()
|
|
||||||
database.Log = logger
|
|
||||||
|
|
||||||
// 设置连接函数
|
|
||||||
database.SetConnect(func(err ...*common.Error) (master, slave *sql.DB) {
|
|
||||||
// 主数据库连接
|
|
||||||
master, dbErr := sql.Open("mysql", "root:password@tcp(localhost:3306)/testdb?charset=utf8&parseTime=true")
|
|
||||||
if dbErr != nil {
|
|
||||||
log.Fatal("数据库连接失败:", dbErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从数据库连接(可选,用于读写分离)
|
|
||||||
slave = master // 这里使用同一个连接,实际项目中可以连接到从库
|
|
||||||
|
|
||||||
return master, slave
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Println("数据库初始化完成")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 示例2: 基本CRUD操作(修正版)
|
|
||||||
func Example2_BasicCRUD_Fixed(db *db.HoTimeDB) {
|
|
||||||
// 创建用户
|
|
||||||
fmt.Println("=== 创建用户 ===")
|
|
||||||
userId := db.Insert("user", common.Map{
|
|
||||||
"name": "张三",
|
|
||||||
"email": "zhangsan@example.com",
|
|
||||||
"age": 25,
|
|
||||||
"status": 1,
|
|
||||||
"balance": 1000.50,
|
|
||||||
"created_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
fmt.Printf("新用户ID: %d\n", userId)
|
|
||||||
|
|
||||||
// 查询用户(单条件,可以不用AND)
|
|
||||||
fmt.Println("\n=== 查询用户 ===")
|
|
||||||
user := db.Get("user", "*", common.Map{
|
|
||||||
"id": userId,
|
|
||||||
})
|
|
||||||
if user != nil {
|
|
||||||
fmt.Printf("用户信息: %+v\n", user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户(多条件必须用AND包装)
|
|
||||||
fmt.Println("\n=== 更新用户 ===")
|
|
||||||
affected := db.Update("user", common.Map{
|
|
||||||
"name": "李四",
|
|
||||||
"age": 26,
|
|
||||||
"updated_time[#]": "NOW()",
|
|
||||||
}, common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"id": userId,
|
|
||||||
"status": 1, // 确保只更新正常状态的用户
|
|
||||||
},
|
|
||||||
})
|
|
||||||
fmt.Printf("更新记录数: %d\n", affected)
|
|
||||||
|
|
||||||
// 软删除用户
|
|
||||||
fmt.Println("\n=== 软删除用户 ===")
|
|
||||||
affected = db.Update("user", common.Map{
|
|
||||||
"deleted_at[#]": "NOW()",
|
|
||||||
"status": 0,
|
|
||||||
}, common.Map{
|
|
||||||
"id": userId, // 单条件,不需要AND
|
|
||||||
})
|
|
||||||
fmt.Printf("软删除记录数: %d\n", affected)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 示例3: 条件查询语法(修正版)
|
|
||||||
func Example3_ConditionQuery_Fixed(db *db.HoTimeDB) {
|
|
||||||
fmt.Println("=== 条件查询语法示例 ===")
|
|
||||||
|
|
||||||
// ✅ 正确:单个条件
|
|
||||||
users1 := db.Select("user", "*", common.Map{
|
|
||||||
"status": 1,
|
|
||||||
})
|
|
||||||
fmt.Printf("活跃用户: %d个\n", len(users1))
|
|
||||||
|
|
||||||
// ✅ 正确:多个条件用AND包装
|
|
||||||
users2 := db.Select("user", "*", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
"age[<=]": 60,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
fmt.Printf("活跃的成年用户: %d个\n", len(users2))
|
|
||||||
|
|
||||||
// ✅ 正确:OR条件
|
|
||||||
users3 := db.Select("user", "*", common.Map{
|
|
||||||
"OR": common.Map{
|
|
||||||
"level": "vip",
|
|
||||||
"balance[>]": 5000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
fmt.Printf("VIP或高余额用户: %d个\n", len(users3))
|
|
||||||
|
|
||||||
// ✅ 正确:条件 + 特殊参数
|
|
||||||
users4 := db.Select("user", "*", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>=]": 18,
|
|
||||||
},
|
|
||||||
"ORDER": "created_time DESC",
|
|
||||||
"LIMIT": 10,
|
|
||||||
})
|
|
||||||
fmt.Printf("最近的活跃成年用户: %d个\n", len(users4))
|
|
||||||
|
|
||||||
// ✅ 正确:复杂嵌套条件
|
|
||||||
users5 := db.Select("user", "*", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"OR": common.Map{
|
|
||||||
"age[<]": 30,
|
|
||||||
"level": "vip",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"ORDER": []string{"level DESC", "created_time DESC"},
|
|
||||||
"LIMIT": []int{0, 20},
|
|
||||||
})
|
|
||||||
fmt.Printf("年轻或VIP的活跃用户: %d个\n", len(users5))
|
|
||||||
|
|
||||||
// ✅ 正确:模糊查询
|
|
||||||
users6 := db.Select("user", "*", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"name[~]": "张", // 姓名包含"张"
|
|
||||||
"email[~!]": "gmail", // 邮箱以gmail开头
|
|
||||||
"status": 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
fmt.Printf("姓张的gmail活跃用户: %d个\n", len(users6))
|
|
||||||
|
|
||||||
// ✅ 正确:范围查询
|
|
||||||
users7 := db.Select("user", "*", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"age[<>]": []int{18, 35}, // 年龄在18-35之间
|
|
||||||
"balance[><]": []float64{0, 100}, // 余额不在0-100之间
|
|
||||||
"status": 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
fmt.Printf("18-35岁且余额>100的活跃用户: %d个\n", len(users7))
|
|
||||||
|
|
||||||
// ✅ 正确:IN查询
|
|
||||||
users8 := db.Select("user", "*", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"id": []int{1, 2, 3, 4, 5}, // ID在指定范围内
|
|
||||||
"status[!]": []int{0, -1}, // 状态不为0或-1
|
|
||||||
},
|
|
||||||
})
|
|
||||||
fmt.Printf("指定ID的活跃用户: %d个\n", len(users8))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 示例4: 链式查询操作(正确版)
|
|
||||||
func Example4_ChainQuery_Fixed(db *db.HoTimeDB) {
|
|
||||||
fmt.Println("=== 链式查询示例 ===")
|
|
||||||
|
|
||||||
// 链式查询(链式语法允许单独的Where,然后用And添加更多条件)
|
|
||||||
users := db.Table("user").
|
|
||||||
Where("status", 1). // 链式中可以单独Where
|
|
||||||
And("age[>=]", 18). // 然后用And添加条件
|
|
||||||
And("age[<=]", 60). // 再添加条件
|
|
||||||
Or(common.Map{ // 或者用Or添加OR条件组
|
|
||||||
"level": "vip",
|
|
||||||
"balance[>]": 5000,
|
|
||||||
}).
|
|
||||||
Order("created_time DESC", "id ASC"). // 排序
|
|
||||||
Limit(0, 10). // 限制结果
|
|
||||||
Select("id,name,email,age,balance,level")
|
|
||||||
|
|
||||||
fmt.Printf("链式查询到 %d 个用户\n", len(users))
|
|
||||||
for i, user := range users {
|
|
||||||
fmt.Printf("用户%d: %s (年龄:%v, 余额:%v)\n",
|
|
||||||
i+1,
|
|
||||||
user.GetString("name"),
|
|
||||||
user.Get("age"),
|
|
||||||
user.Get("balance"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 链式统计查询
|
|
||||||
count := db.Table("user").
|
|
||||||
Where("status", 1).
|
|
||||||
And("age[>=]", 18).
|
|
||||||
Count()
|
|
||||||
fmt.Printf("符合条件的用户总数: %d\n", count)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 示例5: JOIN查询操作(修正版)
|
|
||||||
func Example5_JoinQuery_Fixed(db *db.HoTimeDB) {
|
|
||||||
fmt.Println("=== JOIN查询示例 ===")
|
|
||||||
|
|
||||||
// 链式JOIN查询
|
|
||||||
orders := db.Table("order").
|
|
||||||
LeftJoin("user", "order.user_id = user.id").
|
|
||||||
LeftJoin("product", "order.product_id = product.id").
|
|
||||||
Where("order.status", "paid"). // 链式中单个条件可以直接Where
|
|
||||||
And("order.created_time[>]", "2023-01-01"). // 用And添加更多条件
|
|
||||||
Order("order.created_time DESC").
|
|
||||||
Select(`
|
|
||||||
order.id as order_id,
|
|
||||||
order.amount,
|
|
||||||
order.status,
|
|
||||||
order.created_time,
|
|
||||||
user.name as user_name,
|
|
||||||
user.email as user_email,
|
|
||||||
product.title as product_title,
|
|
||||||
product.price as product_price
|
|
||||||
`)
|
|
||||||
|
|
||||||
fmt.Printf("链式JOIN查询到 %d 个订单\n", len(orders))
|
|
||||||
for _, order := range orders {
|
|
||||||
fmt.Printf("订单ID:%v, 用户:%s, 商品:%s, 金额:%v\n",
|
|
||||||
order.Get("order_id"),
|
|
||||||
order.GetString("user_name"),
|
|
||||||
order.GetString("product_title"),
|
|
||||||
order.Get("amount"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 传统JOIN语法(多个条件必须用AND包装)
|
|
||||||
orders2 := db.Select("order",
|
|
||||||
common.Slice{
|
|
||||||
common.Map{"[>]user": "order.user_id = user.id"},
|
|
||||||
common.Map{"[>]product": "order.product_id = product.id"},
|
|
||||||
},
|
|
||||||
"order.*, user.name as user_name, product.title as product_title",
|
|
||||||
common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"order.status": "paid",
|
|
||||||
"order.created_time[>]": "2023-01-01",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Printf("传统JOIN语法查询到 %d 个订单\n", len(orders2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 示例6: 分页查询(修正版)
|
|
||||||
func Example6_PaginationQuery_Fixed(db *db.HoTimeDB) {
|
|
||||||
fmt.Println("=== 分页查询示例 ===")
|
|
||||||
|
|
||||||
page := 2
|
|
||||||
pageSize := 10
|
|
||||||
|
|
||||||
// 获取总数(单条件)
|
|
||||||
total := db.Count("user", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"deleted_at": nil,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 分页数据(链式方式)
|
|
||||||
users := db.Table("user").
|
|
||||||
Where("status", 1).
|
|
||||||
And("deleted_at", nil).
|
|
||||||
Order("created_time DESC").
|
|
||||||
Page(page, pageSize).
|
|
||||||
Select("id,name,email,created_time")
|
|
||||||
|
|
||||||
// 使用传统方式的分页查询
|
|
||||||
users2 := db.Page(page, pageSize).PageSelect("user", "*", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"deleted_at": nil,
|
|
||||||
},
|
|
||||||
"ORDER": "created_time DESC",
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算分页信息
|
|
||||||
totalPages := (total + pageSize - 1) / pageSize
|
|
||||||
offset := (page - 1) * pageSize
|
|
||||||
|
|
||||||
fmt.Printf("总记录数: %d\n", total)
|
|
||||||
fmt.Printf("总页数: %d\n", totalPages)
|
|
||||||
fmt.Printf("当前页: %d\n", page)
|
|
||||||
fmt.Printf("每页大小: %d\n", pageSize)
|
|
||||||
fmt.Printf("偏移量: %d\n", offset)
|
|
||||||
fmt.Printf("链式查询当前页记录数: %d\n", len(users))
|
|
||||||
fmt.Printf("传统查询当前页记录数: %d\n", len(users2))
|
|
||||||
|
|
||||||
for i, user := range users {
|
|
||||||
fmt.Printf(" %d. %s (%s) - %v\n",
|
|
||||||
offset+i+1,
|
|
||||||
user.GetString("name"),
|
|
||||||
user.GetString("email"),
|
|
||||||
user.Get("created_time"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 示例7: 聚合函数查询(修正版)
|
|
||||||
func Example7_AggregateQuery_Fixed(db *db.HoTimeDB) {
|
|
||||||
fmt.Println("=== 聚合函数查询示例 ===")
|
|
||||||
|
|
||||||
// 基本统计
|
|
||||||
userCount := db.Count("user")
|
|
||||||
activeUserCount := db.Count("user", common.Map{"status": 1})
|
|
||||||
totalBalance := db.Sum("user", "balance", common.Map{"status": 1})
|
|
||||||
|
|
||||||
fmt.Printf("总用户数: %d\n", userCount)
|
|
||||||
fmt.Printf("活跃用户数: %d\n", activeUserCount)
|
|
||||||
fmt.Printf("活跃用户总余额: %.2f\n", totalBalance)
|
|
||||||
|
|
||||||
// 分组统计(正确语法)
|
|
||||||
stats := db.Select("user",
|
|
||||||
"level, COUNT(*) as user_count, AVG(age) as avg_age, SUM(balance) as total_balance",
|
|
||||||
common.Map{
|
|
||||||
"status": 1, // 单条件,不需要AND
|
|
||||||
"GROUP": "level",
|
|
||||||
"ORDER": "user_count DESC",
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Println("\n按等级分组统计:")
|
|
||||||
for _, stat := range stats {
|
|
||||||
fmt.Printf("等级:%v, 用户数:%v, 平均年龄:%v, 总余额:%v\n",
|
|
||||||
stat.Get("level"),
|
|
||||||
stat.Get("user_count"),
|
|
||||||
stat.Get("avg_age"),
|
|
||||||
stat.Get("total_balance"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关联统计(修正版)
|
|
||||||
orderStats := db.Select("order",
|
|
||||||
common.Slice{
|
|
||||||
common.Map{"[>]user": "order.user_id = user.id"},
|
|
||||||
},
|
|
||||||
"user.level, COUNT(order.id) as order_count, SUM(order.amount) as total_amount",
|
|
||||||
common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"order.status": "paid",
|
|
||||||
"order.created_time[>]": "2023-01-01",
|
|
||||||
},
|
|
||||||
"GROUP": "user.level",
|
|
||||||
"ORDER": "total_amount DESC",
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Println("\n用户等级订单统计:")
|
|
||||||
for _, stat := range orderStats {
|
|
||||||
fmt.Printf("等级:%v, 订单数:%v, 总金额:%v\n",
|
|
||||||
stat.Get("level"),
|
|
||||||
stat.Get("order_count"),
|
|
||||||
stat.Get("total_amount"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 示例8: 事务处理(修正版)
|
|
||||||
func Example8_Transaction_Fixed(database *db.HoTimeDB) {
|
|
||||||
fmt.Println("=== 事务处理示例 ===")
|
|
||||||
|
|
||||||
// 模拟转账操作
|
|
||||||
fromUserId := int64(1)
|
|
||||||
toUserId := int64(2)
|
|
||||||
amount := 100.0
|
|
||||||
|
|
||||||
success := database.Action(func(tx db.HoTimeDB) bool {
|
|
||||||
// 检查转出账户余额(单条件)
|
|
||||||
fromUser := tx.Get("user", "balance", common.Map{"id": fromUserId})
|
|
||||||
if fromUser == nil {
|
|
||||||
fmt.Println("转出用户不存在")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fromBalance := fromUser.GetFloat64("balance")
|
|
||||||
if fromBalance < amount {
|
|
||||||
fmt.Println("余额不足")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扣减转出账户余额(多条件必须用AND)
|
|
||||||
affected1 := tx.Update("user", common.Map{
|
|
||||||
"balance[#]": fmt.Sprintf("balance - %.2f", amount),
|
|
||||||
"updated_time[#]": "NOW()",
|
|
||||||
}, common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"id": fromUserId,
|
|
||||||
"balance[>=]": amount, // 再次确保余额足够
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if affected1 == 0 {
|
|
||||||
fmt.Println("扣减余额失败")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 增加转入账户余额(单条件)
|
|
||||||
affected2 := tx.Update("user", common.Map{
|
|
||||||
"balance[#]": fmt.Sprintf("balance + %.2f", amount),
|
|
||||||
"updated_time[#]": "NOW()",
|
|
||||||
}, common.Map{
|
|
||||||
"id": toUserId,
|
|
||||||
})
|
|
||||||
|
|
||||||
if affected2 == 0 {
|
|
||||||
fmt.Println("增加余额失败")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录转账日志
|
|
||||||
logId := tx.Insert("transfer_log", common.Map{
|
|
||||||
"from_user_id": fromUserId,
|
|
||||||
"to_user_id": toUserId,
|
|
||||||
"amount": amount,
|
|
||||||
"status": "success",
|
|
||||||
"created_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
|
|
||||||
if logId == 0 {
|
|
||||||
fmt.Println("记录日志失败")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("转账成功: 用户%d -> 用户%d, 金额:%.2f\n", fromUserId, toUserId, amount)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if success {
|
|
||||||
fmt.Println("事务执行成功")
|
|
||||||
} else {
|
|
||||||
fmt.Println("事务回滚")
|
|
||||||
if database.LastErr.GetError() != nil {
|
|
||||||
fmt.Println("错误原因:", database.LastErr.GetError())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 示例9: 缓存机制(修正版)
|
|
||||||
func Example9_CacheSystem_Fixed(database *db.HoTimeDB) {
|
|
||||||
fmt.Println("=== 缓存机制示例 ===")
|
|
||||||
|
|
||||||
// 设置缓存(实际项目中需要配置缓存参数)
|
|
||||||
database.HoTimeCache = &cache.HoTimeCache{}
|
|
||||||
|
|
||||||
// 第一次查询(会缓存结果)
|
|
||||||
fmt.Println("第一次查询(会缓存)...")
|
|
||||||
users1 := database.Select("user", "*", common.Map{
|
|
||||||
"status": 1, // 单条件
|
|
||||||
"LIMIT": 10,
|
|
||||||
})
|
|
||||||
fmt.Printf("查询到 %d 个用户\n", len(users1))
|
|
||||||
|
|
||||||
// 第二次相同查询(从缓存获取)
|
|
||||||
fmt.Println("第二次相同查询(从缓存获取)...")
|
|
||||||
users2 := database.Select("user", "*", common.Map{
|
|
||||||
"status": 1, // 单条件
|
|
||||||
"LIMIT": 10,
|
|
||||||
})
|
|
||||||
fmt.Printf("查询到 %d 个用户\n", len(users2))
|
|
||||||
|
|
||||||
// 更新操作会清除缓存
|
|
||||||
fmt.Println("执行更新操作(会清除缓存)...")
|
|
||||||
affected := database.Update("user", common.Map{
|
|
||||||
"updated_time[#]": "NOW()",
|
|
||||||
}, common.Map{
|
|
||||||
"id": 1, // 单条件
|
|
||||||
})
|
|
||||||
fmt.Printf("更新 %d 条记录\n", affected)
|
|
||||||
|
|
||||||
// 再次查询(重新从数据库获取并缓存)
|
|
||||||
fmt.Println("更新后再次查询(重新缓存)...")
|
|
||||||
users3 := database.Select("user", "*", common.Map{
|
|
||||||
"status": 1, // 单条件
|
|
||||||
"LIMIT": 10,
|
|
||||||
})
|
|
||||||
fmt.Printf("查询到 %d 个用户\n", len(users3))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 示例10: 性能优化技巧(修正版)
|
|
||||||
func Example10_PerformanceOptimization_Fixed(database *db.HoTimeDB) {
|
|
||||||
fmt.Println("=== 性能优化技巧示例 ===")
|
|
||||||
|
|
||||||
// IN查询优化(连续数字自动转为BETWEEN)
|
|
||||||
fmt.Println("IN查询优化示例...")
|
|
||||||
users := database.Select("user", "*", common.Map{
|
|
||||||
"id": []int{1, 2, 3, 4, 5, 10, 11, 12, 13, 20}, // 单个IN条件,会被优化
|
|
||||||
})
|
|
||||||
fmt.Printf("查询到 %d 个用户\n", len(users))
|
|
||||||
fmt.Println("执行的SQL:", database.LastQuery)
|
|
||||||
|
|
||||||
// 批量插入(使用事务)
|
|
||||||
fmt.Println("\n批量插入示例...")
|
|
||||||
success := database.Action(func(tx db.HoTimeDB) bool {
|
|
||||||
for i := 1; i <= 100; i++ {
|
|
||||||
id := tx.Insert("user_batch", common.Map{
|
|
||||||
"name": fmt.Sprintf("批量用户%d", i),
|
|
||||||
"email": fmt.Sprintf("batch%d@example.com", i),
|
|
||||||
"status": 1,
|
|
||||||
"created_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
if id == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每10个用户输出一次进度
|
|
||||||
if i%10 == 0 {
|
|
||||||
fmt.Printf("已插入 %d 个用户\n", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if success {
|
|
||||||
fmt.Println("批量插入完成")
|
|
||||||
} else {
|
|
||||||
fmt.Println("批量插入失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 索引友好的查询(修正版)
|
|
||||||
fmt.Println("\n索引友好的查询...")
|
|
||||||
recentUsers := database.Select("user", "*", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"created_time[>]": "2023-01-01", // 假设created_time有索引
|
|
||||||
"status": 1, // 假设status有索引
|
|
||||||
},
|
|
||||||
"ORDER": "created_time DESC", // 利用索引排序
|
|
||||||
"LIMIT": 20,
|
|
||||||
})
|
|
||||||
fmt.Printf("查询到 %d 个近期用户\n", len(recentUsers))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 完整的应用示例(修正版)
|
|
||||||
func CompleteExample_Fixed() {
|
|
||||||
fmt.Println("=== HoTimeDB完整应用示例(修正版) ===")
|
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
database := &db.HoTimeDB{
|
|
||||||
Prefix: "app_",
|
|
||||||
Mode: 1, // 测试模式
|
|
||||||
Type: "mysql",
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置连接
|
|
||||||
database.SetConnect(func(err ...*common.Error) (master, slave *sql.DB) {
|
|
||||||
// 这里使用实际的数据库连接字符串
|
|
||||||
dsn := "root:password@tcp(localhost:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
|
|
||||||
master, dbErr := sql.Open("mysql", dsn)
|
|
||||||
if dbErr != nil {
|
|
||||||
log.Fatal("数据库连接失败:", dbErr)
|
|
||||||
}
|
|
||||||
return master, master
|
|
||||||
})
|
|
||||||
|
|
||||||
// 用户管理系统示例
|
|
||||||
fmt.Println("\n=== 用户管理系统 ===")
|
|
||||||
|
|
||||||
// 1. 创建用户
|
|
||||||
userId := database.Insert("user", common.Map{
|
|
||||||
"name": "示例用户",
|
|
||||||
"email": "example@test.com",
|
|
||||||
"password": "hashed_password",
|
|
||||||
"age": 28,
|
|
||||||
"status": 1,
|
|
||||||
"level": "normal",
|
|
||||||
"balance": 500.00,
|
|
||||||
"created_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
fmt.Printf("创建用户成功,ID: %d\n", userId)
|
|
||||||
|
|
||||||
// 2. 用户登录更新(多条件用AND)
|
|
||||||
database.Update("user", common.Map{
|
|
||||||
"last_login[#]": "NOW()",
|
|
||||||
"login_count[#]": "login_count + 1",
|
|
||||||
}, common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"id": userId,
|
|
||||||
"status": 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 3. 创建订单
|
|
||||||
orderId := database.Insert("order", common.Map{
|
|
||||||
"user_id": userId,
|
|
||||||
"amount": 299.99,
|
|
||||||
"status": "pending",
|
|
||||||
"created_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
fmt.Printf("创建订单成功,ID: %d\n", orderId)
|
|
||||||
|
|
||||||
// 4. 订单支付(使用事务,修正版)
|
|
||||||
paymentSuccess := database.Action(func(tx db.HoTimeDB) bool {
|
|
||||||
// 更新订单状态(单条件)
|
|
||||||
affected1 := tx.Update("order", common.Map{
|
|
||||||
"status": "paid",
|
|
||||||
"paid_time[#]": "NOW()",
|
|
||||||
}, common.Map{
|
|
||||||
"id": orderId,
|
|
||||||
})
|
|
||||||
|
|
||||||
if affected1 == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扣减用户余额(多条件用AND)
|
|
||||||
affected2 := tx.Update("user", common.Map{
|
|
||||||
"balance[#]": "balance - 299.99",
|
|
||||||
}, common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"id": userId,
|
|
||||||
"balance[>=]": 299.99, // 确保余额足够
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if affected2 == 0 {
|
|
||||||
fmt.Println("余额不足或用户不存在")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录支付日志
|
|
||||||
logId := tx.Insert("payment_log", common.Map{
|
|
||||||
"user_id": userId,
|
|
||||||
"order_id": orderId,
|
|
||||||
"amount": 299.99,
|
|
||||||
"type": "order_payment",
|
|
||||||
"status": "success",
|
|
||||||
"created_time[#]": "NOW()",
|
|
||||||
})
|
|
||||||
|
|
||||||
return logId > 0
|
|
||||||
})
|
|
||||||
|
|
||||||
if paymentSuccess {
|
|
||||||
fmt.Println("订单支付成功")
|
|
||||||
} else {
|
|
||||||
fmt.Println("订单支付失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 查询用户订单列表(链式查询)
|
|
||||||
userOrders := database.Table("order").
|
|
||||||
LeftJoin("user", "order.user_id = user.id").
|
|
||||||
Where("order.user_id", userId). // 链式中单个条件可以直接Where
|
|
||||||
Order("order.created_time DESC").
|
|
||||||
Select(`
|
|
||||||
order.id,
|
|
||||||
order.amount,
|
|
||||||
order.status,
|
|
||||||
order.created_time,
|
|
||||||
order.paid_time,
|
|
||||||
user.name as user_name
|
|
||||||
`)
|
|
||||||
|
|
||||||
fmt.Printf("\n用户订单列表 (%d个订单):\n", len(userOrders))
|
|
||||||
for _, order := range userOrders {
|
|
||||||
fmt.Printf(" 订单ID:%v, 金额:%v, 状态:%s, 创建时间:%v\n",
|
|
||||||
order.Get("id"),
|
|
||||||
order.Get("amount"),
|
|
||||||
order.GetString("status"),
|
|
||||||
order.Get("created_time"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 生成统计报表(修正版)
|
|
||||||
stats := database.Select("order",
|
|
||||||
common.Slice{
|
|
||||||
common.Map{"[>]user": "order.user_id = user.id"},
|
|
||||||
},
|
|
||||||
`
|
|
||||||
DATE(order.created_time) as date,
|
|
||||||
COUNT(order.id) as order_count,
|
|
||||||
SUM(order.amount) as total_amount,
|
|
||||||
AVG(order.amount) as avg_amount
|
|
||||||
`,
|
|
||||||
common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"order.status": "paid",
|
|
||||||
"order.created_time[>]": "2023-01-01",
|
|
||||||
},
|
|
||||||
"GROUP": "DATE(order.created_time)",
|
|
||||||
"ORDER": "date DESC",
|
|
||||||
"LIMIT": 30,
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Printf("\n最近30天订单统计:\n")
|
|
||||||
for _, stat := range stats {
|
|
||||||
fmt.Printf("日期:%v, 订单数:%v, 总金额:%v, 平均金额:%v\n",
|
|
||||||
stat.Get("date"),
|
|
||||||
stat.Get("order_count"),
|
|
||||||
stat.Get("total_amount"),
|
|
||||||
stat.Get("avg_amount"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("\n示例执行完成!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 语法对比示例
|
|
||||||
func SyntaxComparison() {
|
|
||||||
fmt.Println("=== HoTimeDB语法对比 ===")
|
|
||||||
|
|
||||||
// 模拟数据库对象
|
|
||||||
var db *db.HoTimeDB
|
|
||||||
|
|
||||||
fmt.Println("❌ 错误语法示例(不支持):")
|
|
||||||
fmt.Println(`
|
|
||||||
// 这样写是错误的,多个条件不能直接放在根Map中
|
|
||||||
wrongUsers := db.Select("user", "*", common.Map{
|
|
||||||
"status": 1, // ❌ 错误
|
|
||||||
"age[>]": 18, // ❌ 错误
|
|
||||||
"ORDER": "id DESC",
|
|
||||||
})
|
|
||||||
`)
|
|
||||||
|
|
||||||
fmt.Println("✅ 正确语法示例:")
|
|
||||||
fmt.Println(`
|
|
||||||
// 单个条件可以直接写
|
|
||||||
correctUsers1 := db.Select("user", "*", common.Map{
|
|
||||||
"status": 1, // ✅ 正确,单个条件
|
|
||||||
})
|
|
||||||
|
|
||||||
// 多个条件必须用AND包装
|
|
||||||
correctUsers2 := db.Select("user", "*", common.Map{
|
|
||||||
"AND": common.Map{ // ✅ 正确,多个条件用AND包装
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
},
|
|
||||||
"ORDER": "id DESC", // ✅ 正确,特殊参数与条件同级
|
|
||||||
})
|
|
||||||
|
|
||||||
// OR条件
|
|
||||||
correctUsers3 := db.Select("user", "*", common.Map{
|
|
||||||
"OR": common.Map{ // ✅ 正确,OR条件
|
|
||||||
"level": "vip",
|
|
||||||
"balance[>]": 1000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 嵌套条件
|
|
||||||
correctUsers4 := db.Select("user", "*", common.Map{
|
|
||||||
"AND": common.Map{ // ✅ 正确,嵌套条件
|
|
||||||
"status": 1,
|
|
||||||
"OR": common.Map{
|
|
||||||
"age[<]": 30,
|
|
||||||
"level": "vip",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"ORDER": "created_time DESC",
|
|
||||||
"LIMIT": 20,
|
|
||||||
})
|
|
||||||
`)
|
|
||||||
|
|
||||||
// 实际不执行查询,只是展示语法
|
|
||||||
_ = db
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertsExample 批量插入示例
|
|
||||||
func InsertsExample(database *db.HoTimeDB) {
|
|
||||||
fmt.Println("\n=== 批量插入示例 ===")
|
|
||||||
|
|
||||||
// 批量插入用户(使用 []Map 格式)
|
|
||||||
affected := database.Inserts("user", []common.Map{
|
|
||||||
{"name": "批量用户1", "email": "batch1@example.com", "age": 25, "status": 1},
|
|
||||||
{"name": "批量用户2", "email": "batch2@example.com", "age": 30, "status": 1},
|
|
||||||
{"name": "批量用户3", "email": "batch3@example.com", "age": 28, "status": 1},
|
|
||||||
})
|
|
||||||
fmt.Printf("批量插入了 %d 条用户记录\n", affected)
|
|
||||||
|
|
||||||
// 批量插入日志(使用 [#] 标记直接 SQL)
|
|
||||||
logAffected := database.Inserts("log", []common.Map{
|
|
||||||
{"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": 3, "action": "view", "ip": "192.168.1.3", "created_time[#]": "NOW()"},
|
|
||||||
})
|
|
||||||
fmt.Printf("批量插入了 %d 条日志记录\n", logAffected)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpsertExample Upsert(插入或更新)示例
|
|
||||||
func UpsertExample(database *db.HoTimeDB) {
|
|
||||||
fmt.Println("\n=== Upsert示例 ===")
|
|
||||||
|
|
||||||
// Upsert 用户数据(使用 Slice 格式)
|
|
||||||
// 如果 id 存在则更新,不存在则插入
|
|
||||||
affected := database.Upsert("user",
|
|
||||||
common.Map{
|
|
||||||
"id": 1,
|
|
||||||
"name": "张三更新",
|
|
||||||
"email": "zhang_updated@example.com",
|
|
||||||
"age": 26,
|
|
||||||
},
|
|
||||||
common.Slice{"id"}, // 唯一键
|
|
||||||
common.Slice{"name", "email", "age"}, // 冲突时更新的字段
|
|
||||||
)
|
|
||||||
fmt.Printf("Upsert 影响了 %d 行\n", affected)
|
|
||||||
|
|
||||||
// Upsert 使用 [#] 直接 SQL
|
|
||||||
affected2 := database.Upsert("user_stats",
|
|
||||||
common.Map{
|
|
||||||
"user_id": 1,
|
|
||||||
"login_count[#]": "login_count + 1",
|
|
||||||
"last_login[#]": "NOW()",
|
|
||||||
},
|
|
||||||
common.Slice{"user_id"},
|
|
||||||
common.Slice{"login_count", "last_login"},
|
|
||||||
)
|
|
||||||
fmt.Printf("统计 Upsert 影响了 %d 行\n", affected2)
|
|
||||||
|
|
||||||
// 也支持可变参数形式
|
|
||||||
affected3 := database.Upsert("user",
|
|
||||||
common.Map{"id": 2, "name": "李四", "status": 1},
|
|
||||||
common.Slice{"id"},
|
|
||||||
"name", "status", // 可变参数
|
|
||||||
)
|
|
||||||
fmt.Printf("可变参数 Upsert 影响了 %d 行\n", affected3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 运行所有修正后的示例
|
|
||||||
func RunAllFixedExamples() {
|
|
||||||
fmt.Println("开始运行HoTimeDB所有修正后的示例...")
|
|
||||||
fmt.Println("注意:实际运行时需要确保数据库连接正确,并且相关表存在")
|
|
||||||
|
|
||||||
// 展示语法对比
|
|
||||||
SyntaxComparison()
|
|
||||||
|
|
||||||
fmt.Println("请根据实际环境配置数据库连接后运行相应示例")
|
|
||||||
fmt.Println("所有示例代码已修正完毕,语法正确!")
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("新增功能示例说明:")
|
|
||||||
fmt.Println(" - InsertsExample(db): 批量插入示例,使用 []Map 格式")
|
|
||||||
fmt.Println(" - UpsertExample(db): 插入或更新示例")
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// 运行语法对比示例
|
|
||||||
RunAllFixedExamples()
|
|
||||||
}
|
|
||||||
1
go.mod
1
go.mod
@ -12,6 +12,5 @@ require (
|
|||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.364
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.364
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ocr v1.0.364
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ocr v1.0.364
|
||||||
go.mongodb.org/mongo-driver v1.10.1
|
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
||||||
)
|
)
|
||||||
|
|||||||
30
go.sum
30
go.sum
@ -14,16 +14,10 @@ github.com/go-pay/gopay v1.5.78 h1:wIHp8g/jK0ik5bZo2MWt3jAQsktT3nkdXZxlRZvljko=
|
|||||||
github.com/go-pay/gopay v1.5.78/go.mod h1:M6Nlk2VdZHCbWphOw3rtbnz4SiOk6Xvxg6mxwDfg+Ps=
|
github.com/go-pay/gopay v1.5.78/go.mod h1:M6Nlk2VdZHCbWphOw3rtbnz4SiOk6Xvxg6mxwDfg+Ps=
|
||||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
|
||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/gomodule/redigo v1.8.5 h1:nRAxCa+SVsyjSBrtZmG/cqb6VbTmuRzpg/PoTFlpumc=
|
github.com/gomodule/redigo v1.8.5 h1:nRAxCa+SVsyjSBrtZmG/cqb6VbTmuRzpg/PoTFlpumc=
|
||||||
github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
|
github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||||
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
|
|
||||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
@ -31,12 +25,8 @@ github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ
|
|||||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
|
|
||||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
|
||||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/silenceper/wechat/v2 v2.1.2 h1:+QfIMiYfwST2ZloTwmYp0O0p5Y1LYRZxfLWfMuSE30k=
|
github.com/silenceper/wechat/v2 v2.1.2 h1:+QfIMiYfwST2ZloTwmYp0O0p5Y1LYRZxfLWfMuSE30k=
|
||||||
@ -49,36 +39,20 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.364 h1:X1Jws4XqrTH+p7FBQ7BpjW4qFXObKHWm0/XhW/GvqRs=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.364 h1:X1Jws4XqrTH+p7FBQ7BpjW4qFXObKHWm0/XhW/GvqRs=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.364/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.364/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ocr v1.0.364 h1:kbor60vo37v7Hu+i17gooox9Rw281fVHNna8zwtDG1w=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ocr v1.0.364 h1:kbor60vo37v7Hu+i17gooox9Rw281fVHNna8zwtDG1w=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ocr v1.0.364/go.mod h1:LeIUBOLhc+Y5YCEpZrULPD9lgoXXV4/EmIcoEvmHz9c=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ocr v1.0.364/go.mod h1:LeIUBOLhc+Y5YCEpZrULPD9lgoXXV4/EmIcoEvmHz9c=
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
|
||||||
github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
|
|
||||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
|
||||||
github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
|
|
||||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
|
||||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
|
|
||||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
|
||||||
go.mongodb.org/mongo-driver v1.10.1 h1:NujsPveKwHaWuKUer/ceo9DzEe7HIj1SlJ6uvXZG0S4=
|
|
||||||
go.mongodb.org/mongo-driver v1.10.1/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
|
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
|
||||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@ -92,16 +66,12 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
|
|||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0=
|
gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0=
|
||||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|||||||
115
log/logrus.go
115
log/logrus.go
@ -2,13 +2,12 @@ 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 {
|
||||||
@ -74,98 +73,38 @@ func (that *MyHook) Fire(entry *log.Entry) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最大框架层数限制 - 超过这个层数后不再跳过,防止误过滤应用层
|
// 对caller进行递归查询, 直到找到非logrus包产生的第一个调用.
|
||||||
const maxFrameworkDepth = 10
|
// 因为filename我获取到了上层目录名, 因此所有logrus包的调用的文件名都是 logrus/...
|
||||||
|
// 因此通过排除logrus开头的文件名, 就可以排除所有logrus包的自己的函数调用
|
||||||
// isHoTimeFrameworkFile 判断是否是 HoTime 框架文件
|
|
||||||
// 更精确的匹配:只有明确属于框架的文件才会被跳过
|
|
||||||
func isHoTimeFrameworkFile(file string) bool {
|
|
||||||
// 1. logrus 日志库内部文件
|
|
||||||
if strings.HasPrefix(file, "logrus/") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Go 运行时文件
|
|
||||||
if strings.HasPrefix(file, "runtime/") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 框架核心文件(在 hotime 根目录下的 .go 文件)
|
|
||||||
if strings.HasSuffix(file, "application.go") ||
|
|
||||||
strings.HasSuffix(file, "context.go") ||
|
|
||||||
strings.HasSuffix(file, "session.go") ||
|
|
||||||
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 {
|
func findCaller(skip int) string {
|
||||||
frameworkCount := 0 // 连续框架层计数
|
file := ""
|
||||||
|
line := 0
|
||||||
// 遍历调用栈,找到第一个非框架文件
|
for i := 0; i < 10; i++ {
|
||||||
for i := 0; i < 20; i++ {
|
file, line = getCaller(skip + i)
|
||||||
file, line := getCaller(skip + i)
|
if !strings.HasPrefix(file, "logrus") {
|
||||||
if file == "" {
|
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)
|
||||||
|
}
|
||||||
|
if file == "code/makecode.go" {
|
||||||
|
file, line = getCaller(skip + i + j)
|
||||||
|
}
|
||||||
|
if strings.Contains(file, "application.go") {
|
||||||
|
file, line = getCaller(skip + i + j)
|
||||||
|
}
|
||||||
|
if j == 5 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if isHoTimeFrameworkFile(file) {
|
|
||||||
frameworkCount++
|
|
||||||
// 层数限制:如果已经跳过太多层,停止跳过
|
|
||||||
if frameworkCount >= maxFrameworkDepth {
|
|
||||||
return fmt.Sprintf("%s:%d", file, line)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到非框架文件,返回应用层代码位置
|
break
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
51
session.go
51
session.go
@ -3,7 +3,6 @@ package hotime
|
|||||||
import (
|
import (
|
||||||
. "code.hoteas.com/golang/hotime/cache"
|
. "code.hoteas.com/golang/hotime/cache"
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//session对象
|
//session对象
|
||||||
@ -12,72 +11,46 @@ type SessionIns struct {
|
|||||||
SessionId string
|
SessionId string
|
||||||
Map
|
Map
|
||||||
ContextBase
|
ContextBase
|
||||||
mutex sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set 保存 session 到缓存,必须在锁内调用或传入深拷贝的 map
|
func (that *SessionIns) set() {
|
||||||
func (that *SessionIns) setWithCopy() {
|
that.HoTimeCache.Session(HEAD_SESSION_ADD+that.SessionId, that.Map)
|
||||||
// 深拷贝 Map 防止并发修改
|
|
||||||
that.mutex.RLock()
|
|
||||||
copyMap := make(Map, len(that.Map))
|
|
||||||
for k, v := range that.Map {
|
|
||||||
copyMap[k] = v
|
|
||||||
}
|
|
||||||
that.mutex.RUnlock()
|
|
||||||
|
|
||||||
that.HoTimeCache.Session(HEAD_SESSION_ADD+that.SessionId, copyMap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (that *SessionIns) Session(key string, data ...interface{}) *Obj {
|
func (that *SessionIns) Session(key string, data ...interface{}) *Obj {
|
||||||
that.mutex.Lock()
|
|
||||||
if that.Map == nil {
|
if that.Map == nil {
|
||||||
that.getWithoutLock()
|
that.get()
|
||||||
}
|
}
|
||||||
that.mutex.Unlock()
|
|
||||||
|
|
||||||
if len(data) != 0 {
|
if len(data) != 0 {
|
||||||
that.mutex.Lock()
|
|
||||||
if data[0] == nil {
|
if data[0] == nil {
|
||||||
delete(that.Map, key)
|
delete(that.Map, key)
|
||||||
|
that.set()
|
||||||
} else {
|
} else {
|
||||||
that.Map[key] = data[0]
|
that.Map[key] = data[0]
|
||||||
|
that.set()
|
||||||
}
|
}
|
||||||
that.mutex.Unlock()
|
|
||||||
|
|
||||||
// 使用深拷贝版本保存,避免并发问题
|
|
||||||
that.setWithCopy()
|
|
||||||
return &Obj{Data: nil}
|
return &Obj{Data: nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
that.mutex.RLock()
|
return &Obj{Data: that.Map.Get(key)}
|
||||||
result := &Obj{Data: that.Map.Get(key)}
|
|
||||||
that.mutex.RUnlock()
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWithoutLock 内部使用,调用前需要已持有锁
|
func (that *SessionIns) get() {
|
||||||
func (that *SessionIns) getWithoutLock() {
|
|
||||||
that.Map = that.HoTimeCache.Session(HEAD_SESSION_ADD + that.SessionId).ToMap()
|
that.Map = that.HoTimeCache.Session(HEAD_SESSION_ADD + that.SessionId).ToMap()
|
||||||
if that.Map != nil {
|
if that.Map != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
that.Map = Map{}
|
that.Map = Map{}
|
||||||
// 保存时也需要深拷贝
|
that.HoTimeCache.Session(HEAD_SESSION_ADD+that.SessionId, that.Map)
|
||||||
copyMap := make(Map, len(that.Map))
|
|
||||||
for k, v := range that.Map {
|
|
||||||
copyMap[k] = v
|
|
||||||
}
|
|
||||||
that.HoTimeCache.Session(HEAD_SESSION_ADD+that.SessionId, copyMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (that *SessionIns) get() {
|
return
|
||||||
that.mutex.Lock()
|
|
||||||
defer that.mutex.Unlock()
|
|
||||||
that.getWithoutLock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (that *SessionIns) Init(cache *HoTimeCache) {
|
func (that *SessionIns) Init(cache *HoTimeCache) {
|
||||||
that.mutex = sync.RWMutex{}
|
|
||||||
that.HoTimeCache = cache
|
that.HoTimeCache = cache
|
||||||
}
|
}
|
||||||
|
|||||||
1
vendor/github.com/360EntSecGroup-Skylar/excelize/.gitignore
generated
vendored
1
vendor/github.com/360EntSecGroup-Skylar/excelize/.gitignore
generated
vendored
@ -1 +0,0 @@
|
|||||||
test/Test*.xlsx
|
|
||||||
26
vendor/github.com/360EntSecGroup-Skylar/excelize/.travis.yml
generated
vendored
26
vendor/github.com/360EntSecGroup-Skylar/excelize/.travis.yml
generated
vendored
@ -1,26 +0,0 @@
|
|||||||
language: go
|
|
||||||
|
|
||||||
install:
|
|
||||||
- go get -d -t -v ./... && go build -v ./...
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.8.x
|
|
||||||
- 1.9.x
|
|
||||||
- 1.10.x
|
|
||||||
- 1.11.x
|
|
||||||
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
- osx
|
|
||||||
|
|
||||||
env:
|
|
||||||
matrix:
|
|
||||||
- GOARCH=amd64
|
|
||||||
- GOARCH=386
|
|
||||||
|
|
||||||
script:
|
|
||||||
- go vet ./...
|
|
||||||
- go test ./... -v -coverprofile=coverage.txt -covermode=atomic
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
|
||||||
46
vendor/github.com/360EntSecGroup-Skylar/excelize/CODE_OF_CONDUCT.md
generated
vendored
46
vendor/github.com/360EntSecGroup-Skylar/excelize/CODE_OF_CONDUCT.md
generated
vendored
@ -1,46 +0,0 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
||||||
|
|
||||||
## Our Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [xuri.me](https://xuri.me). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
|
||||||
|
|
||||||
[homepage]: http://contributor-covenant.org
|
|
||||||
[version]: http://contributor-covenant.org/version/1/4/
|
|
||||||
464
vendor/github.com/360EntSecGroup-Skylar/excelize/CONTRIBUTING.md
generated
vendored
464
vendor/github.com/360EntSecGroup-Skylar/excelize/CONTRIBUTING.md
generated
vendored
@ -1,464 +0,0 @@
|
|||||||
<!-- use this template to generate the contributor docs with the following command: `$ lingo run docs --template CONTRIBUTING_TEMPLATE.md --output CONTRIBUTING.md` -->
|
|
||||||
# Contributing to excelize
|
|
||||||
|
|
||||||
Want to hack on excelize? Awesome! This page contains information about reporting issues as well as some tips and
|
|
||||||
guidelines useful to experienced open source contributors. Finally, make sure
|
|
||||||
you read our [community guidelines](#community-guidelines) before you
|
|
||||||
start participating.
|
|
||||||
|
|
||||||
## Topics
|
|
||||||
|
|
||||||
* [Reporting Security Issues](#reporting-security-issues)
|
|
||||||
* [Design and Cleanup Proposals](#design-and-cleanup-proposals)
|
|
||||||
* [Reporting Issues](#reporting-other-issues)
|
|
||||||
* [Quick Contribution Tips and Guidelines](#quick-contribution-tips-and-guidelines)
|
|
||||||
* [Community Guidelines](#community-guidelines)
|
|
||||||
|
|
||||||
## Reporting security issues
|
|
||||||
|
|
||||||
The excelize maintainers take security seriously. If you discover a security
|
|
||||||
issue, please bring it to their attention right away!
|
|
||||||
|
|
||||||
Please **DO NOT** file a public issue, instead send your report privately to
|
|
||||||
[xuri.me](https://xuri.me).
|
|
||||||
|
|
||||||
Security reports are greatly appreciated and we will publicly thank you for it.
|
|
||||||
We currently do not offer a paid security bounty program, but are not
|
|
||||||
ruling it out in the future.
|
|
||||||
|
|
||||||
## Reporting other issues
|
|
||||||
|
|
||||||
A great way to contribute to the project is to send a detailed report when you
|
|
||||||
encounter an issue. We always appreciate a well-written, thorough bug report,
|
|
||||||
and will thank you for it!
|
|
||||||
|
|
||||||
Check that [our issue database](https://github.com/360EntSecGroup-Skylar/excelize/issues)
|
|
||||||
doesn't already include that problem or suggestion before submitting an issue.
|
|
||||||
If you find a match, you can use the "subscribe" button to get notified on
|
|
||||||
updates. Do *not* leave random "+1" or "I have this too" comments, as they
|
|
||||||
only clutter the discussion, and don't help resolving it. However, if you
|
|
||||||
have ways to reproduce the issue or have additional information that may help
|
|
||||||
resolving the issue, please leave a comment.
|
|
||||||
|
|
||||||
When reporting issues, always include the output of `go env`.
|
|
||||||
|
|
||||||
Also include the steps required to reproduce the problem if possible and
|
|
||||||
applicable. This information will help us review and fix your issue faster.
|
|
||||||
When sending lengthy log-files, consider posting them as a gist [https://gist.github.com](https://gist.github.com).
|
|
||||||
Don't forget to remove sensitive data from your logfiles before posting (you can
|
|
||||||
replace those parts with "REDACTED").
|
|
||||||
|
|
||||||
## Quick contribution tips and guidelines
|
|
||||||
|
|
||||||
This section gives the experienced contributor some tips and guidelines.
|
|
||||||
|
|
||||||
### Pull requests are always welcome
|
|
||||||
|
|
||||||
Not sure if that typo is worth a pull request? Found a bug and know how to fix
|
|
||||||
it? Do it! We will appreciate it. Any significant improvement should be
|
|
||||||
documented as [a GitHub issue](https://github.com/360EntSecGroup-Skylar/excelize/issues) before
|
|
||||||
anybody starts working on it.
|
|
||||||
|
|
||||||
We are always thrilled to receive pull requests. We do our best to process them
|
|
||||||
quickly. If your pull request is not accepted on the first try,
|
|
||||||
don't get discouraged!
|
|
||||||
|
|
||||||
### Design and cleanup proposals
|
|
||||||
|
|
||||||
You can propose new designs for existing excelize features. You can also design
|
|
||||||
entirely new features. We really appreciate contributors who want to refactor or
|
|
||||||
otherwise cleanup our project.
|
|
||||||
|
|
||||||
We try hard to keep excelize lean and focused. Excelize can't do everything for
|
|
||||||
everybody. This means that we might decide against incorporating a new feature.
|
|
||||||
However, there might be a way to implement that feature *on top of* excelize.
|
|
||||||
|
|
||||||
### Conventions
|
|
||||||
|
|
||||||
Fork the repository and make changes on your fork in a feature branch:
|
|
||||||
|
|
||||||
* If it's a bug fix branch, name it XXXX-something where XXXX is the number of
|
|
||||||
the issue.
|
|
||||||
* If it's a feature branch, create an enhancement issue to announce
|
|
||||||
your intentions, and name it XXXX-something where XXXX is the number of the
|
|
||||||
issue.
|
|
||||||
|
|
||||||
Submit unit tests for your changes. Go has a great test framework built in; use
|
|
||||||
it! Take a look at existing tests for inspiration. Run the full test on your branch before
|
|
||||||
submitting a pull request.
|
|
||||||
|
|
||||||
Update the documentation when creating or modifying features. Test your
|
|
||||||
documentation changes for clarity, concision, and correctness, as well as a
|
|
||||||
clean documentation build.
|
|
||||||
|
|
||||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
|
||||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
|
||||||
committing your changes. Most editors have plug-ins that do this automatically.
|
|
||||||
|
|
||||||
Pull request descriptions should be as clear as possible and include a reference
|
|
||||||
to all the issues that they address.
|
|
||||||
|
|
||||||
### Successful Changes
|
|
||||||
|
|
||||||
Before contributing large or high impact changes, make the effort to coordinate
|
|
||||||
with the maintainers of the project before submitting a pull request. This
|
|
||||||
prevents you from doing extra work that may or may not be merged.
|
|
||||||
|
|
||||||
Large PRs that are just submitted without any prior communication are unlikely
|
|
||||||
to be successful.
|
|
||||||
|
|
||||||
While pull requests are the methodology for submitting changes to code, changes
|
|
||||||
are much more likely to be accepted if they are accompanied by additional
|
|
||||||
engineering work. While we don't define this explicitly, most of these goals
|
|
||||||
are accomplished through communication of the design goals and subsequent
|
|
||||||
solutions. Often times, it helps to first state the problem before presenting
|
|
||||||
solutions.
|
|
||||||
|
|
||||||
Typically, the best methods of accomplishing this are to submit an issue,
|
|
||||||
stating the problem. This issue can include a problem statement and a
|
|
||||||
checklist with requirements. If solutions are proposed, alternatives should be
|
|
||||||
listed and eliminated. Even if the criteria for elimination of a solution is
|
|
||||||
frivolous, say so.
|
|
||||||
|
|
||||||
Larger changes typically work best with design documents. These are focused on
|
|
||||||
providing context to the design at the time the feature was conceived and can
|
|
||||||
inform future documentation contributions.
|
|
||||||
|
|
||||||
### Commit Messages
|
|
||||||
|
|
||||||
Commit messages must start with a capitalized and short summary
|
|
||||||
written in the imperative, followed by an optional, more detailed explanatory
|
|
||||||
text which is separated from the summary by an empty line.
|
|
||||||
|
|
||||||
Commit messages should follow best practices, including explaining the context
|
|
||||||
of the problem and how it was solved, including in caveats or follow up changes
|
|
||||||
required. They should tell the story of the change and provide readers
|
|
||||||
understanding of what led to it.
|
|
||||||
|
|
||||||
In practice, the best approach to maintaining a nice commit message is to
|
|
||||||
leverage a `git add -p` and `git commit --amend` to formulate a solid
|
|
||||||
changeset. This allows one to piece together a change, as information becomes
|
|
||||||
available.
|
|
||||||
|
|
||||||
If you squash a series of commits, don't just submit that. Re-write the commit
|
|
||||||
message, as if the series of commits was a single stroke of brilliance.
|
|
||||||
|
|
||||||
That said, there is no requirement to have a single commit for a PR, as long as
|
|
||||||
each commit tells the story. For example, if there is a feature that requires a
|
|
||||||
package, it might make sense to have the package in a separate commit then have
|
|
||||||
a subsequent commit that uses it.
|
|
||||||
|
|
||||||
Remember, you're telling part of the story with the commit message. Don't make
|
|
||||||
your chapter weird.
|
|
||||||
|
|
||||||
### Review
|
|
||||||
|
|
||||||
Code review comments may be added to your pull request. Discuss, then make the
|
|
||||||
suggested modifications and push additional commits to your feature branch. Post
|
|
||||||
a comment after pushing. New commits show up in the pull request automatically,
|
|
||||||
but the reviewers are notified only when you comment.
|
|
||||||
|
|
||||||
Pull requests must be cleanly rebased on top of master without multiple branches
|
|
||||||
mixed into the PR.
|
|
||||||
|
|
||||||
**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
|
|
||||||
feature branch to update your pull request rather than `merge master`.
|
|
||||||
|
|
||||||
Before you make a pull request, squash your commits into logical units of work
|
|
||||||
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
|
|
||||||
set of patches that should be reviewed together: for example, upgrading the
|
|
||||||
version of a vendored dependency and taking advantage of its now available new
|
|
||||||
feature constitute two separate units of work. Implementing a new function and
|
|
||||||
calling it in another file constitute a single logical unit of work. The very
|
|
||||||
high majority of submissions should have a single commit, so if in doubt: squash
|
|
||||||
down to one.
|
|
||||||
|
|
||||||
After every commit, make sure the test passes. Include documentation
|
|
||||||
changes in the same pull request so that a revert would remove all traces of
|
|
||||||
the feature or fix.
|
|
||||||
|
|
||||||
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in commits that
|
|
||||||
close an issue. Including references automatically closes the issue on a merge.
|
|
||||||
|
|
||||||
Please see the [Coding Style](#coding-style) for further guidelines.
|
|
||||||
|
|
||||||
### Merge approval
|
|
||||||
|
|
||||||
The excelize maintainers use LGTM (Looks Good To Me) in comments on the code review to
|
|
||||||
indicate acceptance.
|
|
||||||
|
|
||||||
### Sign your work
|
|
||||||
|
|
||||||
The sign-off is a simple line at the end of the explanation for the patch. Your
|
|
||||||
signature certifies that you wrote the patch or otherwise have the right to pass
|
|
||||||
it on as an open-source patch. The rules are pretty simple: if you can certify
|
|
||||||
the below (from [developercertificate.org](http://developercertificate.org/)):
|
|
||||||
|
|
||||||
```text
|
|
||||||
Developer Certificate of Origin
|
|
||||||
Version 1.1
|
|
||||||
|
|
||||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
|
||||||
1 Letterman Drive
|
|
||||||
Suite D4700
|
|
||||||
San Francisco, CA, 94129
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Developer's Certificate of Origin 1.1
|
|
||||||
|
|
||||||
By making a contribution to this project, I certify that:
|
|
||||||
|
|
||||||
(a) The contribution was created in whole or in part by me and I
|
|
||||||
have the right to submit it under the open source license
|
|
||||||
indicated in the file; or
|
|
||||||
|
|
||||||
(b) The contribution is based upon previous work that, to the best
|
|
||||||
of my knowledge, is covered under an appropriate open source
|
|
||||||
license and I have the right under that license to submit that
|
|
||||||
work with modifications, whether created in whole or in part
|
|
||||||
by me, under the same open source license (unless I am
|
|
||||||
permitted to submit under a different license), as indicated
|
|
||||||
in the file; or
|
|
||||||
|
|
||||||
(c) The contribution was provided directly to me by some other
|
|
||||||
person who certified (a), (b) or (c) and I have not modified
|
|
||||||
it.
|
|
||||||
|
|
||||||
(d) I understand and agree that this project and the contribution
|
|
||||||
are public and that a record of the contribution (including all
|
|
||||||
personal information I submit with it, including my sign-off) is
|
|
||||||
maintained indefinitely and may be redistributed consistent with
|
|
||||||
this project or the open source license(s) involved.
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you just add a line to every git commit message:
|
|
||||||
|
|
||||||
Signed-off-by: Ri Xu https://xuri.me
|
|
||||||
|
|
||||||
Use your real name (sorry, no pseudonyms or anonymous contributions.)
|
|
||||||
|
|
||||||
If you set your `user.name` and `user.email` git configs, you can sign your
|
|
||||||
commit automatically with `git commit -s`.
|
|
||||||
|
|
||||||
### How can I become a maintainer
|
|
||||||
|
|
||||||
First, all maintainers have 3 things
|
|
||||||
|
|
||||||
* They share responsibility in the project's success.
|
|
||||||
* They have made a long-term, recurring time investment to improve the project.
|
|
||||||
* They spend that time doing whatever needs to be done, not necessarily what
|
|
||||||
is the most interesting or fun.
|
|
||||||
|
|
||||||
Maintainers are often under-appreciated, because their work is harder to appreciate.
|
|
||||||
It's easy to appreciate a really cool and technically advanced feature. It's harder
|
|
||||||
to appreciate the absence of bugs, the slow but steady improvement in stability,
|
|
||||||
or the reliability of a release process. But those things distinguish a good
|
|
||||||
project from a great one.
|
|
||||||
|
|
||||||
Don't forget: being a maintainer is a time investment. Make sure you
|
|
||||||
will have time to make yourself available. You don't have to be a
|
|
||||||
maintainer to make a difference on the project!
|
|
||||||
|
|
||||||
If you want to become a meintainer, contact [xuri.me](https://xuri.me) and given a introduction of you.
|
|
||||||
|
|
||||||
## Community guidelines
|
|
||||||
|
|
||||||
We want to keep the community awesome, growing and collaborative. We need
|
|
||||||
your help to keep it that way. To help with this we've come up with some general
|
|
||||||
guidelines for the community as a whole:
|
|
||||||
|
|
||||||
* Be nice: Be courteous, respectful and polite to fellow community members:
|
|
||||||
no regional, racial, gender, or other abuse will be tolerated. We like
|
|
||||||
nice people way better than mean ones!
|
|
||||||
|
|
||||||
* Encourage diversity and participation: Make everyone in our community feel
|
|
||||||
welcome, regardless of their background and the extent of their
|
|
||||||
contributions, and do everything possible to encourage participation in
|
|
||||||
our community.
|
|
||||||
|
|
||||||
* Keep it legal: Basically, don't get us in trouble. Share only content that
|
|
||||||
you own, do not share private or sensitive information, and don't break
|
|
||||||
the law.
|
|
||||||
|
|
||||||
* Stay on topic: Make sure that you are posting to the correct channel and
|
|
||||||
avoid off-topic discussions. Remember when you update an issue or respond
|
|
||||||
to an email you are potentially sending to a large number of people. Please
|
|
||||||
consider this before you update. Also remember that nobody likes spam.
|
|
||||||
|
|
||||||
* Don't send email to the maintainers: There's no need to send email to the
|
|
||||||
maintainers to ask them to investigate an issue or to take a look at a
|
|
||||||
pull request. Instead of sending an email, GitHub mentions should be
|
|
||||||
used to ping maintainers to review a pull request, a proposal or an
|
|
||||||
issue.
|
|
||||||
|
|
||||||
### Guideline violations — 3 strikes method
|
|
||||||
|
|
||||||
The point of this section is not to find opportunities to punish people, but we
|
|
||||||
do need a fair way to deal with people who are making our community suck.
|
|
||||||
|
|
||||||
1. First occurrence: We'll give you a friendly, but public reminder that the
|
|
||||||
behavior is inappropriate according to our guidelines.
|
|
||||||
|
|
||||||
2. Second occurrence: We will send you a private message with a warning that
|
|
||||||
any additional violations will result in removal from the community.
|
|
||||||
|
|
||||||
3. Third occurrence: Depending on the violation, we may need to delete or ban
|
|
||||||
your account.
|
|
||||||
|
|
||||||
**Notes:**
|
|
||||||
|
|
||||||
* Obvious spammers are banned on first occurrence. If we don't do this, we'll
|
|
||||||
have spam all over the place.
|
|
||||||
|
|
||||||
* Violations are forgiven after 6 months of good behavior, and we won't hold a
|
|
||||||
grudge.
|
|
||||||
|
|
||||||
* People who commit minor infractions will get some education, rather than
|
|
||||||
hammering them in the 3 strikes process.
|
|
||||||
|
|
||||||
* The rules apply equally to everyone in the community, no matter how much
|
|
||||||
you've contributed.
|
|
||||||
|
|
||||||
* Extreme violations of a threatening, abusive, destructive or illegal nature
|
|
||||||
will be addressed immediately and are not subject to 3 strikes or forgiveness.
|
|
||||||
|
|
||||||
* Contact [xuri.me](https://xuri.me) to report abuse or appeal violations. In the case of
|
|
||||||
appeals, we know that mistakes happen, and we'll work with you to come up with a
|
|
||||||
fair solution if there has been a misunderstanding.
|
|
||||||
|
|
||||||
## Coding Style
|
|
||||||
|
|
||||||
Unless explicitly stated, we follow all coding guidelines from the Go
|
|
||||||
community. While some of these standards may seem arbitrary, they somehow seem
|
|
||||||
to result in a solid, consistent codebase.
|
|
||||||
|
|
||||||
It is possible that the code base does not currently comply with these
|
|
||||||
guidelines. We are not looking for a massive PR that fixes this, since that
|
|
||||||
goes against the spirit of the guidelines. All new contributions should make a
|
|
||||||
best effort to clean up and make the code base better than they left it.
|
|
||||||
Obviously, apply your best judgement. Remember, the goal here is to make the
|
|
||||||
code base easier for humans to navigate and understand. Always keep that in
|
|
||||||
mind when nudging others to comply.
|
|
||||||
|
|
||||||
The rules:
|
|
||||||
|
|
||||||
1. All code should be formatted with `gofmt -s`.
|
|
||||||
2. All code should pass the default levels of
|
|
||||||
[`golint`](https://github.com/golang/lint).
|
|
||||||
3. All code should follow the guidelines covered in [Effective
|
|
||||||
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
|
|
||||||
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
|
||||||
4. Comment the code. Tell us the why, the history and the context.
|
|
||||||
5. Document _all_ declarations and methods, even private ones. Declare
|
|
||||||
expectations, caveats and anything else that may be important. If a type
|
|
||||||
gets exported, having the comments already there will ensure it's ready.
|
|
||||||
6. Variable name length should be proportional to its context and no longer.
|
|
||||||
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
|
|
||||||
In practice, short methods will have short variable names and globals will
|
|
||||||
have longer names.
|
|
||||||
7. No underscores in package names. If you need a compound name, step back,
|
|
||||||
and re-examine why you need a compound name. If you still think you need a
|
|
||||||
compound name, lose the underscore.
|
|
||||||
8. No utils or helpers packages. If a function is not general enough to
|
|
||||||
warrant its own package, it has not been written generally enough to be a
|
|
||||||
part of a util package. Just leave it unexported and well-documented.
|
|
||||||
9. All tests should run with `go test` and outside tooling should not be
|
|
||||||
required. No, we don't need another unit testing framework. Assertion
|
|
||||||
packages are acceptable if they provide _real_ incremental value.
|
|
||||||
10. Even though we call these "rules" above, they are actually just
|
|
||||||
guidelines. Since you've read all the rules, you now know that.
|
|
||||||
|
|
||||||
If you are having trouble getting into the mood of idiomatic Go, we recommend
|
|
||||||
reading through [Effective Go](https://golang.org/doc/effective_go.html). The
|
|
||||||
[Go Blog](https://blog.golang.org) is also a great resource. Drinking the
|
|
||||||
kool-aid is a lot easier than going thirsty.
|
|
||||||
|
|
||||||
## Code Review Comments and Effective Go Guidelines
|
|
||||||
[CodeLingo](https://codelingo.io) automatically checks every pull request against the following guidelines from [Effective Go](https://golang.org/doc/effective_go.html) and [Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
|
||||||
|
|
||||||
|
|
||||||
### Package Comment
|
|
||||||
Every package should have a package comment, a block comment preceding the package clause.
|
|
||||||
For multi-file packages, the package comment only needs to be present in one file, and any one will do.
|
|
||||||
The package comment should introduce the package and provide information relevant to the package as a
|
|
||||||
whole. It will appear first on the godoc page and should set up the detailed documentation that follows.
|
|
||||||
|
|
||||||
|
|
||||||
### Single Method Interface Name
|
|
||||||
By convention, one-method interfaces are named by the method name plus an -er suffix
|
|
||||||
or similar modification to construct an agent noun: Reader, Writer, Formatter, CloseNotifier etc.
|
|
||||||
|
|
||||||
There are a number of such names and it's productive to honor them and the function names they capture.
|
|
||||||
Read, Write, Close, Flush, String and so on have canonical signatures and meanings. To avoid confusion,
|
|
||||||
don't give your method one of those names unless it has the same signature and meaning. Conversely,
|
|
||||||
if your type implements a method with the same meaning as a method on a well-known type, give it the
|
|
||||||
same name and signature; call your string-converter method String not ToString.
|
|
||||||
|
|
||||||
|
|
||||||
### Avoid Annotations in Comments
|
|
||||||
Comments do not need extra formatting such as banners of stars. The generated output
|
|
||||||
may not even be presented in a fixed-width font, so don't depend on spacing for alignment—godoc,
|
|
||||||
like gofmt, takes care of that. The comments are uninterpreted plain text, so HTML and other
|
|
||||||
annotations such as _this_ will reproduce verbatim and should not be used. One adjustment godoc
|
|
||||||
does do is to display indented text in a fixed-width font, suitable for program snippets.
|
|
||||||
The package comment for the fmt package uses this to good effect.
|
|
||||||
|
|
||||||
|
|
||||||
### Comment First Word as Subject
|
|
||||||
Doc comments work best as complete sentences, which allow a wide variety of automated presentations.
|
|
||||||
The first sentence should be a one-sentence summary that starts with the name being declared.
|
|
||||||
|
|
||||||
|
|
||||||
### Good Package Name
|
|
||||||
It's helpful if everyone using the package can use the same name
|
|
||||||
to refer to its contents, which implies that the package name should
|
|
||||||
be good: short, concise, evocative. By convention, packages are
|
|
||||||
given lower case, single-word names; there should be no need for
|
|
||||||
underscores or mixedCaps. Err on the side of brevity, since everyone
|
|
||||||
using your package will be typing that name. And don't worry about
|
|
||||||
collisions a priori. The package name is only the default name for
|
|
||||||
imports; it need not be unique across all source code, and in the
|
|
||||||
rare case of a collision the importing package can choose a different
|
|
||||||
name to use locally. In any case, confusion is rare because the file
|
|
||||||
name in the import determines just which package is being used.
|
|
||||||
|
|
||||||
|
|
||||||
### Avoid Renaming Imports
|
|
||||||
Avoid renaming imports except to avoid a name collision; good package names
|
|
||||||
should not require renaming. In the event of collision, prefer to rename the
|
|
||||||
most local or project-specific import.
|
|
||||||
|
|
||||||
|
|
||||||
### Context as First Argument
|
|
||||||
Values of the context.Context type carry security credentials, tracing information,
|
|
||||||
deadlines, and cancellation signals across API and process boundaries. Go programs
|
|
||||||
pass Contexts explicitly along the entire function call chain from incoming RPCs
|
|
||||||
and HTTP requests to outgoing requests.
|
|
||||||
|
|
||||||
Most functions that use a Context should accept it as their first parameter.
|
|
||||||
|
|
||||||
|
|
||||||
### Do Not Discard Errors
|
|
||||||
Do not discard errors using _ variables. If a function returns an error,
|
|
||||||
check it to make sure the function succeeded. Handle the error, return it, or,
|
|
||||||
in truly exceptional situations, panic.
|
|
||||||
|
|
||||||
|
|
||||||
### Go Error Format
|
|
||||||
Error strings should not be capitalized (unless beginning with proper nouns
|
|
||||||
or acronyms) or end with punctuation, since they are usually printed following
|
|
||||||
other context. That is, use fmt.Errorf("something bad") not fmt.Errorf("Something bad"),
|
|
||||||
so that log.Printf("Reading %s: %v", filename, err) formats without a spurious
|
|
||||||
capital letter mid-message. This does not apply to logging, which is implicitly
|
|
||||||
line-oriented and not combined inside other messages.
|
|
||||||
|
|
||||||
|
|
||||||
### Use Crypto Rand
|
|
||||||
Do not use package math/rand to generate keys, even
|
|
||||||
throwaway ones. Unseeded, the generator is completely predictable.
|
|
||||||
Seeded with time.Nanoseconds(), there are just a few bits of entropy.
|
|
||||||
Instead, use crypto/rand's Reader, and if you need text, print to
|
|
||||||
hexadecimal or base64
|
|
||||||
|
|
||||||
384
vendor/github.com/360EntSecGroup-Skylar/excelize/CONTRIBUTING_TEMPLATE.md
generated
vendored
384
vendor/github.com/360EntSecGroup-Skylar/excelize/CONTRIBUTING_TEMPLATE.md
generated
vendored
@ -1,384 +0,0 @@
|
|||||||
<!-- use this template to generate the contributor docs with the following command: `$ lingo run docs --template CONTRIBUTING_TEMPLATE.md --output CONTRIBUTING.md` -->
|
|
||||||
# Contributing to excelize
|
|
||||||
|
|
||||||
Want to hack on excelize? Awesome! This page contains information about reporting issues as well as some tips and
|
|
||||||
guidelines useful to experienced open source contributors. Finally, make sure
|
|
||||||
you read our [community guidelines](#community-guidelines) before you
|
|
||||||
start participating.
|
|
||||||
|
|
||||||
## Topics
|
|
||||||
|
|
||||||
* [Reporting Security Issues](#reporting-security-issues)
|
|
||||||
* [Design and Cleanup Proposals](#design-and-cleanup-proposals)
|
|
||||||
* [Reporting Issues](#reporting-other-issues)
|
|
||||||
* [Quick Contribution Tips and Guidelines](#quick-contribution-tips-and-guidelines)
|
|
||||||
* [Community Guidelines](#community-guidelines)
|
|
||||||
|
|
||||||
## Reporting security issues
|
|
||||||
|
|
||||||
The excelize maintainers take security seriously. If you discover a security
|
|
||||||
issue, please bring it to their attention right away!
|
|
||||||
|
|
||||||
Please **DO NOT** file a public issue, instead send your report privately to
|
|
||||||
[xuri.me](https://xuri.me).
|
|
||||||
|
|
||||||
Security reports are greatly appreciated and we will publicly thank you for it.
|
|
||||||
We currently do not offer a paid security bounty program, but are not
|
|
||||||
ruling it out in the future.
|
|
||||||
|
|
||||||
## Reporting other issues
|
|
||||||
|
|
||||||
A great way to contribute to the project is to send a detailed report when you
|
|
||||||
encounter an issue. We always appreciate a well-written, thorough bug report,
|
|
||||||
and will thank you for it!
|
|
||||||
|
|
||||||
Check that [our issue database](https://github.com/360EntSecGroup-Skylar/excelize/issues)
|
|
||||||
doesn't already include that problem or suggestion before submitting an issue.
|
|
||||||
If you find a match, you can use the "subscribe" button to get notified on
|
|
||||||
updates. Do *not* leave random "+1" or "I have this too" comments, as they
|
|
||||||
only clutter the discussion, and don't help resolving it. However, if you
|
|
||||||
have ways to reproduce the issue or have additional information that may help
|
|
||||||
resolving the issue, please leave a comment.
|
|
||||||
|
|
||||||
When reporting issues, always include the output of `go env`.
|
|
||||||
|
|
||||||
Also include the steps required to reproduce the problem if possible and
|
|
||||||
applicable. This information will help us review and fix your issue faster.
|
|
||||||
When sending lengthy log-files, consider posting them as a gist [https://gist.github.com](https://gist.github.com).
|
|
||||||
Don't forget to remove sensitive data from your logfiles before posting (you can
|
|
||||||
replace those parts with "REDACTED").
|
|
||||||
|
|
||||||
## Quick contribution tips and guidelines
|
|
||||||
|
|
||||||
This section gives the experienced contributor some tips and guidelines.
|
|
||||||
|
|
||||||
### Pull requests are always welcome
|
|
||||||
|
|
||||||
Not sure if that typo is worth a pull request? Found a bug and know how to fix
|
|
||||||
it? Do it! We will appreciate it. Any significant improvement should be
|
|
||||||
documented as [a GitHub issue](https://github.com/360EntSecGroup-Skylar/excelize/issues) before
|
|
||||||
anybody starts working on it.
|
|
||||||
|
|
||||||
We are always thrilled to receive pull requests. We do our best to process them
|
|
||||||
quickly. If your pull request is not accepted on the first try,
|
|
||||||
don't get discouraged!
|
|
||||||
|
|
||||||
### Design and cleanup proposals
|
|
||||||
|
|
||||||
You can propose new designs for existing excelize features. You can also design
|
|
||||||
entirely new features. We really appreciate contributors who want to refactor or
|
|
||||||
otherwise cleanup our project.
|
|
||||||
|
|
||||||
We try hard to keep excelize lean and focused. Excelize can't do everything for
|
|
||||||
everybody. This means that we might decide against incorporating a new feature.
|
|
||||||
However, there might be a way to implement that feature *on top of* excelize.
|
|
||||||
|
|
||||||
### Conventions
|
|
||||||
|
|
||||||
Fork the repository and make changes on your fork in a feature branch:
|
|
||||||
|
|
||||||
* If it's a bug fix branch, name it XXXX-something where XXXX is the number of
|
|
||||||
the issue.
|
|
||||||
* If it's a feature branch, create an enhancement issue to announce
|
|
||||||
your intentions, and name it XXXX-something where XXXX is the number of the
|
|
||||||
issue.
|
|
||||||
|
|
||||||
Submit unit tests for your changes. Go has a great test framework built in; use
|
|
||||||
it! Take a look at existing tests for inspiration. Run the full test on your branch before
|
|
||||||
submitting a pull request.
|
|
||||||
|
|
||||||
Update the documentation when creating or modifying features. Test your
|
|
||||||
documentation changes for clarity, concision, and correctness, as well as a
|
|
||||||
clean documentation build.
|
|
||||||
|
|
||||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
|
||||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
|
||||||
committing your changes. Most editors have plug-ins that do this automatically.
|
|
||||||
|
|
||||||
Pull request descriptions should be as clear as possible and include a reference
|
|
||||||
to all the issues that they address.
|
|
||||||
|
|
||||||
### Successful Changes
|
|
||||||
|
|
||||||
Before contributing large or high impact changes, make the effort to coordinate
|
|
||||||
with the maintainers of the project before submitting a pull request. This
|
|
||||||
prevents you from doing extra work that may or may not be merged.
|
|
||||||
|
|
||||||
Large PRs that are just submitted without any prior communication are unlikely
|
|
||||||
to be successful.
|
|
||||||
|
|
||||||
While pull requests are the methodology for submitting changes to code, changes
|
|
||||||
are much more likely to be accepted if they are accompanied by additional
|
|
||||||
engineering work. While we don't define this explicitly, most of these goals
|
|
||||||
are accomplished through communication of the design goals and subsequent
|
|
||||||
solutions. Often times, it helps to first state the problem before presenting
|
|
||||||
solutions.
|
|
||||||
|
|
||||||
Typically, the best methods of accomplishing this are to submit an issue,
|
|
||||||
stating the problem. This issue can include a problem statement and a
|
|
||||||
checklist with requirements. If solutions are proposed, alternatives should be
|
|
||||||
listed and eliminated. Even if the criteria for elimination of a solution is
|
|
||||||
frivolous, say so.
|
|
||||||
|
|
||||||
Larger changes typically work best with design documents. These are focused on
|
|
||||||
providing context to the design at the time the feature was conceived and can
|
|
||||||
inform future documentation contributions.
|
|
||||||
|
|
||||||
### Commit Messages
|
|
||||||
|
|
||||||
Commit messages must start with a capitalized and short summary
|
|
||||||
written in the imperative, followed by an optional, more detailed explanatory
|
|
||||||
text which is separated from the summary by an empty line.
|
|
||||||
|
|
||||||
Commit messages should follow best practices, including explaining the context
|
|
||||||
of the problem and how it was solved, including in caveats or follow up changes
|
|
||||||
required. They should tell the story of the change and provide readers
|
|
||||||
understanding of what led to it.
|
|
||||||
|
|
||||||
In practice, the best approach to maintaining a nice commit message is to
|
|
||||||
leverage a `git add -p` and `git commit --amend` to formulate a solid
|
|
||||||
changeset. This allows one to piece together a change, as information becomes
|
|
||||||
available.
|
|
||||||
|
|
||||||
If you squash a series of commits, don't just submit that. Re-write the commit
|
|
||||||
message, as if the series of commits was a single stroke of brilliance.
|
|
||||||
|
|
||||||
That said, there is no requirement to have a single commit for a PR, as long as
|
|
||||||
each commit tells the story. For example, if there is a feature that requires a
|
|
||||||
package, it might make sense to have the package in a separate commit then have
|
|
||||||
a subsequent commit that uses it.
|
|
||||||
|
|
||||||
Remember, you're telling part of the story with the commit message. Don't make
|
|
||||||
your chapter weird.
|
|
||||||
|
|
||||||
### Review
|
|
||||||
|
|
||||||
Code review comments may be added to your pull request. Discuss, then make the
|
|
||||||
suggested modifications and push additional commits to your feature branch. Post
|
|
||||||
a comment after pushing. New commits show up in the pull request automatically,
|
|
||||||
but the reviewers are notified only when you comment.
|
|
||||||
|
|
||||||
Pull requests must be cleanly rebased on top of master without multiple branches
|
|
||||||
mixed into the PR.
|
|
||||||
|
|
||||||
**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
|
|
||||||
feature branch to update your pull request rather than `merge master`.
|
|
||||||
|
|
||||||
Before you make a pull request, squash your commits into logical units of work
|
|
||||||
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
|
|
||||||
set of patches that should be reviewed together: for example, upgrading the
|
|
||||||
version of a vendored dependency and taking advantage of its now available new
|
|
||||||
feature constitute two separate units of work. Implementing a new function and
|
|
||||||
calling it in another file constitute a single logical unit of work. The very
|
|
||||||
high majority of submissions should have a single commit, so if in doubt: squash
|
|
||||||
down to one.
|
|
||||||
|
|
||||||
After every commit, make sure the test passes. Include documentation
|
|
||||||
changes in the same pull request so that a revert would remove all traces of
|
|
||||||
the feature or fix.
|
|
||||||
|
|
||||||
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in commits that
|
|
||||||
close an issue. Including references automatically closes the issue on a merge.
|
|
||||||
|
|
||||||
Please see the [Coding Style](#coding-style) for further guidelines.
|
|
||||||
|
|
||||||
### Merge approval
|
|
||||||
|
|
||||||
The excelize maintainers use LGTM (Looks Good To Me) in comments on the code review to
|
|
||||||
indicate acceptance.
|
|
||||||
|
|
||||||
### Sign your work
|
|
||||||
|
|
||||||
The sign-off is a simple line at the end of the explanation for the patch. Your
|
|
||||||
signature certifies that you wrote the patch or otherwise have the right to pass
|
|
||||||
it on as an open-source patch. The rules are pretty simple: if you can certify
|
|
||||||
the below (from [developercertificate.org](http://developercertificate.org/)):
|
|
||||||
|
|
||||||
```text
|
|
||||||
Developer Certificate of Origin
|
|
||||||
Version 1.1
|
|
||||||
|
|
||||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
|
||||||
1 Letterman Drive
|
|
||||||
Suite D4700
|
|
||||||
San Francisco, CA, 94129
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Developer's Certificate of Origin 1.1
|
|
||||||
|
|
||||||
By making a contribution to this project, I certify that:
|
|
||||||
|
|
||||||
(a) The contribution was created in whole or in part by me and I
|
|
||||||
have the right to submit it under the open source license
|
|
||||||
indicated in the file; or
|
|
||||||
|
|
||||||
(b) The contribution is based upon previous work that, to the best
|
|
||||||
of my knowledge, is covered under an appropriate open source
|
|
||||||
license and I have the right under that license to submit that
|
|
||||||
work with modifications, whether created in whole or in part
|
|
||||||
by me, under the same open source license (unless I am
|
|
||||||
permitted to submit under a different license), as indicated
|
|
||||||
in the file; or
|
|
||||||
|
|
||||||
(c) The contribution was provided directly to me by some other
|
|
||||||
person who certified (a), (b) or (c) and I have not modified
|
|
||||||
it.
|
|
||||||
|
|
||||||
(d) I understand and agree that this project and the contribution
|
|
||||||
are public and that a record of the contribution (including all
|
|
||||||
personal information I submit with it, including my sign-off) is
|
|
||||||
maintained indefinitely and may be redistributed consistent with
|
|
||||||
this project or the open source license(s) involved.
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you just add a line to every git commit message:
|
|
||||||
|
|
||||||
Signed-off-by: Ri Xu https://xuri.me
|
|
||||||
|
|
||||||
Use your real name (sorry, no pseudonyms or anonymous contributions.)
|
|
||||||
|
|
||||||
If you set your `user.name` and `user.email` git configs, you can sign your
|
|
||||||
commit automatically with `git commit -s`.
|
|
||||||
|
|
||||||
### How can I become a maintainer
|
|
||||||
|
|
||||||
First, all maintainers have 3 things
|
|
||||||
|
|
||||||
* They share responsibility in the project's success.
|
|
||||||
* They have made a long-term, recurring time investment to improve the project.
|
|
||||||
* They spend that time doing whatever needs to be done, not necessarily what
|
|
||||||
is the most interesting or fun.
|
|
||||||
|
|
||||||
Maintainers are often under-appreciated, because their work is harder to appreciate.
|
|
||||||
It's easy to appreciate a really cool and technically advanced feature. It's harder
|
|
||||||
to appreciate the absence of bugs, the slow but steady improvement in stability,
|
|
||||||
or the reliability of a release process. But those things distinguish a good
|
|
||||||
project from a great one.
|
|
||||||
|
|
||||||
Don't forget: being a maintainer is a time investment. Make sure you
|
|
||||||
will have time to make yourself available. You don't have to be a
|
|
||||||
maintainer to make a difference on the project!
|
|
||||||
|
|
||||||
If you want to become a meintainer, contact [xuri.me](https://xuri.me) and given a introduction of you.
|
|
||||||
|
|
||||||
## Community guidelines
|
|
||||||
|
|
||||||
We want to keep the community awesome, growing and collaborative. We need
|
|
||||||
your help to keep it that way. To help with this we've come up with some general
|
|
||||||
guidelines for the community as a whole:
|
|
||||||
|
|
||||||
* Be nice: Be courteous, respectful and polite to fellow community members:
|
|
||||||
no regional, racial, gender, or other abuse will be tolerated. We like
|
|
||||||
nice people way better than mean ones!
|
|
||||||
|
|
||||||
* Encourage diversity and participation: Make everyone in our community feel
|
|
||||||
welcome, regardless of their background and the extent of their
|
|
||||||
contributions, and do everything possible to encourage participation in
|
|
||||||
our community.
|
|
||||||
|
|
||||||
* Keep it legal: Basically, don't get us in trouble. Share only content that
|
|
||||||
you own, do not share private or sensitive information, and don't break
|
|
||||||
the law.
|
|
||||||
|
|
||||||
* Stay on topic: Make sure that you are posting to the correct channel and
|
|
||||||
avoid off-topic discussions. Remember when you update an issue or respond
|
|
||||||
to an email you are potentially sending to a large number of people. Please
|
|
||||||
consider this before you update. Also remember that nobody likes spam.
|
|
||||||
|
|
||||||
* Don't send email to the maintainers: There's no need to send email to the
|
|
||||||
maintainers to ask them to investigate an issue or to take a look at a
|
|
||||||
pull request. Instead of sending an email, GitHub mentions should be
|
|
||||||
used to ping maintainers to review a pull request, a proposal or an
|
|
||||||
issue.
|
|
||||||
|
|
||||||
### Guideline violations — 3 strikes method
|
|
||||||
|
|
||||||
The point of this section is not to find opportunities to punish people, but we
|
|
||||||
do need a fair way to deal with people who are making our community suck.
|
|
||||||
|
|
||||||
1. First occurrence: We'll give you a friendly, but public reminder that the
|
|
||||||
behavior is inappropriate according to our guidelines.
|
|
||||||
|
|
||||||
2. Second occurrence: We will send you a private message with a warning that
|
|
||||||
any additional violations will result in removal from the community.
|
|
||||||
|
|
||||||
3. Third occurrence: Depending on the violation, we may need to delete or ban
|
|
||||||
your account.
|
|
||||||
|
|
||||||
**Notes:**
|
|
||||||
|
|
||||||
* Obvious spammers are banned on first occurrence. If we don't do this, we'll
|
|
||||||
have spam all over the place.
|
|
||||||
|
|
||||||
* Violations are forgiven after 6 months of good behavior, and we won't hold a
|
|
||||||
grudge.
|
|
||||||
|
|
||||||
* People who commit minor infractions will get some education, rather than
|
|
||||||
hammering them in the 3 strikes process.
|
|
||||||
|
|
||||||
* The rules apply equally to everyone in the community, no matter how much
|
|
||||||
you've contributed.
|
|
||||||
|
|
||||||
* Extreme violations of a threatening, abusive, destructive or illegal nature
|
|
||||||
will be addressed immediately and are not subject to 3 strikes or forgiveness.
|
|
||||||
|
|
||||||
* Contact [xuri.me](https://xuri.me) to report abuse or appeal violations. In the case of
|
|
||||||
appeals, we know that mistakes happen, and we'll work with you to come up with a
|
|
||||||
fair solution if there has been a misunderstanding.
|
|
||||||
|
|
||||||
## Coding Style
|
|
||||||
|
|
||||||
Unless explicitly stated, we follow all coding guidelines from the Go
|
|
||||||
community. While some of these standards may seem arbitrary, they somehow seem
|
|
||||||
to result in a solid, consistent codebase.
|
|
||||||
|
|
||||||
It is possible that the code base does not currently comply with these
|
|
||||||
guidelines. We are not looking for a massive PR that fixes this, since that
|
|
||||||
goes against the spirit of the guidelines. All new contributions should make a
|
|
||||||
best effort to clean up and make the code base better than they left it.
|
|
||||||
Obviously, apply your best judgement. Remember, the goal here is to make the
|
|
||||||
code base easier for humans to navigate and understand. Always keep that in
|
|
||||||
mind when nudging others to comply.
|
|
||||||
|
|
||||||
The rules:
|
|
||||||
|
|
||||||
1. All code should be formatted with `gofmt -s`.
|
|
||||||
2. All code should pass the default levels of
|
|
||||||
[`golint`](https://github.com/golang/lint).
|
|
||||||
3. All code should follow the guidelines covered in [Effective
|
|
||||||
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
|
|
||||||
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
|
||||||
4. Comment the code. Tell us the why, the history and the context.
|
|
||||||
5. Document _all_ declarations and methods, even private ones. Declare
|
|
||||||
expectations, caveats and anything else that may be important. If a type
|
|
||||||
gets exported, having the comments already there will ensure it's ready.
|
|
||||||
6. Variable name length should be proportional to its context and no longer.
|
|
||||||
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
|
|
||||||
In practice, short methods will have short variable names and globals will
|
|
||||||
have longer names.
|
|
||||||
7. No underscores in package names. If you need a compound name, step back,
|
|
||||||
and re-examine why you need a compound name. If you still think you need a
|
|
||||||
compound name, lose the underscore.
|
|
||||||
8. No utils or helpers packages. If a function is not general enough to
|
|
||||||
warrant its own package, it has not been written generally enough to be a
|
|
||||||
part of a util package. Just leave it unexported and well-documented.
|
|
||||||
9. All tests should run with `go test` and outside tooling should not be
|
|
||||||
required. No, we don't need another unit testing framework. Assertion
|
|
||||||
packages are acceptable if they provide _real_ incremental value.
|
|
||||||
10. Even though we call these "rules" above, they are actually just
|
|
||||||
guidelines. Since you've read all the rules, you now know that.
|
|
||||||
|
|
||||||
If you are having trouble getting into the mood of idiomatic Go, we recommend
|
|
||||||
reading through [Effective Go](https://golang.org/doc/effective_go.html). The
|
|
||||||
[Go Blog](https://blog.golang.org) is also a great resource. Drinking the
|
|
||||||
kool-aid is a lot easier than going thirsty.
|
|
||||||
|
|
||||||
## Code Review Comments and Effective Go Guidelines
|
|
||||||
[CodeLingo](https://codelingo.io) automatically checks every pull request against the following guidelines from [Effective Go](https://golang.org/doc/effective_go.html) and [Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
|
||||||
|
|
||||||
{{range .}}
|
|
||||||
### {{.title}}
|
|
||||||
{{.body}}
|
|
||||||
{{end}}
|
|
||||||
29
vendor/github.com/360EntSecGroup-Skylar/excelize/LICENSE
generated
vendored
29
vendor/github.com/360EntSecGroup-Skylar/excelize/LICENSE
generated
vendored
@ -1,29 +0,0 @@
|
|||||||
BSD 3-Clause License
|
|
||||||
|
|
||||||
Copyright (c) 2016 - 2019 360 Enterprise Security Group, Endpoint Security,
|
|
||||||
inc. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of Excelize nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
45
vendor/github.com/360EntSecGroup-Skylar/excelize/PULL_REQUEST_TEMPLATE.md
generated
vendored
45
vendor/github.com/360EntSecGroup-Skylar/excelize/PULL_REQUEST_TEMPLATE.md
generated
vendored
@ -1,45 +0,0 @@
|
|||||||
# PR Details
|
|
||||||
|
|
||||||
<!--- Provide a general summary of your changes in the Title above -->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!--- Describe your changes in detail -->
|
|
||||||
|
|
||||||
## Related Issue
|
|
||||||
|
|
||||||
<!--- This project only accepts pull requests related to open issues -->
|
|
||||||
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
|
|
||||||
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
|
|
||||||
<!--- Please link to the issue here: -->
|
|
||||||
|
|
||||||
## Motivation and Context
|
|
||||||
|
|
||||||
<!--- Why is this change required? What problem does it solve? -->
|
|
||||||
|
|
||||||
## How Has This Been Tested
|
|
||||||
|
|
||||||
<!--- Please describe in detail how you tested your changes. -->
|
|
||||||
<!--- Include details of your testing environment, and the tests you ran to -->
|
|
||||||
<!--- see how your change affects other areas of the code, etc. -->
|
|
||||||
|
|
||||||
## Types of changes
|
|
||||||
|
|
||||||
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
|
||||||
|
|
||||||
- [ ] Docs change / refactoring / dependency upgrade
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
|
||||||
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
|
||||||
|
|
||||||
- [ ] My code follows the code style of this project.
|
|
||||||
- [ ] My change requires a change to the documentation.
|
|
||||||
- [ ] I have updated the documentation accordingly.
|
|
||||||
- [ ] I have read the **CONTRIBUTING** document.
|
|
||||||
- [ ] I have added tests to cover my changes.
|
|
||||||
- [ ] All new and existing tests passed.
|
|
||||||
179
vendor/github.com/360EntSecGroup-Skylar/excelize/README.md
generated
vendored
179
vendor/github.com/360EntSecGroup-Skylar/excelize/README.md
generated
vendored
@ -1,179 +0,0 @@
|
|||||||
<p align="center"><img width="650" src="./excelize.png" alt="Excelize logo"></p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://travis-ci.org/360EntSecGroup-Skylar/excelize"><img src="https://travis-ci.org/360EntSecGroup-Skylar/excelize.svg?branch=master" alt="Build Status"></a>
|
|
||||||
<a href="https://codecov.io/gh/360EntSecGroup-Skylar/excelize"><img src="https://codecov.io/gh/360EntSecGroup-Skylar/excelize/branch/master/graph/badge.svg" alt="Code Coverage"></a>
|
|
||||||
<a href="https://goreportcard.com/report/github.com/360EntSecGroup-Skylar/excelize"><img src="https://goreportcard.com/badge/github.com/360EntSecGroup-Skylar/excelize" alt="Go Report Card"></a>
|
|
||||||
<a href="https://godoc.org/github.com/360EntSecGroup-Skylar/excelize"><img src="https://godoc.org/github.com/360EntSecGroup-Skylar/excelize?status.svg" alt="GoDoc"></a>
|
|
||||||
<a href="https://opensource.org/licenses/BSD-3-Clause"><img src="https://img.shields.io/badge/license-bsd-orange.svg" alt="Licenses"></a>
|
|
||||||
<a href="https://www.paypal.me/xuri"><img src="https://img.shields.io/badge/Donate-PayPal-green.svg" alt="Donate"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# Excelize
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
Excelize is a library written in pure Go and providing a set of functions that allow you to write to and read from XLSX files. Support reads and writes XLSX file generated by Microsoft Excel™ 2007 and later. Support save file without losing original charts of XLSX. This library needs Go version 1.8 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/).
|
|
||||||
|
|
||||||
## Basic Usage
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```go
|
|
||||||
go get github.com/360EntSecGroup-Skylar/excelize
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create XLSX file
|
|
||||||
|
|
||||||
Here is a minimal example usage that will create XLSX file.
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/360EntSecGroup-Skylar/excelize"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
xlsx := excelize.NewFile()
|
|
||||||
// Create a new sheet.
|
|
||||||
index := xlsx.NewSheet("Sheet2")
|
|
||||||
// Set value of a cell.
|
|
||||||
xlsx.SetCellValue("Sheet2", "A2", "Hello world.")
|
|
||||||
xlsx.SetCellValue("Sheet1", "B2", 100)
|
|
||||||
// Set active sheet of the workbook.
|
|
||||||
xlsx.SetActiveSheet(index)
|
|
||||||
// Save xlsx file by the given path.
|
|
||||||
err := xlsx.SaveAs("./Book1.xlsx")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reading XLSX file
|
|
||||||
|
|
||||||
The following constitutes the bare to read a XLSX document.
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/360EntSecGroup-Skylar/excelize"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
xlsx, err := excelize.OpenFile("./Book1.xlsx")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Get value from cell by given worksheet name and axis.
|
|
||||||
cell := xlsx.GetCellValue("Sheet1", "B2")
|
|
||||||
fmt.Println(cell)
|
|
||||||
// Get all the rows in the Sheet1.
|
|
||||||
rows := xlsx.GetRows("Sheet1")
|
|
||||||
for _, row := range rows {
|
|
||||||
for _, colCell := range row {
|
|
||||||
fmt.Print(colCell, "\t")
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add chart to XLSX file
|
|
||||||
|
|
||||||
With Excelize chart generation and management is as easy as a few lines of code. You can build charts based off data in your worksheet or generate charts without any data in your worksheet at all.
|
|
||||||
|
|
||||||
<p align="center"><img width="650" src="./test/images/chart.png" alt="Excelize"></p>
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/360EntSecGroup-Skylar/excelize"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"}
|
|
||||||
values := map[string]int{"B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8}
|
|
||||||
xlsx := excelize.NewFile()
|
|
||||||
for k, v := range categories {
|
|
||||||
xlsx.SetCellValue("Sheet1", k, v)
|
|
||||||
}
|
|
||||||
for k, v := range values {
|
|
||||||
xlsx.SetCellValue("Sheet1", k, v)
|
|
||||||
}
|
|
||||||
xlsx.AddChart("Sheet1", "E1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`)
|
|
||||||
// Save xlsx file by the given path.
|
|
||||||
err := xlsx.SaveAs("./Book1.xlsx")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add picture to XLSX file
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
_ "image/gif"
|
|
||||||
_ "image/jpeg"
|
|
||||||
_ "image/png"
|
|
||||||
|
|
||||||
"github.com/360EntSecGroup-Skylar/excelize"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
xlsx, err := excelize.OpenFile("./Book1.xlsx")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Insert a picture.
|
|
||||||
err = xlsx.AddPicture("Sheet1", "A2", "./image1.png", "")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
// Insert a picture to worksheet with scaling.
|
|
||||||
err = xlsx.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
// Insert a picture offset in the cell with printing support.
|
|
||||||
err = xlsx.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
// Save the xlsx file with the origin path.
|
|
||||||
err = xlsx.Save()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Open a pull request to fix a bug, or open an issue to discuss a new feature or change. XML is compliant with [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](http://www.ecma-international.org/publications/standards/Ecma-376.htm).
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
Some struct of XML originally by [tealeg/xlsx](https://github.com/tealeg/xlsx).
|
|
||||||
|
|
||||||
## Licenses
|
|
||||||
|
|
||||||
This program is under the terms of the BSD 3-Clause License. See [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause).
|
|
||||||
|
|
||||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize?ref=badge_large)
|
|
||||||
179
vendor/github.com/360EntSecGroup-Skylar/excelize/README_zh.md
generated
vendored
179
vendor/github.com/360EntSecGroup-Skylar/excelize/README_zh.md
generated
vendored
@ -1,179 +0,0 @@
|
|||||||
<p align="center"><img width="650" src="./excelize.png" alt="Excelize logo"></p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://travis-ci.org/360EntSecGroup-Skylar/excelize"><img src="https://travis-ci.org/360EntSecGroup-Skylar/excelize.svg?branch=master" alt="Build Status"></a>
|
|
||||||
<a href="https://codecov.io/gh/360EntSecGroup-Skylar/excelize"><img src="https://codecov.io/gh/360EntSecGroup-Skylar/excelize/branch/master/graph/badge.svg" alt="Code Coverage"></a>
|
|
||||||
<a href="https://goreportcard.com/report/github.com/360EntSecGroup-Skylar/excelize"><img src="https://goreportcard.com/badge/github.com/360EntSecGroup-Skylar/excelize" alt="Go Report Card"></a>
|
|
||||||
<a href="https://godoc.org/github.com/360EntSecGroup-Skylar/excelize"><img src="https://godoc.org/github.com/360EntSecGroup-Skylar/excelize?status.svg" alt="GoDoc"></a>
|
|
||||||
<a href="https://opensource.org/licenses/BSD-3-Clause"><img src="https://img.shields.io/badge/license-bsd-orange.svg" alt="Licenses"></a>
|
|
||||||
<a href="https://www.paypal.me/xuri"><img src="https://img.shields.io/badge/Donate-PayPal-green.svg" alt="Donate"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# Excelize
|
|
||||||
|
|
||||||
## 简介
|
|
||||||
|
|
||||||
Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.8 或更高版本,完整的 API 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [参考文档](https://xuri.me/excelize/)。
|
|
||||||
|
|
||||||
## 快速上手
|
|
||||||
|
|
||||||
### 安装
|
|
||||||
|
|
||||||
```go
|
|
||||||
go get github.com/360EntSecGroup-Skylar/excelize
|
|
||||||
```
|
|
||||||
|
|
||||||
### 创建 Excel 文档
|
|
||||||
|
|
||||||
下面是一个创建 Excel 文档的简单例子:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/360EntSecGroup-Skylar/excelize"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
xlsx := excelize.NewFile()
|
|
||||||
// 创建一个工作表
|
|
||||||
index := xlsx.NewSheet("Sheet2")
|
|
||||||
// 设置单元格的值
|
|
||||||
xlsx.SetCellValue("Sheet2", "A2", "Hello world.")
|
|
||||||
xlsx.SetCellValue("Sheet1", "B2", 100)
|
|
||||||
// 设置工作簿的默认工作表
|
|
||||||
xlsx.SetActiveSheet(index)
|
|
||||||
// 根据指定路径保存文件
|
|
||||||
err := xlsx.SaveAs("./Book1.xlsx")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 读取 Excel 文档
|
|
||||||
|
|
||||||
下面是读取 Excel 文档的例子:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/360EntSecGroup-Skylar/excelize"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
xlsx, err := excelize.OpenFile("./Book1.xlsx")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 获取工作表中指定单元格的值
|
|
||||||
cell := xlsx.GetCellValue("Sheet1", "B2")
|
|
||||||
fmt.Println(cell)
|
|
||||||
// 获取 Sheet1 上所有单元格
|
|
||||||
rows := xlsx.GetRows("Sheet1")
|
|
||||||
for _, row := range rows {
|
|
||||||
for _, colCell := range row {
|
|
||||||
fmt.Print(colCell, "\t")
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 在 Excel 文档中创建图表
|
|
||||||
|
|
||||||
使用 Excelize 生成图表十分简单,仅需几行代码。您可以根据工作表中的已有数据构建图表,或向工作表中添加数据并创建图表。
|
|
||||||
|
|
||||||
<p align="center"><img width="650" src="./test/images/chart.png" alt="Excelize"></p>
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/360EntSecGroup-Skylar/excelize"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"}
|
|
||||||
values := map[string]int{"B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8}
|
|
||||||
xlsx := excelize.NewFile()
|
|
||||||
for k, v := range categories {
|
|
||||||
xlsx.SetCellValue("Sheet1", k, v)
|
|
||||||
}
|
|
||||||
for k, v := range values {
|
|
||||||
xlsx.SetCellValue("Sheet1", k, v)
|
|
||||||
}
|
|
||||||
xlsx.AddChart("Sheet1", "E1", `{"type":"col3DClustered","series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`)
|
|
||||||
// 根据指定路径保存文件
|
|
||||||
err := xlsx.SaveAs("./Book1.xlsx")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### 向 Excel 文档中插入图片
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
_ "image/gif"
|
|
||||||
_ "image/jpeg"
|
|
||||||
_ "image/png"
|
|
||||||
|
|
||||||
"github.com/360EntSecGroup-Skylar/excelize"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
xlsx, err := excelize.OpenFile("./Book1.xlsx")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 插入图片
|
|
||||||
err = xlsx.AddPicture("Sheet1", "A2", "./image1.png", "")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
// 在工作表中插入图片,并设置图片的缩放比例
|
|
||||||
err = xlsx.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
// 在工作表中插入图片,并设置图片的打印属性
|
|
||||||
err = xlsx.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
// 保存文件
|
|
||||||
err = xlsx.Save()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 社区合作
|
|
||||||
|
|
||||||
欢迎您为此项目贡献代码,提出建议或问题、修复 Bug 以及参与讨论对新功能的想法。 XML 符合标准: [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](http://www.ecma-international.org/publications/standards/Ecma-376.htm)。
|
|
||||||
|
|
||||||
## 致谢
|
|
||||||
|
|
||||||
本类库中部分 XML 结构体的定义参考了开源项目:[tealeg/xlsx](https://github.com/tealeg/xlsx).
|
|
||||||
|
|
||||||
## 开源许可
|
|
||||||
|
|
||||||
本项目遵循 BSD 3-Clause 开源许可协议,访问 [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) 查看许可协议文件。
|
|
||||||
|
|
||||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize?ref=badge_large)
|
|
||||||
599
vendor/github.com/360EntSecGroup-Skylar/excelize/cell.go
generated
vendored
599
vendor/github.com/360EntSecGroup-Skylar/excelize/cell.go
generated
vendored
@ -1,599 +0,0 @@
|
|||||||
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
|
|
||||||
// this source code is governed by a BSD-style license that can be found in
|
|
||||||
// the LICENSE file.
|
|
||||||
//
|
|
||||||
// Package excelize providing a set of functions that allow you to write to
|
|
||||||
// and read from XLSX files. Support reads and writes XLSX file generated by
|
|
||||||
// Microsoft Excel™ 2007 and later. Support save file without losing original
|
|
||||||
// charts of XLSX. This library needs Go version 1.8 or later.
|
|
||||||
|
|
||||||
package excelize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// STCellFormulaTypeArray defined the formula is an array formula.
|
|
||||||
STCellFormulaTypeArray = "array"
|
|
||||||
// STCellFormulaTypeDataTable defined the formula is a data table formula.
|
|
||||||
STCellFormulaTypeDataTable = "dataTable"
|
|
||||||
// STCellFormulaTypeNormal defined the formula is a regular cell formula.
|
|
||||||
STCellFormulaTypeNormal = "normal"
|
|
||||||
// STCellFormulaTypeShared defined the formula is part of a shared formula.
|
|
||||||
STCellFormulaTypeShared = "shared"
|
|
||||||
)
|
|
||||||
|
|
||||||
// mergeCellsParser provides a function to check merged cells in worksheet by
|
|
||||||
// given axis.
|
|
||||||
func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string {
|
|
||||||
axis = strings.ToUpper(axis)
|
|
||||||
if xlsx.MergeCells != nil {
|
|
||||||
for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
|
|
||||||
if checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) {
|
|
||||||
axis = strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return axis
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCellValue provides a function to set value of a cell. The following
|
|
||||||
// shows the supported data types:
|
|
||||||
//
|
|
||||||
// int
|
|
||||||
// int8
|
|
||||||
// int16
|
|
||||||
// int32
|
|
||||||
// int64
|
|
||||||
// uint
|
|
||||||
// uint8
|
|
||||||
// uint16
|
|
||||||
// uint32
|
|
||||||
// uint64
|
|
||||||
// float32
|
|
||||||
// float64
|
|
||||||
// string
|
|
||||||
// []byte
|
|
||||||
// time.Duration
|
|
||||||
// time.Time
|
|
||||||
// bool
|
|
||||||
// nil
|
|
||||||
//
|
|
||||||
// Note that default date format is m/d/yy h:mm of time.Time type value. You can
|
|
||||||
// set numbers format by SetCellStyle() method.
|
|
||||||
func (f *File) SetCellValue(sheet, axis string, value interface{}) {
|
|
||||||
switch t := value.(type) {
|
|
||||||
case float32:
|
|
||||||
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float32)), 'f', -1, 32))
|
|
||||||
case float64:
|
|
||||||
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float64)), 'f', -1, 64))
|
|
||||||
case string:
|
|
||||||
f.SetCellStr(sheet, axis, t)
|
|
||||||
case []byte:
|
|
||||||
f.SetCellStr(sheet, axis, string(t))
|
|
||||||
case time.Duration:
|
|
||||||
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(time.Duration).Seconds()/86400), 'f', -1, 32))
|
|
||||||
f.setDefaultTimeStyle(sheet, axis, 21)
|
|
||||||
case time.Time:
|
|
||||||
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(timeToExcelTime(timeToUTCTime(value.(time.Time)))), 'f', -1, 64))
|
|
||||||
f.setDefaultTimeStyle(sheet, axis, 22)
|
|
||||||
case nil:
|
|
||||||
f.SetCellStr(sheet, axis, "")
|
|
||||||
case bool:
|
|
||||||
f.SetCellBool(sheet, axis, bool(value.(bool)))
|
|
||||||
default:
|
|
||||||
f.setCellIntValue(sheet, axis, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setCellIntValue provides a function to set int value of a cell.
|
|
||||||
func (f *File) setCellIntValue(sheet, axis string, value interface{}) {
|
|
||||||
switch value.(type) {
|
|
||||||
case int:
|
|
||||||
f.SetCellInt(sheet, axis, value.(int))
|
|
||||||
case int8:
|
|
||||||
f.SetCellInt(sheet, axis, int(value.(int8)))
|
|
||||||
case int16:
|
|
||||||
f.SetCellInt(sheet, axis, int(value.(int16)))
|
|
||||||
case int32:
|
|
||||||
f.SetCellInt(sheet, axis, int(value.(int32)))
|
|
||||||
case int64:
|
|
||||||
f.SetCellInt(sheet, axis, int(value.(int64)))
|
|
||||||
case uint:
|
|
||||||
f.SetCellInt(sheet, axis, int(value.(uint)))
|
|
||||||
case uint8:
|
|
||||||
f.SetCellInt(sheet, axis, int(value.(uint8)))
|
|
||||||
case uint16:
|
|
||||||
f.SetCellInt(sheet, axis, int(value.(uint16)))
|
|
||||||
case uint32:
|
|
||||||
f.SetCellInt(sheet, axis, int(value.(uint32)))
|
|
||||||
case uint64:
|
|
||||||
f.SetCellInt(sheet, axis, int(value.(uint64)))
|
|
||||||
default:
|
|
||||||
f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCellBool provides a function to set bool type value of a cell by given
|
|
||||||
// worksheet name, cell coordinates and cell value.
|
|
||||||
func (f *File) SetCellBool(sheet, axis string, value bool) {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
axis = f.mergeCellsParser(xlsx, axis)
|
|
||||||
col := string(strings.Map(letterOnlyMapF, axis))
|
|
||||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
xAxis := row - 1
|
|
||||||
yAxis := TitleToNumber(col)
|
|
||||||
|
|
||||||
rows := xAxis + 1
|
|
||||||
cell := yAxis + 1
|
|
||||||
|
|
||||||
completeRow(xlsx, rows, cell)
|
|
||||||
completeCol(xlsx, rows, cell)
|
|
||||||
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S)
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].T = "b"
|
|
||||||
if value {
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].V = "1"
|
|
||||||
} else {
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].V = "0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCellValue provides a function to get formatted value from cell by given
|
|
||||||
// worksheet name and axis in XLSX file. If it is possible to apply a format
|
|
||||||
// to the cell value, it will do so, if not then an error will be returned,
|
|
||||||
// along with the raw value of the cell.
|
|
||||||
func (f *File) GetCellValue(sheet, axis string) string {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
axis = f.mergeCellsParser(xlsx, axis)
|
|
||||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
xAxis := row - 1
|
|
||||||
rows := len(xlsx.SheetData.Row)
|
|
||||||
if rows > 1 {
|
|
||||||
lastRow := xlsx.SheetData.Row[rows-1].R
|
|
||||||
if lastRow >= rows {
|
|
||||||
rows = lastRow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rows < xAxis {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
for k := range xlsx.SheetData.Row {
|
|
||||||
if xlsx.SheetData.Row[k].R == row {
|
|
||||||
for i := range xlsx.SheetData.Row[k].C {
|
|
||||||
if axis == xlsx.SheetData.Row[k].C[i].R {
|
|
||||||
val, _ := xlsx.SheetData.Row[k].C[i].getValueFrom(f, f.sharedStringsReader())
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// formattedValue provides a function to returns a value after formatted. If
|
|
||||||
// it is possible to apply a format to the cell value, it will do so, if not
|
|
||||||
// then an error will be returned, along with the raw value of the cell.
|
|
||||||
func (f *File) formattedValue(s int, v string) string {
|
|
||||||
if s == 0 {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
styleSheet := f.stylesReader()
|
|
||||||
ok := builtInNumFmtFunc[styleSheet.CellXfs.Xf[s].NumFmtID]
|
|
||||||
if ok != nil {
|
|
||||||
return ok(styleSheet.CellXfs.Xf[s].NumFmtID, v)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCellStyle provides a function to get cell style index by given worksheet
|
|
||||||
// name and cell coordinates.
|
|
||||||
func (f *File) GetCellStyle(sheet, axis string) int {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
axis = f.mergeCellsParser(xlsx, axis)
|
|
||||||
col := string(strings.Map(letterOnlyMapF, axis))
|
|
||||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
xAxis := row - 1
|
|
||||||
yAxis := TitleToNumber(col)
|
|
||||||
|
|
||||||
rows := xAxis + 1
|
|
||||||
cell := yAxis + 1
|
|
||||||
|
|
||||||
completeRow(xlsx, rows, cell)
|
|
||||||
completeCol(xlsx, rows, cell)
|
|
||||||
|
|
||||||
return f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCellFormula provides a function to get formula from cell by given
|
|
||||||
// worksheet name and axis in XLSX file.
|
|
||||||
func (f *File) GetCellFormula(sheet, axis string) string {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
axis = f.mergeCellsParser(xlsx, axis)
|
|
||||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
xAxis := row - 1
|
|
||||||
rows := len(xlsx.SheetData.Row)
|
|
||||||
if rows > 1 {
|
|
||||||
lastRow := xlsx.SheetData.Row[rows-1].R
|
|
||||||
if lastRow >= rows {
|
|
||||||
rows = lastRow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rows < xAxis {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
for k := range xlsx.SheetData.Row {
|
|
||||||
if xlsx.SheetData.Row[k].R == row {
|
|
||||||
for i := range xlsx.SheetData.Row[k].C {
|
|
||||||
if axis == xlsx.SheetData.Row[k].C[i].R {
|
|
||||||
if xlsx.SheetData.Row[k].C[i].F == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if xlsx.SheetData.Row[k].C[i].F.T == STCellFormulaTypeShared {
|
|
||||||
return getSharedForumula(xlsx, xlsx.SheetData.Row[k].C[i].F.Si)
|
|
||||||
}
|
|
||||||
return xlsx.SheetData.Row[k].C[i].F.Content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// getSharedForumula find a cell contains the same formula as another cell,
|
|
||||||
// the "shared" value can be used for the t attribute and the si attribute can
|
|
||||||
// be used to refer to the cell containing the formula. Two formulas are
|
|
||||||
// considered to be the same when their respective representations in
|
|
||||||
// R1C1-reference notation, are the same.
|
|
||||||
//
|
|
||||||
// Note that this function not validate ref tag to check the cell if or not in
|
|
||||||
// allow area, and always return origin shared formula.
|
|
||||||
func getSharedForumula(xlsx *xlsxWorksheet, si string) string {
|
|
||||||
for k := range xlsx.SheetData.Row {
|
|
||||||
for i := range xlsx.SheetData.Row[k].C {
|
|
||||||
if xlsx.SheetData.Row[k].C[i].F == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if xlsx.SheetData.Row[k].C[i].F.T != STCellFormulaTypeShared {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if xlsx.SheetData.Row[k].C[i].F.Si != si {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if xlsx.SheetData.Row[k].C[i].F.Ref != "" {
|
|
||||||
return xlsx.SheetData.Row[k].C[i].F.Content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCellFormula provides a function to set cell formula by given string and
|
|
||||||
// worksheet name.
|
|
||||||
func (f *File) SetCellFormula(sheet, axis, formula string) {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
axis = f.mergeCellsParser(xlsx, axis)
|
|
||||||
col := string(strings.Map(letterOnlyMapF, axis))
|
|
||||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
xAxis := row - 1
|
|
||||||
yAxis := TitleToNumber(col)
|
|
||||||
|
|
||||||
rows := xAxis + 1
|
|
||||||
cell := yAxis + 1
|
|
||||||
|
|
||||||
completeRow(xlsx, rows, cell)
|
|
||||||
completeCol(xlsx, rows, cell)
|
|
||||||
|
|
||||||
if xlsx.SheetData.Row[xAxis].C[yAxis].F != nil {
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].F.Content = formula
|
|
||||||
} else {
|
|
||||||
f := xlsxF{
|
|
||||||
Content: formula,
|
|
||||||
}
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].F = &f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCellHyperLink provides a function to set cell hyperlink by given
|
|
||||||
// worksheet name and link URL address. LinkType defines two types of
|
|
||||||
// hyperlink "External" for web site or "Location" for moving to one of cell
|
|
||||||
// in this workbook. The below is example for external link.
|
|
||||||
//
|
|
||||||
// xlsx.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External")
|
|
||||||
// // Set underline and font color style for the cell.
|
|
||||||
// style, _ := xlsx.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`)
|
|
||||||
// xlsx.SetCellStyle("Sheet1", "A3", "A3", style)
|
|
||||||
//
|
|
||||||
// A this is another example for "Location":
|
|
||||||
//
|
|
||||||
// xlsx.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location")
|
|
||||||
//
|
|
||||||
func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
axis = f.mergeCellsParser(xlsx, axis)
|
|
||||||
linkTypes := map[string]xlsxHyperlink{
|
|
||||||
"External": {},
|
|
||||||
"Location": {Location: link},
|
|
||||||
}
|
|
||||||
hyperlink, ok := linkTypes[linkType]
|
|
||||||
if !ok || axis == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hyperlink.Ref = axis
|
|
||||||
if linkType == "External" {
|
|
||||||
rID := f.addSheetRelationships(sheet, SourceRelationshipHyperLink, link, linkType)
|
|
||||||
hyperlink.RID = "rId" + strconv.Itoa(rID)
|
|
||||||
}
|
|
||||||
if xlsx.Hyperlinks == nil {
|
|
||||||
xlsx.Hyperlinks = &xlsxHyperlinks{}
|
|
||||||
}
|
|
||||||
xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, hyperlink)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCellHyperLink provides a function to get cell hyperlink by given
|
|
||||||
// worksheet name and axis. Boolean type value link will be ture if the cell
|
|
||||||
// has a hyperlink and the target is the address of the hyperlink. Otherwise,
|
|
||||||
// the value of link will be false and the value of the target will be a blank
|
|
||||||
// string. For example get hyperlink of Sheet1!H6:
|
|
||||||
//
|
|
||||||
// link, target := xlsx.GetCellHyperLink("Sheet1", "H6")
|
|
||||||
//
|
|
||||||
func (f *File) GetCellHyperLink(sheet, axis string) (bool, string) {
|
|
||||||
var link bool
|
|
||||||
var target string
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
axis = f.mergeCellsParser(xlsx, axis)
|
|
||||||
if xlsx.Hyperlinks == nil || axis == "" {
|
|
||||||
return link, target
|
|
||||||
}
|
|
||||||
for h := range xlsx.Hyperlinks.Hyperlink {
|
|
||||||
if xlsx.Hyperlinks.Hyperlink[h].Ref == axis {
|
|
||||||
link = true
|
|
||||||
target = xlsx.Hyperlinks.Hyperlink[h].Location
|
|
||||||
if xlsx.Hyperlinks.Hyperlink[h].RID != "" {
|
|
||||||
target = f.getSheetRelationshipsTargetByID(sheet, xlsx.Hyperlinks.Hyperlink[h].RID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return link, target
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeCell provides a function to merge cells by given coordinate area and
|
|
||||||
// sheet name. For example create a merged cell of D3:E9 on Sheet1:
|
|
||||||
//
|
|
||||||
// xlsx.MergeCell("Sheet1", "D3", "E9")
|
|
||||||
//
|
|
||||||
// If you create a merged cell that overlaps with another existing merged cell,
|
|
||||||
// those merged cells that already exist will be removed.
|
|
||||||
func (f *File) MergeCell(sheet, hcell, vcell string) {
|
|
||||||
if hcell == vcell {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hcell = strings.ToUpper(hcell)
|
|
||||||
vcell = strings.ToUpper(vcell)
|
|
||||||
|
|
||||||
// Coordinate conversion, convert C1:B3 to 2,0,1,2.
|
|
||||||
hcol := string(strings.Map(letterOnlyMapF, hcell))
|
|
||||||
hrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, hcell))
|
|
||||||
hyAxis := hrow - 1
|
|
||||||
hxAxis := TitleToNumber(hcol)
|
|
||||||
|
|
||||||
vcol := string(strings.Map(letterOnlyMapF, vcell))
|
|
||||||
vrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, vcell))
|
|
||||||
vyAxis := vrow - 1
|
|
||||||
vxAxis := TitleToNumber(vcol)
|
|
||||||
|
|
||||||
if vxAxis < hxAxis {
|
|
||||||
hcell, vcell = vcell, hcell
|
|
||||||
vxAxis, hxAxis = hxAxis, vxAxis
|
|
||||||
}
|
|
||||||
|
|
||||||
if vyAxis < hyAxis {
|
|
||||||
hcell, vcell = vcell, hcell
|
|
||||||
vyAxis, hyAxis = hyAxis, vyAxis
|
|
||||||
}
|
|
||||||
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
if xlsx.MergeCells != nil {
|
|
||||||
mergeCell := xlsxMergeCell{}
|
|
||||||
// Correct the coordinate area, such correct C1:B3 to B1:C3.
|
|
||||||
mergeCell.Ref = ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1)
|
|
||||||
// Delete the merged cells of the overlapping area.
|
|
||||||
for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
|
|
||||||
if checkCellInArea(hcell, xlsx.MergeCells.Cells[i].Ref) || checkCellInArea(strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0], mergeCell.Ref) {
|
|
||||||
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...)
|
|
||||||
} else if checkCellInArea(vcell, xlsx.MergeCells.Cells[i].Ref) || checkCellInArea(strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[1], mergeCell.Ref) {
|
|
||||||
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &mergeCell)
|
|
||||||
} else {
|
|
||||||
mergeCell := xlsxMergeCell{}
|
|
||||||
// Correct the coordinate area, such correct C1:B3 to B1:C3.
|
|
||||||
mergeCell.Ref = ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1)
|
|
||||||
mergeCells := xlsxMergeCells{}
|
|
||||||
mergeCells.Cells = append(mergeCells.Cells, &mergeCell)
|
|
||||||
xlsx.MergeCells = &mergeCells
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCellInt provides a function to set int type value of a cell by given
|
|
||||||
// worksheet name, cell coordinates and cell value.
|
|
||||||
func (f *File) SetCellInt(sheet, axis string, value int) {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
axis = f.mergeCellsParser(xlsx, axis)
|
|
||||||
col := string(strings.Map(letterOnlyMapF, axis))
|
|
||||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
xAxis := row - 1
|
|
||||||
yAxis := TitleToNumber(col)
|
|
||||||
|
|
||||||
rows := xAxis + 1
|
|
||||||
cell := yAxis + 1
|
|
||||||
|
|
||||||
completeRow(xlsx, rows, cell)
|
|
||||||
completeCol(xlsx, rows, cell)
|
|
||||||
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S)
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].T = ""
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].V = strconv.Itoa(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepareCellStyle provides a function to prepare style index of cell in
|
|
||||||
// worksheet by given column index and style index.
|
|
||||||
func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int {
|
|
||||||
if xlsx.Cols != nil && style == 0 {
|
|
||||||
for _, v := range xlsx.Cols.Col {
|
|
||||||
if v.Min <= col && col <= v.Max {
|
|
||||||
style = v.Style
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCellStr provides a function to set string type value of a cell. Total
|
|
||||||
// number of characters that a cell can contain 32767 characters.
|
|
||||||
func (f *File) SetCellStr(sheet, axis, value string) {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
axis = f.mergeCellsParser(xlsx, axis)
|
|
||||||
if len(value) > 32767 {
|
|
||||||
value = value[0:32767]
|
|
||||||
}
|
|
||||||
col := string(strings.Map(letterOnlyMapF, axis))
|
|
||||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
xAxis := row - 1
|
|
||||||
yAxis := TitleToNumber(col)
|
|
||||||
|
|
||||||
rows := xAxis + 1
|
|
||||||
cell := yAxis + 1
|
|
||||||
|
|
||||||
completeRow(xlsx, rows, cell)
|
|
||||||
completeCol(xlsx, rows, cell)
|
|
||||||
|
|
||||||
// Leading space(s) character detection.
|
|
||||||
if len(value) > 0 {
|
|
||||||
if value[0] == 32 {
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].XMLSpace = xml.Attr{
|
|
||||||
Name: xml.Name{Space: NameSpaceXML, Local: "space"},
|
|
||||||
Value: "preserve",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S)
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].T = "str"
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].V = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCellDefault provides a function to set string type value of a cell as
|
|
||||||
// default format without escaping the cell.
|
|
||||||
func (f *File) SetCellDefault(sheet, axis, value string) {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
axis = f.mergeCellsParser(xlsx, axis)
|
|
||||||
col := string(strings.Map(letterOnlyMapF, axis))
|
|
||||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
xAxis := row - 1
|
|
||||||
yAxis := TitleToNumber(col)
|
|
||||||
|
|
||||||
rows := xAxis + 1
|
|
||||||
cell := yAxis + 1
|
|
||||||
|
|
||||||
completeRow(xlsx, rows, cell)
|
|
||||||
completeCol(xlsx, rows, cell)
|
|
||||||
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S)
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].T = ""
|
|
||||||
xlsx.SheetData.Row[xAxis].C[yAxis].V = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSheetRow writes an array to row by given worksheet name, starting
|
|
||||||
// coordinate and a pointer to array type 'slice'. For example, writes an
|
|
||||||
// array to row 6 start with the cell B6 on Sheet1:
|
|
||||||
//
|
|
||||||
// xlsx.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2})
|
|
||||||
//
|
|
||||||
func (f *File) SetSheetRow(sheet, axis string, slice interface{}) {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
axis = f.mergeCellsParser(xlsx, axis)
|
|
||||||
col := string(strings.Map(letterOnlyMapF, axis))
|
|
||||||
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Make sure 'slice' is a Ptr to Slice
|
|
||||||
v := reflect.ValueOf(slice)
|
|
||||||
if v.Kind() != reflect.Ptr {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
v = v.Elem()
|
|
||||||
if v.Kind() != reflect.Slice {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
xAxis := row - 1
|
|
||||||
yAxis := TitleToNumber(col)
|
|
||||||
|
|
||||||
rows := xAxis + 1
|
|
||||||
cell := yAxis + 1
|
|
||||||
|
|
||||||
completeRow(xlsx, rows, cell)
|
|
||||||
completeCol(xlsx, rows, cell)
|
|
||||||
|
|
||||||
idx := 0
|
|
||||||
for i := cell - 1; i < v.Len()+cell-1; i++ {
|
|
||||||
c := ToAlphaString(i) + strconv.Itoa(row)
|
|
||||||
f.SetCellValue(sheet, c, v.Index(idx).Interface())
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkCellInArea provides a function to determine if a given coordinate is
|
|
||||||
// within an area.
|
|
||||||
func checkCellInArea(cell, area string) bool {
|
|
||||||
cell = strings.ToUpper(cell)
|
|
||||||
area = strings.ToUpper(area)
|
|
||||||
|
|
||||||
ref := strings.Split(area, ":")
|
|
||||||
if len(ref) < 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
from := ref[0]
|
|
||||||
to := ref[1]
|
|
||||||
|
|
||||||
col, row := getCellColRow(cell)
|
|
||||||
fromCol, fromRow := getCellColRow(from)
|
|
||||||
toCol, toRow := getCellColRow(to)
|
|
||||||
|
|
||||||
return axisLowerOrEqualThan(fromCol, col) && axisLowerOrEqualThan(col, toCol) && axisLowerOrEqualThan(fromRow, row) && axisLowerOrEqualThan(row, toRow)
|
|
||||||
}
|
|
||||||
1291
vendor/github.com/360EntSecGroup-Skylar/excelize/chart.go
generated
vendored
1291
vendor/github.com/360EntSecGroup-Skylar/excelize/chart.go
generated
vendored
File diff suppressed because it is too large
Load Diff
3
vendor/github.com/360EntSecGroup-Skylar/excelize/codelingo.yaml
generated
vendored
3
vendor/github.com/360EntSecGroup-Skylar/excelize/codelingo.yaml
generated
vendored
@ -1,3 +0,0 @@
|
|||||||
tenets:
|
|
||||||
- import: codelingo/effective-go
|
|
||||||
- import: codelingo/code-review-comments
|
|
||||||
376
vendor/github.com/360EntSecGroup-Skylar/excelize/col.go
generated
vendored
376
vendor/github.com/360EntSecGroup-Skylar/excelize/col.go
generated
vendored
@ -1,376 +0,0 @@
|
|||||||
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
|
|
||||||
// this source code is governed by a BSD-style license that can be found in
|
|
||||||
// the LICENSE file.
|
|
||||||
//
|
|
||||||
// Package excelize providing a set of functions that allow you to write to
|
|
||||||
// and read from XLSX files. Support reads and writes XLSX file generated by
|
|
||||||
// Microsoft Excel™ 2007 and later. Support save file without losing original
|
|
||||||
// charts of XLSX. This library needs Go version 1.8 or later.
|
|
||||||
|
|
||||||
package excelize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Define the default cell size and EMU unit of measurement.
|
|
||||||
const (
|
|
||||||
defaultColWidthPixels float64 = 64
|
|
||||||
defaultRowHeightPixels float64 = 20
|
|
||||||
EMU int = 9525
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetColVisible provides a function to get visible of a single column by given
|
|
||||||
// worksheet name and column name. For example, get visible state of column D
|
|
||||||
// in Sheet1:
|
|
||||||
//
|
|
||||||
// xlsx.GetColVisible("Sheet1", "D")
|
|
||||||
//
|
|
||||||
func (f *File) GetColVisible(sheet, column string) bool {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
col := TitleToNumber(strings.ToUpper(column)) + 1
|
|
||||||
visible := true
|
|
||||||
if xlsx.Cols == nil {
|
|
||||||
return visible
|
|
||||||
}
|
|
||||||
for c := range xlsx.Cols.Col {
|
|
||||||
if xlsx.Cols.Col[c].Min <= col && col <= xlsx.Cols.Col[c].Max {
|
|
||||||
visible = !xlsx.Cols.Col[c].Hidden
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return visible
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetColVisible provides a function to set visible of a single column by given
|
|
||||||
// worksheet name and column name. For example, hide column D in Sheet1:
|
|
||||||
//
|
|
||||||
// xlsx.SetColVisible("Sheet1", "D", false)
|
|
||||||
//
|
|
||||||
func (f *File) SetColVisible(sheet, column string, visible bool) {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
c := TitleToNumber(strings.ToUpper(column)) + 1
|
|
||||||
col := xlsxCol{
|
|
||||||
Min: c,
|
|
||||||
Max: c,
|
|
||||||
Hidden: !visible,
|
|
||||||
CustomWidth: true,
|
|
||||||
}
|
|
||||||
if xlsx.Cols == nil {
|
|
||||||
cols := xlsxCols{}
|
|
||||||
cols.Col = append(cols.Col, col)
|
|
||||||
xlsx.Cols = &cols
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for v := range xlsx.Cols.Col {
|
|
||||||
if xlsx.Cols.Col[v].Min <= c && c <= xlsx.Cols.Col[v].Max {
|
|
||||||
col = xlsx.Cols.Col[v]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
col.Min = c
|
|
||||||
col.Max = c
|
|
||||||
col.Hidden = !visible
|
|
||||||
col.CustomWidth = true
|
|
||||||
xlsx.Cols.Col = append(xlsx.Cols.Col, col)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetColOutlineLevel provides a function to get outline level of a single
|
|
||||||
// column by given worksheet name and column name. For example, get outline
|
|
||||||
// level of column D in Sheet1:
|
|
||||||
//
|
|
||||||
// xlsx.GetColOutlineLevel("Sheet1", "D")
|
|
||||||
//
|
|
||||||
func (f *File) GetColOutlineLevel(sheet, column string) uint8 {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
col := TitleToNumber(strings.ToUpper(column)) + 1
|
|
||||||
level := uint8(0)
|
|
||||||
if xlsx.Cols == nil {
|
|
||||||
return level
|
|
||||||
}
|
|
||||||
for c := range xlsx.Cols.Col {
|
|
||||||
if xlsx.Cols.Col[c].Min <= col && col <= xlsx.Cols.Col[c].Max {
|
|
||||||
level = xlsx.Cols.Col[c].OutlineLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return level
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetColOutlineLevel provides a function to set outline level of a single
|
|
||||||
// column by given worksheet name and column name. For example, set outline
|
|
||||||
// level of column D in Sheet1 to 2:
|
|
||||||
//
|
|
||||||
// xlsx.SetColOutlineLevel("Sheet1", "D", 2)
|
|
||||||
//
|
|
||||||
func (f *File) SetColOutlineLevel(sheet, column string, level uint8) {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
c := TitleToNumber(strings.ToUpper(column)) + 1
|
|
||||||
col := xlsxCol{
|
|
||||||
Min: c,
|
|
||||||
Max: c,
|
|
||||||
OutlineLevel: level,
|
|
||||||
CustomWidth: true,
|
|
||||||
}
|
|
||||||
if xlsx.Cols == nil {
|
|
||||||
cols := xlsxCols{}
|
|
||||||
cols.Col = append(cols.Col, col)
|
|
||||||
xlsx.Cols = &cols
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for v := range xlsx.Cols.Col {
|
|
||||||
if xlsx.Cols.Col[v].Min <= c && c <= xlsx.Cols.Col[v].Max {
|
|
||||||
col = xlsx.Cols.Col[v]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
col.Min = c
|
|
||||||
col.Max = c
|
|
||||||
col.OutlineLevel = level
|
|
||||||
col.CustomWidth = true
|
|
||||||
xlsx.Cols.Col = append(xlsx.Cols.Col, col)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetColWidth provides a function to set the width of a single column or
|
|
||||||
// multiple columns. For example:
|
|
||||||
//
|
|
||||||
// xlsx := excelize.NewFile()
|
|
||||||
// xlsx.SetColWidth("Sheet1", "A", "H", 20)
|
|
||||||
// err := xlsx.Save()
|
|
||||||
// if err != nil {
|
|
||||||
// fmt.Println(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) {
|
|
||||||
min := TitleToNumber(strings.ToUpper(startcol)) + 1
|
|
||||||
max := TitleToNumber(strings.ToUpper(endcol)) + 1
|
|
||||||
if min > max {
|
|
||||||
min, max = max, min
|
|
||||||
}
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
col := xlsxCol{
|
|
||||||
Min: min,
|
|
||||||
Max: max,
|
|
||||||
Width: width,
|
|
||||||
CustomWidth: true,
|
|
||||||
}
|
|
||||||
if xlsx.Cols != nil {
|
|
||||||
xlsx.Cols.Col = append(xlsx.Cols.Col, col)
|
|
||||||
} else {
|
|
||||||
cols := xlsxCols{}
|
|
||||||
cols.Col = append(cols.Col, col)
|
|
||||||
xlsx.Cols = &cols
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// positionObjectPixels calculate the vertices that define the position of a
|
|
||||||
// graphical object within the worksheet in pixels.
|
|
||||||
//
|
|
||||||
// +------------+------------+
|
|
||||||
// | A | B |
|
|
||||||
// +-----+------------+------------+
|
|
||||||
// | |(x1,y1) | |
|
|
||||||
// | 1 |(A1)._______|______ |
|
|
||||||
// | | | | |
|
|
||||||
// | | | | |
|
|
||||||
// +-----+----| OBJECT |-----+
|
|
||||||
// | | | | |
|
|
||||||
// | 2 | |______________. |
|
|
||||||
// | | | (B2)|
|
|
||||||
// | | | (x2,y2)|
|
|
||||||
// +-----+------------+------------+
|
|
||||||
//
|
|
||||||
// Example of an object that covers some of the area from cell A1 to B2.
|
|
||||||
//
|
|
||||||
// Based on the width and height of the object we need to calculate 8 vars:
|
|
||||||
//
|
|
||||||
// colStart, rowStart, colEnd, rowEnd, x1, y1, x2, y2.
|
|
||||||
//
|
|
||||||
// We also calculate the absolute x and y position of the top left vertex of
|
|
||||||
// the object. This is required for images.
|
|
||||||
//
|
|
||||||
// The width and height of the cells that the object occupies can be
|
|
||||||
// variable and have to be taken into account.
|
|
||||||
//
|
|
||||||
// The values of col_start and row_start are passed in from the calling
|
|
||||||
// function. The values of col_end and row_end are calculated by
|
|
||||||
// subtracting the width and height of the object from the width and
|
|
||||||
// height of the underlying cells.
|
|
||||||
//
|
|
||||||
// colStart # Col containing upper left corner of object.
|
|
||||||
// x1 # Distance to left side of object.
|
|
||||||
//
|
|
||||||
// rowStart # Row containing top left corner of object.
|
|
||||||
// y1 # Distance to top of object.
|
|
||||||
//
|
|
||||||
// colEnd # Col containing lower right corner of object.
|
|
||||||
// x2 # Distance to right side of object.
|
|
||||||
//
|
|
||||||
// rowEnd # Row containing bottom right corner of object.
|
|
||||||
// y2 # Distance to bottom of object.
|
|
||||||
//
|
|
||||||
// width # Width of object frame.
|
|
||||||
// height # Height of object frame.
|
|
||||||
//
|
|
||||||
// xAbs # Absolute distance to left side of object.
|
|
||||||
// yAbs # Absolute distance to top side of object.
|
|
||||||
//
|
|
||||||
func (f *File) positionObjectPixels(sheet string, colStart, rowStart, x1, y1, width, height int) (int, int, int, int, int, int, int, int) {
|
|
||||||
xAbs := 0
|
|
||||||
yAbs := 0
|
|
||||||
|
|
||||||
// Calculate the absolute x offset of the top-left vertex.
|
|
||||||
for colID := 1; colID <= colStart; colID++ {
|
|
||||||
xAbs += f.getColWidth(sheet, colID)
|
|
||||||
}
|
|
||||||
xAbs += x1
|
|
||||||
|
|
||||||
// Calculate the absolute y offset of the top-left vertex.
|
|
||||||
// Store the column change to allow optimisations.
|
|
||||||
for rowID := 1; rowID <= rowStart; rowID++ {
|
|
||||||
yAbs += f.getRowHeight(sheet, rowID)
|
|
||||||
}
|
|
||||||
yAbs += y1
|
|
||||||
|
|
||||||
// Adjust start column for offsets that are greater than the col width.
|
|
||||||
for x1 >= f.getColWidth(sheet, colStart) {
|
|
||||||
x1 -= f.getColWidth(sheet, colStart)
|
|
||||||
colStart++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust start row for offsets that are greater than the row height.
|
|
||||||
for y1 >= f.getRowHeight(sheet, rowStart) {
|
|
||||||
y1 -= f.getRowHeight(sheet, rowStart)
|
|
||||||
rowStart++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialise end cell to the same as the start cell.
|
|
||||||
colEnd := colStart
|
|
||||||
rowEnd := rowStart
|
|
||||||
|
|
||||||
width += x1
|
|
||||||
height += y1
|
|
||||||
|
|
||||||
// Subtract the underlying cell widths to find end cell of the object.
|
|
||||||
for width >= f.getColWidth(sheet, colEnd) {
|
|
||||||
colEnd++
|
|
||||||
width -= f.getColWidth(sheet, colEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtract the underlying cell heights to find end cell of the object.
|
|
||||||
for height >= f.getRowHeight(sheet, rowEnd) {
|
|
||||||
rowEnd++
|
|
||||||
height -= f.getRowHeight(sheet, rowEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The end vertices are whatever is left from the width and height.
|
|
||||||
x2 := width
|
|
||||||
y2 := height
|
|
||||||
return colStart, rowStart, xAbs, yAbs, colEnd, rowEnd, x2, y2
|
|
||||||
}
|
|
||||||
|
|
||||||
// getColWidth provides a function to get column width in pixels by given
|
|
||||||
// sheet name and column index.
|
|
||||||
func (f *File) getColWidth(sheet string, col int) int {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
if xlsx.Cols != nil {
|
|
||||||
var width float64
|
|
||||||
for _, v := range xlsx.Cols.Col {
|
|
||||||
if v.Min <= col && col <= v.Max {
|
|
||||||
width = v.Width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if width != 0 {
|
|
||||||
return int(convertColWidthToPixels(width))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Optimisation for when the column widths haven't changed.
|
|
||||||
return int(defaultColWidthPixels)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetColWidth provides a function to get column width by given worksheet name
|
|
||||||
// and column index.
|
|
||||||
func (f *File) GetColWidth(sheet, column string) float64 {
|
|
||||||
col := TitleToNumber(strings.ToUpper(column)) + 1
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
if xlsx.Cols != nil {
|
|
||||||
var width float64
|
|
||||||
for _, v := range xlsx.Cols.Col {
|
|
||||||
if v.Min <= col && col <= v.Max {
|
|
||||||
width = v.Width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if width != 0 {
|
|
||||||
return width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Optimisation for when the column widths haven't changed.
|
|
||||||
return defaultColWidthPixels
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertCol provides a function to insert a new column before given column
|
|
||||||
// index. For example, create a new column before column C in Sheet1:
|
|
||||||
//
|
|
||||||
// xlsx.InsertCol("Sheet1", "C")
|
|
||||||
//
|
|
||||||
func (f *File) InsertCol(sheet, column string) {
|
|
||||||
col := TitleToNumber(strings.ToUpper(column))
|
|
||||||
f.adjustHelper(sheet, col, -1, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveCol provides a function to remove single column by given worksheet
|
|
||||||
// name and column index. For example, remove column C in Sheet1:
|
|
||||||
//
|
|
||||||
// xlsx.RemoveCol("Sheet1", "C")
|
|
||||||
//
|
|
||||||
func (f *File) RemoveCol(sheet, column string) {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
for r := range xlsx.SheetData.Row {
|
|
||||||
for k, v := range xlsx.SheetData.Row[r].C {
|
|
||||||
axis := v.R
|
|
||||||
col := string(strings.Map(letterOnlyMapF, axis))
|
|
||||||
if col == column {
|
|
||||||
xlsx.SheetData.Row[r].C = append(xlsx.SheetData.Row[r].C[:k], xlsx.SheetData.Row[r].C[k+1:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
col := TitleToNumber(strings.ToUpper(column))
|
|
||||||
f.adjustHelper(sheet, col, -1, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeCol provieds function to completion column element tags of XML in a
|
|
||||||
// sheet.
|
|
||||||
func completeCol(xlsx *xlsxWorksheet, row, cell int) {
|
|
||||||
buffer := bytes.Buffer{}
|
|
||||||
for r := range xlsx.SheetData.Row {
|
|
||||||
if len(xlsx.SheetData.Row[r].C) < cell {
|
|
||||||
start := len(xlsx.SheetData.Row[r].C)
|
|
||||||
for iii := start; iii < cell; iii++ {
|
|
||||||
buffer.WriteString(ToAlphaString(iii))
|
|
||||||
buffer.WriteString(strconv.Itoa(r + 1))
|
|
||||||
xlsx.SheetData.Row[r].C = append(xlsx.SheetData.Row[r].C, xlsxC{
|
|
||||||
R: buffer.String(),
|
|
||||||
})
|
|
||||||
buffer.Reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertColWidthToPixels provieds function to convert the width of a cell
|
|
||||||
// from user's units to pixels. Excel rounds the column width to the nearest
|
|
||||||
// pixel. If the width hasn't been set by the user we use the default value.
|
|
||||||
// If the column is hidden it has a value of zero.
|
|
||||||
func convertColWidthToPixels(width float64) float64 {
|
|
||||||
var padding float64 = 5
|
|
||||||
var pixels float64
|
|
||||||
var maxDigitWidth float64 = 7
|
|
||||||
if width == 0 {
|
|
||||||
return pixels
|
|
||||||
}
|
|
||||||
if width < 1 {
|
|
||||||
pixels = (width * 12) + 0.5
|
|
||||||
return math.Ceil(pixels)
|
|
||||||
}
|
|
||||||
pixels = (width*maxDigitWidth + 0.5) + padding
|
|
||||||
return math.Ceil(pixels)
|
|
||||||
}
|
|
||||||
273
vendor/github.com/360EntSecGroup-Skylar/excelize/comment.go
generated
vendored
273
vendor/github.com/360EntSecGroup-Skylar/excelize/comment.go
generated
vendored
@ -1,273 +0,0 @@
|
|||||||
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
|
|
||||||
// this source code is governed by a BSD-style license that can be found in
|
|
||||||
// the LICENSE file.
|
|
||||||
//
|
|
||||||
// Package excelize providing a set of functions that allow you to write to
|
|
||||||
// and read from XLSX files. Support reads and writes XLSX file generated by
|
|
||||||
// Microsoft Excel™ 2007 and later. Support save file without losing original
|
|
||||||
// charts of XLSX. This library needs Go version 1.8 or later.
|
|
||||||
|
|
||||||
package excelize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// parseFormatCommentsSet provides a function to parse the format settings of
|
|
||||||
// the comment with default value.
|
|
||||||
func parseFormatCommentsSet(formatSet string) (*formatComment, error) {
|
|
||||||
format := formatComment{
|
|
||||||
Author: "Author:",
|
|
||||||
Text: " ",
|
|
||||||
}
|
|
||||||
err := json.Unmarshal([]byte(formatSet), &format)
|
|
||||||
return &format, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetComments retrieves all comments and returns a map of worksheet name to
|
|
||||||
// the worksheet comments.
|
|
||||||
func (f *File) GetComments() (comments map[string][]Comment) {
|
|
||||||
comments = map[string][]Comment{}
|
|
||||||
for n := range f.sheetMap {
|
|
||||||
commentID := f.GetSheetIndex(n)
|
|
||||||
commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml"
|
|
||||||
c, ok := f.XLSX[commentsXML]
|
|
||||||
if ok {
|
|
||||||
d := xlsxComments{}
|
|
||||||
xml.Unmarshal([]byte(c), &d)
|
|
||||||
sheetComments := []Comment{}
|
|
||||||
for _, comment := range d.CommentList.Comment {
|
|
||||||
sheetComment := Comment{}
|
|
||||||
if comment.AuthorID < len(d.Authors) {
|
|
||||||
sheetComment.Author = d.Authors[comment.AuthorID].Author
|
|
||||||
}
|
|
||||||
sheetComment.Ref = comment.Ref
|
|
||||||
sheetComment.AuthorID = comment.AuthorID
|
|
||||||
for _, text := range comment.Text.R {
|
|
||||||
sheetComment.Text += text.T
|
|
||||||
}
|
|
||||||
sheetComments = append(sheetComments, sheetComment)
|
|
||||||
}
|
|
||||||
comments[n] = sheetComments
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddComment provides the method to add comment in a sheet by given worksheet
|
|
||||||
// index, cell and format set (such as author and text). Note that the max
|
|
||||||
// author length is 255 and the max text length is 32512. For example, add a
|
|
||||||
// comment in Sheet1!$A$30:
|
|
||||||
//
|
|
||||||
// xlsx.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`)
|
|
||||||
//
|
|
||||||
func (f *File) AddComment(sheet, cell, format string) error {
|
|
||||||
formatSet, err := parseFormatCommentsSet(format)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Read sheet data.
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
commentID := f.countComments() + 1
|
|
||||||
drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
|
|
||||||
sheetRelationshipsComments := "../comments" + strconv.Itoa(commentID) + ".xml"
|
|
||||||
sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
|
|
||||||
if xlsx.LegacyDrawing != nil {
|
|
||||||
// The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
|
|
||||||
sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, xlsx.LegacyDrawing.RID)
|
|
||||||
commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
|
|
||||||
drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1)
|
|
||||||
} else {
|
|
||||||
// Add first comment for given sheet.
|
|
||||||
rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
|
|
||||||
f.addSheetRelationships(sheet, SourceRelationshipComments, sheetRelationshipsComments, "")
|
|
||||||
f.addSheetLegacyDrawing(sheet, rID)
|
|
||||||
}
|
|
||||||
commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml"
|
|
||||||
f.addComment(commentsXML, cell, formatSet)
|
|
||||||
var colCount int
|
|
||||||
for i, l := range strings.Split(formatSet.Text, "\n") {
|
|
||||||
if ll := len(l); ll > colCount {
|
|
||||||
if i == 0 {
|
|
||||||
ll += len(formatSet.Author)
|
|
||||||
}
|
|
||||||
colCount = ll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.addDrawingVML(commentID, drawingVML, cell, strings.Count(formatSet.Text, "\n")+1, colCount)
|
|
||||||
f.addContentTypePart(commentID, "comments")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// addDrawingVML provides a function to create comment as
|
|
||||||
// xl/drawings/vmlDrawing%d.vml by given commit ID and cell.
|
|
||||||
func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) {
|
|
||||||
col := string(strings.Map(letterOnlyMapF, cell))
|
|
||||||
row, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell))
|
|
||||||
xAxis := row - 1
|
|
||||||
yAxis := TitleToNumber(col)
|
|
||||||
vml := vmlDrawing{
|
|
||||||
XMLNSv: "urn:schemas-microsoft-com:vml",
|
|
||||||
XMLNSo: "urn:schemas-microsoft-com:office:office",
|
|
||||||
XMLNSx: "urn:schemas-microsoft-com:office:excel",
|
|
||||||
XMLNSmv: "http://macVmlSchemaUri",
|
|
||||||
Shapelayout: &xlsxShapelayout{
|
|
||||||
Ext: "edit",
|
|
||||||
IDmap: &xlsxIDmap{
|
|
||||||
Ext: "edit",
|
|
||||||
Data: commentID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Shapetype: &xlsxShapetype{
|
|
||||||
ID: "_x0000_t202",
|
|
||||||
Coordsize: "21600,21600",
|
|
||||||
Spt: 202,
|
|
||||||
Path: "m0,0l0,21600,21600,21600,21600,0xe",
|
|
||||||
Stroke: &xlsxStroke{
|
|
||||||
Joinstyle: "miter",
|
|
||||||
},
|
|
||||||
VPath: &vPath{
|
|
||||||
Gradientshapeok: "t",
|
|
||||||
Connecttype: "miter",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
sp := encodeShape{
|
|
||||||
Fill: &vFill{
|
|
||||||
Color2: "#fbfe82",
|
|
||||||
Angle: -180,
|
|
||||||
Type: "gradient",
|
|
||||||
Fill: &oFill{
|
|
||||||
Ext: "view",
|
|
||||||
Type: "gradientUnscaled",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Shadow: &vShadow{
|
|
||||||
On: "t",
|
|
||||||
Color: "black",
|
|
||||||
Obscured: "t",
|
|
||||||
},
|
|
||||||
Path: &vPath{
|
|
||||||
Connecttype: "none",
|
|
||||||
},
|
|
||||||
Textbox: &vTextbox{
|
|
||||||
Style: "mso-direction-alt:auto",
|
|
||||||
Div: &xlsxDiv{
|
|
||||||
Style: "text-align:left",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ClientData: &xClientData{
|
|
||||||
ObjectType: "Note",
|
|
||||||
Anchor: fmt.Sprintf(
|
|
||||||
"%d, 23, %d, 0, %d, %d, %d, 5",
|
|
||||||
1+yAxis, 1+xAxis, 2+yAxis+lineCount, colCount+yAxis, 2+xAxis+lineCount),
|
|
||||||
AutoFill: "True",
|
|
||||||
Row: xAxis,
|
|
||||||
Column: yAxis,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
s, _ := xml.Marshal(sp)
|
|
||||||
shape := xlsxShape{
|
|
||||||
ID: "_x0000_s1025",
|
|
||||||
Type: "#_x0000_t202",
|
|
||||||
Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
|
|
||||||
Fillcolor: "#fbf6d6",
|
|
||||||
Strokecolor: "#edeaa1",
|
|
||||||
Val: string(s[13 : len(s)-14]),
|
|
||||||
}
|
|
||||||
c, ok := f.XLSX[drawingVML]
|
|
||||||
if ok {
|
|
||||||
d := decodeVmlDrawing{}
|
|
||||||
_ = xml.Unmarshal(namespaceStrictToTransitional(c), &d)
|
|
||||||
for _, v := range d.Shape {
|
|
||||||
s := xlsxShape{
|
|
||||||
ID: "_x0000_s1025",
|
|
||||||
Type: "#_x0000_t202",
|
|
||||||
Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
|
|
||||||
Fillcolor: "#fbf6d6",
|
|
||||||
Strokecolor: "#edeaa1",
|
|
||||||
Val: v.Val,
|
|
||||||
}
|
|
||||||
vml.Shape = append(vml.Shape, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vml.Shape = append(vml.Shape, shape)
|
|
||||||
v, _ := xml.Marshal(vml)
|
|
||||||
f.XLSX[drawingVML] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// addComment provides a function to create chart as xl/comments%d.xml by
|
|
||||||
// given cell and format sets.
|
|
||||||
func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) {
|
|
||||||
a := formatSet.Author
|
|
||||||
t := formatSet.Text
|
|
||||||
if len(a) > 255 {
|
|
||||||
a = a[0:255]
|
|
||||||
}
|
|
||||||
if len(t) > 32512 {
|
|
||||||
t = t[0:32512]
|
|
||||||
}
|
|
||||||
comments := xlsxComments{
|
|
||||||
Authors: []xlsxAuthor{
|
|
||||||
{
|
|
||||||
Author: formatSet.Author,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
cmt := xlsxComment{
|
|
||||||
Ref: cell,
|
|
||||||
AuthorID: 0,
|
|
||||||
Text: xlsxText{
|
|
||||||
R: []xlsxR{
|
|
||||||
{
|
|
||||||
RPr: &xlsxRPr{
|
|
||||||
B: " ",
|
|
||||||
Sz: &attrValFloat{Val: 9},
|
|
||||||
Color: &xlsxColor{
|
|
||||||
Indexed: 81,
|
|
||||||
},
|
|
||||||
RFont: &attrValString{Val: "Calibri"},
|
|
||||||
Family: &attrValInt{Val: 2},
|
|
||||||
},
|
|
||||||
T: a,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RPr: &xlsxRPr{
|
|
||||||
Sz: &attrValFloat{Val: 9},
|
|
||||||
Color: &xlsxColor{
|
|
||||||
Indexed: 81,
|
|
||||||
},
|
|
||||||
RFont: &attrValString{Val: "Calibri"},
|
|
||||||
Family: &attrValInt{Val: 2},
|
|
||||||
},
|
|
||||||
T: t,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
c, ok := f.XLSX[commentsXML]
|
|
||||||
if ok {
|
|
||||||
d := xlsxComments{}
|
|
||||||
_ = xml.Unmarshal(namespaceStrictToTransitional(c), &d)
|
|
||||||
comments.CommentList.Comment = append(comments.CommentList.Comment, d.CommentList.Comment...)
|
|
||||||
}
|
|
||||||
comments.CommentList.Comment = append(comments.CommentList.Comment, cmt)
|
|
||||||
v, _ := xml.Marshal(comments)
|
|
||||||
f.saveFileList(commentsXML, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// countComments provides a function to get comments files count storage in
|
|
||||||
// the folder xl.
|
|
||||||
func (f *File) countComments() int {
|
|
||||||
count := 0
|
|
||||||
for k := range f.XLSX {
|
|
||||||
if strings.Contains(k, "xl/comments") {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
233
vendor/github.com/360EntSecGroup-Skylar/excelize/datavalidation.go
generated
vendored
233
vendor/github.com/360EntSecGroup-Skylar/excelize/datavalidation.go
generated
vendored
@ -1,233 +0,0 @@
|
|||||||
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
|
|
||||||
// this source code is governed by a BSD-style license that can be found in
|
|
||||||
// the LICENSE file.
|
|
||||||
//
|
|
||||||
// Package excelize providing a set of functions that allow you to write to
|
|
||||||
// and read from XLSX files. Support reads and writes XLSX file generated by
|
|
||||||
// Microsoft Excel™ 2007 and later. Support save file without losing original
|
|
||||||
// charts of XLSX. This library needs Go version 1.8 or later.
|
|
||||||
|
|
||||||
package excelize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DataValidationType defined the type of data validation.
|
|
||||||
type DataValidationType int
|
|
||||||
|
|
||||||
// Data validation types.
|
|
||||||
const (
|
|
||||||
_DataValidationType = iota
|
|
||||||
typeNone // inline use
|
|
||||||
DataValidationTypeCustom
|
|
||||||
DataValidationTypeDate
|
|
||||||
DataValidationTypeDecimal
|
|
||||||
typeList // inline use
|
|
||||||
DataValidationTypeTextLeng
|
|
||||||
DataValidationTypeTime
|
|
||||||
// DataValidationTypeWhole Integer
|
|
||||||
DataValidationTypeWhole
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// dataValidationFormulaStrLen 255 characters+ 2 quotes
|
|
||||||
dataValidationFormulaStrLen = 257
|
|
||||||
// dataValidationFormulaStrLenErr
|
|
||||||
dataValidationFormulaStrLenErr = "data validation must be 0-255 characters"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DataValidationErrorStyle defined the style of data validation error alert.
|
|
||||||
type DataValidationErrorStyle int
|
|
||||||
|
|
||||||
// Data validation error styles.
|
|
||||||
const (
|
|
||||||
_ DataValidationErrorStyle = iota
|
|
||||||
DataValidationErrorStyleStop
|
|
||||||
DataValidationErrorStyleWarning
|
|
||||||
DataValidationErrorStyleInformation
|
|
||||||
)
|
|
||||||
|
|
||||||
// Data validation error styles.
|
|
||||||
const (
|
|
||||||
styleStop = "stop"
|
|
||||||
styleWarning = "warning"
|
|
||||||
styleInformation = "information"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DataValidationOperator operator enum.
|
|
||||||
type DataValidationOperator int
|
|
||||||
|
|
||||||
// Data validation operators.
|
|
||||||
const (
|
|
||||||
_DataValidationOperator = iota
|
|
||||||
DataValidationOperatorBetween
|
|
||||||
DataValidationOperatorEqual
|
|
||||||
DataValidationOperatorGreaterThan
|
|
||||||
DataValidationOperatorGreaterThanOrEqual
|
|
||||||
DataValidationOperatorLessThan
|
|
||||||
DataValidationOperatorLessThanOrEqual
|
|
||||||
DataValidationOperatorNotBetween
|
|
||||||
DataValidationOperatorNotEqual
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewDataValidation return data validation struct.
|
|
||||||
func NewDataValidation(allowBlank bool) *DataValidation {
|
|
||||||
return &DataValidation{
|
|
||||||
AllowBlank: allowBlank,
|
|
||||||
ShowErrorMessage: false,
|
|
||||||
ShowInputMessage: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetError set error notice.
|
|
||||||
func (dd *DataValidation) SetError(style DataValidationErrorStyle, title, msg string) {
|
|
||||||
dd.Error = &msg
|
|
||||||
dd.ErrorTitle = &title
|
|
||||||
strStyle := styleStop
|
|
||||||
switch style {
|
|
||||||
case DataValidationErrorStyleStop:
|
|
||||||
strStyle = styleStop
|
|
||||||
case DataValidationErrorStyleWarning:
|
|
||||||
strStyle = styleWarning
|
|
||||||
case DataValidationErrorStyleInformation:
|
|
||||||
strStyle = styleInformation
|
|
||||||
|
|
||||||
}
|
|
||||||
dd.ShowErrorMessage = true
|
|
||||||
dd.ErrorStyle = &strStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetInput set prompt notice.
|
|
||||||
func (dd *DataValidation) SetInput(title, msg string) {
|
|
||||||
dd.ShowInputMessage = true
|
|
||||||
dd.PromptTitle = &title
|
|
||||||
dd.Prompt = &msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDropList data validation list.
|
|
||||||
func (dd *DataValidation) SetDropList(keys []string) error {
|
|
||||||
dd.Formula1 = "\"" + strings.Join(keys, ",") + "\""
|
|
||||||
dd.Type = convDataValidationType(typeList)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRange provides function to set data validation range in drop list.
|
|
||||||
func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValidationOperator) error {
|
|
||||||
formula1 := fmt.Sprintf("%d", f1)
|
|
||||||
formula2 := fmt.Sprintf("%d", f2)
|
|
||||||
if dataValidationFormulaStrLen < len(dd.Formula1) || dataValidationFormulaStrLen < len(dd.Formula2) {
|
|
||||||
return fmt.Errorf(dataValidationFormulaStrLenErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
dd.Formula1 = formula1
|
|
||||||
dd.Formula2 = formula2
|
|
||||||
dd.Type = convDataValidationType(t)
|
|
||||||
dd.Operator = convDataValidationOperatior(o)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSqrefDropList provides set data validation on a range with source
|
|
||||||
// reference range of the worksheet by given data validation object and
|
|
||||||
// worksheet name. The data validation object can be created by
|
|
||||||
// NewDataValidation function. For example, set data validation on
|
|
||||||
// Sheet1!A7:B8 with validation criteria source Sheet1!E1:E3 settings, create
|
|
||||||
// in-cell dropdown by allowing list source:
|
|
||||||
//
|
|
||||||
// dvRange := excelize.NewDataValidation(true)
|
|
||||||
// dvRange.Sqref = "A7:B8"
|
|
||||||
// dvRange.SetSqrefDropList("E1:E3", true)
|
|
||||||
// xlsx.AddDataValidation("Sheet1", dvRange)
|
|
||||||
//
|
|
||||||
func (dd *DataValidation) SetSqrefDropList(sqref string, isCurrentSheet bool) error {
|
|
||||||
if isCurrentSheet {
|
|
||||||
dd.Formula1 = sqref
|
|
||||||
dd.Type = convDataValidationType(typeList)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("cross-sheet sqref cell are not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSqref provides function to set data validation range in drop list.
|
|
||||||
func (dd *DataValidation) SetSqref(sqref string) {
|
|
||||||
if dd.Sqref == "" {
|
|
||||||
dd.Sqref = sqref
|
|
||||||
} else {
|
|
||||||
dd.Sqref = fmt.Sprintf("%s %s", dd.Sqref, sqref)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// convDataValidationType get excel data validation type.
|
|
||||||
func convDataValidationType(t DataValidationType) string {
|
|
||||||
typeMap := map[DataValidationType]string{
|
|
||||||
typeNone: "none",
|
|
||||||
DataValidationTypeCustom: "custom",
|
|
||||||
DataValidationTypeDate: "date",
|
|
||||||
DataValidationTypeDecimal: "decimal",
|
|
||||||
typeList: "list",
|
|
||||||
DataValidationTypeTextLeng: "textLength",
|
|
||||||
DataValidationTypeTime: "time",
|
|
||||||
DataValidationTypeWhole: "whole",
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeMap[t]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// convDataValidationOperatior get excel data validation operator.
|
|
||||||
func convDataValidationOperatior(o DataValidationOperator) string {
|
|
||||||
typeMap := map[DataValidationOperator]string{
|
|
||||||
DataValidationOperatorBetween: "between",
|
|
||||||
DataValidationOperatorEqual: "equal",
|
|
||||||
DataValidationOperatorGreaterThan: "greaterThan",
|
|
||||||
DataValidationOperatorGreaterThanOrEqual: "greaterThanOrEqual",
|
|
||||||
DataValidationOperatorLessThan: "lessThan",
|
|
||||||
DataValidationOperatorLessThanOrEqual: "lessThanOrEqual",
|
|
||||||
DataValidationOperatorNotBetween: "notBetween",
|
|
||||||
DataValidationOperatorNotEqual: "notEqual",
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeMap[o]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddDataValidation provides set data validation on a range of the worksheet
|
|
||||||
// by given data validation object and worksheet name. The data validation
|
|
||||||
// object can be created by NewDataValidation function.
|
|
||||||
//
|
|
||||||
// Example 1, set data validation on Sheet1!A1:B2 with validation criteria
|
|
||||||
// settings, show error alert after invalid data is entered with "Stop" style
|
|
||||||
// and custom title "error body":
|
|
||||||
//
|
|
||||||
// dvRange := excelize.NewDataValidation(true)
|
|
||||||
// dvRange.Sqref = "A1:B2"
|
|
||||||
// dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorBetween)
|
|
||||||
// dvRange.SetError(excelize.DataValidationErrorStyleStop, "error title", "error body")
|
|
||||||
// xlsx.AddDataValidation("Sheet1", dvRange)
|
|
||||||
//
|
|
||||||
// Example 2, set data validation on Sheet1!A3:B4 with validation criteria
|
|
||||||
// settings, and show input message when cell is selected:
|
|
||||||
//
|
|
||||||
// dvRange = excelize.NewDataValidation(true)
|
|
||||||
// dvRange.Sqref = "A3:B4"
|
|
||||||
// dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorGreaterThan)
|
|
||||||
// dvRange.SetInput("input title", "input body")
|
|
||||||
// xlsx.AddDataValidation("Sheet1", dvRange)
|
|
||||||
//
|
|
||||||
// Example 3, set data validation on Sheet1!A5:B6 with validation criteria
|
|
||||||
// settings, create in-cell dropdown by allowing list source:
|
|
||||||
//
|
|
||||||
// dvRange = excelize.NewDataValidation(true)
|
|
||||||
// dvRange.Sqref = "A5:B6"
|
|
||||||
// dvRange.SetDropList([]string{"1", "2", "3"})
|
|
||||||
// xlsx.AddDataValidation("Sheet1", dvRange)
|
|
||||||
//
|
|
||||||
func (f *File) AddDataValidation(sheet string, dv *DataValidation) {
|
|
||||||
xlsx := f.workSheetReader(sheet)
|
|
||||||
if nil == xlsx.DataValidations {
|
|
||||||
xlsx.DataValidations = new(xlsxDataValidations)
|
|
||||||
}
|
|
||||||
xlsx.DataValidations.DataValidation = append(xlsx.DataValidations.DataValidation, dv)
|
|
||||||
xlsx.DataValidations.Count = len(xlsx.DataValidations.DataValidation)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user