Compare commits

..

No commits in common. "master" and "v1.5.0" have entirely different histories.

37 changed files with 2078 additions and 5125 deletions

392
.cursor/debug.log Normal file

File diff suppressed because one or more lines are too long

View File

@ -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()` 正确性

View File

@ -1,144 +0,0 @@
---
name: 缓存数据库表重构
overview: 重构数据库缓存模块,将 cached 表替换为符合设计规范的 hotime_cache 表,支持 MySQL/SQLite/PostgreSQL修复并发问题支持可配置的历史记录功能和自动迁移。
todos:
- id: update-cache-db
content: 重构 cache_db.go新表、UPSERT、历史记录、自动迁移、问题修复
status: completed
- id: update-cache-go
content: 修改 cache.go添加 HistorySet 配置传递
status: completed
- id: update-makecode
content: 修改 makecode.go跳过新表名
status: completed
---
# 缓存数据库表重构计划
## 设计概要
将原 `cached` 表替换为 `hotime_cache` 表,遵循数据库设计规范,支持:
- 多数据库MySQL、SQLite、PostgreSQL
- 可配置的历史记录功能(仅日志,无读取接口)
- 自动迁移旧 cached 表数据
## 文件改动清单
| 文件 | 改动 | 状态 |
|------|------|------|
| [cache/cache_db.go](cache/cache_db.go) | 完全重构新表、UPSERT、历史记录、自动迁移 | 待完成 |
| [cache/cache.go](cache/cache.go) | 添加 `HistorySet: db.GetBool("history")` 配置传递 | 待完成 |
| [code/makecode.go](code/makecode.go) | 跳过 hotime_cache 和 hotime_cache_history | 已完成 |
## 表结构设计
### 主表 `hotime_cache`
```sql
-- MySQL
CREATE TABLE `hotime_cache` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`key` varchar(64) NOT NULL COMMENT '缓存键',
`value` text DEFAULT NULL COMMENT '缓存值',
`end_time` datetime DEFAULT NULL COMMENT '过期时间',
`state` int(2) DEFAULT '0' COMMENT '状态:0-正常,1-异常,2-隐藏',
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
`modify_time` datetime DEFAULT NULL COMMENT '变更时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_key` (`key`),
KEY `idx_end_time` (`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='缓存管理';
-- SQLite
CREATE TABLE "hotime_cache" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"key" TEXT NOT NULL UNIQUE,
"value" TEXT,
"end_time" TEXT,
"state" INTEGER DEFAULT 0,
"create_time" TEXT,
"modify_time" TEXT
);
-- PostgreSQL
CREATE TABLE "hotime_cache" (
"id" SERIAL PRIMARY KEY,
"key" VARCHAR(64) NOT NULL UNIQUE,
"value" TEXT,
"end_time" TIMESTAMP,
"state" INTEGER DEFAULT 0,
"create_time" TIMESTAMP,
"modify_time" TIMESTAMP
);
CREATE INDEX "idx_hotime_cache_end_time" ON "hotime_cache" ("end_time");
```
### 历史表 `hotime_cache_history`(配置开启时创建,创建后永不自动删除)
与主表结构相同,但 `id` 改为 `hotime_cache_id` 作为外键关联。
## 问题修复清单
| 问题 | 原状态 | 修复方案 |
|------|--------|----------|
| key 无索引 | 全表扫描 | 唯一索引 uk_key |
| 并发竞态 | Update+Insert 可能重复 | 使用 UPSERT 语法 |
| 时间字段混乱 | time(纳秒) + endtime(秒) | 统一 datetime 格式 |
| value 长度限制 | varchar(2000) | TEXT 类型 |
| TimeOut=0 立即过期 | 无默认值 | 默认 24 小时 |
| get 时删除过期数据 | 每次写操作 | 惰性删除,只返回 nil |
| 旧表 key 重复 | 无约束 | 迁移时取最后一条 |
| value 包装冗余 | `{"data": value}` | 直接存储 |
## 主要代码改动点
### cache_db.go 改动
1. 新增常量:表名、默认过期时间 24 小时
2. 结构体添加 `HistorySet bool`
3. 使用 common 包 `Time2Str(time.Now())` 格式化时间
4. `initDbTable()`: 支持三种数据库、自动迁移、创建历史表
5. `migrateFromCached()`: 去重迁移(取最后一条)、删除旧表
6. `writeHistory()`: 查询数据写入历史表(仅日志)
7. `set()`: 使用 UPSERT、调用 writeHistory
8. `get()`: 惰性删除
9. `Cache()`: TimeOut=0 时用默认值
### cache.go 改动
第 268 行添加:`HistorySet: db.GetBool("history")`
## 配置示例
```json
{
"cache": {
"db": {
"db": true,
"session": true,
"timeout": 72000,
"history": false
}
}
}
```
## 历史记录逻辑
- 新增/修改后查询完整数据id 改为 hotime_cache_id插入历史表
- 删除:不记录历史
- 历史表仅日志记录,无读取接口,人工在数据库操作

View File

@ -1,103 +0,0 @@
---
name: 规范文档创建
overview: 创建两个规范文档一个数据库设计规范文档一个管理后台配置规范文档包含admin.json和rule.json的配置说明并在README.md中添加链接。
todos:
- id: create-db-doc
content: 创建 docs/DatabaseDesign_数据库设计规范.md
status: pending
- id: create-admin-doc
content: 创建 docs/AdminConfig_管理后台配置规范.md
status: pending
- id: update-readme
content: 在 README.md 文档表格中添加两个新文档链接
status: pending
dependencies:
- create-db-doc
- create-admin-doc
---
# 创建规范文档
## 任务概述
`D:\work\hotimev1.5\docs` 目录下创建两个规范文档,并更新 README.md 添加链接。
---
## 文档一:数据库设计规范
**文件**: [docs/DatabaseDesign_数据库设计规范.md](docs/DatabaseDesign_数据库设计规范.md)
### 内容结构
| 章节 | 内容 |
|------|------|
| 表命名规则 | 不加前缀、可用简称、关联表命名主表_关联表 |
| 字段命名规则 | 主键id、外键表名_id、全局唯一性要求、层级字段parent_id/parent_ids/level |
| 注释规则 | select类型格式`状态:0-正常,1-异常`、时间用datetime |
| 必有字段 | state、create_time、modify_time |
| 示例 | 完整建表SQL示例 |
---
## 文档二:管理后台配置规范
**文件**: [docs/AdminConfig_管理后台配置规范.md](docs/AdminConfig_管理后台配置规范.md)
### 内容结构
| 章节 | 内容 |
|------|------|
| admin.json 配置 | |
| - flow配置 | 数据流控制定义表间权限关系sql条件、stop标志 |
| - labelConfig | 操作按钮标签show/add/delete/edit/info/download |
| - label | 菜单/表的显示名称 |
| - menus配置 | 菜单结构嵌套menus、icon、table、name、auth |
| - auth配置 | 权限数组show/add/delete/edit/info/download |
| - icon配置 | 菜单图标如Setting |
| - table/name配置 | table指定数据表name用于分组标识 |
| - stop配置 | 不允许用户修改自身关联数据的表 |
| rule.json 配置 | |
| - 字段默认权限 | add/edit/info/list/must/strict/type 各字段含义 |
| - 内置字段规则 | id、parent_id、create_time、modify_time、password等 |
| - type类型说明 | select/time/image/file/password/textArea/auth/form等 |
---
## 更新 README.md
在文档表格中添加两个新链接:
```markdown
| [数据库设计规范](docs/DatabaseDesign_数据库设计规范.md) | 表命名、字段命名、注释规则、必有字段 |
| [管理后台配置规范](docs/AdminConfig_管理后台配置规范.md) | admin.json、rule.json 配置说明 |
```
---
## 关键参考文件
- [`code/config.go`](code/config.go): RuleConfig 默认规则定义
- [`code/makecode.go`](code/makecode.go): 外键自动关联逻辑
- [`example/config/admin.json`](example/config/admin.json): 完整配置示例
- [`example/config/rule.json`](example/config/rule.json): 字段规则示例

View File

@ -1,5 +0,0 @@
{
"setup-worktree": [
"npm install"
]
}

6
.gitignore vendored
View File

@ -1,6 +1,6 @@
/.idea/* /.idea/*
.idea .idea
/example/config/app.json
/example/tpt/demo/ /example/tpt/demo/
*.exe /example/config/
/example/config /*.exe
/.cursor/*.log

205
README.md
View File

@ -2,45 +2,75 @@
**高性能 Go Web 服务框架** **高性能 Go Web 服务框架**
一个"小而全"的 Go Web 框架,内置 ORM、三级缓存、Session 管理,让你专注于业务逻辑。 ## 特性
## 核心特性
- **高性能** - 单机 10万+ QPS支持百万级并发用户 - **高性能** - 单机 10万+ QPS支持百万级并发用户
- **内置 ORM** - 类 Medoo 语法,链式查询,支持 MySQL/SQLite/PostgreSQL - **多数据库支持** - MySQL、SQLite3支持主从分离
- **三级缓存** - Memory > Redis > DB自动穿透与回填 - **三级缓存系统** - Memory > Redis > DB自动穿透与回填
- **Session管理** - 内置会话管理,支持多种存储后端 - **Session管理** - 内置会话管理,支持多种存储后端
- **代码生成** - 根据数据库表自动生成 CRUD 接口 - **自动代码生成** - 根据数据库表自动生成 CRUD 接口
- **开箱即用** - 微信支付/公众号/小程序、阿里云、腾讯云等 SDK 内置 - **丰富工具类** - 上下文管理、类型转换、加密解密等
## 文档 ## 快速开始
| 文档 | 说明 | ### 安装
|------|------|
| [快速上手指南](docs/QUICKSTART.md) | 5 分钟入门,安装配置、路由、中间件、基础数据库操作 |
| [HoTimeDB 使用说明](docs/HoTimeDB_使用说明.md) | 完整数据库 ORM 教程 |
| [HoTimeDB API 参考](docs/HoTimeDB_API参考.md) | 数据库 API 速查手册 |
| [Common 工具类](docs/Common_工具类使用说明.md) | Map/Slice/Obj 类型、类型转换、工具函数 |
| [代码生成器](docs/CodeGen_使用说明.md) | 自动 CRUD 代码生成、配置规则 |
| [数据库设计规范](docs/DatabaseDesign_数据库设计规范.md) | 表命名、字段命名、时间类型、必有字段规范 |
| [代码生成配置规范](docs/CodeConfig_代码生成配置规范.md) | codeConfig、菜单权限、字段规则配置说明 |
| [改进规划](docs/ROADMAP_改进规划.md) | 待改进项、设计思考、版本迭代规划 |
## 安装
```bash ```bash
go get code.hoteas.com/golang/hotime go get code.hoteas.com/golang/hotime
``` ```
## 性能 ### 最小示例
| 并发数 | QPS | 成功率 | 平均延迟 | ```go
|--------|-----|--------|----------| package main
| 500 | 99,960 | 100% | 5.0ms |
| **1000** | **102,489** | **100%** | **9.7ms** |
| 2000 | 75,801 | 99.99% | 26.2ms |
> 测试环境24 核 CPUWindows 10Go 1.19.3 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
## 性能测试报告
### 测试环境
| 项目 | 配置 |
|------|------|
| CPU | 24 核心 |
| 系统 | Windows 10 |
| Go 版本 | 1.19.3 |
### 测试结果
| 并发数 | QPS | 成功率 | 平均延迟 | P99延迟 |
|--------|-----|--------|----------|---------|
| 500 | 99,960 | 100% | 5.0ms | 25.2ms |
| **1000** | **102,489** | **100%** | **9.7ms** | **56.8ms** |
| 2000 | 75,801 | 99.99% | 26.2ms | 127.7ms |
| 5000 | 12,611 | 99.95% | 391.4ms | 781.4ms |
### 性能总结
```
最高 QPS: 102,489 请求/秒
最佳并发数: 1,000
```
### 并发用户估算 ### 并发用户估算
@ -49,9 +79,24 @@ go get code.hoteas.com/golang/hotime
| 高频交互 | 1次/秒 | ~10万 | | 高频交互 | 1次/秒 | ~10万 |
| 活跃用户 | 1次/5秒 | ~50万 | | 活跃用户 | 1次/5秒 | ~50万 |
| 普通浏览 | 1次/10秒 | ~100万 | | 普通浏览 | 1次/10秒 | ~100万 |
| 低频访问 | 1次/30秒 | ~300万 |
**生产环境建议**: 保留 30-50% 性能余量,安全并发用户数约 **50万 - 70万**
### 与主流框架性能对比
| 框架 | 典型QPS | 基础实现 | 性能评级 |
|------|---------|----------|----------|
| **HoTime** | **~100K** | net/http | 第一梯队 |
| Fiber | ~100K+ | fasthttp | 第一梯队 |
| Gin | ~60-80K | net/http | 第二梯队 |
| Echo | ~60-80K | net/http | 第二梯队 |
| Chi | ~50-60K | net/http | 第二梯队 |
## 框架对比 ## 框架对比
### 功能特性对比
| 特性 | HoTime | Gin | Echo | Fiber | | 特性 | HoTime | Gin | Echo | Fiber |
|------|--------|-----|------|-------| |------|--------|-----|------|-------|
| 性能 | 100K QPS | 70K QPS | 70K QPS | 100K QPS | | 性能 | 100K QPS | 70K QPS | 70K QPS | 100K QPS |
@ -60,8 +105,17 @@ go get code.hoteas.com/golang/hotime
| Session | ✅ 内置 | ❌ 需插件 | ❌ 需插件 | ❌ 需插件 | | Session | ✅ 内置 | ❌ 需插件 | ❌ 需插件 | ❌ 需插件 |
| 代码生成 | ✅ | ❌ | ❌ | ❌ | | 代码生成 | ✅ | ❌ | ❌ | ❌ |
| 微信/支付集成 | ✅ 内置 | ❌ | ❌ | ❌ | | 微信/支付集成 | ✅ 内置 | ❌ | ❌ | ❌ |
| 路由灵活性 | 中等 | 优秀 | 优秀 | 优秀 |
| 社区生态 | 较小 | 庞大 | 较大 | 较大 |
## 适用场景 ### HoTime 优势
1. **开箱即用** - 内置 ORM + 缓存 + Session无需额外集成
2. **三级缓存** - Memory > Redis > DB自动穿透与回填
3. **开发效率高** - 链式查询语法简洁,内置微信/云服务SDK
4. **性能优异** - 100K QPS媲美最快的 Fiber 框架
### 适用场景
| 场景 | 推荐度 | 说明 | | 场景 | 推荐度 | 说明 |
|------|--------|------| |------|--------|------|
@ -71,14 +125,97 @@ go get code.hoteas.com/golang/hotime
| 高并发API服务 | ⭐⭐⭐⭐ | 性能足够 | | 高并发API服务 | ⭐⭐⭐⭐ | 性能足够 |
| 大型微服务 | ⭐⭐⭐ | 建议用Gin/Echo | | 大型微服务 | ⭐⭐⭐ | 建议用Gin/Echo |
### 总体评价
| 维度 | 评分 | 说明 |
|------|------|------|
| 性能 | 95分 | 第一梯队媲美Fiber |
| 功能集成 | 90分 | 远超主流框架 |
| 开发效率 | 85分 | 适合快速开发 |
| 生态/社区 | 50分 | 持续建设中 |
> **总结**: HoTime 是"小而全"的高性能框架,性能不输主流,集成度远超主流,适合独立开发者或小团队快速构建中小型项目。
---
## 数据库操作
### 基础 CRUD
```go
// 查询单条
user := that.Db.Get("user", "*", Map{"id": 1})
// 查询列表
users := that.Db.Select("user", "*", Map{"status": 1, "ORDER": "id DESC"})
// 插入数据
id := that.Db.Insert("user", Map{"name": "test", "age": 18})
// 更新数据
rows := that.Db.Update("user", Map{"name": "new"}, Map{"id": 1})
// 删除数据
rows := that.Db.Delete("user", Map{"id": 1})
```
### 链式查询
```go
users := that.Db.Table("user").
LeftJoin("order", "user.id=order.user_id").
And("status", 1).
Order("id DESC").
Page(1, 10).
Select("*")
```
### 条件语法
| 语法 | 说明 | 示例 |
|------|------|------|
| key | 等于 | "id": 1 |
| key[>] | 大于 | "age[>]": 18 |
| key[<] | 小于 | "age[<]": 60 |
| key[!] | 不等于 | "status[!]": 0 |
| key[~] | LIKE | "name[~]": "test" |
| key[<>] | BETWEEN | "age[<>]": Slice{18, 60} |
## 缓存系统
三级缓存: **Memory > Redis > Database**
```go
// 通用缓存
that.Cache("key", value) // 设置
data := that.Cache("key") // 获取
that.Cache("key", nil) // 删除
// Session 缓存
that.Session("user_id", 123) // 设置
userId := that.Session("user_id") // 获取
```
## 中间件
```go
appIns.SetConnectListener(func(that *Context) bool {
if that.Session("user_id").Data == nil {
that.Display(2, "请先登录")
return true // 终止请求
}
return false // 继续处理
})
```
## 扩展功能 ## 扩展功能
- 微信支付/公众号/小程序 - `dri/wechat/` - **微信支付/公众号/小程序** - dri/wechat/
- 阿里云服务 - `dri/aliyun/` - **阿里云服务** - dri/aliyun/
- 腾讯云服务 - `dri/tencent/` - **腾讯云服务** - dri/tencent/
- 文件上传下载 - `dri/upload/`, `dri/download/` - **文件上传下载** - dri/upload/, dri/download/
- MongoDB - `dri/mongodb/` - **MongoDB** - dri/mongodb/
- RSA 加解密 - `dri/rsa/` - **RSA加解密** - dri/rsa/
## License ## License

10
cache/cache.go vendored
View File

@ -265,13 +265,9 @@ func (that *HoTimeCache) Init(config Map, hotimeDb HoTimeDBInterface, err ...*Er
} }
that.Config["db"] = db that.Config["db"] = db
that.dbCache = &CacheDb{ that.dbCache = &CacheDb{TimeOut: db.GetCeilInt64("timeout"),
TimeOut: db.GetCeilInt64("timeout"), DbSet: db.GetBool("db"), SessionSet: db.GetBool("session"),
DbSet: db.GetBool("db"), Db: hotimeDb}
SessionSet: db.GetBool("session"),
HistorySet: db.GetBool("history"),
Db: hotimeDb,
}
if err[0] != nil { if err[0] != nil {
that.dbCache.SetError(err[0]) that.dbCache.SetError(err[0])

487
cache/cache_db.go vendored
View File

@ -1,38 +1,11 @@
package cache package cache
import ( import (
. "code.hoteas.com/golang/hotime/common"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt"
"os"
"strings" "strings"
"time" "time"
. "code.hoteas.com/golang/hotime/common"
)
// #region agent log
func debugLog(hypothesisId, location, message string, data map[string]interface{}) {
logEntry := fmt.Sprintf(`{"hypothesisId":"%s","location":"%s","message":"%s","data":%s,"timestamp":%d,"sessionId":"debug-session"}`,
hypothesisId, location, message, toJSON(data), time.Now().UnixMilli())
f, _ := os.OpenFile(`d:\work\hotimev1.5\.cursor\debug.log`, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if f != nil {
f.WriteString(logEntry + "\n")
f.Close()
}
}
func toJSON(data map[string]interface{}) string {
b, _ := json.Marshal(data)
return string(b)
}
// #endregion
// 表名常量
const (
CacheTableName = "hotime_cache"
CacheHistoryTableName = "hotime_cache_history"
DefaultCacheTimeout = 24 * 60 * 60 // 默认过期时间 24 小时
) )
type HoTimeDBInterface interface { type HoTimeDBInterface interface {
@ -51,7 +24,6 @@ type CacheDb struct {
TimeOut int64 TimeOut int64
DbSet bool DbSet bool
SessionSet bool SessionSet bool
HistorySet bool // 是否开启历史记录
Db HoTimeDBInterface Db HoTimeDBInterface
*Error *Error
ContextBase ContextBase
@ -59,468 +31,143 @@ type CacheDb struct {
} }
func (that *CacheDb) GetError() *Error { func (that *CacheDb) GetError() *Error {
return that.Error return that.Error
} }
func (that *CacheDb) SetError(err *Error) { func (that *CacheDb) SetError(err *Error) {
that.Error = err that.Error = err
} }
// getTableName 获取带前缀的表名
func (that *CacheDb) getTableName() string {
return that.Db.GetPrefix() + CacheTableName
}
// getHistoryTableName 获取带前缀的历史表名
func (that *CacheDb) getHistoryTableName() string {
return that.Db.GetPrefix() + CacheHistoryTableName
}
// initDbTable 初始化数据库表
func (that *CacheDb) initDbTable() { func (that *CacheDb) initDbTable() {
if that.isInit { if that.isInit {
return return
} }
if that.Db.GetType() == "mysql" {
dbType := that.Db.GetType() dbNames := that.Db.Query("SELECT DATABASE()")
tableName := that.getTableName()
historyTableName := that.getHistoryTableName()
// #region agent log if len(dbNames) == 0 {
debugLog("F", "cache_db.go:initDbTable", "initDbTable started", map[string]interface{}{ return
"dbType": dbType, "tableName": tableName, "historyTableName": historyTableName, "HistorySet": that.HistorySet, }
}) dbName := dbNames[0].GetString("DATABASE()")
// #endregion res := that.Db.Query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='" + dbName + "' AND TABLE_NAME='" + that.Db.GetPrefix() + "cached'")
if len(res) != 0 {
// 检查并创建主表 that.isInit = true
if !that.tableExists(tableName) { return
that.createMainTable(dbType, tableName)
}
// 检查并迁移旧 cached 表
oldTableName := that.Db.GetPrefix() + "cached"
if that.tableExists(oldTableName) {
that.migrateFromCached(dbType, oldTableName, tableName)
}
// 检查并创建历史表(开启历史记录时)
historyTableExists := that.tableExists(historyTableName)
// #region agent log
debugLog("F", "cache_db.go:initDbTable", "history table check", map[string]interface{}{
"HistorySet": that.HistorySet, "historyTableName": historyTableName, "exists": historyTableExists,
"shouldCreate": that.HistorySet && !historyTableExists,
})
// #endregion
if that.HistorySet && !historyTableExists {
that.createHistoryTable(dbType, historyTableName)
// #region agent log
debugLog("F", "cache_db.go:initDbTable", "createHistoryTable called", map[string]interface{}{
"dbType": dbType, "historyTableName": historyTableName,
})
// #endregion
} }
_, e := that.Db.Exec("CREATE TABLE `" + that.Db.GetPrefix() + "cached` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `key` varchar(60) DEFAULT NULL, `value` varchar(2000) DEFAULT NULL, `time` bigint(20) DEFAULT NULL, `endtime` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=198740 DEFAULT CHARSET=utf8")
if e.GetError() == nil {
that.isInit = true that.isInit = true
} }
// tableExists 检查表是否存在
func (that *CacheDb) tableExists(tableName string) bool {
dbType := that.Db.GetType()
switch dbType {
case "mysql":
dbNames := that.Db.Query("SELECT DATABASE()")
if len(dbNames) == 0 {
return false
}
dbName := dbNames[0].GetString("DATABASE()")
res := that.Db.Query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='" + dbName + "' AND TABLE_NAME='" + tableName + "'")
return len(res) != 0
case "sqlite":
res := that.Db.Query(`SELECT name FROM sqlite_master WHERE type='table' AND name='` + tableName + `'`)
return len(res) != 0
case "postgres":
res := that.Db.Query(`SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename='` + tableName + `'`)
return len(res) != 0
} }
return false if that.Db.GetType() == "sqlite" {
} res := that.Db.Query(`select * from sqlite_master where type = 'table' and name = '` + that.Db.GetPrefix() + `cached'`)
// createMainTable 创建主表 if len(res) != 0 {
func (that *CacheDb) createMainTable(dbType, tableName string) { that.isInit = true
var createSQL string
switch dbType {
case "mysql":
createSQL = "CREATE TABLE `" + tableName + "` (" +
"`id` int(11) unsigned NOT NULL AUTO_INCREMENT," +
"`key` varchar(64) NOT NULL COMMENT '缓存键'," +
"`value` text DEFAULT NULL COMMENT '缓存值'," +
"`end_time` datetime DEFAULT NULL COMMENT '过期时间'," +
"`state` int(2) DEFAULT '0' COMMENT '状态:0-正常,1-异常,2-隐藏'," +
"`create_time` datetime DEFAULT NULL COMMENT '创建日期'," +
"`modify_time` datetime DEFAULT NULL COMMENT '变更时间'," +
"PRIMARY KEY (`id`)," +
"UNIQUE KEY `uk_key` (`key`)," +
"KEY `idx_end_time` (`end_time`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='缓存管理'"
case "sqlite":
createSQL = `CREATE TABLE "` + tableName + `" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"key" TEXT NOT NULL UNIQUE,
"value" TEXT,
"end_time" TEXT,
"state" INTEGER DEFAULT 0,
"create_time" TEXT,
"modify_time" TEXT
)`
case "postgres":
createSQL = `CREATE TABLE "` + tableName + `" (
"id" SERIAL PRIMARY KEY,
"key" VARCHAR(64) NOT NULL UNIQUE,
"value" TEXT,
"end_time" TIMESTAMP,
"state" INTEGER DEFAULT 0,
"create_time" TIMESTAMP,
"modify_time" TIMESTAMP
)`
that.Db.Exec(createSQL)
// 创建索引
that.Db.Exec(`CREATE INDEX "idx_` + tableName + `_end_time" ON "` + tableName + `" ("end_time")`)
return return
} }
_, e := that.Db.Exec(`CREATE TABLE "` + that.Db.GetPrefix() + `cached" (
that.Db.Exec(createSQL) "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"key" TEXT(60),
"value" TEXT(2000),
"time" integer,
"endtime" integer
);`)
if e.GetError() == nil {
that.isInit = true
} }
// createHistoryTable 创建历史表
func (that *CacheDb) createHistoryTable(dbType, tableName string) {
var createSQL string
switch dbType {
case "mysql":
createSQL = "CREATE TABLE `" + tableName + "` (" +
"`id` int(11) unsigned NOT NULL AUTO_INCREMENT," +
"`hotime_cache_id` int(11) unsigned DEFAULT NULL COMMENT '缓存ID'," +
"`key` varchar(64) DEFAULT NULL COMMENT '缓存键'," +
"`value` text DEFAULT NULL COMMENT '缓存值'," +
"`end_time` datetime DEFAULT NULL COMMENT '过期时间'," +
"`state` int(2) DEFAULT '0' COMMENT '状态:0-正常,1-异常,2-隐藏'," +
"`create_time` datetime DEFAULT NULL COMMENT '创建日期'," +
"`modify_time` datetime DEFAULT NULL COMMENT '变更时间'," +
"PRIMARY KEY (`id`)," +
"KEY `idx_hotime_cache_id` (`hotime_cache_id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='缓存历史'"
case "sqlite":
createSQL = `CREATE TABLE "` + tableName + `" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"hotime_cache_id" INTEGER,
"key" TEXT,
"value" TEXT,
"end_time" TEXT,
"state" INTEGER DEFAULT 0,
"create_time" TEXT,
"modify_time" TEXT
)`
case "postgres":
createSQL = `CREATE TABLE "` + tableName + `" (
"id" SERIAL PRIMARY KEY,
"hotime_cache_id" INTEGER,
"key" VARCHAR(64),
"value" TEXT,
"end_time" TIMESTAMP,
"state" INTEGER DEFAULT 0,
"create_time" TIMESTAMP,
"modify_time" TIMESTAMP
)`
that.Db.Exec(createSQL)
// 创建索引
that.Db.Exec(`CREATE INDEX "idx_` + tableName + `_cache_id" ON "` + tableName + `" ("hotime_cache_id")`)
return
} }
that.Db.Exec(createSQL)
} }
// migrateFromCached 从旧 cached 表迁移数据 // 获取Cache键只能为string类型
func (that *CacheDb) migrateFromCached(dbType, oldTableName, newTableName string) {
var migrateSQL string
switch dbType {
case "mysql":
// 去重迁移:取每个 key 的最后一条记录id 最大)
migrateSQL = "INSERT INTO `" + newTableName + "` (`key`, `value`, `end_time`, `state`, `create_time`, `modify_time`) " +
"SELECT c.`key`, c.`value`, FROM_UNIXTIME(c.`endtime`), 0, " +
"FROM_UNIXTIME(c.`time` / 1000000000), FROM_UNIXTIME(c.`time` / 1000000000) " +
"FROM `" + oldTableName + "` c " +
"INNER JOIN (SELECT `key`, MAX(id) as max_id FROM `" + oldTableName + "` GROUP BY `key`) m " +
"ON c.id = m.max_id"
case "sqlite":
migrateSQL = `INSERT INTO "` + newTableName + `" ("key", "value", "end_time", "state", "create_time", "modify_time") ` +
`SELECT c."key", c."value", datetime(c."endtime", 'unixepoch'), 0, ` +
`datetime(c."time" / 1000000000, 'unixepoch'), datetime(c."time" / 1000000000, 'unixepoch') ` +
`FROM "` + oldTableName + `" c ` +
`INNER JOIN (SELECT "key", MAX(id) as max_id FROM "` + oldTableName + `" GROUP BY "key") m ` +
`ON c.id = m.max_id`
case "postgres":
migrateSQL = `INSERT INTO "` + newTableName + `" ("key", "value", "end_time", "state", "create_time", "modify_time") ` +
`SELECT c."key", c."value", to_timestamp(c."endtime"), 0, ` +
`to_timestamp(c."time" / 1000000000), to_timestamp(c."time" / 1000000000) ` +
`FROM "` + oldTableName + `" c ` +
`INNER JOIN (SELECT "key", MAX(id) as max_id FROM "` + oldTableName + `" GROUP BY "key") m ` +
`ON c.id = m.max_id`
}
// 执行迁移
_, err := that.Db.Exec(migrateSQL)
if err.GetError() == nil {
// 迁移成功,删除旧表
var dropSQL string
switch dbType {
case "mysql":
dropSQL = "DROP TABLE `" + oldTableName + "`"
case "sqlite", "postgres":
dropSQL = `DROP TABLE "` + oldTableName + `"`
}
that.Db.Exec(dropSQL)
}
}
// writeHistory 写入历史记录
func (that *CacheDb) writeHistory(key string) {
if !that.HistorySet {
return
}
tableName := that.getTableName()
historyTableName := that.getHistoryTableName()
// 查询当前数据
cached := that.Db.Get(tableName, "*", Map{"key": key})
if cached == nil {
return
}
// 构建历史记录数据
historyData := Map{
"hotime_cache_id": cached.GetInt64("id"),
"key": cached.GetString("key"),
"value": cached.GetString("value"),
"end_time": cached.GetString("end_time"),
"state": cached.GetInt("state"),
"create_time": cached.GetString("create_time"),
"modify_time": cached.GetString("modify_time"),
}
// 插入历史表
that.Db.Insert(historyTableName, historyData)
// #region agent log
logFile3, _ := os.OpenFile(`d:\work\hotimev1.5\.cursor\debug.log`, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if logFile3 != nil {
fmt.Fprintf(logFile3, `{"hypothesisId":"C","location":"cache_db.go:writeHistory","message":"history written","data":{"key":"%s","cacheId":%d},"timestamp":%d,"sessionId":"debug-session"}`+"\n", key, cached.GetInt64("id"), time.Now().UnixMilli())
logFile3.Close()
}
// #endregion
}
// get 获取缓存
func (that *CacheDb) get(key string) interface{} { func (that *CacheDb) get(key string) interface{} {
tableName := that.getTableName()
cached := that.Db.Get(tableName, "*", Map{"key": key})
// #region agent log cached := that.Db.Get("cached", "*", Map{"key": key})
logFile4, _ := os.OpenFile(`d:\work\hotimev1.5\.cursor\debug.log`, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if logFile4 != nil {
found := cached != nil
fmt.Fprintf(logFile4, `{"hypothesisId":"D","location":"cache_db.go:get","message":"get query","data":{"key":"%s","found":%t},"timestamp":%d,"sessionId":"debug-session"}`+"\n", key, found, time.Now().UnixMilli())
logFile4.Close()
}
// #endregion
if cached == nil { if cached == nil {
return nil return nil
} }
//data:=cacheMap[key];
if cached.GetInt64("endtime") <= time.Now().Unix() {
// 使用字符串比较判断过期ISO 格式天然支持) that.Db.Delete("cached", Map{"id": cached.GetString("id")})
endTime := cached.GetString("end_time")
nowTime := Time2Str(time.Now())
if endTime != "" && endTime <= nowTime {
// 惰性删除:过期只返回 nil不立即删除
// 依赖随机清理批量删除过期数据
return nil return nil
} }
// 直接解析 value不再需要 {"data": value} 包装 data := Map{}
valueStr := cached.GetString("value") data.JsonToMap(cached.GetString("value"))
if valueStr == "" {
return nil return data.Get("data")
} }
var data interface{} // key value ,时间为时间戳
err := json.Unmarshal([]byte(valueStr), &data) func (that *CacheDb) set(key string, value interface{}, tim int64) {
if err != nil {
return nil bte, _ := json.Marshal(Map{"data": value})
num := that.Db.Update("cached", Map{"value": string(bte), "time": time.Now().UnixNano(), "endtime": tim}, Map{"key": key})
if num == int64(0) {
that.Db.Insert("cached", Map{"value": string(bte), "time": time.Now().UnixNano(), "endtime": tim, "key": key})
} }
// #region agent log //随机执行删除命令
logFile5, _ := os.OpenFile(`d:\work\hotimev1.5\.cursor\debug.log`, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if logFile5 != nil {
fmt.Fprintf(logFile5, `{"hypothesisId":"D","location":"cache_db.go:get","message":"get success","data":{"key":"%s","hasData":true},"timestamp":%d,"sessionId":"debug-session"}`+"\n", key, time.Now().UnixMilli())
logFile5.Close()
}
// #endregion
return data
}
// set 设置缓存
func (that *CacheDb) set(key string, value interface{}, endTime time.Time) {
// 直接序列化 value不再包装
bte, _ := json.Marshal(value)
nowTime := Time2Str(time.Now())
endTimeStr := Time2Str(endTime)
dbType := that.Db.GetType()
tableName := that.getTableName()
// 使用 UPSERT 语法解决并发问题
switch dbType {
case "mysql":
upsertSQL := "INSERT INTO `" + tableName + "` (`key`, `value`, `end_time`, `state`, `create_time`, `modify_time`) " +
"VALUES (?, ?, ?, 0, ?, ?) " +
"ON DUPLICATE KEY UPDATE `value`=VALUES(`value`), `end_time`=VALUES(`end_time`), `modify_time`=VALUES(`modify_time`)"
that.Db.Exec(upsertSQL, key, string(bte), endTimeStr, nowTime, nowTime)
case "sqlite":
// SQLite: INSERT OR REPLACE 会删除后插入,所以用 UPSERT 语法
upsertSQL := `INSERT INTO "` + tableName + `" ("key", "value", "end_time", "state", "create_time", "modify_time") ` +
`VALUES (?, ?, ?, 0, ?, ?) ` +
`ON CONFLICT("key") DO UPDATE SET "value"=excluded."value", "end_time"=excluded."end_time", "modify_time"=excluded."modify_time"`
that.Db.Exec(upsertSQL, key, string(bte), endTimeStr, nowTime, nowTime)
case "postgres":
upsertSQL := `INSERT INTO "` + tableName + `" ("key", "value", "end_time", "state", "create_time", "modify_time") ` +
`VALUES ($1, $2, $3, 0, $4, $5) ` +
`ON CONFLICT ("key") DO UPDATE SET "value"=EXCLUDED."value", "end_time"=EXCLUDED."end_time", "modify_time"=EXCLUDED."modify_time"`
that.Db.Exec(upsertSQL, key, string(bte), endTimeStr, nowTime, nowTime)
default:
// 兼容其他数据库:使用 Update + Insert
num := that.Db.Update(tableName, Map{
"value": string(bte),
"end_time": endTimeStr,
"modify_time": nowTime,
}, Map{"key": key})
if num == 0 {
that.Db.Insert(tableName, Map{
"key": key,
"value": string(bte),
"end_time": endTimeStr,
"state": 0,
"create_time": nowTime,
"modify_time": nowTime,
})
}
}
// #region agent log
logFile2, _ := os.OpenFile(`d:\work\hotimev1.5\.cursor\debug.log`, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if logFile2 != nil {
fmt.Fprintf(logFile2, `{"hypothesisId":"B","location":"cache_db.go:set","message":"set completed","data":{"key":"%s","endTime":"%s","dbType":"%s"},"timestamp":%d,"sessionId":"debug-session"}`+"\n", key, endTimeStr, dbType, time.Now().UnixMilli())
logFile2.Close()
}
// #endregion
// 写入历史记录
that.writeHistory(key)
// 随机执行删除过期数据命令5% 概率)
if Rand(1000) > 950 { if Rand(1000) > 950 {
nowTimeStr := Time2Str(time.Now()) that.Db.Delete("cached", Map{"endtime[<]": time.Now().Unix()})
that.Db.Delete(tableName, Map{"end_time[<]": nowTimeStr})
} }
} }
// delete 删除缓存
func (that *CacheDb) delete(key string) { func (that *CacheDb) delete(key string) {
tableName := that.getTableName()
del := strings.Index(key, "*") del := strings.Index(key, "*")
//如果通配删除 //如果通配删除
if del != -1 { if del != -1 {
key = Substr(key, 0, del) key = Substr(key, 0, del)
that.Db.Delete(tableName, Map{"key[~]": key + "%"}) that.Db.Delete("cached", Map{"key": key + "%"})
} else { } else {
that.Db.Delete(tableName, Map{"key": key}) that.Db.Delete("cached", Map{"key": key})
} }
} }
// Cache 缓存操作入口
// 用法:
// - Cache(key) - 获取缓存
// - Cache(key, value) - 设置缓存(使用默认过期时间)
// - Cache(key, value, timeout) - 设置缓存(指定过期时间,单位:秒)
// - Cache(key, nil) - 删除缓存
func (that *CacheDb) Cache(key string, data ...interface{}) *Obj { func (that *CacheDb) Cache(key string, data ...interface{}) *Obj {
that.initDbTable() that.initDbTable()
// #region agent log
logFile, _ := os.OpenFile(`d:\work\hotimev1.5\.cursor\debug.log`, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if logFile != nil {
op := "get"
if len(data) == 1 && data[0] == nil {
op = "delete"
} else if len(data) >= 1 {
op = "set"
}
fmt.Fprintf(logFile, `{"hypothesisId":"A","location":"cache_db.go:Cache","message":"Cache called","data":{"key":"%s","operation":"%s","dataLen":%d},"timestamp":%d,"sessionId":"debug-session"}`+"\n", key, op, len(data), time.Now().UnixMilli())
logFile.Close()
}
// #endregion
// 获取缓存
if len(data) == 0 { if len(data) == 0 {
return &Obj{Data: that.get(key)} return &Obj{Data: that.get(key)}
} }
tim := time.Now().Unix()
// 删除缓存
if len(data) == 1 && data[0] == nil { if len(data) == 1 && data[0] == nil {
that.delete(key) that.delete(key)
return &Obj{Data: nil} return &Obj{Data: nil}
} }
// 计算过期时间
var timeout int64
if len(data) == 1 { if len(data) == 1 {
// 使用配置的 TimeOut如果为 0 则使用默认值 if that.TimeOut == 0 {
timeout = that.TimeOut //that.Time = Config.GetInt64("cacheLongTime")
if timeout == 0 {
timeout = DefaultCacheTimeout
} }
} else if len(data) >= 2 { tim += that.TimeOut
// 使用指定的超时时间 }
if len(data) == 2 {
that.SetError(nil) that.SetError(nil)
tempTimeout := ObjToInt64(data[1], that.Error) tempt := ObjToInt64(data[1], that.Error)
if that.GetError() == nil && tempTimeout > 0 {
timeout = tempTimeout
} else {
timeout = that.TimeOut
if timeout == 0 {
timeout = DefaultCacheTimeout
}
}
}
endTime := time.Now().Add(time.Duration(timeout) * time.Second) if tempt > tim {
that.set(key, data[0], endTime) tim = tempt
} else if that.GetError() == nil {
tim = tim + tempt
}
}
that.set(key, data[0], tim)
return &Obj{Data: nil} return &Obj{Data: nil}
} }

View File

@ -120,9 +120,9 @@ func (that *MakeCode) Db2JSON(db *db.HoTimeDB, config Map) {
} }
//idSlice=append(idSlice,nowTables) //idSlice=append(idSlice,nowTables)
for _, v := range nowTables { for _, v := range nowTables {
// if v.GetString("name") == "cached" { if v.GetString("name") == "cached" {
// continue continue
// } }
if that.TableConfig.GetMap(v.GetString("name")) == nil { if that.TableConfig.GetMap(v.GetString("name")) == nil {
if v.GetString("label") == "" { if v.GetString("label") == "" {
v["label"] = v.GetString("name") v["label"] = v.GetString("name")

19
config/app.json Normal file
View File

@ -0,0 +1,19 @@
{
"flow": {},
"id": "d751713988987e9331980363e24189ce",
"label": "HoTime管理平台",
"labelConfig": {
"add": "添加",
"delete": "删除",
"download": "下载清单",
"edit": "编辑",
"info": "查看详情",
"show": "开启"
},
"menus": [],
"name": "admin",
"stop": [
"role",
"org"
]
}

38
config/config.json Normal file
View File

@ -0,0 +1,38 @@
{
"cache": {
"memory": {
"db": true,
"session": true,
"timeout": 7200
}
},
"codeConfig": [
{
"config": "config/app.json",
"mode": 0,
"name": "",
"rule": "config/rule.json",
"table": "admin"
}
],
"db": {
"sqlite": {
"path": "config/data.db"
}
},
"defFile": [
"index.html",
"index.htm"
],
"error": {
"1": "内部系统异常",
"2": "访问权限异常",
"3": "请求参数异常",
"4": "数据处理异常",
"5": "数据结果异常"
},
"mode": 2,
"port": "80",
"sessionName": "HOTIME",
"tpt": "tpt"
}

79
config/configNote.json Normal file
View 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": "默认服务ip127.0.0.1必须如果需要使用redis服务时配置",
"password": "默认密码空必须如果需要使用redis服务时配置默认密码空",
"port": "默认服务端口6379必须如果需要使用redis服务时配置",
"session": "默认true非必须缓存web session同时缓存session保持的用户缓存",
"timeout": "默认60 * 60 * 24 * 15非必须过期时间超时自动删除"
},
"注释": "可配置memorydbredis默认启用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开启此功能"
}

0
config/data.db Normal file
View File

422
config/rule.json Normal file
View File

@ -0,0 +1,422 @@
[
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "idcard",
"strict": false,
"type": ""
},
{
"add": false,
"edit": false,
"info": true,
"list": true,
"must": false,
"name": "id",
"strict": true,
"type": ""
},
{
"add": false,
"edit": false,
"info": true,
"list": true,
"must": false,
"name": "sn",
"strict": false,
"type": ""
},
{
"add": false,
"edit": false,
"info": false,
"list": false,
"must": false,
"name": "parent_ids",
"strict": true,
"type": "index"
},
{
"add": false,
"edit": false,
"info": false,
"list": false,
"must": false,
"name": "index",
"strict": true,
"type": "index"
},
{
"add": true,
"edit": true,
"info": true,
"list": true,
"must": false,
"name": "parent_id",
"true": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": true,
"must": false,
"name": "amount",
"strict": true,
"type": "money"
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "info",
"strict": false,
"type": "textArea"
},
{
"add": true,
"edit": true,
"info": true,
"list": true,
"must": false,
"name": "status",
"strict": false,
"type": "select"
},
{
"add": true,
"edit": true,
"info": true,
"list": true,
"must": false,
"name": "state",
"strict": false,
"type": "select"
},
{
"add": true,
"edit": true,
"info": true,
"list": true,
"must": false,
"name": "sex",
"strict": false,
"type": "select"
},
{
"add": false,
"edit": false,
"info": false,
"list": false,
"must": false,
"name": "delete",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "lat",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "lng",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "latitude",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "longitude",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": false,
"list": false,
"must": false,
"name": "password",
"strict": false,
"type": "password"
},
{
"add": true,
"edit": true,
"info": false,
"list": false,
"must": false,
"name": "pwd",
"strict": false,
"type": "password"
},
{
"add": false,
"edit": false,
"info": false,
"list": false,
"must": false,
"name": "version",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "seq",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "sort",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "note",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "description",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "abstract",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "content",
"strict": false,
"type": "textArea"
},
{
"add": true,
"edit": true,
"info": true,
"list": true,
"must": false,
"name": "address",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "full_name",
"strict": false,
"type": ""
},
{
"add": false,
"edit": false,
"info": true,
"list": false,
"must": false,
"name": "create_time",
"strict": true,
"type": "time"
},
{
"add": false,
"edit": false,
"info": true,
"list": true,
"must": false,
"name": "modify_time",
"strict": true,
"type": "time"
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "image",
"strict": false,
"type": "image"
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "img",
"strict": false,
"type": "image"
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "avatar",
"strict": false,
"type": "image"
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "icon",
"strict": false,
"type": "image"
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "file",
"strict": false,
"type": "file"
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "age",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "email",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": true,
"must": false,
"name": "time",
"strict": false,
"type": "time"
},
{
"add": false,
"edit": false,
"info": true,
"list": false,
"must": false,
"name": "level",
"strict": false,
"type": ""
},
{
"add": true,
"edit": true,
"info": true,
"list": true,
"must": false,
"name": "rule",
"strict": false,
"type": "form"
},
{
"add": true,
"edit": true,
"info": true,
"list": false,
"must": false,
"name": "auth",
"strict": false,
"type": "auth"
},
{
"add": false,
"edit": false,
"info": true,
"list": true,
"must": false,
"name": "table",
"strict": false,
"type": "table"
},
{
"add": false,
"edit": false,
"info": true,
"list": true,
"must": false,
"name": "table_id",
"strict": false,
"type": "table_id"
}
]

View File

@ -98,17 +98,17 @@ id := database.Insert("table", dataMap)
// 返回新插入记录的ID // 返回新插入记录的ID
``` ```
### 批量插入 (Inserts) - 新增 ### 批量插入 (BatchInsert) - 新增
```go ```go
// 使用 []Map 格式,更直观简洁 // 使用 []Map 格式,更直观简洁
affected := database.Inserts("table", []Map{ affected := database.BatchInsert("table", []Map{
{"col1": "val1", "col2": "val2", "col3": "val3"}, {"col1": "val1", "col2": "val2", "col3": "val3"},
{"col1": "val4", "col2": "val5", "col3": "val6"}, {"col1": "val4", "col2": "val5", "col3": "val6"},
}) })
// 返回受影响的行数 // 返回受影响的行数
// 支持 [#] 标记直接 SQL // 支持 [#] 标记直接 SQL
affected := database.Inserts("log", []Map{ affected := database.BatchInsert("log", []Map{
{"user_id": 1, "created_time[#]": "NOW()"}, {"user_id": 1, "created_time[#]": "NOW()"},
{"user_id": 2, "created_time[#]": "NOW()"}, {"user_id": 2, "created_time[#]": "NOW()"},
}) })
@ -465,7 +465,7 @@ stats := database.Select("order",
### 批量操作 ### 批量操作
```go ```go
// 批量插入(使用 []Map 格式) // 批量插入(使用 []Map 格式)
affected := database.Inserts("user", []Map{ affected := database.BatchInsert("user", []Map{
{"name": "用户1", "email": "user1@example.com", "status": 1}, {"name": "用户1", "email": "user1@example.com", "status": 1},
{"name": "用户2", "email": "user2@example.com", "status": 1}, {"name": "用户2", "email": "user2@example.com", "status": 1},
{"name": "用户3", "email": "user3@example.com", "status": 1}, {"name": "用户3", "email": "user3@example.com", "status": 1},
@ -511,6 +511,3 @@ result := database.Table("order").
*快速参考版本: 2.0* *快速参考版本: 2.0*
*更新日期: 2026年1月* *更新日期: 2026年1月*
**详细说明:**
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md) - 完整教程

View File

@ -12,7 +12,7 @@ HoTimeDB是一个基于Golang实现的轻量级ORM框架参考PHP Medoo设计
- [查询(Select)](#查询select) - [查询(Select)](#查询select)
- [获取单条记录(Get)](#获取单条记录get) - [获取单条记录(Get)](#获取单条记录get)
- [插入(Insert)](#插入insert) - [插入(Insert)](#插入insert)
- [批量插入(Inserts)](#批量插入Inserts) - [批量插入(BatchInsert)](#批量插入batchinsert)
- [更新(Update)](#更新update) - [更新(Update)](#更新update)
- [Upsert操作](#upsert操作) - [Upsert操作](#upsert操作)
- [删除(Delete)](#删除delete) - [删除(Delete)](#删除delete)
@ -223,11 +223,11 @@ id := database.Insert("user", common.Map{
fmt.Println("插入的用户ID:", id) fmt.Println("插入的用户ID:", id)
``` ```
### 批量插入(Inserts) ### 批量插入(BatchInsert)
```go ```go
// 批量插入多条记录(使用 []Map 格式,更直观) // 批量插入多条记录(使用 []Map 格式,更直观)
affected := database.Inserts("user", []common.Map{ affected := database.BatchInsert("user", []common.Map{
{"name": "张三", "email": "zhang@example.com", "age": 25}, {"name": "张三", "email": "zhang@example.com", "age": 25},
{"name": "李四", "email": "li@example.com", "age": 30}, {"name": "李四", "email": "li@example.com", "age": 30},
{"name": "王五", "email": "wang@example.com", "age": 28}, {"name": "王五", "email": "wang@example.com", "age": 28},
@ -237,7 +237,7 @@ affected := database.Inserts("user", []common.Map{
fmt.Printf("批量插入 %d 条记录\n", affected) fmt.Printf("批量插入 %d 条记录\n", affected)
// 支持 [#] 标记直接插入 SQL 表达式 // 支持 [#] 标记直接插入 SQL 表达式
affected := database.Inserts("log", []common.Map{ affected := database.BatchInsert("log", []common.Map{
{"user_id": 1, "action": "login", "created_time[#]": "NOW()"}, {"user_id": 1, "action": "login", "created_time[#]": "NOW()"},
{"user_id": 2, "action": "logout", "created_time[#]": "NOW()"}, {"user_id": 2, "action": "logout", "created_time[#]": "NOW()"},
}) })
@ -808,7 +808,7 @@ A1: 不再需要!现在多条件会自动用 AND 连接。当然,使用 `AND
A2: 在 `Action` 函数中返回 `false` 即可触发回滚,所有操作都会被撤销。 A2: 在 `Action` 函数中返回 `false` 即可触发回滚,所有操作都会被撤销。
### Q3: 缓存何时会被清除? ### Q3: 缓存何时会被清除?
A3: 执行 `Insert``Update``Delete``Upsert``Inserts` 操作时会自动清除对应表的缓存。 A3: 执行 `Insert``Update``Delete``Upsert``BatchInsert` 操作时会自动清除对应表的缓存。
### Q4: 如何执行复杂的原生SQL ### Q4: 如何执行复杂的原生SQL
A4: 使用 `Query` 方法执行查询,使用 `Exec` 方法执行更新操作。 A4: 使用 `Query` 方法执行查询,使用 `Exec` 方法执行更新操作。
@ -828,6 +828,3 @@ A7: 框架会自动处理差异(占位符、引号等),代码无需修改
*最后更新: 2026年1月* *最后更新: 2026年1月*
> 本文档基于HoTimeDB源码分析生成如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念但根据Golang语言特性进行了适配和优化。 > 本文档基于HoTimeDB源码分析生成如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念但根据Golang语言特性进行了适配和优化。
**更多参考:**
- [HoTimeDB API 参考](HoTimeDB_API参考.md) - API 速查手册

252
db/README.md Normal file
View File

@ -0,0 +1,252 @@
# HoTimeDB ORM 文档集合
这是HoTimeDB ORM框架的完整文档集合包含使用说明、API参考、示例代码和测试数据。
## ⚠️ 重要更新说明
**语法修正通知**经过对源码的深入分析发现HoTimeDB的条件查询语法有特定规则
- ✅ **单个条件**可以直接写在Map中
- ⚠️ **多个条件**:必须使用`AND``OR`包装
- 📝 所有文档和示例代码已按正确语法更新
## 📚 文档列表
### 1. [HoTimeDB_使用说明.md](./HoTimeDB_使用说明.md)
**完整使用说明书** - 详细的功能介绍和使用指南
- 🚀 快速开始
- ⚙️ 数据库配置
- 🔧 基本操作 (CRUD)
- 🔗 链式查询构建器
- 🔍 条件查询语法
- 🔄 JOIN操作
- 📄 分页查询
- 📊 聚合函数
- 🔐 事务处理
- 💾 缓存机制
- ⚡ 高级特性
### 2. [HoTimeDB_API参考.md](./HoTimeDB_API参考.md)
**快速API参考手册** - 开发时的速查手册
- 📖 基本方法
- 🔧 CRUD操作
- 📊 聚合函数
- 📄 分页查询
- 🔍 条件语法参考
- 🔗 JOIN语法
- 🔐 事务处理
- 🛠️ 工具方法
### 3. [示例代码文件](../examples/hotimedb_examples.go)
**完整示例代码集合** - 可运行的实际应用示例(语法已修正)
- 🏗️ 基本初始化和配置
- 📝 基本CRUD操作
- 🔗 链式查询操作
- 🤝 JOIN查询操作
- 🔍 条件查询语法
- 📄 分页查询
- 📊 聚合函数查询
- 🔐 事务处理
- 💾 缓存机制
- 🔧 原生SQL执行
- 🚨 错误处理和调试
- ⚡ 性能优化技巧
- 🎯 完整应用示例
### 4. [test_tables.sql](./test_tables.sql)
**测试数据库结构** - 快速搭建测试环境
- 🏗️ 完整的表结构定义
- 📊 测试数据插入
- 🔍 索引优化
- 👁️ 视图示例
- 🔧 存储过程示例
## 🎯 核心特性
### 🌟 主要优势
- **类Medoo语法**: 参考PHP Medoo设计语法简洁易懂
- **链式查询**: 支持流畅的链式查询构建器
- **条件丰富**: 支持丰富的条件查询语法
- **事务支持**: 完整的事务处理机制
- **缓存集成**: 内置查询结果缓存
- **读写分离**: 支持主从数据库配置
- **类型安全**: 基于Golang的强类型系统
### 🔧 支持的数据库
- ✅ MySQL
- ✅ SQLite
- ✅ 其他标准SQL数据库
## 🚀 快速开始
### 1. 安装依赖
```bash
go mod init your-project
go get github.com/go-sql-driver/mysql
go get github.com/sirupsen/logrus
```
### 2. 创建测试数据库
```bash
mysql -u root -p < test_tables.sql
```
### 3. 基本使用
```go
import (
"code.hoteas.com/golang/hotime/db"
"code.hoteas.com/golang/hotime/common"
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
// 初始化数据库
database := &db.HoTimeDB{
Prefix: "app_",
Mode: 2, // 开发模式
}
database.SetConnect(func() (master, slave *sql.DB) {
master, _ = sql.Open("mysql", "user:pass@tcp(localhost:3306)/dbname")
return master, master
})
// 链式查询链式语法支持单独Where然后用And添加条件
users := database.Table("user").
Where("status", 1). // 链式中可以单独Where
And("age[>]", 18). // 用And添加更多条件
Order("created_time DESC").
Limit(0, 10).
Select("id,name,email")
// 或者使用传统语法多个条件必须用AND包装
users2 := database.Select("user", "id,name,email", common.Map{
"AND": common.Map{
"status": 1,
"age[>]": 18,
},
"ORDER": "created_time DESC",
"LIMIT": []int{0, 10},
})
```
## ⚠️ 重要语法规则
**条件查询语法规则:**
- ✅ **单个条件**可以直接写在Map中
- ✅ **多个条件**:必须使用`AND``OR`包装
- ✅ **特殊参数**`ORDER``GROUP``LIMIT`与条件同级
```go
// ✅ 正确:单个条件
Map{"status": 1}
// ✅ 正确多个条件用AND包装
Map{
"AND": Map{
"status": 1,
"age[>]": 18,
},
"ORDER": "id DESC",
}
// ❌ 错误多个条件不用AND包装
Map{
"status": 1,
"age[>]": 18, // 不支持!
}
```
## 📝 条件查询语法速查
| 语法 | SQL | 说明 |
|------|-----|------|
| `"field": value` | `field = ?` | 等于 |
| `"field[!]": value` | `field != ?` | 不等于 |
| `"field[>]": value` | `field > ?` | 大于 |
| `"field[>=]": value` | `field >= ?` | 大于等于 |
| `"field[<]": value` | `field < ?` | 小于 |
| `"field[<=]": value` | `field <= ?` | 小于等于 |
| `"field[~]": "keyword"` | `field LIKE '%keyword%'` | 包含 |
| `"field[<>]": [min, max]` | `field BETWEEN ? AND ?` | 区间内 |
| `"field": [v1, v2, v3]` | `field IN (?, ?, ?)` | 在集合中 |
| `"field": nil` | `field IS NULL` | 为空 |
| `"field[#]": "NOW()"` | `field = NOW()` | 直接SQL |
## 🔗 JOIN语法速查
| 语法 | SQL | 说明 |
|------|-----|------|
| `"[>]table"` | `LEFT JOIN` | 左连接 |
| `"[<]table"` | `RIGHT JOIN` | 右连接 |
| `"[><]table"` | `INNER JOIN` | 内连接 |
| `"[<>]table"` | `FULL JOIN` | 全连接 |
## 🛠️ 链式方法速查
```go
db.Table("table") // 指定表名
.Where(key, value) // WHERE条件
.And(key, value) // AND条件
.Or(map) // OR条件
.LeftJoin(table, on) // LEFT JOIN
.Order(fields...) // ORDER BY
.Group(fields...) // GROUP BY
.Limit(offset, limit) // LIMIT
.Page(page, pageSize) // 分页
.Select(fields...) // 查询
.Get(fields...) // 获取单条
.Count() // 计数
.Update(data) // 更新
.Delete() // 删除
```
## ⚡ 性能优化建议
### 🔍 查询优化
- 使用合适的索引字段作为查询条件
- IN查询会自动优化为BETWEEN连续数字
- 避免SELECT *,指定需要的字段
- 合理使用LIMIT限制结果集大小
### 💾 缓存使用
- 查询结果会自动缓存
- 增删改操作会自动清除缓存
- `cached`表不参与缓存
### 🔐 事务处理
- 批量操作使用事务提高性能
- 事务中避免长时间操作
- 合理设置事务隔离级别
## 🚨 注意事项
### 🔒 安全相关
- 使用参数化查询防止SQL注入
- `[#]`语法需要注意防止注入
- 敏感数据加密存储
### 🎯 最佳实践
- 开发时设置`Mode = 2`便于调试
- 生产环境设置`Mode = 0`
- 合理设置表前缀
- 定期检查慢查询日志
## 🤝 与PHP Medoo的差异
1. **类型系统**: 使用`common.Map``common.Slice`
2. **错误处理**: Golang风格的错误处理
3. **链式调用**: 提供更丰富的链式API
4. **缓存集成**: 内置缓存功能
5. **并发安全**: 需要注意并发使用
## 📞 技术支持
- 📧 查看源码:`hotimedb.go`
- 📖 参考文档:本目录下的各个文档文件
- 🔧 示例代码:运行`HoTimeDB_示例代码.go`中的示例
---
**HoTimeDB ORM框架 - 让数据库操作更简单!** 🎉
> 本文档基于HoTimeDB源码分析生成参考了PHP Medoo的设计理念并根据Golang语言特性进行了优化。

View File

@ -87,26 +87,17 @@ func (that *HoTimeDB) Select(table string, qu ...interface{}) []Map {
join = true join = true
} }
processor := that.GetProcessor()
if len(qu) > 0 { if len(qu) > 0 {
if reflect.ValueOf(qu[intQs]).Type().String() == "string" { if reflect.ValueOf(qu[intQs]).Type().String() == "string" {
// 字段列表字符串,使用处理器处理 table.column 格式 query += " " + qu[intQs].(string)
fieldStr := qu[intQs].(string)
if fieldStr != "*" {
fieldStr = processor.ProcessFieldList(fieldStr)
}
query += " " + fieldStr
} else { } else {
data := ObjToSlice(qu[intQs]) data := ObjToSlice(qu[intQs])
for i := 0; i < len(data); i++ { for i := 0; i < len(data); i++ {
k := data.GetString(i) k := data.GetString(i)
if strings.Contains(k, " AS ") || strings.Contains(k, ".") { if strings.Contains(k, " AS ") || strings.Contains(k, ".") {
// 处理 table.column 格式 query += " " + k + " "
query += " " + processor.ProcessFieldList(k) + " "
} else { } else {
// 单独的列名 query += " `" + k + "` "
query += " " + processor.ProcessColumnNoPrefix(k) + " "
} }
if i+1 != len(data) { if i+1 != len(data) {
@ -118,8 +109,11 @@ func (that *HoTimeDB) Select(table string, qu ...interface{}) []Map {
query += " *" query += " *"
} }
// 处理表名(添加前缀和正确的引号) if !strings.Contains(table, ".") && !strings.Contains(table, " AS ") {
query += " FROM " + processor.ProcessTableName(table) + " " query += " FROM `" + that.Prefix + table + "` "
} else {
query += " FROM " + that.Prefix + table + " "
}
if join { if join {
query += that.buildJoin(qu[0]) query += that.buildJoin(qu[0])
@ -163,7 +157,6 @@ func (that *HoTimeDB) buildJoin(joinData interface{}) string {
query := "" query := ""
var testQu = []string{} var testQu = []string{}
testQuData := Map{} testQuData := Map{}
processor := that.GetProcessor()
if reflect.ValueOf(joinData).Type().String() == "common.Map" { if reflect.ValueOf(joinData).Type().String() == "common.Map" {
testQuData = joinData.(Map) testQuData = joinData.(Map)
@ -191,34 +184,36 @@ func (that *HoTimeDB) buildJoin(joinData interface{}) string {
case "[>]": case "[>]":
func() { func() {
table := Substr(k, 3, len(k)-3) table := Substr(k, 3, len(k)-3)
// 处理表名(添加前缀和正确的引号) if !strings.Contains(table, " ") {
table = processor.ProcessTableName(table) table = "`" + table + "`"
// 处理 ON 条件中的 table.column }
onCondition := processor.ProcessConditionString(v.(string)) query += " LEFT JOIN " + table + " ON " + v.(string) + " "
query += " LEFT JOIN " + table + " ON " + onCondition + " "
}() }()
case "[<]": case "[<]":
func() { func() {
table := Substr(k, 3, len(k)-3) table := Substr(k, 3, len(k)-3)
table = processor.ProcessTableName(table) if !strings.Contains(table, " ") {
onCondition := processor.ProcessConditionString(v.(string)) table = "`" + table + "`"
query += " RIGHT JOIN " + table + " ON " + onCondition + " " }
query += " RIGHT JOIN " + table + " ON " + v.(string) + " "
}() }()
} }
switch Substr(k, 0, 4) { switch Substr(k, 0, 4) {
case "[<>]": case "[<>]":
func() { func() {
table := Substr(k, 4, len(k)-4) table := Substr(k, 4, len(k)-4)
table = processor.ProcessTableName(table) if !strings.Contains(table, " ") {
onCondition := processor.ProcessConditionString(v.(string)) table = "`" + table + "`"
query += " FULL JOIN " + table + " ON " + onCondition + " " }
query += " FULL JOIN " + table + " ON " + v.(string) + " "
}() }()
case "[><]": case "[><]":
func() { func() {
table := Substr(k, 4, len(k)-4) table := Substr(k, 4, len(k)-4)
table = processor.ProcessTableName(table) if !strings.Contains(table, " ") {
onCondition := processor.ProcessConditionString(v.(string)) table = "`" + table + "`"
query += " INNER JOIN " + table + " ON " + onCondition + " " }
query += " INNER JOIN " + table + " ON " + v.(string) + " "
}() }()
} }
} }
@ -228,16 +223,15 @@ func (that *HoTimeDB) buildJoin(joinData interface{}) string {
// Get 获取单条记录 // Get 获取单条记录
func (that *HoTimeDB) Get(table string, qu ...interface{}) Map { func (that *HoTimeDB) Get(table string, qu ...interface{}) Map {
if len(qu) == 0 { if len(qu) == 1 {
// 没有参数时,添加默认字段和 LIMIT
qu = append(qu, "*", Map{"LIMIT": 1})
} else if len(qu) == 1 {
qu = append(qu, Map{"LIMIT": 1}) qu = append(qu, Map{"LIMIT": 1})
} else if len(qu) == 2 { }
if len(qu) == 2 {
temp := qu[1].(Map) temp := qu[1].(Map)
temp["LIMIT"] = 1 temp["LIMIT"] = 1
qu[1] = temp qu[1] = temp
} else if len(qu) == 3 { }
if len(qu) == 3 {
temp := qu[2].(Map) temp := qu[2].(Map)
temp["LIMIT"] = 1 temp["LIMIT"] = 1
qu[2] = temp qu[2] = temp
@ -255,7 +249,6 @@ func (that *HoTimeDB) Insert(table string, data map[string]interface{}) int64 {
values := make([]interface{}, 0) values := make([]interface{}, 0)
queryString := " (" queryString := " ("
valueString := " (" valueString := " ("
processor := that.GetProcessor()
lens := len(data) lens := len(data)
tempLen := 0 tempLen := 0
@ -268,25 +261,25 @@ func (that *HoTimeDB) Insert(table string, data map[string]interface{}) int64 {
k = strings.Replace(k, "[#]", "", -1) k = strings.Replace(k, "[#]", "", -1)
vstr = ObjToStr(v) vstr = ObjToStr(v)
if tempLen < lens { if tempLen < lens {
queryString += processor.ProcessColumnNoPrefix(k) + "," queryString += "`" + k + "`,"
valueString += vstr + "," valueString += vstr + ","
} else { } else {
queryString += processor.ProcessColumnNoPrefix(k) + ") " queryString += "`" + k + "`) "
valueString += vstr + ");" valueString += vstr + ");"
} }
} else { } else {
values = append(values, v) values = append(values, v)
if tempLen < lens { if tempLen < lens {
queryString += processor.ProcessColumnNoPrefix(k) + "," queryString += "`" + k + "`,"
valueString += "?," valueString += "?,"
} else { } else {
queryString += processor.ProcessColumnNoPrefix(k) + ") " queryString += "`" + k + "`) "
valueString += "?);" valueString += "?);"
} }
} }
} }
query := "INSERT INTO " + processor.ProcessTableName(table) + " " + queryString + "VALUES" + valueString query := "INSERT INTO `" + that.Prefix + table + "` " + queryString + "VALUES" + valueString
res, err := that.Exec(query, values...) res, err := that.Exec(query, values...)
@ -307,19 +300,19 @@ func (that *HoTimeDB) Insert(table string, data map[string]interface{}) int64 {
return id return id
} }
// Inserts 批量插入数据 // BatchInsert 批量插入数据
// table: 表名 // table: 表名
// dataList: 数据列表,每个元素是一个 Map // dataList: 数据列表,每个元素是一个 Map
// 返回受影响的行数 // 返回受影响的行数
// //
// 示例: // 示例:
// //
// affected := db.Inserts("user", []Map{ // affected := db.BatchInsert("user", []Map{
// {"name": "张三", "age": 25, "email": "zhang@example.com"}, // {"name": "张三", "age": 25, "email": "zhang@example.com"},
// {"name": "李四", "age": 30, "email": "li@example.com"}, // {"name": "李四", "age": 30, "email": "li@example.com"},
// {"name": "王五", "age": 28, "email": "wang@example.com"}, // {"name": "王五", "age": 28, "email": "wang@example.com"},
// }) // })
func (that *HoTimeDB) Inserts(table string, dataList []Map) int64 { func (that *HoTimeDB) BatchInsert(table string, dataList []Map) int64 {
if len(dataList) == 0 { if len(dataList) == 0 {
return 0 return 0
} }
@ -340,12 +333,10 @@ func (that *HoTimeDB) Inserts(table string, dataList []Map) int64 {
// 排序列名以确保一致性 // 排序列名以确保一致性
sort.Strings(columns) sort.Strings(columns)
processor := that.GetProcessor()
// 构建列名部分 // 构建列名部分
quotedCols := make([]string, len(columns)) quotedCols := make([]string, len(columns))
for i, col := range columns { for i, col := range columns {
quotedCols[i] = processor.ProcessColumnNoPrefix(col) quotedCols[i] = "`" + col + "`"
} }
colStr := strings.Join(quotedCols, ", ") colStr := strings.Join(quotedCols, ", ")
@ -376,7 +367,7 @@ func (that *HoTimeDB) Inserts(table string, dataList []Map) int64 {
placeholders[i] = "(" + strings.Join(rowPlaceholders, ", ") + ")" placeholders[i] = "(" + strings.Join(rowPlaceholders, ", ") + ")"
} }
query := "INSERT INTO " + processor.ProcessTableName(table) + " (" + colStr + ") VALUES " + strings.Join(placeholders, ", ") query := "INSERT INTO `" + that.Prefix + table + "` (" + colStr + ") VALUES " + strings.Join(placeholders, ", ")
res, err := that.Exec(query, values...) res, err := that.Exec(query, values...)
@ -503,12 +494,11 @@ func (that *HoTimeDB) Upsert(table string, data Map, uniqueKeys Slice, updateCol
func (that *HoTimeDB) buildMySQLUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string { func (that *HoTimeDB) buildMySQLUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string {
// INSERT INTO table (col1, col2) VALUES (?, ?) // INSERT INTO table (col1, col2) VALUES (?, ?)
// ON DUPLICATE KEY UPDATE col1 = VALUES(col1), col2 = VALUES(col2) // ON DUPLICATE KEY UPDATE col1 = VALUES(col1), col2 = VALUES(col2)
processor := that.GetProcessor()
quotedCols := make([]string, len(columns)) quotedCols := make([]string, len(columns))
valueParts := make([]string, len(columns)) valueParts := make([]string, len(columns))
for i, col := range columns { for i, col := range columns {
quotedCols[i] = processor.ProcessColumnNoPrefix(col) quotedCols[i] = "`" + col + "`"
if raw, ok := rawValues[col]; ok { if raw, ok := rawValues[col]; ok {
valueParts[i] = raw valueParts[i] = raw
} else { } else {
@ -518,15 +508,14 @@ func (that *HoTimeDB) buildMySQLUpsert(table string, columns []string, uniqueKey
updateParts := make([]string, len(updateColumns)) updateParts := make([]string, len(updateColumns))
for i, col := range updateColumns { for i, col := range updateColumns {
quotedCol := processor.ProcessColumnNoPrefix(col)
if raw, ok := rawValues[col]; ok { if raw, ok := rawValues[col]; ok {
updateParts[i] = quotedCol + " = " + raw updateParts[i] = "`" + col + "` = " + raw
} else { } else {
updateParts[i] = quotedCol + " = VALUES(" + quotedCol + ")" updateParts[i] = "`" + col + "` = VALUES(`" + col + "`)"
} }
} }
return "INSERT INTO " + processor.ProcessTableName(table) + " (" + strings.Join(quotedCols, ", ") + return "INSERT INTO `" + that.Prefix + table + "` (" + strings.Join(quotedCols, ", ") +
") VALUES (" + strings.Join(valueParts, ", ") + ") VALUES (" + strings.Join(valueParts, ", ") +
") ON DUPLICATE KEY UPDATE " + strings.Join(updateParts, ", ") ") ON DUPLICATE KEY UPDATE " + strings.Join(updateParts, ", ")
} }
@ -535,14 +524,12 @@ func (that *HoTimeDB) buildMySQLUpsert(table string, columns []string, uniqueKey
func (that *HoTimeDB) buildPostgresUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string { 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) // INSERT INTO table (col1, col2) VALUES ($1, $2)
// ON CONFLICT (unique_key) DO UPDATE SET col1 = EXCLUDED.col1 // ON CONFLICT (unique_key) DO UPDATE SET col1 = EXCLUDED.col1
processor := that.GetProcessor()
dialect := that.GetDialect()
quotedCols := make([]string, len(columns)) quotedCols := make([]string, len(columns))
valueParts := make([]string, len(columns)) valueParts := make([]string, len(columns))
paramIndex := 1 paramIndex := 1
for i, col := range columns { for i, col := range columns {
quotedCols[i] = dialect.QuoteIdentifier(col) quotedCols[i] = "\"" + col + "\""
if raw, ok := rawValues[col]; ok { if raw, ok := rawValues[col]; ok {
valueParts[i] = raw valueParts[i] = raw
} else { } else {
@ -553,20 +540,19 @@ func (that *HoTimeDB) buildPostgresUpsert(table string, columns []string, unique
quotedUniqueKeys := make([]string, len(uniqueKeys)) quotedUniqueKeys := make([]string, len(uniqueKeys))
for i, key := range uniqueKeys { for i, key := range uniqueKeys {
quotedUniqueKeys[i] = dialect.QuoteIdentifier(key) quotedUniqueKeys[i] = "\"" + key + "\""
} }
updateParts := make([]string, len(updateColumns)) updateParts := make([]string, len(updateColumns))
for i, col := range updateColumns { for i, col := range updateColumns {
quotedCol := dialect.QuoteIdentifier(col)
if raw, ok := rawValues[col]; ok { if raw, ok := rawValues[col]; ok {
updateParts[i] = quotedCol + " = " + raw updateParts[i] = "\"" + col + "\" = " + raw
} else { } else {
updateParts[i] = quotedCol + " = EXCLUDED." + quotedCol updateParts[i] = "\"" + col + "\" = EXCLUDED.\"" + col + "\""
} }
} }
return "INSERT INTO " + processor.ProcessTableName(table) + " (" + strings.Join(quotedCols, ", ") + return "INSERT INTO \"" + that.Prefix + table + "\" (" + strings.Join(quotedCols, ", ") +
") VALUES (" + strings.Join(valueParts, ", ") + ") VALUES (" + strings.Join(valueParts, ", ") +
") ON CONFLICT (" + strings.Join(quotedUniqueKeys, ", ") + ") ON CONFLICT (" + strings.Join(quotedUniqueKeys, ", ") +
") DO UPDATE SET " + strings.Join(updateParts, ", ") ") DO UPDATE SET " + strings.Join(updateParts, ", ")
@ -576,13 +562,11 @@ func (that *HoTimeDB) buildPostgresUpsert(table string, columns []string, unique
func (that *HoTimeDB) buildSQLiteUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string { func (that *HoTimeDB) buildSQLiteUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string {
// INSERT INTO table (col1, col2) VALUES (?, ?) // INSERT INTO table (col1, col2) VALUES (?, ?)
// ON CONFLICT (unique_key) DO UPDATE SET col1 = excluded.col1 // ON CONFLICT (unique_key) DO UPDATE SET col1 = excluded.col1
processor := that.GetProcessor()
dialect := that.GetDialect()
quotedCols := make([]string, len(columns)) quotedCols := make([]string, len(columns))
valueParts := make([]string, len(columns)) valueParts := make([]string, len(columns))
for i, col := range columns { for i, col := range columns {
quotedCols[i] = dialect.QuoteIdentifier(col) quotedCols[i] = "\"" + col + "\""
if raw, ok := rawValues[col]; ok { if raw, ok := rawValues[col]; ok {
valueParts[i] = raw valueParts[i] = raw
} else { } else {
@ -592,20 +576,19 @@ func (that *HoTimeDB) buildSQLiteUpsert(table string, columns []string, uniqueKe
quotedUniqueKeys := make([]string, len(uniqueKeys)) quotedUniqueKeys := make([]string, len(uniqueKeys))
for i, key := range uniqueKeys { for i, key := range uniqueKeys {
quotedUniqueKeys[i] = dialect.QuoteIdentifier(key) quotedUniqueKeys[i] = "\"" + key + "\""
} }
updateParts := make([]string, len(updateColumns)) updateParts := make([]string, len(updateColumns))
for i, col := range updateColumns { for i, col := range updateColumns {
quotedCol := dialect.QuoteIdentifier(col)
if raw, ok := rawValues[col]; ok { if raw, ok := rawValues[col]; ok {
updateParts[i] = quotedCol + " = " + raw updateParts[i] = "\"" + col + "\" = " + raw
} else { } else {
updateParts[i] = quotedCol + " = excluded." + quotedCol updateParts[i] = "\"" + col + "\" = excluded.\"" + col + "\""
} }
} }
return "INSERT INTO " + processor.ProcessTableName(table) + " (" + strings.Join(quotedCols, ", ") + return "INSERT INTO \"" + that.Prefix + table + "\" (" + strings.Join(quotedCols, ", ") +
") VALUES (" + strings.Join(valueParts, ", ") + ") VALUES (" + strings.Join(valueParts, ", ") +
") ON CONFLICT (" + strings.Join(quotedUniqueKeys, ", ") + ") ON CONFLICT (" + strings.Join(quotedUniqueKeys, ", ") +
") DO UPDATE SET " + strings.Join(updateParts, ", ") ") DO UPDATE SET " + strings.Join(updateParts, ", ")
@ -613,8 +596,7 @@ func (that *HoTimeDB) buildSQLiteUpsert(table string, columns []string, uniqueKe
// Update 更新数据 // Update 更新数据
func (that *HoTimeDB) Update(table string, data Map, where Map) int64 { func (that *HoTimeDB) Update(table string, data Map, where Map) int64 {
processor := that.GetProcessor() query := "UPDATE `" + that.Prefix + table + "` SET "
query := "UPDATE " + processor.ProcessTableName(table) + " SET "
qs := make([]interface{}, 0) qs := make([]interface{}, 0)
tp := len(data) tp := len(data)
@ -626,7 +608,7 @@ func (that *HoTimeDB) Update(table string, data Map, where Map) int64 {
} else { } else {
qs = append(qs, v) qs = append(qs, v)
} }
query += processor.ProcessColumnNoPrefix(k) + "=" + vstr + " " query += "`" + k + "`=" + vstr + " "
if tp--; tp != 0 { if tp--; tp != 0 {
query += ", " query += ", "
} }
@ -656,8 +638,7 @@ func (that *HoTimeDB) Update(table string, data Map, where Map) int64 {
// Delete 删除数据 // Delete 删除数据
func (that *HoTimeDB) Delete(table string, data map[string]interface{}) int64 { func (that *HoTimeDB) Delete(table string, data map[string]interface{}) int64 {
processor := that.GetProcessor() query := "DELETE FROM `" + that.Prefix + table + "` "
query := "DELETE FROM " + processor.ProcessTableName(table) + " "
temp, resWhere := that.where(data) temp, resWhere := that.where(data)
query += temp + ";" query += temp + ";"

View File

@ -4,12 +4,10 @@ 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"
"database/sql" "database/sql"
"strings"
"sync"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"sync"
) )
// HoTimeDB 数据库操作核心结构体 // HoTimeDB 数据库操作核心结构体
@ -100,48 +98,3 @@ func (that *HoTimeDB) GetType() string {
func (that *HoTimeDB) GetPrefix() string { func (that *HoTimeDB) GetPrefix() string {
return that.Prefix 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
}

View File

@ -14,15 +14,6 @@ type Dialect interface {
// SQLite 使用双引号或方括号 "name" 或 [name] // SQLite 使用双引号或方括号 "name" 或 [name]
Quote(name string) string Quote(name string) string
// QuoteIdentifier 处理单个标识符(去除已有引号,添加正确引号)
// 输入可能带有反引号或双引号,会先去除再添加正确格式
QuoteIdentifier(name string) string
// QuoteChar 返回引号字符
// MySQL: `
// PostgreSQL/SQLite: "
QuoteChar() string
// Placeholder 生成占位符 // Placeholder 生成占位符
// MySQL/SQLite 使用 ? // MySQL/SQLite 使用 ?
// PostgreSQL 使用 $1, $2, $3... // PostgreSQL 使用 $1, $2, $3...
@ -63,16 +54,6 @@ func (d *MySQLDialect) Quote(name string) string {
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 { func (d *MySQLDialect) Placeholder(index int) string {
return "?" return "?"
} }
@ -140,16 +121,6 @@ func (d *PostgreSQLDialect) Quote(name string) string {
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 { func (d *PostgreSQLDialect) Placeholder(index int) string {
return fmt.Sprintf("$%d", index) return fmt.Sprintf("$%d", index)
} }
@ -221,16 +192,6 @@ func (d *SQLiteDialect) Quote(name string) string {
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 { func (d *SQLiteDialect) Placeholder(index int) string {
return "?" return "?"
} }

View File

@ -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"
}

View File

@ -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
}

View File

@ -1,13 +1,12 @@
package db package db
import ( import (
. "code.hoteas.com/golang/hotime/common"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"reflect" "reflect"
"strings" "strings"
. "code.hoteas.com/golang/hotime/common"
) )
// md5 生成查询的 MD5 哈希(用于缓存) // md5 生成查询的 MD5 哈希(用于缓存)
@ -24,9 +23,6 @@ func (that *HoTimeDB) Query(query string, args ...interface{}) []Map {
// queryWithRetry 内部查询方法,支持重试标记 // queryWithRetry 内部查询方法,支持重试标记
func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interface{}) []Map { func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interface{}) []Map {
// 预处理数组占位符 ?[]
query, args = that.expandArrayPlaceholder(query, args)
// 保存调试信息(加锁保护) // 保存调试信息(加锁保护)
that.mu.Lock() that.mu.Lock()
that.LastQuery = query that.LastQuery = query
@ -86,9 +82,6 @@ func (that *HoTimeDB) Exec(query string, args ...interface{}) (sql.Result, *Erro
// execWithRetry 内部执行方法,支持重试标记 // execWithRetry 内部执行方法,支持重试标记
func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interface{}) (sql.Result, *Error) { func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interface{}) (sql.Result, *Error) {
// 预处理数组占位符 ?[]
query, args = that.expandArrayPlaceholder(query, args)
// 保存调试信息(加锁保护) // 保存调试信息(加锁保护)
that.mu.Lock() that.mu.Lock()
that.LastQuery = query that.LastQuery = query
@ -162,202 +155,6 @@ func (that *HoTimeDB) processArgs(args []interface{}) []interface{} {
return processedArgs 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 数据库数据解析 // Row 数据库数据解析
func (that *HoTimeDB) Row(resl *sql.Rows) []Map { func (that *HoTimeDB) Row(resl *sql.Rows) []Map {
dest := make([]Map, 0) dest := make([]Map, 0)

332
db/test_tables.sql Normal file
View File

@ -0,0 +1,332 @@
-- HoTimeDB 测试表结构
-- 用于测试和示例的MySQL表结构定义
-- 请根据实际需要修改表结构和字段类型
-- 创建数据库(可选)
CREATE DATABASE IF NOT EXISTS `hotimedb_test` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `hotimedb_test`;
-- 用户表
DROP TABLE IF EXISTS `app_user`;
CREATE TABLE `app_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '用户姓名',
`email` varchar(255) NOT NULL DEFAULT '' COMMENT '邮箱地址',
`password` varchar(255) NOT NULL DEFAULT '' COMMENT '密码hash',
`phone` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
`age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
`gender` tinyint(4) NOT NULL DEFAULT '0' COMMENT '性别 0-未知 1-男 2-女',
`avatar` varchar(500) NOT NULL DEFAULT '' COMMENT '头像URL',
`level` varchar(20) NOT NULL DEFAULT 'normal' COMMENT '用户等级 normal-普通 vip-会员 svip-超级会员',
`balance` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '账户余额',
`login_count` int(11) NOT NULL DEFAULT '0' COMMENT '登录次数',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态 1-正常 0-禁用 -1-删除',
`last_login` datetime DEFAULT NULL COMMENT '最后登录时间',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_email` (`email`),
KEY `idx_status` (`status`),
KEY `idx_level` (`level`),
KEY `idx_created_time` (`created_time`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
-- 用户资料表
DROP TABLE IF EXISTS `app_profile`;
CREATE TABLE `app_profile` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
`real_name` varchar(50) NOT NULL DEFAULT '' COMMENT '真实姓名',
`id_card` varchar(20) NOT NULL DEFAULT '' COMMENT '身份证号',
`address` varchar(500) NOT NULL DEFAULT '' COMMENT '地址',
`bio` text COMMENT '个人简介',
`verified` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否认证 1-是 0-否',
`preferences` json DEFAULT NULL COMMENT '用户偏好设置JSON',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_id` (`user_id`),
KEY `idx_verified` (`verified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资料表';
-- 部门表
DROP TABLE IF EXISTS `app_department`;
CREATE TABLE `app_department` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '部门ID',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '部门名称',
`parent_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '上级部门ID',
`manager_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '部门经理ID',
`description` text COMMENT '部门描述',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态 1-正常 0-禁用',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_parent_id` (`parent_id`),
KEY `idx_manager_id` (`manager_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门表';
-- 商品表
DROP TABLE IF EXISTS `app_product`;
CREATE TABLE `app_product` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`title` varchar(200) NOT NULL DEFAULT '' COMMENT '商品标题',
`description` text COMMENT '商品描述',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '商品价格',
`stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存数量',
`category_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '分类ID',
`brand` varchar(100) NOT NULL DEFAULT '' COMMENT '品牌',
`tags` varchar(500) NOT NULL DEFAULT '' COMMENT '标签,逗号分隔',
`images` json DEFAULT NULL COMMENT '商品图片JSON数组',
`attributes` json DEFAULT NULL COMMENT '商品属性JSON',
`sales_count` int(11) NOT NULL DEFAULT '0' COMMENT '销售数量',
`view_count` int(11) NOT NULL DEFAULT '0' COMMENT '浏览数量',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态 1-上架 0-下架',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_category_id` (`category_id`),
KEY `idx_price` (`price`),
KEY `idx_status` (`status`),
KEY `idx_created_time` (`created_time`),
FULLTEXT KEY `ft_title_description` (`title`,`description`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品表';
-- 订单表
DROP TABLE IF EXISTS `app_order`;
CREATE TABLE `app_order` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(50) NOT NULL DEFAULT '' COMMENT '订单号',
`user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
`product_id` bigint(20) unsigned NOT NULL COMMENT '商品ID',
`quantity` int(11) NOT NULL DEFAULT '1' COMMENT '数量',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '单价',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '总金额',
`discount_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠金额',
`final_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '实付金额',
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT '订单状态 pending-待付款 paid-已付款 shipped-已发货 completed-已完成 cancelled-已取消',
`payment_method` varchar(20) NOT NULL DEFAULT '' COMMENT '支付方式',
`shipping_address` json DEFAULT NULL COMMENT '收货地址JSON',
`remark` text COMMENT '订单备注',
`paid_time` datetime DEFAULT NULL COMMENT '支付时间',
`shipped_time` datetime DEFAULT NULL COMMENT '发货时间',
`completed_time` datetime DEFAULT NULL COMMENT '完成时间',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_product_id` (`product_id`),
KEY `idx_status` (`status`),
KEY `idx_created_time` (`created_time`),
KEY `idx_paid_time` (`paid_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';
-- 订单详情表
DROP TABLE IF EXISTS `app_order_detail`;
CREATE TABLE `app_order_detail` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`order_id` bigint(20) unsigned NOT NULL COMMENT '订单ID',
`product_id` bigint(20) unsigned NOT NULL COMMENT '商品ID',
`product_title` varchar(200) NOT NULL DEFAULT '' COMMENT '商品标题',
`product_image` varchar(500) NOT NULL DEFAULT '' COMMENT '商品图片',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '单价',
`quantity` int(11) NOT NULL DEFAULT '1' COMMENT '数量',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '小计',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单详情表';
-- 支付日志表
DROP TABLE IF EXISTS `app_payment_log`;
CREATE TABLE `app_payment_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
`order_id` bigint(20) unsigned DEFAULT NULL COMMENT '订单ID',
`transaction_id` varchar(100) NOT NULL DEFAULT '' COMMENT '交易ID',
`type` varchar(20) NOT NULL DEFAULT '' COMMENT '类型 order_payment-订单支付 recharge-充值 refund-退款',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '金额',
`method` varchar(20) NOT NULL DEFAULT '' COMMENT '支付方式 balance-余额 alipay-支付宝 wechat-微信',
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT '状态 pending-处理中 success-成功 failed-失败',
`description` varchar(255) NOT NULL DEFAULT '' COMMENT '描述',
`extra_data` json DEFAULT NULL COMMENT '额外数据JSON',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_transaction_id` (`transaction_id`),
KEY `idx_type` (`type`),
KEY `idx_status` (`status`),
KEY `idx_created_time` (`created_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付日志表';
-- 转账日志表
DROP TABLE IF EXISTS `app_transfer_log`;
CREATE TABLE `app_transfer_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`from_user_id` bigint(20) unsigned NOT NULL COMMENT '转出用户ID',
`to_user_id` bigint(20) unsigned NOT NULL COMMENT '转入用户ID',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '转账金额',
`type` varchar(20) NOT NULL DEFAULT 'transfer' COMMENT '类型',
`status` varchar(20) NOT NULL DEFAULT 'success' COMMENT '状态',
`description` varchar(255) NOT NULL DEFAULT '' COMMENT '描述',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_from_user_id` (`from_user_id`),
KEY `idx_to_user_id` (`to_user_id`),
KEY `idx_created_time` (`created_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='转账日志表';
-- 操作日志表
DROP TABLE IF EXISTS `app_operation_log`;
CREATE TABLE `app_operation_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`module` varchar(50) NOT NULL DEFAULT '' COMMENT '模块',
`action` varchar(50) NOT NULL DEFAULT '' COMMENT '操作',
`description` varchar(255) NOT NULL DEFAULT '' COMMENT '描述',
`ip` varchar(45) NOT NULL DEFAULT '' COMMENT 'IP地址',
`user_agent` varchar(500) NOT NULL DEFAULT '' COMMENT '用户代理',
`request_data` json DEFAULT NULL COMMENT '请求数据JSON',
`response_data` json DEFAULT NULL COMMENT '响应数据JSON',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态 1-成功 0-失败',
`execution_time` int(11) NOT NULL DEFAULT '0' COMMENT '执行时间(毫秒)',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_module` (`module`),
KEY `idx_action` (`action`),
KEY `idx_created_time` (`created_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
-- 缓存表HoTimeDB内置缓存使用
DROP TABLE IF EXISTS `app_cached`;
CREATE TABLE `app_cached` (
`key` varchar(255) NOT NULL COMMENT '缓存键',
`value` longtext COMMENT '缓存值',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`key`),
KEY `idx_expire_time` (`expire_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='缓存表';
-- 批量用户表(用于批量操作示例)
DROP TABLE IF EXISTS `app_user_batch`;
CREATE TABLE `app_user_batch` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '用户姓名',
`email` varchar(255) NOT NULL DEFAULT '' COMMENT '邮箱地址',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_created_time` (`created_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='批量用户表';
-- 插入测试数据
INSERT INTO `app_user` (`name`, `email`, `password`, `age`, `level`, `balance`, `status`, `created_time`) VALUES
('张三', 'zhangsan@example.com', 'hashed_password_1', 25, 'normal', 1000.00, 1, '2023-01-15 10:30:00'),
('李四', 'lisi@example.com', 'hashed_password_2', 30, 'vip', 5000.00, 1, '2023-02-20 14:20:00'),
('王五', 'wangwu@example.com', 'hashed_password_3', 28, 'svip', 10000.00, 1, '2023-03-10 16:45:00'),
('赵六', 'zhaoliu@example.com', 'hashed_password_4', 35, 'normal', 500.00, 1, '2023-04-05 09:15:00'),
('钱七', 'qianqi@example.com', 'hashed_password_5', 22, 'vip', 2500.00, 0, '2023-05-12 11:30:00');
INSERT INTO `app_profile` (`user_id`, `real_name`, `verified`) VALUES
(1, '张三', 1),
(2, '李四', 1),
(3, '王五', 1),
(4, '赵六', 0),
(5, '钱七', 0);
INSERT INTO `app_department` (`name`, `parent_id`, `description`) VALUES
('技术部', 0, '负责技术开发和维护'),
('产品部', 0, '负责产品设计和规划'),
('市场部', 0, '负责市场推广和销售'),
('前端组', 1, '负责前端开发'),
('后端组', 1, '负责后端开发');
INSERT INTO `app_product` (`title`, `description`, `price`, `stock`, `category_id`, `brand`, `sales_count`) VALUES
('苹果手机', '最新款苹果手机,性能强劲', 6999.00, 100, 1, '苹果', 50),
('华为手机', '国产精品手机,拍照出色', 4999.00, 200, 1, '华为', 80),
('小米手机', '性价比之王,配置丰富', 2999.00, 300, 1, '小米', 120),
('联想笔记本', '商务办公首选,稳定可靠', 5999.00, 50, 2, '联想', 30),
('戴尔笔记本', '游戏性能出色,散热良好', 8999.00, 30, 2, '戴尔', 15);
INSERT INTO `app_order` (`order_no`, `user_id`, `product_id`, `quantity`, `price`, `amount`, `final_amount`, `status`, `created_time`) VALUES
('ORD202301150001', 1, 1, 1, 6999.00, 6999.00, 6999.00, 'paid', '2023-01-15 15:30:00'),
('ORD202301160001', 2, 2, 2, 4999.00, 9998.00, 9998.00, 'paid', '2023-01-16 10:20:00'),
('ORD202301170001', 3, 3, 1, 2999.00, 2999.00, 2999.00, 'completed', '2023-01-17 14:15:00'),
('ORD202301180001', 1, 4, 1, 5999.00, 5999.00, 5999.00, 'pending', '2023-01-18 16:45:00'),
('ORD202301190001', 4, 5, 1, 8999.00, 8999.00, 8999.00, 'cancelled', '2023-01-19 11:30:00');
INSERT INTO `app_order_detail` (`order_id`, `product_id`, `product_title`, `price`, `quantity`, `amount`) VALUES
(1, 1, '苹果手机', 6999.00, 1, 6999.00),
(2, 2, '华为手机', 4999.00, 2, 9998.00),
(3, 3, '小米手机', 2999.00, 1, 2999.00),
(4, 4, '联想笔记本', 5999.00, 1, 5999.00),
(5, 5, '戴尔笔记本', 8999.00, 1, 8999.00);
INSERT INTO `app_payment_log` (`user_id`, `order_id`, `type`, `amount`, `method`, `status`, `description`) VALUES
(1, 1, 'order_payment', 6999.00, 'balance', 'success', '订单支付'),
(2, 2, 'order_payment', 9998.00, 'alipay', 'success', '订单支付'),
(3, 3, 'order_payment', 2999.00, 'wechat', 'success', '订单支付'),
(2, NULL, 'recharge', 10000.00, 'alipay', 'success', '账户充值'),
(3, NULL, 'recharge', 5000.00, 'wechat', 'success', '账户充值');
-- 创建索引优化查询性能
CREATE INDEX idx_user_email_status ON app_user(email, status);
CREATE INDEX idx_order_user_status ON app_order(user_id, status);
CREATE INDEX idx_order_created_status ON app_order(created_time, status);
CREATE INDEX idx_product_category_status ON app_product(category_id, status);
-- 创建视图(可选)
CREATE OR REPLACE VIEW v_user_order_stats AS
SELECT
u.id as user_id,
u.name as user_name,
u.email,
u.level,
COUNT(o.id) as order_count,
COALESCE(SUM(o.final_amount), 0) as total_amount,
COALESCE(AVG(o.final_amount), 0) as avg_amount,
MAX(o.created_time) as last_order_time
FROM app_user u
LEFT JOIN app_order o ON u.id = o.user_id AND o.status IN ('paid', 'completed')
WHERE u.status = 1
GROUP BY u.id;
-- 存储过程示例(可选)
DELIMITER //
CREATE PROCEDURE GetUserOrderSummary(IN p_user_id BIGINT)
BEGIN
SELECT
u.name,
u.email,
u.level,
u.balance,
COUNT(o.id) as order_count,
COALESCE(SUM(CASE WHEN o.status = 'paid' THEN o.final_amount END), 0) as paid_amount,
COALESCE(SUM(CASE WHEN o.status = 'completed' THEN o.final_amount END), 0) as completed_amount
FROM app_user u
LEFT JOIN app_order o ON u.id = o.user_id
WHERE u.id = p_user_id AND u.status = 1
GROUP BY u.id;
END //
DELIMITER ;
-- 显示表结构信息
SELECT
TABLE_NAME as '表名',
TABLE_COMMENT as '表注释',
TABLE_ROWS as '预估行数',
ROUND(DATA_LENGTH/1024/1024, 2) as '数据大小(MB)'
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE 'app_%'
ORDER BY TABLE_NAME;

View File

@ -70,8 +70,8 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
} }
sort.Strings(testQu) sort.Strings(testQu)
// 追踪条件数量,用于自动添加 AND // 追踪普通条件数量,用于自动添加 AND
condCount := 0 normalCondCount := 0
for _, k := range testQu { for _, k := range testQu {
v := data[k] v := data[k]
@ -79,16 +79,8 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
// 检查是否是 AND/OR 条件关键字 // 检查是否是 AND/OR 条件关键字
if isConditionKey(k) { if isConditionKey(k) {
tw, ts := that.cond(strings.ToUpper(k), v.(Map)) tw, ts := that.cond(strings.ToUpper(k), v.(Map))
if tw != "" && strings.TrimSpace(tw) != "" { where += tw
// 与前面的条件用 AND 连接
if condCount > 0 {
where += " AND "
}
// 用括号包裹 OR/AND 组条件
where += "(" + strings.TrimSpace(tw) + ")"
condCount++
res = append(res, ts...) res = append(res, ts...)
}
continue continue
} }
@ -98,61 +90,37 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
} }
// 处理普通条件字段 // 处理普通条件字段
// 空切片的 IN 条件应该生成永假条件1=0而不是跳过
if v != nil && reflect.ValueOf(v).Type().String() == "common.Slice" && len(v.(Slice)) == 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 continue
} }
if v != nil && strings.Contains(reflect.ValueOf(v).Type().String(), "[]") && len(ObjToSlice(v)) == 0 { 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 continue
} }
tv, vv := that.varCond(k, v) tv, vv := that.varCond(k, v)
if tv != "" { if tv != "" {
// 自动添加 AND 连接符 // 自动添加 AND 连接符
if condCount > 0 { if normalCondCount > 0 {
where += " AND " where += " AND "
} }
where += tv where += tv
condCount++ normalCondCount++
res = append(res, vv...) res = append(res, vv...)
} }
} }
// 添加 WHERE 关键字 // 添加 WHERE 关键字
// 先去除首尾空格,检查是否有实际条件内容 if len(where) != 0 {
trimmedWhere := strings.TrimSpace(where)
if len(trimmedWhere) != 0 {
hasWhere := true hasWhere := true
for _, v := range vcond { for _, v := range vcond {
if strings.Index(trimmedWhere, v) == 0 { if strings.Index(where, v) == 0 {
hasWhere = false hasWhere = false
} }
} }
if hasWhere { if hasWhere {
where = " WHERE " + trimmedWhere + " " where = " WHERE " + where + " "
} }
} else {
// 没有实际条件内容,重置 where
where = ""
} }
// 处理特殊字符按固定顺序GROUP, HAVING, ORDER, LIMIT, OFFSET // 处理特殊字符按固定顺序GROUP, HAVING, ORDER, LIMIT, OFFSET
@ -214,7 +182,6 @@ func (that *HoTimeDB) varCond(k string, v interface{}) (string, []interface{}) {
where := "" where := ""
res := make([]interface{}, 0) res := make([]interface{}, 0)
length := len(k) length := len(k)
processor := that.GetProcessor()
if k == "[#]" { if k == "[#]" {
k = strings.Replace(k, "[#]", "", -1) k = strings.Replace(k, "[#]", "", -1)
@ -228,53 +195,73 @@ func (that *HoTimeDB) varCond(k string, v interface{}) (string, []interface{}) {
switch Substr(k, length-3, 3) { switch Substr(k, length-3, 3) {
case "[>]": case "[>]":
k = strings.Replace(k, "[>]", "", -1) k = strings.Replace(k, "[>]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += k + ">? " where += k + ">? "
res = append(res, v) res = append(res, v)
case "[<]": case "[<]":
k = strings.Replace(k, "[<]", "", -1) k = strings.Replace(k, "[<]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += k + "<? " where += k + "<? "
res = append(res, v) res = append(res, v)
case "[!]": case "[!]":
k = strings.Replace(k, "[!]", "", -1) k = strings.Replace(k, "[!]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where, res = that.notIn(k, v, where, res) where, res = that.notIn(k, v, where, res)
case "[#]": case "[#]":
k = strings.Replace(k, "[#]", "", -1) k = strings.Replace(k, "[#]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += " " + k + "=" + ObjToStr(v) + " " where += " " + k + "=" + ObjToStr(v) + " "
case "[##]": // 直接添加value到sql需要考虑防注入 case "[##]": // 直接添加value到sql需要考虑防注入
where += " " + ObjToStr(v) where += " " + ObjToStr(v)
case "[#!]": case "[#!]":
k = strings.Replace(k, "[#!]", "", -1) k = strings.Replace(k, "[#!]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += " " + k + "!=" + ObjToStr(v) + " " where += " " + k + "!=" + ObjToStr(v) + " "
case "[!#]": case "[!#]":
k = strings.Replace(k, "[!#]", "", -1) k = strings.Replace(k, "[!#]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += " " + k + "!=" + ObjToStr(v) + " " where += " " + k + "!=" + ObjToStr(v) + " "
case "[~]": case "[~]":
k = strings.Replace(k, "[~]", "", -1) k = strings.Replace(k, "[~]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += k + " LIKE ? " where += k + " LIKE ? "
v = "%" + ObjToStr(v) + "%" v = "%" + ObjToStr(v) + "%"
res = append(res, v) res = append(res, v)
case "[!~]": // 左边任意 case "[!~]": // 左边任意
k = strings.Replace(k, "[!~]", "", -1) k = strings.Replace(k, "[!~]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += k + " LIKE ? " where += k + " LIKE ? "
v = "%" + ObjToStr(v) + "" v = "%" + ObjToStr(v) + ""
res = append(res, v) res = append(res, v)
case "[~!]": // 右边任意 case "[~!]": // 右边任意
k = strings.Replace(k, "[~!]", "", -1) k = strings.Replace(k, "[~!]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += k + " LIKE ? " where += k + " LIKE ? "
v = ObjToStr(v) + "%" v = ObjToStr(v) + "%"
res = append(res, v) res = append(res, v)
case "[~~]": // 手动任意 case "[~~]": // 手动任意
k = strings.Replace(k, "[~~]", "", -1) k = strings.Replace(k, "[~~]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += k + " LIKE ? " where += k + " LIKE ? "
res = append(res, v) res = append(res, v)
default: default:
@ -285,24 +272,32 @@ func (that *HoTimeDB) varCond(k string, v interface{}) (string, []interface{}) {
switch Substr(k, length-4, 4) { switch Substr(k, length-4, 4) {
case "[>=]": case "[>=]":
k = strings.Replace(k, "[>=]", "", -1) k = strings.Replace(k, "[>=]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += k + ">=? " where += k + ">=? "
res = append(res, v) res = append(res, v)
case "[<=]": case "[<=]":
k = strings.Replace(k, "[<=]", "", -1) k = strings.Replace(k, "[<=]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += k + "<=? " where += k + "<=? "
res = append(res, v) res = append(res, v)
case "[><]": case "[><]":
k = strings.Replace(k, "[><]", "", -1) k = strings.Replace(k, "[><]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += k + " NOT BETWEEN ? AND ? " where += k + " NOT BETWEEN ? AND ? "
vs := ObjToSlice(v) vs := ObjToSlice(v)
res = append(res, vs[0]) res = append(res, vs[0])
res = append(res, vs[1]) res = append(res, vs[1])
case "[<>]": case "[<>]":
k = strings.Replace(k, "[<>]", "", -1) k = strings.Replace(k, "[<>]", "", -1)
k = processor.ProcessColumn(k) + " " if !strings.Contains(k, ".") {
k = "`" + k + "` "
}
where += k + " BETWEEN ? AND ? " where += k + " BETWEEN ? AND ? "
vs := ObjToSlice(v) vs := ObjToSlice(v)
res = append(res, vs[0]) res = append(res, vs[0])
@ -320,14 +315,13 @@ func (that *HoTimeDB) varCond(k string, v interface{}) (string, []interface{}) {
// handleDefaultCondition 处理默认条件(带方括号但不是特殊操作符) // handleDefaultCondition 处理默认条件(带方括号但不是特殊操作符)
func (that *HoTimeDB) handleDefaultCondition(k string, v interface{}, where string, res []interface{}) (string, []interface{}) { func (that *HoTimeDB) handleDefaultCondition(k string, v interface{}, where string, res []interface{}) (string, []interface{}) {
processor := that.GetProcessor() if !strings.Contains(k, ".") {
k = processor.ProcessColumn(k) + " " k = "`" + k + "` "
}
if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") { if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
vs := ObjToSlice(v) vs := ObjToSlice(v)
if len(vs) == 0 { if len(vs) == 0 {
// IN 空数组 -> 生成永假条件
where += "1=0 "
return where, res return where, res
} }
@ -349,16 +343,15 @@ func (that *HoTimeDB) handleDefaultCondition(k string, v interface{}, where stri
// handlePlainField 处理普通字段(无方括号) // handlePlainField 处理普通字段(无方括号)
func (that *HoTimeDB) handlePlainField(k string, v interface{}, where string, res []interface{}) (string, []interface{}) { func (that *HoTimeDB) handlePlainField(k string, v interface{}, where string, res []interface{}) (string, []interface{}) {
processor := that.GetProcessor() if !strings.Contains(k, ".") {
k = processor.ProcessColumn(k) + " " k = "`" + k + "` "
}
if v == nil { if v == nil {
where += k + " IS NULL " where += k + " IS NULL "
} else if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") { } else if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
vs := ObjToSlice(v) vs := ObjToSlice(v)
if len(vs) == 0 { if len(vs) == 0 {
// IN 空数组 -> 生成永假条件
where += "1=0 "
return where, res return where, res
} }

View File

@ -1,757 +0,0 @@
# 代码生成配置规范
本文档详细说明 HoTime 框架代码生成器的配置体系,包括 `config.json` 中的 `codeConfig`、菜单权限配置和字段规则配置。
---
## 配置体系概览
```
config.json
└── codeConfig[] # 代码生成配置数组(支持多套)
├── config # 菜单权限配置文件路径(如 admin.json
├── configDB # 数据库生成的完整配置
├── rule # 字段规则配置文件路径(如 rule.json
├── table # 管理员表名
├── name # 生成代码的包名
└── mode # 生成模式
```
---
## 一、config.json 中的 codeConfig
### 配置结构
```json
{
"codeConfig": [
{
"config": "config/admin.json",
"configDB": "config/adminDB.json",
"mode": 0,
"name": "",
"rule": "config/rule.json",
"table": "admin"
}
]
}
```
### 配置项说明
| 字段 | 类型 | 说明 |
|------|------|------|
| config | string | 菜单权限配置文件路径,用于定义菜单结构、权限控制 |
| configDB | string | 代码生成器输出的完整配置文件(自动生成) |
| rule | string | 字段规则配置文件路径,定义字段在增删改查中的行为 |
| table | string | 管理员/用户表名,用于身份验证和权限控制 |
| name | string | 生成的代码包名,为空则不生成代码文件 |
| mode | int | 生成模式0-仅配置不生成代码非0-生成代码文件 |
### 多配置支持
可以配置多个独立的代码生成实例,适用于多端场景:
```json
{
"codeConfig": [
{
"config": "config/admin.json",
"table": "admin",
"rule": "config/rule.json"
},
{
"config": "config/user.json",
"table": "user",
"rule": "config/rule.json"
}
]
}
```
---
## 二、菜单权限配置(如 admin.json
### 配置文件更新机制
- **首次运行**:代码生成器会根据数据库表结构自动创建配置文件(如 admin.json
- **后续运行**:配置文件**不会自动更新**,避免覆盖手动修改的内容
- **重新生成**:如需重新生成,删除配置文件后重新运行即可
- **参考更新**:可参考 `configDB` 指定的文件(如 adminDB.json查看最新的数据库结构变化手动调整配置
### 完整配置结构
```json
{
"id": "唯一标识(自动生成)",
"name": "admin",
"label": "管理平台名称",
"labelConfig": { ... },
"menus": [ ... ],
"flow": { ... }
}
```
### 2.1 label 配置
定义系统显示名称:
```json
{
"label": "HoTime管理平台"
}
```
### 2.2 labelConfig 配置
定义权限的显示文字:
```json
{
"labelConfig": {
"show": "开启",
"add": "添加",
"delete": "删除",
"edit": "编辑",
"info": "查看详情",
"download": "下载清单"
}
}
```
| 操作 | 说明 |
|------|------|
| show | 显示/查看列表权限 |
| add | 添加数据权限 |
| delete | 删除数据权限 |
| edit | 编辑数据权限 |
| info | 查看详情权限 |
| download | 下载/导出权限 |
### 2.3 menus 配置
定义菜单结构,支持多级嵌套:
```json
{
"menus": [
{
"label": "系统管理",
"name": "sys",
"icon": "Setting",
"auth": ["show"],
"menus": [
{
"label": "用户管理",
"table": "user",
"auth": ["show", "add", "delete", "edit", "info", "download"]
},
{
"label": "角色管理",
"table": "role",
"auth": ["show", "add", "delete", "edit", "info"]
}
]
},
{
"label": "文章管理",
"table": "article",
"icon": "Document",
"auth": ["show", "add", "edit", "info"]
}
]
}
```
#### menus 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| label | string | 菜单显示名称 |
| name | string | 菜单标识(用于分组,不绑定表时使用) |
| table | string | 绑定的数据表名(用于自动生成 CRUD |
| icon | string | 菜单图标名称 |
| auth | array | 权限数组,定义该菜单/表拥有的操作权限 |
| menus | array | 子菜单数组(支持嵌套) |
#### name vs table
- **table**: 绑定数据表,拥有该表的增删查改等权限,自动生成 CRUD 接口
- **name**: 自定义功能标识,不绑定表,前端根据 name 和 auth 来显示和操作自定义内容(如首页 home、仪表盘 dashboard 等)
- 如果配置了 `menus` 子菜单,则作为分组功能,前端展开显示下级菜单
- 如果没有 `menus`,则作为独立的自定义功能入口
**注意**:目前只支持**两级菜单**,不允许更多层级嵌套。
```json
// 使用 table绑定数据表有增删查改权限
{ "label": "用户管理", "table": "user", "auth": ["show", "add", "edit", "delete"] }
// 使用 name自定义功能无子菜单
{ "label": "首页", "name": "home", "icon": "House", "auth": ["show"] }
// 使用 name分组功能有子菜单
{ "label": "系统管理", "name": "sys", "icon": "Setting", "auth": ["show"], "menus": [...] }
```
#### 自动分组规则
代码生成器在自动生成配置时,会根据表名的 `_` 分词进行自动分组:
- 表名 `sys_logs``sys_menus``sys_config` → 自动归入 `sys` 分组
- 表名 `article``article_tag` → 自动归入 `article` 分组
分组的显示名称label使用该分组下**第一张表的名字**,如果表有备注则使用备注名。
### 2.4 auth 配置
权限数组定义菜单/表拥有的操作权限:
```json
{
"auth": ["show", "add", "delete", "edit", "info", "download"]
}
```
#### 内置权限
| 权限 | 对应接口 | 说明 |
|------|----------|------|
| show | /search | 列表查询 |
| add | /add | 新增数据 |
| delete | /remove | 删除数据 |
| edit | /update | 编辑数据 |
| info | /info | 查看详情 |
| download | /search?download=1 | 导出数据 |
#### 自定义权限扩展
auth 数组**可以自由增删**,新增的权限项会:
- 在前端菜单/功能中显示
- 在角色管理role的权限设置中显示供管理员分配
```json
// 示例:为文章表添加自定义权限
{
"label": "文章管理",
"table": "article",
"auth": ["show", "add", "edit", "delete", "info", "publish", "audit", "top"]
}
```
上例中 `publish`(发布)、`audit`(审核)、`top`(置顶)为自定义权限,前端可根据这些权限控制对应按钮的显示和操作。
### 2.5 icon 配置
菜单图标,使用 Element Plus 图标名称:
```json
{ "icon": "Setting" } // 设置图标
{ "icon": "User" } // 用户图标
{ "icon": "Document" } // 文档图标
{ "icon": "Folder" } // 文件夹图标
```
### 2.6 flow 配置
flow 是一个简易的数据权限控制机制,用于限制用户只能操作自己权限范围内的数据。
#### 基本结构
```json
{
"flow": {
"role": {
"table": "role",
"stop": true,
"sql": {
"id": "role_id"
}
},
"article": {
"table": "article",
"stop": false,
"sql": {
"admin_id": "id"
}
},
"org": {
"table": "org",
"stop": false,
"sql": {
"parent_ids[~]": ",org_id,"
}
}
}
}
```
#### flow 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| table | string | 表名 |
| stop | bool | 是否禁止修改自身关联的数据 |
| sql | object | 数据过滤条件,自动填充查询/操作条件 |
#### stop 配置详解
`stop` 用于防止用户修改自己当前关联的敏感数据。
**场景示例**:当前登录用户是 admin 表的用户,其 `role_id = 1`
```json
"role": {
"table": "role",
"stop": true,
"sql": { "id": "role_id" }
}
```
**效果**
- 用户**不能修改** role 表中 `id = 1` 的这行数据(自己的角色)
- 用户**可以修改**其他 role 记录(如果有权限的话)
**典型用途**
- 防止用户提升自己的角色权限
- 防止用户修改自己所属的组织
#### sql 配置详解
`sql` 用于自动填充数据过滤条件,实现数据隔离。
**格式**`{ "目标表字段": "当前用户字段" }`
**示例 1精确匹配**
```json
"article": {
"sql": { "admin_id": "id" }
}
```
**效果**:查询/操作 article 表时,自动添加条件 `WHERE admin_id = 当前用户.id`
即:用户只能看到/操作自己创建的文章。
**示例 2角色关联**
```json
"role": {
"sql": { "id": "role_id" }
}
```
**效果**:查询/操作 role 表时,自动添加条件 `WHERE id = 当前用户.role_id`
即:用户只能看到自己的角色。
**示例 3树形结构模糊匹配**
```json
"org": {
"sql": { "parent_ids[~]": ",org_id," }
}
```
**效果**:查询/操作 org 表时,自动添加条件 `WHERE parent_ids LIKE '%,用户.org_id,%'`
即:用户只能看到自己组织及其下级组织。
#### sql 条件语法
| 格式 | 含义 | SQL 等价 |
|------|------|----------|
| `"field": "user_field"` | 精确匹配 | `field = 用户.user_field` |
| `"field[~]": ",value,"` | 模糊匹配 | `field LIKE '%,value,%'` |
#### 完整示例
假设当前登录用户数据:
```json
{ "id": 5, "role_id": 2, "org_id": 10 }
```
flow 配置:
```json
{
"flow": {
"role": {
"table": "role",
"stop": true,
"sql": { "id": "role_id" }
},
"article": {
"table": "article",
"stop": false,
"sql": { "admin_id": "id" }
},
"org": {
"table": "org",
"stop": false,
"sql": { "parent_ids[~]": ",org_id," }
}
}
}
```
**效果**
| 表 | 查询条件 | stop 效果 |
|-----|----------|-----------|
| role | `WHERE id = 2` | 不能修改 id=2 的角色 |
| article | `WHERE admin_id = 5` | 可以修改自己的文章 |
| org | `WHERE parent_ids LIKE '%,10,%'` | 可以修改下级组织 |
---
## 三、字段规则配置rule.json
定义字段在增删改查操作中的默认行为。
### 配置结构
```json
[
{
"name": "id",
"add": false,
"edit": false,
"info": true,
"list": true,
"must": false,
"strict": true,
"type": ""
}
]
```
### 字段属性说明
| 属性 | 类型 | 说明 |
|------|------|------|
| name | string | 字段名或字段名包含的关键词 |
| add | bool | 新增时是否显示该字段 |
| edit | bool | 编辑时是否显示该字段 |
| info | bool | 详情页是否显示该字段 |
| list | bool | 列表页是否显示该字段 |
| must | bool | 是否必填(详见下方说明) |
| strict | bool | 是否严格匹配字段名true=完全匹配false=包含匹配) |
| type | string | 字段类型(影响前端控件和数据处理) |
### must 必填字段规则
`must` 字段用于控制前端表单的必填验证:
**自动识别规则**
1. **MySQL**:如果字段设置为 `NOT NULL`(即 `IS_NULLABLE='NO'`),自动设为 `must=true`
2. **SQLite**:如果字段是主键(`pk=1`),自动设为 `must=true`
**规则配置覆盖**
- `rule.json` 中的 `must` 设置会覆盖数据库的自动识别结果
- 可以将数据库中 NOT NULL 的字段在规则中设为 `must=false`,反之亦然
**前端效果**
- `must=true` 的字段在新增/编辑表单中显示必填标记(*
- 提交时前端会验证必填字段
**后端验证**
- 新增操作时,如果 `must=true` 的字段为空,返回"请求参数不足"
### type 类型说明
| 类型 | 说明 | 前端控件 |
|------|------|----------|
| (空) | 普通文本 | 文本输入框 |
| text | 文本 | 文本输入框 |
| number | 数字 | 数字输入框 |
| select | 选择 | 下拉选择框(根据注释自动生成选项) |
| time | 时间datetime | 日期时间选择器 |
| unixTime | 时间戳 | 日期时间选择器(存储为 Unix 时间戳) |
| password | 密码 | 密码输入框(自动 MD5 加密) |
| textArea | 多行文本 | 文本域 |
| image | 图片 | 图片上传 |
| file | 文件 | 文件上传 |
| money | 金额 | 金额输入框 |
| auth | 权限 | 权限树选择器 |
| form | 表单 | 动态表单 |
| index | 索引 | 隐藏字段(用于 parent_ids 等) |
| table | 动态表 | 表名选择器 |
| table_id | 动态表ID | 根据 table 字段动态关联 |
### 内置字段规则
以下是框架默认的字段规则,可在 `rule.json` 中覆盖:
#### 主键和索引
```json
{"name": "id", "add": false, "list": true, "edit": false, "info": true, "strict": true}
{"name": "sn", "add": false, "list": true, "edit": false, "info": true}
{"name": "parent_ids", "add": false, "list": false, "edit": false, "info": false, "type": "index", "strict": true}
{"name": "index", "add": false, "list": false, "edit": false, "info": false, "type": "index", "strict": true}
```
#### 层级关系
```json
{"name": "parent_id", "add": true, "list": true, "edit": true, "info": true}
{"name": "level", "add": false, "list": false, "edit": false, "info": true}
```
#### 时间字段
```json
{"name": "create_time", "add": false, "list": false, "edit": false, "info": true, "type": "time", "strict": true}
{"name": "modify_time", "add": false, "list": true, "edit": false, "info": true, "type": "time", "strict": true}
{"name": "time", "add": true, "list": true, "edit": true, "info": true, "type": "time"}
```
#### 状态字段
```json
{"name": "status", "add": true, "list": true, "edit": true, "info": true, "type": "select"}
{"name": "state", "add": true, "list": true, "edit": true, "info": true, "type": "select"}
{"name": "sex", "add": true, "list": true, "edit": true, "info": true, "type": "select"}
```
#### 敏感字段
```json
{"name": "password", "add": true, "list": false, "edit": true, "info": false, "type": "password"}
{"name": "pwd", "add": true, "list": false, "edit": true, "info": false, "type": "password"}
{"name": "delete", "add": false, "list": false, "edit": false, "info": false}
{"name": "version", "add": false, "list": false, "edit": false, "info": false}
```
#### 媒体字段
```json
{"name": "image", "add": true, "list": false, "edit": true, "info": true, "type": "image"}
{"name": "img", "add": true, "list": false, "edit": true, "info": true, "type": "image"}
{"name": "avatar", "add": true, "list": false, "edit": true, "info": true, "type": "image"}
{"name": "icon", "add": true, "list": false, "edit": true, "info": true, "type": "image"}
{"name": "file", "add": true, "list": false, "edit": true, "info": true, "type": "file"}
```
#### 文本字段
```json
{"name": "info", "add": true, "list": false, "edit": true, "info": true, "type": "textArea"}
{"name": "content", "add": true, "list": false, "edit": true, "info": true, "type": "textArea"}
{"name": "description", "add": true, "list": false, "edit": true, "info": true}
{"name": "note", "add": true, "list": false, "edit": true, "info": true}
{"name": "address", "add": true, "list": true, "edit": true, "info": true}
```
#### 特殊字段
```json
{"name": "amount", "add": true, "list": true, "edit": true, "info": true, "type": "money", "strict": true}
{"name": "auth", "add": true, "list": false, "edit": true, "info": true, "type": "auth", "strict": true}
{"name": "rule", "add": true, "list": true, "edit": true, "info": true, "type": "form"}
{"name": "table", "add": false, "list": true, "edit": false, "info": true, "type": "table"}
{"name": "table_id", "add": false, "list": true, "edit": false, "info": true, "type": "table_id"}
```
### 自定义字段规则
`rule.json` 中添加项目特定的字段规则:
```json
[
// 项目特定规则
{
"name": "company_name",
"add": true,
"edit": true,
"info": true,
"list": true,
"must": true,
"strict": true,
"type": ""
},
// 表.字段 形式的精确规则
{
"name": "user.nickname",
"add": true,
"edit": true,
"info": true,
"list": true,
"strict": true,
"type": ""
}
]
```
---
## 四、配置示例
### 完整的 admin.json 示例
```json
{
"id": "74a8a59407fa7d6c7fcdc85742dbae57",
"name": "admin",
"label": "后台管理系统",
"labelConfig": {
"show": "开启",
"add": "添加",
"delete": "删除",
"edit": "编辑",
"info": "查看详情",
"download": "下载清单"
},
"menus": [
{
"label": "系统管理",
"name": "sys",
"icon": "Setting",
"auth": ["show"],
"menus": [
{
"label": "日志管理",
"table": "logs",
"auth": ["show", "download"]
},
{
"label": "角色管理",
"table": "role",
"auth": ["show", "add", "delete", "edit", "info"]
},
{
"label": "组织管理",
"table": "org",
"auth": ["show", "add", "delete", "edit", "info"]
},
{
"label": "人员管理",
"table": "admin",
"auth": ["show", "add", "delete", "edit", "info", "download"]
}
]
}
],
"flow": {
"admin": {
"table": "admin",
"stop": false,
"sql": { "role_id": "role_id" }
},
"role": {
"table": "role",
"stop": true,
"sql": { "admin_id": "id", "id": "role_id" }
},
"org": {
"table": "org",
"stop": false,
"sql": { "admin_id": "id" }
},
"logs": {
"table": "logs",
"stop": false,
"sql": {}
}
}
}
```
---
## 五、SQLite 备注替代方案
SQLite 数据库不支持表备注TABLE COMMENT和字段备注COLUMN COMMENT代码生成器会使用表名/字段名作为默认显示名称。
### 通过配置文件设置备注
利用 HoTime 的配置覆盖机制,可以在配置文件中手动设置显示名称和提示:
**步骤**
1. 首次运行,生成配置文件(如 admin.json
2. 编辑配置文件中的 `tables` 部分
### 设置表显示名称
在菜单配置中设置 `label`
```json
{
"menus": [
{
"label": "用户管理", // 手动设置表显示名称
"table": "user",
"auth": ["show", "add", "edit", "delete"]
}
]
}
```
### 设置字段显示名称和提示
`configDB` 文件(如 adminDB.json中的 `tables.表名.columns` 部分设置:
```json
{
"tables": {
"user": {
"label": "用户管理",
"columns": [
{
"name": "id",
"label": "ID",
"type": "number"
},
{
"name": "name",
"label": "用户名",
"ps": "请输入用户名", // 前端输入提示
"must": true
},
{
"name": "phone",
"label": "手机号",
"ps": "请输入11位手机号"
},
{
"name": "status",
"label": "状态",
"type": "select",
"options": [
{"name": "正常", "value": "0"},
{"name": "禁用", "value": "1"}
]
}
]
}
}
}
```
**注意**:直接编辑的是 `configDB` 指定的文件(如 adminDB.json该文件会在每次启动时重新生成。如需持久化修改应将自定义的 columns 配置放入 `config` 指定的文件(如 admin.json中。
---
## 六、配置检查清单
### codeConfig 检查
- [ ] config 文件路径正确
- [ ] rule 文件路径正确
- [ ] table 指定的管理员表存在
### 菜单权限配置检查
- [ ] 所有 table 指向的表在数据库中存在
- [ ] auth 数组包含需要的权限
- [ ] menus 结构正确(有子菜单用 name无子菜单用 table
- [ ] flow 配置的 sql 条件字段存在
### 字段规则配置检查
- [ ] strict=true 的规则字段名完全匹配
- [ ] type 类型与前端控件需求一致
- [ ] 敏感字段password等的 list 和 info 为 false

View File

@ -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)

View File

@ -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)

View File

@ -1,408 +0,0 @@
# 数据库设计规范
本文档定义了 HoTime 框架代码生成器所依赖的数据库设计规范。遵循这些规范可以确保代码生成器正确识别表关系、自动生成 CRUD 接口和管理后台。
---
## 表命名规则
### 1. 关于表名前缀
一般情况下**没必要强行添加前缀分组**,直接使用业务含义命名即可:
```
推荐user、org、role、article
```
**可以使用前缀的场景**:当项目较大、表较多时,可以用前缀进行模块分组(如 `sys_``cms_`),代码生成器会根据 `_` 分词自动将同前缀的表归为一组。
```
sys_user、sys_role、sys_org → 自动归入 sys 分组
cms_article、cms_category → 自动归入 cms 分组
```
**使用前缀的注意事项**
- 必须遵循外键全局唯一性规则(见下文)
- 前缀分组后,关联 ID 命名会更复杂,容易产生重复
- **外键名不允许重复指向不同的表**
### 2. 可使用简称
较长的表名可以使用常见简称:
| 全称 | 简称 |
|------|------|
| organization | org |
| category | ctg |
| configuration | config |
| administrator | admin |
### 3. 关联表命名
多对多关联表使用 `主表_关联表` 格式:
```
user_org -- 用户与组织的关联
user_role -- 用户与角色的关联
article_tag -- 文章与标签的关联
```
---
## 字段命名规则
### 1. 主键字段
所有表的主键统一命名为 `id`,使用自增整数。
```sql
`id` int(11) NOT NULL AUTO_INCREMENT
```
### 2. 外键字段
外键使用 `完整表名_id` 格式:
```
user_id -- 指向 user 表
org_id -- 指向 org 表
role_id -- 指向 role 表
app_category_id -- 指向 app_category 表
```
### 3. 关联表引用
引用关联表时使用 `关联表名_id`
```
user_org_id -- 指向 user_org 表
user_role_id -- 指向 user_role 表
```
### 4. 外键全局唯一性(重要)
**每个 `xxx_id` 字段名必须全局唯一指向一张表**,代码生成器通过 `_id` 后缀自动识别外键关系。
```
✅ 正确设计:
- user_id 只能指向 user 表
- org_id 只能指向 org 表
- user_org_id 只能指向 user_org 表
- sys_user_id 只能指向 sys_user 表(如果使用前缀分组)
❌ 错误设计:
- dd_id 作为"钉钉外部系统ID",但系统中存在 dd 表
→ 代码生成器会误判为指向 dd 表的外键
- 同时存在 user 表和 sys_user 表,都使用 user_id
→ 外键名重复,代码生成器无法正确识别
```
**使用前缀分组时的外键命名**
如果使用了表名前缀(如 `sys_user`),外键应使用完整表名:
| 表名 | 外键命名 |
|------|----------|
| sys_user | sys_user_id |
| sys_org | sys_org_id |
| cms_article | cms_article_id |
**非外键业务标识字段**:避免使用 `xxx_id` 格式
| 业务含义 | 错误命名 | 正确命名 |
|----------|----------|----------|
| 设备唯一标识 | device_id | device_uuid / device_sn |
| 钉钉用户ID | dingtalk_user_id | dt_user_id / dingtalk_uid |
| 微信OpenID | wechat_id | wechat_openid |
| 外部系统编号 | external_id | external_code / external_sn |
### 5. 外键智能匹配机制
代码生成器会按以下优先级自动匹配外键关联的表:
1. **完全匹配**`user_id` → 查找 `user`
2. **带前缀的完全匹配**:如果没找到,尝试 `user_id` → 查找 `sys_user` 表(默认前缀)
3. **去除字段前缀匹配**:如果字段有前缀且找不到对应表,会去掉前缀匹配
**示例**`admin_user_id` 字段的匹配过程:
1. 先查找 `admin_user` 表 → 如果存在,关联到 `admin_user`
2. 如果不存在,去掉前缀查找 `user` 表 → 如果存在,关联到 `user`
**使用场景**:当一张表需要记录多个用户(如创建人、审核人、处理人)时:
```sql
CREATE TABLE `order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) COMMENT '下单用户',
`audit_user_id` int(11) COMMENT '审核人',
`handle_user_id` int(11) COMMENT '处理人',
...
);
```
如果系统中没有 `audit_user``handle_user` 表,代码生成器会自动将 `audit_user_id``handle_user_id` 识别为指向 `user` 表的外键。
### 6. 关联表冗余外键
关联表应包含必要的冗余外键便于查询:
```sql
-- user_app 表(用户与应用的关联)
CREATE TABLE user_app (
id int(11) NOT NULL AUTO_INCREMENT,
user_id int(11) DEFAULT NULL COMMENT '用户ID',
app_id int(11) DEFAULT NULL COMMENT '应用ID',
app_category_id int(11) DEFAULT NULL COMMENT '应用分类ID冗余便于按分类查询',
...
);
```
### 7. 层级关系字段
树形结构的表使用以下字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| parent_id | int | 父级ID顶级为 NULL 或 0 |
| parent_ids | varchar(255) | 完整层级路径,格式:`,1,3,5,`(从根到当前,逗号分隔) |
| level | int | 层级深度,从 0 开始 |
**示例**
```
id=1, parent_id=NULL, parent_ids=",1,", level=0 -- 顶级
id=3, parent_id=1, parent_ids=",1,3,", level=1 -- 一级
id=5, parent_id=3, parent_ids=",1,3,5,", level=2 -- 二级
```
`parent_ids` 的设计便于快速查询所有子级:
```sql
SELECT * FROM org WHERE parent_ids LIKE '%,3,%' -- 查询id=3的所有子级
```
---
## 时间字段类型
不同数据库使用对应的时间类型:
| 数据库 | 类型 | 示例 |
|--------|------|------|
| MySQL | datetime | `2024-01-15 10:30:00` |
| SQLite | TEXT | `2024-01-15 10:30:00` |
| PostgreSQL | timestamp | `2024-01-15 10:30:00` |
---
## 表注释规则
### 表备注命名建议
表备注建议使用"XX管理"格式,便于在管理后台显示:
```sql
-- MySQL 表备注示例
CREATE TABLE `user` (
...
) COMMENT='用户管理';
CREATE TABLE `article` (
...
) COMMENT='文章管理';
CREATE TABLE `order` (
...
) COMMENT='订单管理';
```
### SQLite 表备注
SQLite 不支持表备注,代码生成器会使用表名作为默认显示名称。如需自定义,可在配置文件中手动设置 `label`(详见代码生成配置规范)。
---
## 字段注释规则
### 注释语法格式
字段注释支持以下格式组合:
```
显示名称:选项值 附加备注{前端提示}
```
| 部分 | 分隔符 | 用途 |
|------|--------|------|
| 显示名称 | 无 | 前端表单/列表的字段标签 |
| 选项值 | `:` 冒号 | select 类型的下拉选项(格式:`值-名称,值-名称` |
| 附加备注 | 空格 | 仅在数据库中查看,不传递给前端 |
| 前端提示 | `{}` | 存储到 `ps` 字段,用于前端显示提示文字 |
### 选择类型字段
使用 `标签:值-名称,值-名称` 格式,代码生成器会自动解析为下拉选项:
```sql
`state` int(2) DEFAULT '0' COMMENT '状态:0-正常,1-异常,2-隐藏'
`sex` int(1) DEFAULT '0' COMMENT '性别:0-未知,1-男,2-女'
`status` int(2) DEFAULT '0' COMMENT '审核状态:0-待审核,1-已通过,2-已拒绝'
```
### 普通字段
直接使用中文说明:
```sql
`name` varchar(50) DEFAULT NULL COMMENT '名称'
`phone` varchar(20) DEFAULT NULL COMMENT '手机号'
`email` varchar(100) DEFAULT NULL COMMENT '邮箱'
```
### 附加备注(空格后)
空格后的内容**不会传递给前端**,仅供数据库设计时参考:
```sql
`user_id` int(11) COMMENT '用户ID 关联user表的主键'
`amount` decimal(10,2) COMMENT '金额 单位为元,精确到分'
```
### 前端提示({}内)
`{}` 中的内容存储到 `ps` 字段,前端可用于显示输入提示:
```sql
`phone` varchar(20) COMMENT '手机号{请输入11位手机号}'
`email` varchar(100) COMMENT '邮箱{格式xxx@xxx.com}'
`content` text COMMENT '内容{支持HTML格式}'
```
### 组合使用
```sql
-- 完整格式示例
`status` int(2) DEFAULT '0' COMMENT '状态:0-待审核,1-已通过,2-已拒绝 业务状态流转字段{选择当前审核状态}'
```
解析结果:
- `label` = "状态"
- `options` = [{name:"待审核", value:"0"}, {name:"已通过", value:"1"}, {name:"已拒绝", value:"2"}]
- `ps` = "选择当前审核状态"
- 空格后的"业务状态流转字段"不会传递给前端
### SQLite 字段备注
SQLite 不支持字段备注COMMENT代码生成器会使用字段名作为默认显示名称。如需自定义可在配置文件中手动设置详见代码生成配置规范
---
## 必有字段规则
每张业务表必须包含以下三个字段:
### MySQL
```sql
`state` int(2) DEFAULT '0' COMMENT '状态:0-正常,1-异常,2-隐藏',
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
`modify_time` datetime DEFAULT NULL COMMENT '变更时间',
```
### SQLite
```sql
"state" INTEGER DEFAULT 0, -- 状态:0-正常,1-异常,2-隐藏
"create_time" TEXT DEFAULT NULL, -- 创建日期
"modify_time" TEXT DEFAULT NULL, -- 变更时间
```
### PostgreSQL
```sql
"state" INTEGER DEFAULT 0, -- 状态:0-正常,1-异常,2-隐藏
"create_time" TIMESTAMP DEFAULT NULL, -- 创建日期
"modify_time" TIMESTAMP DEFAULT NULL, -- 变更时间
```
---
## 完整建表示例
### MySQL 示例
```sql
-- 用户表
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(50) DEFAULT NULL COMMENT '用户名',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`password` varchar(64) DEFAULT NULL COMMENT '密码',
`org_id` int(11) DEFAULT NULL COMMENT '组织ID',
`role_id` int(11) DEFAULT NULL COMMENT '角色ID',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像',
`sex` int(1) DEFAULT '0' COMMENT '性别:0-未知,1-男,2-女',
`state` int(2) DEFAULT '0' COMMENT '状态:0-正常,1-异常,2-隐藏',
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
`modify_time` datetime DEFAULT NULL COMMENT '变更时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 组织表(树形结构)
CREATE TABLE `org` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(100) DEFAULT NULL COMMENT '组织名称',
`parent_id` int(11) DEFAULT NULL COMMENT '父级ID',
`parent_ids` varchar(255) DEFAULT NULL COMMENT '层级路径',
`level` int(2) DEFAULT '0' COMMENT '层级深度',
`sort` int(5) DEFAULT '0' COMMENT '排序',
`state` int(2) DEFAULT '0' COMMENT '状态:0-正常,1-异常,2-隐藏',
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
`modify_time` datetime DEFAULT NULL COMMENT '变更时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='组织表';
-- 用户组织关联表
CREATE TABLE `user_org` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` int(11) DEFAULT NULL COMMENT '用户ID',
`org_id` int(11) DEFAULT NULL COMMENT '组织ID',
`state` int(2) DEFAULT '0' COMMENT '状态:0-正常,1-异常,2-隐藏',
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
`modify_time` datetime DEFAULT NULL COMMENT '变更时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户组织关联表';
```
### SQLite 示例
```sql
-- 用户表
CREATE TABLE "user" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"name" TEXT DEFAULT NULL,
"phone" TEXT DEFAULT NULL,
"password" TEXT DEFAULT NULL,
"org_id" INTEGER DEFAULT NULL,
"role_id" INTEGER DEFAULT NULL,
"avatar" TEXT DEFAULT NULL,
"sex" INTEGER DEFAULT 0,
"state" INTEGER DEFAULT 0,
"create_time" TEXT DEFAULT NULL,
"modify_time" TEXT DEFAULT NULL
);
```
---
## 规范检查清单
在设计数据库时,请确认:
- [ ] 表名无系统前缀
- [ ] 主键统一为 `id`
- [ ] 外键格式为 `表名_id`
- [ ] 所有 `xxx_id` 字段都指向实际存在的表
- [ ] 非外键业务标识不使用 `xxx_id` 格式
- [ ] 树形表包含 parent_id、parent_ids、level
- [ ] 所有表包含 state、create_time、modify_time
- [ ] 选择字段注释格式正确(`标签:值-名称`

View File

@ -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 速查手册

View File

@ -1,183 +0,0 @@
# HoTime 改进规划
本文档记录 HoTime 框架的待改进项和设计思考,供后续版本迭代参考。
---
## 一、备注语法优化
### 现状
当前字段备注语法使用空格、冒号、大括号组合:
```sql
`status` int COMMENT '状态:0-正常,1-异常 这是数据库备注{这是前端提示}'
```
### 问题
- 多种分隔符混用,不够直观
- 显示名称如需包含特殊字符可能冲突
### 改进方向
使用 `|` 作为统一分隔符,保持干净清爽:
```sql
-- 方案 A简洁直观
`status` int COMMENT '状态|0-正常,1-异常|请选择状态'
-- 解析:显示名称|选项|提示
-- 方案 B保持兼容空格后内容仍作为数据库备注
`status` int COMMENT '状态|0-正常,1-异常|请选择状态 这是数据库备注'
```
### 待确认
- [ ] 是否需要保留空格后的数据库备注功能
- [ ] 如果只有提示没有选项,如何表示(如 `名称||请输入名称`
- [ ] 是否向后兼容旧语法
---
## 二、SQLite 备注支持
### 现状
SQLite 不支持表/字段备注,需要手动在配置文件中设置。
### 问题
SQLite 项目配置工作量大,不够"快速开发"。
### 待探索方案
1. **配置文件方案(当前)**:通过 admin.json 手动配置,学习成本可接受
2. **轻量级方案**:考虑是否有更简单的替代方案,但不增加复杂度
### 暂时结论
当前方案虽不完美,但符合"简单好用"原则,暂不改动。
---
## 三、软删除支持
### 现状
框架暂不支持软删除。
### 设计考量
软删除存在以下问题:
- 数据安全性:软删除的数据仍可被恢复或误用
- 查询复杂度:所有查询需要额外过滤条件
- 存储膨胀:删除的数据持续占用空间
### 待探索方案
1. **可选软删除**:通过配置决定某张表是否启用软删除
2. **日志表方案**:独立的操作日志表,只写不删不改
- 记录所有增删改操作
- 原表正常物理删除
- 需要时可从日志恢复
3. **归档表方案**:删除时移动到归档表
### 倾向方案
日志表方案更符合数据安全和审计需求:
```sql
CREATE TABLE `_operation_log` (
`id` int AUTO_INCREMENT,
`table_name` varchar(100) COMMENT '操作表名',
`record_id` int COMMENT '记录ID',
`operation` varchar(20) COMMENT '操作类型:insert,update,delete',
`old_data` text COMMENT '操作前数据JSON',
`new_data` text COMMENT '操作后数据JSON',
`operator_id` int COMMENT '操作人ID',
`operator_table` varchar(100) COMMENT '操作人表名',
`create_time` datetime COMMENT '操作时间',
PRIMARY KEY (`id`)
) COMMENT='操作日志';
```
### 待确认
- [ ] 是否所有表都记录日志
- [ ] 日志保留策略(永久/定期归档)
- [ ] 是否提供恢复接口
---
## 四、多对多关联表增强
### 现状
关联表(如 `user_role`)按普通表处理,生成标准 CRUD。
### 可能的增强
1. **自动识别**:只有两个 `_id` 外键的表识别为关联表
2. **专用接口**:生成关联管理接口(批量绑定/解绑)
3. **级联查询**:自动生成带关联数据的查询
### 待确认
- [ ] 是否需要此功能
- [ ] 如何保持简单性
---
## 五、自动填充字段扩展
### 现状
自动填充:
- `create_time`:新增时自动填充当前时间
- `modify_time`:新增/编辑时自动填充当前时间
### 可能的扩展
| 字段 | 填充时机 | 填充内容 |
|------|----------|----------|
| create_by | 新增 | 当前用户ID |
| modify_by | 新增/编辑 | 当前用户ID |
| create_ip | 新增 | 客户端IP |
### 待确认
- [ ] 是否需要扩展
- [ ] 字段命名规范
---
## 六、版本控制/乐观锁
### 现状
`version` 字段规则存在但未启用。
### 待确认
- [ ] 是否需要支持乐观锁
- [ ] 如果不需要,是否从默认规则中移除
---
## 改进优先级
| 优先级 | 改进项 | 状态 |
|--------|--------|------|
| 高 | 备注语法优化(`\|` 分隔符) | 待设计 |
| 中 | 软删除/日志表支持 | 待设计 |
| 低 | 多对多关联表增强 | 待评估 |
| 低 | 自动填充字段扩展 | 待评估 |
---
## 更新记录
| 日期 | 内容 |
|------|------|
| 2026-01-24 | 初始版本,记录分析结果和待改进项 |

View File

@ -1,15 +1,43 @@
package main package main
import ( import (
. "code.hoteas.com/golang/hotime" "encoding/json"
"fmt" "fmt"
"os"
"time" "time"
. "code.hoteas.com/golang/hotime"
. "code.hoteas.com/golang/hotime/common" . "code.hoteas.com/golang/hotime/common"
. "code.hoteas.com/golang/hotime/db" . "code.hoteas.com/golang/hotime/db"
) )
// 调试日志文件路径
const debugLogPath = `d:\work\hotimev1\.cursor\debug.log`
// debugLog 写入调试日志
func debugLog(location, message string, data interface{}, hypothesisId string) {
// #region agent log
logEntry := Map{
"location": location,
"message": message,
"data": data,
"timestamp": time.Now().UnixMilli(),
"sessionId": "debug-session",
"runId": "hotimedb-test-run",
"hypothesisId": hypothesisId,
}
jsonBytes, _ := json.Marshal(logEntry)
f, err := os.OpenFile(debugLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err == nil {
f.WriteString(string(jsonBytes) + "\n")
f.Close()
}
// #endregion
}
func main() { func main() {
debugLog("main.go:35", "开始 HoTimeDB 全功能测试", nil, "START")
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
@ -21,6 +49,7 @@ func main() {
// 测试入口 - 运行所有测试 // 测试入口 - 运行所有测试
"all": func(that *Context) { "all": func(that *Context) {
results := Map{} results := Map{}
debugLog("main.go:48", "开始所有测试", nil, "ALL_TESTS")
// 初始化测试表 // 初始化测试表
initTestTables(that) initTestTables(that)
@ -44,7 +73,7 @@ func main() {
results["6_pagination"] = testPagination(that) results["6_pagination"] = testPagination(that)
// 7. 批量插入测试 // 7. 批量插入测试
results["7_batch_insert"] = testInserts(that) results["7_batch_insert"] = testBatchInsert(that)
// 8. Upsert 测试 // 8. Upsert 测试
results["8_upsert"] = testUpsert(that) results["8_upsert"] = testUpsert(that)
@ -55,13 +84,16 @@ func main() {
// 10. 原生 SQL 测试 // 10. 原生 SQL 测试
results["10_raw_sql"] = testRawSQL(that) results["10_raw_sql"] = testRawSQL(that)
debugLog("main.go:80", "所有测试完成", results, "ALL_TESTS_DONE")
that.Display(0, results) that.Display(0, results)
}, },
// 查询数据库表结构 // 查询数据库表结构
"tables": func(that *Context) { "tables": func(that *Context) {
debugLog("main.go:tables", "查询数据库表结构", nil, "TABLES")
// 查询所有表 // 查询所有表
tables := that.Db.Query("SHOW TABLES") tables := that.Db.Query("SHOW TABLES")
debugLog("main.go:tables", "表列表", tables, "TABLES")
that.Display(0, Map{"tables": tables}) that.Display(0, Map{"tables": tables})
}, },
@ -76,6 +108,7 @@ func main() {
columns := that.Db.Query("DESCRIBE " + tableName) columns := that.Db.Query("DESCRIBE " + tableName)
// 查询表数据前10条 // 查询表数据前10条
data := that.Db.Select(tableName, Map{"LIMIT": 10}) data := that.Db.Select(tableName, Map{"LIMIT": 10})
debugLog("main.go:describe", "表结构", Map{"table": tableName, "columns": columns, "data": data}, "DESCRIBE")
that.Display(0, Map{"table": tableName, "columns": columns, "sample_data": data}) that.Display(0, Map{"table": tableName, "columns": columns, "sample_data": data})
}, },
@ -86,7 +119,7 @@ func main() {
"join": func(that *Context) { that.Display(0, testJoinQuery(that)) }, "join": func(that *Context) { that.Display(0, testJoinQuery(that)) },
"aggregate": func(that *Context) { that.Display(0, testAggregate(that)) }, "aggregate": func(that *Context) { that.Display(0, testAggregate(that)) },
"pagination": func(that *Context) { that.Display(0, testPagination(that)) }, "pagination": func(that *Context) { that.Display(0, testPagination(that)) },
"batch": func(that *Context) { that.Display(0, testInserts(that)) }, "batch": func(that *Context) { that.Display(0, testBatchInsert(that)) },
"upsert": func(that *Context) { that.Display(0, testUpsert(that)) }, "upsert": func(that *Context) { that.Display(0, testUpsert(that)) },
"transaction": func(that *Context) { that.Display(0, testTransaction(that)) }, "transaction": func(that *Context) { that.Display(0, testTransaction(that)) },
"rawsql": func(that *Context) { that.Display(0, testRawSQL(that)) }, "rawsql": func(that *Context) { that.Display(0, testRawSQL(that)) },
@ -107,9 +140,15 @@ func initTestTables(that *Context) {
create_time DATETIME create_time DATETIME
)`) )`)
// 检查 admin 表数据(确认表存在) // 检查 admin 表数据
_ = that.Db.Count("admin") adminCount := that.Db.Count("admin")
_ = that.Db.Count("article") articleCount := that.Db.Count("article")
debugLog("main.go:init", "MySQL数据库初始化检查完成", Map{
"adminCount": adminCount,
"articleCount": articleCount,
"dbType": "MySQL",
}, "INIT")
} }
// ==================== 1. 基础 CRUD 测试 ==================== // ==================== 1. 基础 CRUD 测试 ====================
@ -117,6 +156,8 @@ func testBasicCRUD(that *Context) Map {
result := Map{"name": "基础CRUD测试", "tests": Slice{}} result := Map{"name": "基础CRUD测试", "tests": Slice{}}
tests := Slice{} tests := Slice{}
debugLog("main.go:103", "开始基础 CRUD 测试 (MySQL)", nil, "H1_CRUD")
// 1.1 Insert 测试 - 使用 admin 表 // 1.1 Insert 测试 - 使用 admin 表
insertTest := Map{"name": "Insert 插入测试 (admin表)"} insertTest := Map{"name": "Insert 插入测试 (admin表)"}
adminId := that.Db.Insert("admin", Map{ adminId := that.Db.Insert("admin", Map{
@ -132,6 +173,7 @@ func testBasicCRUD(that *Context) Map {
insertTest["result"] = adminId > 0 insertTest["result"] = adminId > 0
insertTest["adminId"] = adminId insertTest["adminId"] = adminId
insertTest["lastQuery"] = that.Db.LastQuery insertTest["lastQuery"] = that.Db.LastQuery
debugLog("main.go:118", "Insert 测试", Map{"adminId": adminId, "success": adminId > 0, "query": that.Db.LastQuery}, "H1_INSERT")
tests = append(tests, insertTest) tests = append(tests, insertTest)
// 1.2 Get 测试 // 1.2 Get 测试
@ -139,6 +181,7 @@ func testBasicCRUD(that *Context) Map {
admin := that.Db.Get("admin", "*", Map{"id": adminId}) admin := that.Db.Get("admin", "*", Map{"id": adminId})
getTest["result"] = admin != nil && admin.GetInt64("id") == adminId getTest["result"] = admin != nil && admin.GetInt64("id") == adminId
getTest["admin"] = admin getTest["admin"] = admin
debugLog("main.go:126", "Get 测试", Map{"admin": admin, "success": admin != nil}, "H1_GET")
tests = append(tests, getTest) tests = append(tests, getTest)
// 1.3 Select 测试 - 单条件 // 1.3 Select 测试 - 单条件
@ -146,6 +189,7 @@ func testBasicCRUD(that *Context) Map {
admins1 := that.Db.Select("admin", "*", Map{"state": 1, "LIMIT": 5}) admins1 := that.Db.Select("admin", "*", Map{"state": 1, "LIMIT": 5})
selectTest1["result"] = len(admins1) >= 0 // 可能表中没有数据 selectTest1["result"] = len(admins1) >= 0 // 可能表中没有数据
selectTest1["count"] = len(admins1) selectTest1["count"] = len(admins1)
debugLog("main.go:134", "Select 单条件测试", Map{"count": len(admins1)}, "H1_SELECT1")
tests = append(tests, selectTest1) tests = append(tests, selectTest1)
// 1.4 Select 测试 - 多条件(自动 AND // 1.4 Select 测试 - 多条件(自动 AND
@ -159,6 +203,7 @@ func testBasicCRUD(that *Context) Map {
selectTest2["result"] = true // 只要不报错就算成功 selectTest2["result"] = true // 只要不报错就算成功
selectTest2["count"] = len(admins2) selectTest2["count"] = len(admins2)
selectTest2["lastQuery"] = that.Db.LastQuery selectTest2["lastQuery"] = that.Db.LastQuery
debugLog("main.go:149", "Select 多条件自动AND测试", Map{"count": len(admins2), "query": that.Db.LastQuery}, "H1_SELECT2")
tests = append(tests, selectTest2) tests = append(tests, selectTest2)
// 1.5 Update 测试 // 1.5 Update 测试
@ -169,6 +214,7 @@ func testBasicCRUD(that *Context) Map {
}, Map{"id": adminId}) }, Map{"id": adminId})
updateTest["result"] = affected > 0 updateTest["result"] = affected > 0
updateTest["affected"] = affected updateTest["affected"] = affected
debugLog("main.go:160", "Update 测试", Map{"affected": affected}, "H1_UPDATE")
tests = append(tests, updateTest) tests = append(tests, updateTest)
// 1.6 Delete 测试 - 使用 test_batch 表 // 1.6 Delete 测试 - 使用 test_batch 表
@ -183,6 +229,7 @@ func testBasicCRUD(that *Context) Map {
deleteAffected := that.Db.Delete("test_batch", Map{"id": tempId}) deleteAffected := that.Db.Delete("test_batch", Map{"id": tempId})
deleteTest["result"] = deleteAffected > 0 deleteTest["result"] = deleteAffected > 0
deleteTest["affected"] = deleteAffected deleteTest["affected"] = deleteAffected
debugLog("main.go:175", "Delete 测试", Map{"affected": deleteAffected}, "H1_DELETE")
tests = append(tests, deleteTest) tests = append(tests, deleteTest)
result["tests"] = tests result["tests"] = tests
@ -195,11 +242,14 @@ func testConditionSyntax(that *Context) Map {
result := Map{"name": "条件查询语法测试", "tests": Slice{}} result := Map{"name": "条件查询语法测试", "tests": Slice{}}
tests := Slice{} tests := Slice{}
debugLog("main.go:188", "开始条件查询语法测试 (MySQL)", nil, "H2_CONDITION")
// 2.1 等于 (=) - 使用 article 表 // 2.1 等于 (=) - 使用 article 表
test1 := Map{"name": "等于条件 (=)"} test1 := Map{"name": "等于条件 (=)"}
articles1 := that.Db.Select("article", "id,title", Map{"state": 0, "LIMIT": 3}) articles1 := that.Db.Select("article", "id,title", Map{"state": 0, "LIMIT": 3})
test1["result"] = true test1["result"] = true
test1["count"] = len(articles1) test1["count"] = len(articles1)
debugLog("main.go:195", "等于条件测试", Map{"count": len(articles1)}, "H2_EQUAL")
tests = append(tests, test1) tests = append(tests, test1)
// 2.2 不等于 ([!]) // 2.2 不等于 ([!])
@ -208,6 +258,7 @@ func testConditionSyntax(that *Context) Map {
test2["result"] = true test2["result"] = true
test2["count"] = len(articles2) test2["count"] = len(articles2)
test2["lastQuery"] = that.Db.LastQuery test2["lastQuery"] = that.Db.LastQuery
debugLog("main.go:204", "不等于条件测试", Map{"count": len(articles2), "query": that.Db.LastQuery}, "H2_NOT_EQUAL")
tests = append(tests, test2) tests = append(tests, test2)
// 2.3 大于 ([>]) 和 小于 ([<]) // 2.3 大于 ([>]) 和 小于 ([<])
@ -219,6 +270,7 @@ func testConditionSyntax(that *Context) Map {
}) })
test3["result"] = true test3["result"] = true
test3["count"] = len(articles3) test3["count"] = len(articles3)
debugLog("main.go:216", "大于小于条件测试", Map{"count": len(articles3)}, "H2_GREATER_LESS")
tests = append(tests, test3) tests = append(tests, test3)
// 2.4 大于等于 ([>=]) 和 小于等于 ([<=]) // 2.4 大于等于 ([>=]) 和 小于等于 ([<=])
@ -230,6 +282,7 @@ func testConditionSyntax(that *Context) Map {
}) })
test4["result"] = true test4["result"] = true
test4["count"] = len(articles4) test4["count"] = len(articles4)
debugLog("main.go:228", "大于等于小于等于条件测试", Map{"count": len(articles4)}, "H2_GTE_LTE")
tests = append(tests, test4) tests = append(tests, test4)
// 2.5 LIKE 模糊查询 ([~]) // 2.5 LIKE 模糊查询 ([~])
@ -238,6 +291,7 @@ func testConditionSyntax(that *Context) Map {
test5["result"] = true test5["result"] = true
test5["count"] = len(articles5) test5["count"] = len(articles5)
test5["lastQuery"] = that.Db.LastQuery test5["lastQuery"] = that.Db.LastQuery
debugLog("main.go:237", "LIKE 模糊查询测试", Map{"count": len(articles5), "query": that.Db.LastQuery}, "H2_LIKE")
tests = append(tests, test5) tests = append(tests, test5)
// 2.6 右模糊 ([~!]) // 2.6 右模糊 ([~!])
@ -245,6 +299,7 @@ func testConditionSyntax(that *Context) Map {
articles6 := that.Db.Select("admin", "id,name", Map{"name[~!]": "管理", "LIMIT": 3}) articles6 := that.Db.Select("admin", "id,name", Map{"name[~!]": "管理", "LIMIT": 3})
test6["result"] = true test6["result"] = true
test6["count"] = len(articles6) test6["count"] = len(articles6)
debugLog("main.go:245", "右模糊查询测试", Map{"count": len(articles6)}, "H2_LIKE_RIGHT")
tests = append(tests, test6) tests = append(tests, test6)
// 2.7 BETWEEN ([<>]) // 2.7 BETWEEN ([<>])
@ -253,6 +308,7 @@ func testConditionSyntax(that *Context) Map {
test7["result"] = true test7["result"] = true
test7["count"] = len(articles7) test7["count"] = len(articles7)
test7["lastQuery"] = that.Db.LastQuery test7["lastQuery"] = that.Db.LastQuery
debugLog("main.go:254", "BETWEEN 区间查询测试", Map{"count": len(articles7), "query": that.Db.LastQuery}, "H2_BETWEEN")
tests = append(tests, test7) tests = append(tests, test7)
// 2.8 NOT BETWEEN ([><]) // 2.8 NOT BETWEEN ([><])
@ -260,6 +316,7 @@ func testConditionSyntax(that *Context) Map {
articles8 := that.Db.Select("article", "id,title,sort", Map{"sort[><]": Slice{-10, 0}, "LIMIT": 3}) articles8 := that.Db.Select("article", "id,title,sort", Map{"sort[><]": Slice{-10, 0}, "LIMIT": 3})
test8["result"] = true test8["result"] = true
test8["count"] = len(articles8) test8["count"] = len(articles8)
debugLog("main.go:262", "NOT BETWEEN 查询测试", Map{"count": len(articles8)}, "H2_NOT_BETWEEN")
tests = append(tests, test8) tests = append(tests, test8)
// 2.9 IN 查询 // 2.9 IN 查询
@ -268,6 +325,7 @@ func testConditionSyntax(that *Context) Map {
test9["result"] = true test9["result"] = true
test9["count"] = len(articles9) test9["count"] = len(articles9)
test9["lastQuery"] = that.Db.LastQuery test9["lastQuery"] = that.Db.LastQuery
debugLog("main.go:271", "IN 查询测试", Map{"count": len(articles9), "query": that.Db.LastQuery}, "H2_IN")
tests = append(tests, test9) tests = append(tests, test9)
// 2.10 NOT IN ([!]) // 2.10 NOT IN ([!])
@ -275,6 +333,7 @@ func testConditionSyntax(that *Context) Map {
articles10 := that.Db.Select("article", "id,title", Map{"id[!]": Slice{1, 2, 3}, "LIMIT": 5}) articles10 := that.Db.Select("article", "id,title", Map{"id[!]": Slice{1, 2, 3}, "LIMIT": 5})
test10["result"] = true test10["result"] = true
test10["count"] = len(articles10) test10["count"] = len(articles10)
debugLog("main.go:279", "NOT IN 查询测试", Map{"count": len(articles10)}, "H2_NOT_IN")
tests = append(tests, test10) tests = append(tests, test10)
// 2.11 IS NULL // 2.11 IS NULL
@ -282,6 +341,7 @@ func testConditionSyntax(that *Context) Map {
articles11 := that.Db.Select("article", "id,title,img", Map{"img": nil, "LIMIT": 3}) articles11 := that.Db.Select("article", "id,title,img", Map{"img": nil, "LIMIT": 3})
test11["result"] = true test11["result"] = true
test11["count"] = len(articles11) test11["count"] = len(articles11)
debugLog("main.go:287", "IS NULL 查询测试", Map{"count": len(articles11)}, "H2_IS_NULL")
tests = append(tests, test11) tests = append(tests, test11)
// 2.12 IS NOT NULL ([!]) // 2.12 IS NOT NULL ([!])
@ -289,6 +349,7 @@ func testConditionSyntax(that *Context) Map {
articles12 := that.Db.Select("article", "id,title,create_time", Map{"create_time[!]": nil, "LIMIT": 3}) articles12 := that.Db.Select("article", "id,title,create_time", Map{"create_time[!]": nil, "LIMIT": 3})
test12["result"] = true test12["result"] = true
test12["count"] = len(articles12) test12["count"] = len(articles12)
debugLog("main.go:295", "IS NOT NULL 查询测试", Map{"count": len(articles12)}, "H2_IS_NOT_NULL")
tests = append(tests, test12) tests = append(tests, test12)
// 2.13 直接 SQL ([##] 用于 SQL 片段) // 2.13 直接 SQL ([##] 用于 SQL 片段)
@ -300,6 +361,7 @@ func testConditionSyntax(that *Context) Map {
test13["result"] = true test13["result"] = true
test13["count"] = len(articles13) test13["count"] = len(articles13)
test13["lastQuery"] = that.Db.LastQuery test13["lastQuery"] = that.Db.LastQuery
debugLog("main.go:307", "直接 SQL 片段查询测试", Map{"count": len(articles13), "query": that.Db.LastQuery}, "H2_RAW_SQL")
tests = append(tests, test13) tests = append(tests, test13)
// 2.14 显式 AND 条件 // 2.14 显式 AND 条件
@ -313,6 +375,7 @@ func testConditionSyntax(that *Context) Map {
}) })
test14["result"] = true test14["result"] = true
test14["count"] = len(articles14) test14["count"] = len(articles14)
debugLog("main.go:321", "显式 AND 条件测试", Map{"count": len(articles14)}, "H2_EXPLICIT_AND")
tests = append(tests, test14) tests = append(tests, test14)
// 2.15 OR 条件 // 2.15 OR 条件
@ -326,6 +389,7 @@ func testConditionSyntax(that *Context) Map {
}) })
test15["result"] = true test15["result"] = true
test15["count"] = len(articles15) test15["count"] = len(articles15)
debugLog("main.go:335", "OR 条件测试", Map{"count": len(articles15)}, "H2_OR")
tests = append(tests, test15) tests = append(tests, test15)
// 2.16 嵌套 AND/OR 条件 // 2.16 嵌套 AND/OR 条件
@ -343,6 +407,7 @@ func testConditionSyntax(that *Context) Map {
test16["result"] = true test16["result"] = true
test16["count"] = len(articles16) test16["count"] = len(articles16)
test16["lastQuery"] = that.Db.LastQuery test16["lastQuery"] = that.Db.LastQuery
debugLog("main.go:353", "嵌套 AND/OR 条件测试", Map{"count": len(articles16), "query": that.Db.LastQuery}, "H2_NESTED")
tests = append(tests, test16) tests = append(tests, test16)
result["tests"] = tests result["tests"] = tests
@ -355,6 +420,8 @@ func testChainQuery(that *Context) Map {
result := Map{"name": "链式查询测试", "tests": Slice{}} result := Map{"name": "链式查询测试", "tests": Slice{}}
tests := Slice{} tests := Slice{}
debugLog("main.go:366", "开始链式查询测试 (MySQL)", nil, "H3_CHAIN")
// 3.1 基本链式查询 - 使用 article 表 // 3.1 基本链式查询 - 使用 article 表
test1 := Map{"name": "基本链式查询 Table().Where().Select()"} test1 := Map{"name": "基本链式查询 Table().Where().Select()"}
articles1 := that.Db.Table("article"). articles1 := that.Db.Table("article").
@ -362,6 +429,7 @@ func testChainQuery(that *Context) Map {
Select("id,title,author") Select("id,title,author")
test1["result"] = len(articles1) >= 0 test1["result"] = len(articles1) >= 0
test1["count"] = len(articles1) test1["count"] = len(articles1)
debugLog("main.go:375", "基本链式查询测试", Map{"count": len(articles1)}, "H3_BASIC")
tests = append(tests, test1) tests = append(tests, test1)
// 3.2 链式 And 条件 // 3.2 链式 And 条件
@ -373,6 +441,7 @@ func testChainQuery(that *Context) Map {
Select("id,title,click_num") Select("id,title,click_num")
test2["result"] = len(articles2) >= 0 test2["result"] = len(articles2) >= 0
test2["count"] = len(articles2) test2["count"] = len(articles2)
debugLog("main.go:387", "链式 And 条件测试", Map{"count": len(articles2)}, "H3_AND")
tests = append(tests, test2) tests = append(tests, test2)
// 3.3 链式 Or 条件 // 3.3 链式 Or 条件
@ -386,6 +455,7 @@ func testChainQuery(that *Context) Map {
Select("id,title,sort,click_num") Select("id,title,sort,click_num")
test3["result"] = len(articles3) >= 0 test3["result"] = len(articles3) >= 0
test3["count"] = len(articles3) test3["count"] = len(articles3)
debugLog("main.go:401", "链式 Or 条件测试", Map{"count": len(articles3)}, "H3_OR")
tests = append(tests, test3) tests = append(tests, test3)
// 3.4 链式 Order // 3.4 链式 Order
@ -397,6 +467,7 @@ func testChainQuery(that *Context) Map {
Select("id,title,create_time") Select("id,title,create_time")
test4["result"] = len(articles4) >= 0 test4["result"] = len(articles4) >= 0
test4["count"] = len(articles4) test4["count"] = len(articles4)
debugLog("main.go:413", "链式 Order 排序测试", Map{"count": len(articles4)}, "H3_ORDER")
tests = append(tests, test4) tests = append(tests, test4)
// 3.5 链式 Limit // 3.5 链式 Limit
@ -407,6 +478,7 @@ func testChainQuery(that *Context) Map {
Select("id,title") Select("id,title")
test5["result"] = len(articles5) <= 3 test5["result"] = len(articles5) <= 3
test5["count"] = len(articles5) test5["count"] = len(articles5)
debugLog("main.go:424", "链式 Limit 限制测试", Map{"count": len(articles5)}, "H3_LIMIT")
tests = append(tests, test5) tests = append(tests, test5)
// 3.6 链式 Get 单条 // 3.6 链式 Get 单条
@ -416,6 +488,7 @@ func testChainQuery(that *Context) Map {
Get("id,title,author") Get("id,title,author")
test6["result"] = article6 != nil || true // 允许为空 test6["result"] = article6 != nil || true // 允许为空
test6["article"] = article6 test6["article"] = article6
debugLog("main.go:434", "链式 Get 获取单条测试", Map{"article": article6}, "H3_GET")
tests = append(tests, test6) tests = append(tests, test6)
// 3.7 链式 Count // 3.7 链式 Count
@ -425,6 +498,7 @@ func testChainQuery(that *Context) Map {
Count() Count()
test7["result"] = count7 >= 0 test7["result"] = count7 >= 0
test7["count"] = count7 test7["count"] = count7
debugLog("main.go:444", "链式 Count 统计测试", Map{"count": count7}, "H3_COUNT")
tests = append(tests, test7) tests = append(tests, test7)
// 3.8 链式 Page 分页 // 3.8 链式 Page 分页
@ -435,6 +509,7 @@ func testChainQuery(that *Context) Map {
Select("id,title") Select("id,title")
test8["result"] = len(articles8) <= 5 test8["result"] = len(articles8) <= 5
test8["count"] = len(articles8) test8["count"] = len(articles8)
debugLog("main.go:455", "链式 Page 分页测试", Map{"count": len(articles8)}, "H3_PAGE")
tests = append(tests, test8) tests = append(tests, test8)
// 3.9 链式 Group 分组 // 3.9 链式 Group 分组
@ -445,6 +520,7 @@ func testChainQuery(that *Context) Map {
Select("ctg_id, COUNT(*) as cnt") Select("ctg_id, COUNT(*) as cnt")
test9["result"] = len(stats9) >= 0 test9["result"] = len(stats9) >= 0
test9["stats"] = stats9 test9["stats"] = stats9
debugLog("main.go:466", "链式 Group 分组测试", Map{"stats": stats9}, "H3_GROUP")
tests = append(tests, test9) tests = append(tests, test9)
// 3.10 链式 Update // 3.10 链式 Update
@ -461,6 +537,7 @@ func testChainQuery(that *Context) Map {
test10["result"] = true test10["result"] = true
test10["note"] = "无可用测试数据" test10["note"] = "无可用测试数据"
} }
debugLog("main.go:483", "链式 Update 更新测试", test10, "H3_UPDATE")
tests = append(tests, test10) tests = append(tests, test10)
result["tests"] = tests result["tests"] = tests
@ -473,6 +550,8 @@ func testJoinQuery(that *Context) Map {
result := Map{"name": "JOIN查询测试", "tests": Slice{}} result := Map{"name": "JOIN查询测试", "tests": Slice{}}
tests := Slice{} tests := Slice{}
debugLog("main.go:496", "开始 JOIN 查询测试 (MySQL)", nil, "H4_JOIN")
// 4.1 LEFT JOIN 链式 - article 关联 ctg // 4.1 LEFT JOIN 链式 - article 关联 ctg
test1 := Map{"name": "LEFT JOIN 链式查询"} test1 := Map{"name": "LEFT JOIN 链式查询"}
articles1 := that.Db.Table("article"). articles1 := that.Db.Table("article").
@ -483,6 +562,7 @@ func testJoinQuery(that *Context) Map {
test1["result"] = len(articles1) >= 0 test1["result"] = len(articles1) >= 0
test1["count"] = len(articles1) test1["count"] = len(articles1)
test1["data"] = articles1 test1["data"] = articles1
debugLog("main.go:508", "LEFT JOIN 链式查询测试", Map{"count": len(articles1)}, "H4_LEFT_JOIN")
tests = append(tests, test1) tests = append(tests, test1)
// 4.2 传统 JOIN 语法 // 4.2 传统 JOIN 语法
@ -499,6 +579,7 @@ func testJoinQuery(that *Context) Map {
test2["result"] = len(articles2) >= 0 test2["result"] = len(articles2) >= 0
test2["count"] = len(articles2) test2["count"] = len(articles2)
test2["lastQuery"] = that.Db.LastQuery test2["lastQuery"] = that.Db.LastQuery
debugLog("main.go:525", "传统 JOIN 语法测试", Map{"count": len(articles2), "query": that.Db.LastQuery}, "H4_TRADITIONAL_JOIN")
tests = append(tests, test2) tests = append(tests, test2)
// 4.3 多表 JOIN - article 关联 ctg 和 admin // 4.3 多表 JOIN - article 关联 ctg 和 admin
@ -512,6 +593,7 @@ func testJoinQuery(that *Context) Map {
test3["result"] = len(articles3) >= 0 test3["result"] = len(articles3) >= 0
test3["count"] = len(articles3) test3["count"] = len(articles3)
test3["data"] = articles3 test3["data"] = articles3
debugLog("main.go:539", "多表 JOIN 测试", Map{"count": len(articles3)}, "H4_MULTI_JOIN")
tests = append(tests, test3) tests = append(tests, test3)
// 4.4 INNER JOIN // 4.4 INNER JOIN
@ -523,6 +605,7 @@ func testJoinQuery(that *Context) Map {
Select("article.id, article.title, ctg.name as ctg_name") Select("article.id, article.title, ctg.name as ctg_name")
test4["result"] = len(articles4) >= 0 test4["result"] = len(articles4) >= 0
test4["count"] = len(articles4) test4["count"] = len(articles4)
debugLog("main.go:551", "INNER JOIN 测试", Map{"count": len(articles4)}, "H4_INNER_JOIN")
tests = append(tests, test4) tests = append(tests, test4)
result["tests"] = tests result["tests"] = tests
@ -535,11 +618,14 @@ func testAggregate(that *Context) Map {
result := Map{"name": "聚合函数测试", "tests": Slice{}} result := Map{"name": "聚合函数测试", "tests": Slice{}}
tests := Slice{} tests := Slice{}
debugLog("main.go:564", "开始聚合函数测试 (MySQL)", nil, "H5_AGGREGATE")
// 5.1 Count 总数 // 5.1 Count 总数
test1 := Map{"name": "Count 总数统计"} test1 := Map{"name": "Count 总数统计"}
count1 := that.Db.Count("article") count1 := that.Db.Count("article")
test1["result"] = count1 >= 0 test1["result"] = count1 >= 0
test1["count"] = count1 test1["count"] = count1
debugLog("main.go:571", "Count 总数统计测试", Map{"count": count1}, "H5_COUNT")
tests = append(tests, test1) tests = append(tests, test1)
// 5.2 Count 带条件 // 5.2 Count 带条件
@ -547,6 +633,7 @@ func testAggregate(that *Context) Map {
count2 := that.Db.Count("article", Map{"state": 0}) count2 := that.Db.Count("article", Map{"state": 0})
test2["result"] = count2 >= 0 test2["result"] = count2 >= 0
test2["count"] = count2 test2["count"] = count2
debugLog("main.go:579", "Count 条件统计测试", Map{"count": count2}, "H5_COUNT_WHERE")
tests = append(tests, test2) tests = append(tests, test2)
// 5.3 Sum 求和 // 5.3 Sum 求和
@ -554,6 +641,7 @@ func testAggregate(that *Context) Map {
sum3 := that.Db.Sum("article", "click_num", Map{"state": 0}) sum3 := that.Db.Sum("article", "click_num", Map{"state": 0})
test3["result"] = sum3 >= 0 test3["result"] = sum3 >= 0
test3["sum"] = sum3 test3["sum"] = sum3
debugLog("main.go:587", "Sum 求和测试", Map{"sum": sum3}, "H5_SUM")
tests = append(tests, test3) tests = append(tests, test3)
// 5.4 Avg 平均值 // 5.4 Avg 平均值
@ -561,6 +649,7 @@ func testAggregate(that *Context) Map {
avg4 := that.Db.Avg("article", "click_num", Map{"state": 0}) avg4 := that.Db.Avg("article", "click_num", Map{"state": 0})
test4["result"] = avg4 >= 0 test4["result"] = avg4 >= 0
test4["avg"] = avg4 test4["avg"] = avg4
debugLog("main.go:595", "Avg 平均值测试", Map{"avg": avg4}, "H5_AVG")
tests = append(tests, test4) tests = append(tests, test4)
// 5.5 Max 最大值 // 5.5 Max 最大值
@ -568,6 +657,7 @@ func testAggregate(that *Context) Map {
max5 := that.Db.Max("article", "click_num", Map{"state": 0}) max5 := that.Db.Max("article", "click_num", Map{"state": 0})
test5["result"] = max5 >= 0 test5["result"] = max5 >= 0
test5["max"] = max5 test5["max"] = max5
debugLog("main.go:603", "Max 最大值测试", Map{"max": max5}, "H5_MAX")
tests = append(tests, test5) tests = append(tests, test5)
// 5.6 Min 最小值 // 5.6 Min 最小值
@ -575,6 +665,7 @@ func testAggregate(that *Context) Map {
min6 := that.Db.Min("article", "sort", Map{"state": 0}) min6 := that.Db.Min("article", "sort", Map{"state": 0})
test6["result"] = true // sort 可能为 0 test6["result"] = true // sort 可能为 0
test6["min"] = min6 test6["min"] = min6
debugLog("main.go:611", "Min 最小值测试", Map{"min": min6}, "H5_MIN")
tests = append(tests, test6) tests = append(tests, test6)
// 5.7 GROUP BY 分组统计 // 5.7 GROUP BY 分组统计
@ -589,6 +680,7 @@ func testAggregate(that *Context) Map {
}) })
test7["result"] = len(stats7) >= 0 test7["result"] = len(stats7) >= 0
test7["stats"] = stats7 test7["stats"] = stats7
debugLog("main.go:625", "GROUP BY 分组统计测试", Map{"stats": stats7}, "H5_GROUP_BY")
tests = append(tests, test7) tests = append(tests, test7)
result["tests"] = tests result["tests"] = tests
@ -601,6 +693,8 @@ func testPagination(that *Context) Map {
result := Map{"name": "分页查询测试", "tests": Slice{}} result := Map{"name": "分页查询测试", "tests": Slice{}}
tests := Slice{} tests := Slice{}
debugLog("main.go:638", "开始分页查询测试 (MySQL)", nil, "H6_PAGINATION")
// 6.1 PageSelect 分页查询 // 6.1 PageSelect 分页查询
test1 := Map{"name": "PageSelect 分页查询"} test1 := Map{"name": "PageSelect 分页查询"}
articles1 := that.Db.Page(1, 5).PageSelect("article", "*", Map{ articles1 := that.Db.Page(1, 5).PageSelect("article", "*", Map{
@ -609,6 +703,7 @@ func testPagination(that *Context) Map {
}) })
test1["result"] = len(articles1) <= 5 test1["result"] = len(articles1) <= 5
test1["count"] = len(articles1) test1["count"] = len(articles1)
debugLog("main.go:648", "PageSelect 分页查询测试", Map{"count": len(articles1)}, "H6_PAGE_SELECT")
tests = append(tests, test1) tests = append(tests, test1)
// 6.2 第二页 // 6.2 第二页
@ -619,6 +714,7 @@ func testPagination(that *Context) Map {
}) })
test2["result"] = len(articles2) <= 5 test2["result"] = len(articles2) <= 5
test2["count"] = len(articles2) test2["count"] = len(articles2)
debugLog("main.go:659", "PageSelect 第二页测试", Map{"count": len(articles2)}, "H6_PAGE_2")
tests = append(tests, test2) tests = append(tests, test2)
// 6.3 链式分页 // 6.3 链式分页
@ -630,6 +726,7 @@ func testPagination(that *Context) Map {
Select("id,title,author") Select("id,title,author")
test3["result"] = len(articles3) <= 3 test3["result"] = len(articles3) <= 3
test3["count"] = len(articles3) test3["count"] = len(articles3)
debugLog("main.go:671", "链式 Page 分页测试", Map{"count": len(articles3)}, "H6_CHAIN_PAGE")
tests = append(tests, test3) tests = append(tests, test3)
// 6.4 Offset 偏移 // 6.4 Offset 偏移
@ -642,6 +739,7 @@ func testPagination(that *Context) Map {
test4["result"] = len(articles4) <= 3 test4["result"] = len(articles4) <= 3
test4["count"] = len(articles4) test4["count"] = len(articles4)
test4["lastQuery"] = that.Db.LastQuery test4["lastQuery"] = that.Db.LastQuery
debugLog("main.go:684", "Offset 偏移查询测试", Map{"count": len(articles4), "query": that.Db.LastQuery}, "H6_OFFSET")
tests = append(tests, test4) tests = append(tests, test4)
result["tests"] = tests result["tests"] = tests
@ -650,14 +748,16 @@ func testPagination(that *Context) Map {
} }
// ==================== 7. 批量插入测试 ==================== // ==================== 7. 批量插入测试 ====================
func testInserts(that *Context) Map { func testBatchInsert(that *Context) Map {
result := Map{"name": "批量插入测试", "tests": Slice{}} result := Map{"name": "批量插入测试", "tests": Slice{}}
tests := Slice{} tests := Slice{}
debugLog("main.go:697", "开始批量插入测试 (MySQL)", nil, "H7_BATCH")
// 7.1 批量插入 // 7.1 批量插入
test1 := Map{"name": "Inserts 批量插入"} test1 := Map{"name": "BatchInsert 批量插入"}
timestamp := time.Now().UnixNano() timestamp := time.Now().UnixNano()
affected1 := that.Db.Inserts("test_batch", []Map{ affected1 := that.Db.BatchInsert("test_batch", []Map{
{"name": fmt.Sprintf("批量测试1_%d", timestamp), "title": "标题1", "state": 1}, {"name": fmt.Sprintf("批量测试1_%d", timestamp), "title": "标题1", "state": 1},
{"name": fmt.Sprintf("批量测试2_%d", timestamp), "title": "标题2", "state": 1}, {"name": fmt.Sprintf("批量测试2_%d", timestamp), "title": "标题2", "state": 1},
{"name": fmt.Sprintf("批量测试3_%d", timestamp), "title": "标题3", "state": 1}, {"name": fmt.Sprintf("批量测试3_%d", timestamp), "title": "标题3", "state": 1},
@ -665,17 +765,19 @@ func testInserts(that *Context) Map {
test1["result"] = affected1 >= 0 test1["result"] = affected1 >= 0
test1["affected"] = affected1 test1["affected"] = affected1
test1["lastQuery"] = that.Db.LastQuery test1["lastQuery"] = that.Db.LastQuery
debugLog("main.go:710", "BatchInsert 批量插入测试", Map{"affected": affected1, "query": that.Db.LastQuery}, "H7_BATCH_INSERT")
tests = append(tests, test1) tests = append(tests, test1)
// 7.2 带 [#] 的批量插入 // 7.2 带 [#] 的批量插入
test2 := Map{"name": "Inserts 带 [#] 标记"} test2 := Map{"name": "BatchInsert 带 [#] 标记"}
timestamp2 := time.Now().UnixNano() timestamp2 := time.Now().UnixNano()
affected2 := that.Db.Inserts("test_batch", []Map{ affected2 := that.Db.BatchInsert("test_batch", []Map{
{"name": fmt.Sprintf("带时间测试1_%d", timestamp2), "title": "标题带时间1", "state": 1, "create_time[#]": "NOW()"}, {"name": fmt.Sprintf("带时间测试1_%d", timestamp2), "title": "标题带时间1", "state": 1, "create_time[#]": "NOW()"},
{"name": fmt.Sprintf("带时间测试2_%d", timestamp2), "title": "标题带时间2", "state": 1, "create_time[#]": "NOW()"}, {"name": fmt.Sprintf("带时间测试2_%d", timestamp2), "title": "标题带时间2", "state": 1, "create_time[#]": "NOW()"},
}) })
test2["result"] = affected2 >= 0 test2["result"] = affected2 >= 0
test2["affected"] = affected2 test2["affected"] = affected2
debugLog("main.go:722", "BatchInsert 带 [#] 标记测试", Map{"affected": affected2}, "H7_BATCH_RAW")
tests = append(tests, test2) tests = append(tests, test2)
// 清理测试数据 // 清理测试数据
@ -692,6 +794,8 @@ func testUpsert(that *Context) Map {
result := Map{"name": "Upsert测试", "tests": Slice{}} result := Map{"name": "Upsert测试", "tests": Slice{}}
tests := Slice{} tests := Slice{}
debugLog("main.go:739", "开始 Upsert 测试 (MySQL)", nil, "H8_UPSERT")
// 使用 admin 表测试 UpsertMySQL ON DUPLICATE KEY UPDATE // 使用 admin 表测试 UpsertMySQL ON DUPLICATE KEY UPDATE
timestamp := time.Now().Unix() timestamp := time.Now().Unix()
testPhone := fmt.Sprintf("199%08d", timestamp%100000000) testPhone := fmt.Sprintf("199%08d", timestamp%100000000)
@ -715,6 +819,7 @@ func testUpsert(that *Context) Map {
test1["result"] = affected1 >= 0 test1["result"] = affected1 >= 0
test1["affected"] = affected1 test1["affected"] = affected1
test1["lastQuery"] = that.Db.LastQuery test1["lastQuery"] = that.Db.LastQuery
debugLog("main.go:763", "Upsert 插入新记录测试", Map{"affected": affected1, "query": that.Db.LastQuery}, "H8_UPSERT_INSERT")
tests = append(tests, test1) tests = append(tests, test1)
// 8.2 Upsert 更新已存在记录 // 8.2 Upsert 更新已存在记录
@ -735,6 +840,7 @@ func testUpsert(that *Context) Map {
) )
test2["result"] = affected2 >= 0 test2["result"] = affected2 >= 0
test2["affected"] = affected2 test2["affected"] = affected2
debugLog("main.go:783", "Upsert 更新已存在记录测试", Map{"affected": affected2}, "H8_UPSERT_UPDATE")
tests = append(tests, test2) tests = append(tests, test2)
// 验证更新结果 // 验证更新结果
@ -754,6 +860,8 @@ func testTransaction(that *Context) Map {
result := Map{"name": "事务测试", "tests": Slice{}} result := Map{"name": "事务测试", "tests": Slice{}}
tests := Slice{} tests := Slice{}
debugLog("main.go:803", "开始事务测试 (MySQL)", nil, "H9_TRANSACTION")
// 9.1 事务成功提交 // 9.1 事务成功提交
test1 := Map{"name": "事务成功提交"} test1 := Map{"name": "事务成功提交"}
timestamp := time.Now().Unix() timestamp := time.Now().Unix()
@ -767,6 +875,7 @@ func testTransaction(that *Context) Map {
"state": 1, "state": 1,
"create_time[#]": "NOW()", "create_time[#]": "NOW()",
}) })
debugLog("main.go:818", "事务内插入记录", Map{"recordId": recordId}, "H9_TX_INSERT")
return recordId != 0 return recordId != 0
}) })
@ -775,6 +884,7 @@ func testTransaction(that *Context) Map {
// 验证数据是否存在 // 验证数据是否存在
checkRecord := that.Db.Get("test_batch", "*", Map{"name": testName1}) checkRecord := that.Db.Get("test_batch", "*", Map{"name": testName1})
test1["recordExists"] = checkRecord != nil test1["recordExists"] = checkRecord != nil
debugLog("main.go:831", "事务成功提交测试", Map{"success": success1, "recordExists": checkRecord != nil}, "H9_TX_SUCCESS")
tests = append(tests, test1) tests = append(tests, test1)
// 9.2 事务回滚 // 9.2 事务回滚
@ -783,12 +893,13 @@ func testTransaction(that *Context) Map {
success2 := that.Db.Action(func(tx HoTimeDB) bool { success2 := that.Db.Action(func(tx HoTimeDB) bool {
// 插入记录 // 插入记录
_ = tx.Insert("test_batch", Map{ recordId := tx.Insert("test_batch", Map{
"name": testName2, "name": testName2,
"title": "事务回滚测试", "title": "事务回滚测试",
"state": 1, "state": 1,
"create_time[#]": "NOW()", "create_time[#]": "NOW()",
}) })
debugLog("main.go:846", "事务内插入(将回滚)", Map{"recordId": recordId}, "H9_TX_ROLLBACK_INSERT")
// 返回 false 触发回滚 // 返回 false 触发回滚
return false return false
@ -798,6 +909,7 @@ func testTransaction(that *Context) Map {
// 验证数据是否不存在(已回滚) // 验证数据是否不存在(已回滚)
checkRecord2 := that.Db.Get("test_batch", "*", Map{"name": testName2}) checkRecord2 := that.Db.Get("test_batch", "*", Map{"name": testName2})
test2["recordRolledBack"] = checkRecord2 == nil test2["recordRolledBack"] = checkRecord2 == nil
debugLog("main.go:856", "事务回滚测试", Map{"success": success2, "rolledBack": checkRecord2 == nil}, "H9_TX_ROLLBACK")
tests = append(tests, test2) tests = append(tests, test2)
// 清理测试数据 // 清理测试数据
@ -813,11 +925,14 @@ func testRawSQL(that *Context) Map {
result := Map{"name": "原生SQL测试", "tests": Slice{}} result := Map{"name": "原生SQL测试", "tests": Slice{}}
tests := Slice{} tests := Slice{}
debugLog("main.go:872", "开始原生 SQL 测试 (MySQL)", nil, "H10_RAW_SQL")
// 10.1 Query 查询 - 使用真实的 article 表 // 10.1 Query 查询 - 使用真实的 article 表
test1 := Map{"name": "Query 原生查询"} test1 := Map{"name": "Query 原生查询"}
articles1 := that.Db.Query("SELECT id, title, author FROM `article` WHERE state = ? LIMIT ?", 0, 5) articles1 := that.Db.Query("SELECT id, title, author FROM `article` WHERE state = ? LIMIT ?", 0, 5)
test1["result"] = len(articles1) >= 0 test1["result"] = len(articles1) >= 0
test1["count"] = len(articles1) test1["count"] = len(articles1)
debugLog("main.go:879", "Query 原生查询测试", Map{"count": len(articles1)}, "H10_QUERY")
tests = append(tests, test1) tests = append(tests, test1)
// 10.2 Exec 执行 - 使用 article 表 // 10.2 Exec 执行 - 使用 article 表
@ -838,67 +953,9 @@ func testRawSQL(that *Context) Map {
test2["result"] = true test2["result"] = true
test2["note"] = "无可用测试数据" test2["note"] = "无可用测试数据"
} }
debugLog("main.go:899", "Exec 原生执行测试", test2, "H10_EXEC")
tests = append(tests, test2) 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["tests"] = tests
result["success"] = true result["success"] = true
return result return result

BIN
example/test_server.exe Normal file

Binary file not shown.

View File

@ -766,12 +766,12 @@ correctUsers4 := db.Select("user", "*", common.Map{
_ = db _ = db
} }
// InsertsExample 批量插入示例 // BatchInsertExample 批量插入示例
func InsertsExample(database *db.HoTimeDB) { func BatchInsertExample(database *db.HoTimeDB) {
fmt.Println("\n=== 批量插入示例 ===") fmt.Println("\n=== 批量插入示例 ===")
// 批量插入用户(使用 []Map 格式) // 批量插入用户(使用 []Map 格式)
affected := database.Inserts("user", []common.Map{ affected := database.BatchInsert("user", []common.Map{
{"name": "批量用户1", "email": "batch1@example.com", "age": 25, "status": 1}, {"name": "批量用户1", "email": "batch1@example.com", "age": 25, "status": 1},
{"name": "批量用户2", "email": "batch2@example.com", "age": 30, "status": 1}, {"name": "批量用户2", "email": "batch2@example.com", "age": 30, "status": 1},
{"name": "批量用户3", "email": "batch3@example.com", "age": 28, "status": 1}, {"name": "批量用户3", "email": "batch3@example.com", "age": 28, "status": 1},
@ -779,7 +779,7 @@ func InsertsExample(database *db.HoTimeDB) {
fmt.Printf("批量插入了 %d 条用户记录\n", affected) fmt.Printf("批量插入了 %d 条用户记录\n", affected)
// 批量插入日志(使用 [#] 标记直接 SQL // 批量插入日志(使用 [#] 标记直接 SQL
logAffected := database.Inserts("log", []common.Map{ logAffected := database.BatchInsert("log", []common.Map{
{"user_id": 1, "action": "login", "ip": "192.168.1.1", "created_time[#]": "NOW()"}, {"user_id": 1, "action": "login", "ip": "192.168.1.1", "created_time[#]": "NOW()"},
{"user_id": 2, "action": "logout", "ip": "192.168.1.2", "created_time[#]": "NOW()"}, {"user_id": 2, "action": "logout", "ip": "192.168.1.2", "created_time[#]": "NOW()"},
{"user_id": 3, "action": "view", "ip": "192.168.1.3", "created_time[#]": "NOW()"}, {"user_id": 3, "action": "view", "ip": "192.168.1.3", "created_time[#]": "NOW()"},
@ -838,7 +838,7 @@ func RunAllFixedExamples() {
fmt.Println("所有示例代码已修正完毕,语法正确!") fmt.Println("所有示例代码已修正完毕,语法正确!")
fmt.Println("") fmt.Println("")
fmt.Println("新增功能示例说明:") fmt.Println("新增功能示例说明:")
fmt.Println(" - InsertsExample(db): 批量插入示例,使用 []Map 格式") fmt.Println(" - BatchInsertExample(db): 批量插入示例,使用 []Map 格式")
fmt.Println(" - UpsertExample(db): 插入或更新示例") fmt.Println(" - UpsertExample(db): 插入或更新示例")
} }

View File

@ -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,41 @@ 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.Index(file, "common/") == 0 {
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)
} }

1
var.go
View File

@ -107,7 +107,6 @@ var ConfigNote = Map{
"timeout": "默认60 * 60 * 24 * 30非必须过期时间超时自动删除", "timeout": "默认60 * 60 * 24 * 30非必须过期时间超时自动删除",
"db": "默认false非必须缓存数据库启用后能减少数据库的读写压力", "db": "默认false非必须缓存数据库启用后能减少数据库的读写压力",
"session": "默认true非必须缓存web session同时缓存session保持的用户缓存", "session": "默认true非必须缓存web session同时缓存session保持的用户缓存",
"history": "默认false非必须是否开启缓存历史记录开启后每次新增/修改缓存都会记录到历史表,历史表一旦创建不会自动删除",
}, },
"redis": Map{ "redis": Map{
"host": "默认服务ip127.0.0.1必须如果需要使用redis服务时配置", "host": "默认服务ip127.0.0.1必须如果需要使用redis服务时配置",