Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 230dfc5a2b | |||
| f4760c5d3e | |||
| d1b905b780 | |||
| 8dac2aff66 | |||
| cf64276ab1 | |||
| 29a3b6095d | |||
| 650fafad1a | |||
| 6164dfe9bf | |||
| 5ba883cd6b | |||
| c2955d2500 | |||
| 0e1775f72b | |||
| 1a9e7e19b7 | |||
| 4828f3625c |
File diff suppressed because one or more lines are too long
249
.cursor/plans/多数据库方言与前缀支持_d7ceee79.plan.md
Normal file
249
.cursor/plans/多数据库方言与前缀支持_d7ceee79.plan.md
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
---
|
||||||
|
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()` 正确性
|
||||||
144
.cursor/plans/缓存数据库表重构_73959142.plan.md
Normal file
144
.cursor/plans/缓存数据库表重构_73959142.plan.md
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
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,插入历史表
|
||||||
|
- 删除:不记录历史
|
||||||
|
- 历史表仅日志记录,无读取接口,人工在数据库操作
|
||||||
103
.cursor/plans/规范文档创建_6d183386.plan.md
Normal file
103
.cursor/plans/规范文档创建_6d183386.plan.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
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): 字段规则示例
|
||||||
5
.cursor/worktrees.json
Normal file
5
.cursor/worktrees.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"setup-worktree": [
|
||||||
|
"npm install"
|
||||||
|
]
|
||||||
|
}
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,6 +1,6 @@
|
|||||||
/.idea/*
|
/.idea/*
|
||||||
.idea
|
.idea
|
||||||
/example/config/app.json
|
|
||||||
/example/tpt/demo/
|
/example/tpt/demo/
|
||||||
/example/config/
|
*.exe
|
||||||
/*.exe
|
/example/config
|
||||||
|
/.cursor/*.log
|
||||||
|
|||||||
213
README.md
213
README.md
@ -2,75 +2,45 @@
|
|||||||
|
|
||||||
**高性能 Go Web 服务框架**
|
**高性能 Go Web 服务框架**
|
||||||
|
|
||||||
## 特性
|
一个"小而全"的 Go Web 框架,内置 ORM、三级缓存、Session 管理,让你专注于业务逻辑。
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
- **高性能** - 单机 10万+ QPS,支持百万级并发用户
|
- **高性能** - 单机 10万+ QPS,支持百万级并发用户
|
||||||
- **多数据库支持** - MySQL、SQLite3,支持主从分离
|
- **内置 ORM** - 类 Medoo 语法,链式查询,支持 MySQL/SQLite/PostgreSQL
|
||||||
- **三级缓存系统** - Memory > Redis > DB,自动穿透与回填
|
- **三级缓存** - Memory > Redis > DB,自动穿透与回填
|
||||||
- **Session管理** - 内置会话管理,支持多种存储后端
|
- **Session 管理** - 内置会话管理,支持多种存储后端
|
||||||
- **自动代码生成** - 根据数据库表自动生成 CRUD 接口
|
- **代码生成** - 根据数据库表自动生成 CRUD 接口
|
||||||
- **丰富工具类** - 上下文管理、类型转换、加密解密等
|
- **开箱即用** - 微信支付/公众号/小程序、阿里云、腾讯云等 SDK 内置
|
||||||
|
|
||||||
## 快速开始
|
## 文档
|
||||||
|
|
||||||
### 安装
|
| 文档 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| [快速上手指南](docs/QUICKSTART.md) | 5 分钟入门,安装配置、路由、中间件、基础数据库操作 |
|
||||||
|
| [HoTimeDB 使用说明](docs/HoTimeDB_使用说明.md) | 完整数据库 ORM 教程 |
|
||||||
|
| [HoTimeDB API 参考](docs/HoTimeDB_API参考.md) | 数据库 API 速查手册 |
|
||||||
|
| [Common 工具类](docs/Common_工具类使用说明.md) | Map/Slice/Obj 类型、类型转换、工具函数 |
|
||||||
|
| [代码生成器](docs/CodeGen_使用说明.md) | 自动 CRUD 代码生成、配置规则 |
|
||||||
|
| [数据库设计规范](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
|
||||||
```
|
```
|
||||||
|
|
||||||
### 最小示例
|
## 性能
|
||||||
|
|
||||||
```go
|
| 并发数 | QPS | 成功率 | 平均延迟 |
|
||||||
package main
|
|--------|-----|--------|----------|
|
||||||
|
| 500 | 99,960 | 100% | 5.0ms |
|
||||||
|
| **1000** | **102,489** | **100%** | **9.7ms** |
|
||||||
|
| 2000 | 75,801 | 99.99% | 26.2ms |
|
||||||
|
|
||||||
import (
|
> 测试环境:24 核 CPU,Windows 10,Go 1.19.3
|
||||||
. "code.hoteas.com/golang/hotime"
|
|
||||||
. "code.hoteas.com/golang/hotime/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
appIns := Init("config/config.json")
|
|
||||||
|
|
||||||
appIns.Run(Router{
|
|
||||||
"app": {
|
|
||||||
"test": {
|
|
||||||
"hello": func(that *Context) {
|
|
||||||
that.Display(0, Map{"message": "Hello World"})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
访问: http://localhost:8081/app/test/hello
|
|
||||||
|
|
||||||
## 性能测试报告
|
|
||||||
|
|
||||||
### 测试环境
|
|
||||||
|
|
||||||
| 项目 | 配置 |
|
|
||||||
|------|------|
|
|
||||||
| CPU | 24 核心 |
|
|
||||||
| 系统 | Windows 10 |
|
|
||||||
| Go 版本 | 1.19.3 |
|
|
||||||
|
|
||||||
### 测试结果
|
|
||||||
|
|
||||||
| 并发数 | QPS | 成功率 | 平均延迟 | P99延迟 |
|
|
||||||
|--------|-----|--------|----------|---------|
|
|
||||||
| 500 | 99,960 | 100% | 5.0ms | 25.2ms |
|
|
||||||
| **1000** | **102,489** | **100%** | **9.7ms** | **56.8ms** |
|
|
||||||
| 2000 | 75,801 | 99.99% | 26.2ms | 127.7ms |
|
|
||||||
| 5000 | 12,611 | 99.95% | 391.4ms | 781.4ms |
|
|
||||||
|
|
||||||
### 性能总结
|
|
||||||
|
|
||||||
```
|
|
||||||
最高 QPS: 102,489 请求/秒
|
|
||||||
最佳并发数: 1,000
|
|
||||||
```
|
|
||||||
|
|
||||||
### 并发用户估算
|
### 并发用户估算
|
||||||
|
|
||||||
@ -79,24 +49,9 @@ func main() {
|
|||||||
| 高频交互 | 1次/秒 | ~10万 |
|
| 高频交互 | 1次/秒 | ~10万 |
|
||||||
| 活跃用户 | 1次/5秒 | ~50万 |
|
| 活跃用户 | 1次/5秒 | ~50万 |
|
||||||
| 普通浏览 | 1次/10秒 | ~100万 |
|
| 普通浏览 | 1次/10秒 | ~100万 |
|
||||||
| 低频访问 | 1次/30秒 | ~300万 |
|
|
||||||
|
|
||||||
**生产环境建议**: 保留 30-50% 性能余量,安全并发用户数约 **50万 - 70万**
|
|
||||||
|
|
||||||
### 与主流框架性能对比
|
|
||||||
|
|
||||||
| 框架 | 典型QPS | 基础实现 | 性能评级 |
|
|
||||||
|------|---------|----------|----------|
|
|
||||||
| **HoTime** | **~100K** | net/http | 第一梯队 |
|
|
||||||
| Fiber | ~100K+ | fasthttp | 第一梯队 |
|
|
||||||
| Gin | ~60-80K | net/http | 第二梯队 |
|
|
||||||
| Echo | ~60-80K | net/http | 第二梯队 |
|
|
||||||
| Chi | ~50-60K | net/http | 第二梯队 |
|
|
||||||
|
|
||||||
## 框架对比
|
## 框架对比
|
||||||
|
|
||||||
### 功能特性对比
|
|
||||||
|
|
||||||
| 特性 | HoTime | Gin | Echo | Fiber |
|
| 特性 | HoTime | Gin | Echo | Fiber |
|
||||||
|------|--------|-----|------|-------|
|
|------|--------|-----|------|-------|
|
||||||
| 性能 | 100K QPS | 70K QPS | 70K QPS | 100K QPS |
|
| 性能 | 100K QPS | 70K QPS | 70K QPS | 100K QPS |
|
||||||
@ -105,117 +60,25 @@ func main() {
|
|||||||
| Session | ✅ 内置 | ❌ 需插件 | ❌ 需插件 | ❌ 需插件 |
|
| Session | ✅ 内置 | ❌ 需插件 | ❌ 需插件 | ❌ 需插件 |
|
||||||
| 代码生成 | ✅ | ❌ | ❌ | ❌ |
|
| 代码生成 | ✅ | ❌ | ❌ | ❌ |
|
||||||
| 微信/支付集成 | ✅ 内置 | ❌ | ❌ | ❌ |
|
| 微信/支付集成 | ✅ 内置 | ❌ | ❌ | ❌ |
|
||||||
| 路由灵活性 | 中等 | 优秀 | 优秀 | 优秀 |
|
|
||||||
| 社区生态 | 较小 | 庞大 | 较大 | 较大 |
|
|
||||||
|
|
||||||
### HoTime 优势
|
## 适用场景
|
||||||
|
|
||||||
1. **开箱即用** - 内置 ORM + 缓存 + Session,无需额外集成
|
|
||||||
2. **三级缓存** - Memory > Redis > DB,自动穿透与回填
|
|
||||||
3. **开发效率高** - 链式查询语法简洁,内置微信/云服务SDK
|
|
||||||
4. **性能优异** - 100K QPS,媲美最快的 Fiber 框架
|
|
||||||
|
|
||||||
### 适用场景
|
|
||||||
|
|
||||||
| 场景 | 推荐度 | 说明 |
|
| 场景 | 推荐度 | 说明 |
|
||||||
|------|--------|------|
|
|------|--------|------|
|
||||||
| 中小型后台系统 | ⭐⭐⭐⭐⭐ | 完美适配,开发效率最高 |
|
| 中小型后台系统 | ⭐⭐⭐⭐⭐ | 完美适配,开发效率最高 |
|
||||||
| 微信小程序后端 | ⭐⭐⭐⭐⭐ | 内置微信SDK |
|
| 微信小程序后端 | ⭐⭐⭐⭐⭐ | 内置微信 SDK |
|
||||||
| 快速原型开发 | ⭐⭐⭐⭐⭐ | 代码生成 + 全功能集成 |
|
| 快速原型开发 | ⭐⭐⭐⭐⭐ | 代码生成 + 全功能集成 |
|
||||||
| 高并发API服务 | ⭐⭐⭐⭐ | 性能足够 |
|
| 高并发 API 服务 | ⭐⭐⭐⭐ | 性能足够 |
|
||||||
| 大型微服务 | ⭐⭐⭐ | 建议用Gin/Echo |
|
| 大型微服务 | ⭐⭐⭐ | 建议用 Gin/Echo |
|
||||||
|
|
||||||
### 总体评价
|
|
||||||
|
|
||||||
| 维度 | 评分 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| 性能 | 95分 | 第一梯队,媲美Fiber |
|
|
||||||
| 功能集成 | 90分 | 远超主流框架 |
|
|
||||||
| 开发效率 | 85分 | 适合快速开发 |
|
|
||||||
| 生态/社区 | 50分 | 持续建设中 |
|
|
||||||
|
|
||||||
> **总结**: HoTime 是"小而全"的高性能框架,性能不输主流,集成度远超主流,适合独立开发者或小团队快速构建中小型项目。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 数据库操作
|
|
||||||
|
|
||||||
### 基础 CRUD
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 查询单条
|
|
||||||
user := that.Db.Get("user", "*", Map{"id": 1})
|
|
||||||
|
|
||||||
// 查询列表
|
|
||||||
users := that.Db.Select("user", "*", Map{"status": 1, "ORDER": "id DESC"})
|
|
||||||
|
|
||||||
// 插入数据
|
|
||||||
id := that.Db.Insert("user", Map{"name": "test", "age": 18})
|
|
||||||
|
|
||||||
// 更新数据
|
|
||||||
rows := that.Db.Update("user", Map{"name": "new"}, Map{"id": 1})
|
|
||||||
|
|
||||||
// 删除数据
|
|
||||||
rows := that.Db.Delete("user", Map{"id": 1})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 链式查询
|
|
||||||
|
|
||||||
```go
|
|
||||||
users := that.Db.Table("user").
|
|
||||||
LeftJoin("order", "user.id=order.user_id").
|
|
||||||
And("status", 1).
|
|
||||||
Order("id DESC").
|
|
||||||
Page(1, 10).
|
|
||||||
Select("*")
|
|
||||||
```
|
|
||||||
|
|
||||||
### 条件语法
|
|
||||||
|
|
||||||
| 语法 | 说明 | 示例 |
|
|
||||||
|------|------|------|
|
|
||||||
| key | 等于 | "id": 1 |
|
|
||||||
| key[>] | 大于 | "age[>]": 18 |
|
|
||||||
| key[<] | 小于 | "age[<]": 60 |
|
|
||||||
| key[!] | 不等于 | "status[!]": 0 |
|
|
||||||
| key[~] | LIKE | "name[~]": "test" |
|
|
||||||
| key[<>] | BETWEEN | "age[<>]": Slice{18, 60} |
|
|
||||||
|
|
||||||
## 缓存系统
|
|
||||||
|
|
||||||
三级缓存: **Memory > Redis > Database**
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 通用缓存
|
|
||||||
that.Cache("key", value) // 设置
|
|
||||||
data := that.Cache("key") // 获取
|
|
||||||
that.Cache("key", nil) // 删除
|
|
||||||
|
|
||||||
// Session 缓存
|
|
||||||
that.Session("user_id", 123) // 设置
|
|
||||||
userId := that.Session("user_id") // 获取
|
|
||||||
```
|
|
||||||
|
|
||||||
## 中间件
|
|
||||||
|
|
||||||
```go
|
|
||||||
appIns.SetConnectListener(func(that *Context) bool {
|
|
||||||
if that.Session("user_id").Data == nil {
|
|
||||||
that.Display(2, "请先登录")
|
|
||||||
return true // 终止请求
|
|
||||||
}
|
|
||||||
return false // 继续处理
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## 扩展功能
|
## 扩展功能
|
||||||
|
|
||||||
- **微信支付/公众号/小程序** - dri/wechat/
|
- 微信支付/公众号/小程序 - `dri/wechat/`
|
||||||
- **阿里云服务** - dri/aliyun/
|
- 阿里云服务 - `dri/aliyun/`
|
||||||
- **腾讯云服务** - dri/tencent/
|
- 腾讯云服务 - `dri/tencent/`
|
||||||
- **文件上传下载** - dri/upload/, dri/download/
|
- 文件上传下载 - `dri/upload/`, `dri/download/`
|
||||||
- **MongoDB** - dri/mongodb/
|
- MongoDB - `dri/mongodb/`
|
||||||
- **RSA加解密** - dri/rsa/
|
- RSA 加解密 - `dri/rsa/`
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
10
cache/cache.go
vendored
10
cache/cache.go
vendored
@ -265,9 +265,13 @@ func (that *HoTimeCache) Init(config Map, hotimeDb HoTimeDBInterface, err ...*Er
|
|||||||
}
|
}
|
||||||
that.Config["db"] = db
|
that.Config["db"] = db
|
||||||
|
|
||||||
that.dbCache = &CacheDb{TimeOut: db.GetCeilInt64("timeout"),
|
that.dbCache = &CacheDb{
|
||||||
DbSet: db.GetBool("db"), SessionSet: db.GetBool("session"),
|
TimeOut: db.GetCeilInt64("timeout"),
|
||||||
Db: hotimeDb}
|
DbSet: db.GetBool("db"),
|
||||||
|
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])
|
||||||
|
|||||||
517
cache/cache_db.go
vendored
517
cache/cache_db.go
vendored
@ -1,11 +1,38 @@
|
|||||||
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 {
|
||||||
@ -24,6 +51,7 @@ 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
|
||||||
@ -31,143 +59,468 @@ 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" {
|
|
||||||
|
|
||||||
dbNames := that.Db.Query("SELECT DATABASE()")
|
dbType := that.Db.GetType()
|
||||||
|
tableName := that.getTableName()
|
||||||
|
historyTableName := that.getHistoryTableName()
|
||||||
|
|
||||||
if len(dbNames) == 0 {
|
// #region agent log
|
||||||
return
|
debugLog("F", "cache_db.go:initDbTable", "initDbTable started", map[string]interface{}{
|
||||||
|
"dbType": dbType, "tableName": tableName, "historyTableName": historyTableName, "HistorySet": that.HistorySet,
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// 检查并创建主表
|
||||||
|
if !that.tableExists(tableName) {
|
||||||
|
that.createMainTable(dbType, tableName)
|
||||||
}
|
}
|
||||||
dbName := dbNames[0].GetString("DATABASE()")
|
|
||||||
res := that.Db.Query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='" + dbName + "' AND TABLE_NAME='" + that.Db.GetPrefix() + "cached'")
|
// 检查并迁移旧 cached 表
|
||||||
if len(res) != 0 {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
that.isInit = true
|
that.isInit = true
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, 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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if that.Db.GetType() == "sqlite" {
|
|
||||||
res := that.Db.Query(`select * from sqlite_master where type = 'table' and name = '` + that.Db.GetPrefix() + `cached'`)
|
|
||||||
|
|
||||||
if len(res) != 0 {
|
|
||||||
that.isInit = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, e := that.Db.Exec(`CREATE TABLE "` + that.Db.GetPrefix() + `cached" (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
"key" TEXT(60),
|
|
||||||
"value" TEXT(2000),
|
|
||||||
"time" integer,
|
|
||||||
"endtime" integer
|
|
||||||
);`)
|
|
||||||
if e.GetError() == nil {
|
|
||||||
that.isInit = true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取Cache键只能为string类型
|
// tableExists 检查表是否存在
|
||||||
func (that *CacheDb) get(key string) interface{} {
|
func (that *CacheDb) tableExists(tableName string) bool {
|
||||||
|
dbType := that.Db.GetType()
|
||||||
|
|
||||||
cached := that.Db.Get("cached", "*", Map{"key": key})
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMainTable 创建主表
|
||||||
|
func (that *CacheDb) createMainTable(dbType, tableName string) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
that.Db.Exec(createSQL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 表迁移数据
|
||||||
|
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{} {
|
||||||
|
tableName := that.getTableName()
|
||||||
|
cached := that.Db.Get(tableName, "*", Map{"key": key})
|
||||||
|
|
||||||
|
// #region agent log
|
||||||
|
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() {
|
|
||||||
|
|
||||||
that.Db.Delete("cached", Map{"id": cached.GetString("id")})
|
// 使用字符串比较判断过期(ISO 格式天然支持)
|
||||||
|
endTime := cached.GetString("end_time")
|
||||||
|
nowTime := Time2Str(time.Now())
|
||||||
|
if endTime != "" && endTime <= nowTime {
|
||||||
|
// 惰性删除:过期只返回 nil,不立即删除
|
||||||
|
// 依赖随机清理批量删除过期数据
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data := Map{}
|
// 直接解析 value,不再需要 {"data": value} 包装
|
||||||
data.JsonToMap(cached.GetString("value"))
|
valueStr := cached.GetString("value")
|
||||||
|
if valueStr == "" {
|
||||||
return data.Get("data")
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
// key value ,时间为时间戳
|
|
||||||
func (that *CacheDb) set(key string, value interface{}, tim int64) {
|
|
||||||
|
|
||||||
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})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//随机执行删除命令
|
var data interface{}
|
||||||
|
err := json.Unmarshal([]byte(valueStr), &data)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// #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 {
|
||||||
that.Db.Delete("cached", Map{"endtime[<]": time.Now().Unix()})
|
nowTimeStr := Time2Str(time.Now())
|
||||||
|
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("cached", Map{"key": key + "%"})
|
that.Db.Delete(tableName, Map{"key[~]": key + "%"})
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
that.Db.Delete("cached", Map{"key": key})
|
that.Db.Delete(tableName, 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 {
|
||||||
if that.TimeOut == 0 {
|
// 使用配置的 TimeOut,如果为 0 则使用默认值
|
||||||
//that.Time = Config.GetInt64("cacheLongTime")
|
timeout = that.TimeOut
|
||||||
|
if timeout == 0 {
|
||||||
|
timeout = DefaultCacheTimeout
|
||||||
}
|
}
|
||||||
tim += that.TimeOut
|
} else if len(data) >= 2 {
|
||||||
}
|
// 使用指定的超时时间
|
||||||
if len(data) == 2 {
|
|
||||||
that.SetError(nil)
|
that.SetError(nil)
|
||||||
tempt := ObjToInt64(data[1], that.Error)
|
tempTimeout := ObjToInt64(data[1], that.Error)
|
||||||
|
if that.GetError() == nil && tempTimeout > 0 {
|
||||||
if tempt > tim {
|
timeout = tempTimeout
|
||||||
tim = tempt
|
} else {
|
||||||
} else if that.GetError() == nil {
|
timeout = that.TimeOut
|
||||||
|
if timeout == 0 {
|
||||||
tim = tim + tempt
|
timeout = DefaultCacheTimeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
that.set(key, data[0], tim)
|
}
|
||||||
|
|
||||||
|
endTime := time.Now().Add(time.Duration(timeout) * time.Second)
|
||||||
|
that.set(key, data[0], endTime)
|
||||||
return &Obj{Data: nil}
|
return &Obj{Data: nil}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"flow": {},
|
|
||||||
"id": "d751713988987e9331980363e24189ce",
|
|
||||||
"label": "HoTime管理平台",
|
|
||||||
"labelConfig": {
|
|
||||||
"add": "添加",
|
|
||||||
"delete": "删除",
|
|
||||||
"download": "下载清单",
|
|
||||||
"edit": "编辑",
|
|
||||||
"info": "查看详情",
|
|
||||||
"show": "开启"
|
|
||||||
},
|
|
||||||
"menus": [],
|
|
||||||
"name": "admin",
|
|
||||||
"stop": [
|
|
||||||
"role",
|
|
||||||
"org"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
{
|
|
||||||
"cache": {
|
|
||||||
"db": {
|
|
||||||
"db": "默认false,非必须,缓存数据库,启用后能减少数据库的读写压力",
|
|
||||||
"session": "默认true,非必须,缓存web session,同时缓存session保持的用户缓存",
|
|
||||||
"timeout": "默认60 * 60 * 24 * 30,非必须,过期时间,超时自动删除"
|
|
||||||
},
|
|
||||||
"memory": {
|
|
||||||
"db": "默认true,非必须,缓存数据库,启用后能减少数据库的读写压力",
|
|
||||||
"session": "默认true,非必须,缓存web session,同时缓存session保持的用户缓存",
|
|
||||||
"timeout": "默认60 * 60 * 2,非必须,过期时间,超时自动删除"
|
|
||||||
},
|
|
||||||
"redis": {
|
|
||||||
"db": "默认true,非必须,缓存数据库,启用后能减少数据库的读写压力",
|
|
||||||
"host": "默认服务ip:127.0.0.1,必须,如果需要使用redis服务时配置,",
|
|
||||||
"password": "默认密码空,必须,如果需要使用redis服务时配置,默认密码空",
|
|
||||||
"port": "默认服务端口:6379,必须,如果需要使用redis服务时配置,",
|
|
||||||
"session": "默认true,非必须,缓存web session,同时缓存session保持的用户缓存",
|
|
||||||
"timeout": "默认60 * 60 * 24 * 15,非必须,过期时间,超时自动删除"
|
|
||||||
},
|
|
||||||
"注释": "可配置memory,db,redis,默认启用memory,默认优先级为memory\u003eredis\u003edb,memory与数据库缓存设置项一致,缓存数据填充会自动反方向反哺,加入memory缓存过期将自动从redis更新,但memory永远不会更新redis,如果是集群建议不要开启memory,配置即启用"
|
|
||||||
},
|
|
||||||
"codeConfig": [
|
|
||||||
"注释:配置即启用,非必须,默认无",
|
|
||||||
{
|
|
||||||
"config": "默认config/app.json,必须,接口描述配置文件",
|
|
||||||
"configDB": "默认无,非必须,有则每次将数据库数据生成到此目录用于配置读写,无则不生成",
|
|
||||||
"mode": "默认0,非必须,0为内嵌代码模式,1为生成代码模式",
|
|
||||||
"name": "默认无,非必须,有则生成代码到此目录,无则采用缺省模式使用表名,如设置为:admin,将在admin目录生成包名为admin的代码",
|
|
||||||
"rule": "默认config/rule.json,非必须,有则按改规则生成接口,无则按系统内嵌方式生成",
|
|
||||||
"table": "默认admin,必须,根据数据库内当前表名做为用户生成数据"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"crossDomain": "默认空 非必须,空字符串为不开启,如果需要跨域设置,auto为智能开启所有网站允许跨域,http://www.baidu.com为指定域允许跨域",
|
|
||||||
"db": {
|
|
||||||
"mysql": {
|
|
||||||
"host": "默认127.0.0.1,必须,数据库ip地址",
|
|
||||||
"name": "默认test,必须,数据库名称",
|
|
||||||
"password": "默认root,必须,数据库密码",
|
|
||||||
"port": "默认3306,必须,数据库端口",
|
|
||||||
"prefix": "默认空,非必须,数据表前缀",
|
|
||||||
"slave": {
|
|
||||||
"host": "默认127.0.0.1,必须,数据库ip地址",
|
|
||||||
"name": "默认test,必须,数据库名称",
|
|
||||||
"password": "默认root,必须,数据库密码",
|
|
||||||
"port": "默认3306,必须,数据库端口",
|
|
||||||
"user": "默认root,必须,数据库用户名",
|
|
||||||
"注释": "从数据库配置,mysql里配置slave项即启用主从读写,减少数据库压力"
|
|
||||||
},
|
|
||||||
"user": "默认root,必须,数据库用户名",
|
|
||||||
"注释": "除prefix及主从数据库slave项,其他全部必须"
|
|
||||||
},
|
|
||||||
"sqlite": {
|
|
||||||
"path": "默认config/data.db,必须,数据库位置"
|
|
||||||
},
|
|
||||||
"注释": "配置即启用,非必须,默认使用sqlite数据库"
|
|
||||||
},
|
|
||||||
"defFile": "默认访问index.html或者index.htm文件,必须,默认访问文件类型",
|
|
||||||
"error": {
|
|
||||||
"1": "内部系统异常,在环境配置,文件访问权限等基础运行环境条件不足造成严重错误时使用",
|
|
||||||
"2": "访问权限异常,没有登录或者登录异常等时候使用",
|
|
||||||
"3": "请求参数异常,request参数不满足要求,比如参数不足,参数类型错误,参数不满足要求等时候使用",
|
|
||||||
"4": "数据处理异常,数据库操作或者三方请求返回的结果非正常结果,比如数据库突然中断等时候使用",
|
|
||||||
"5": "数据结果异常,一般用于无法给出response要求的格式要求下使用,比如response需要的是string格式但你只能提供int数据时",
|
|
||||||
"注释": "web服务内置错误提示,自定义异常建议10开始"
|
|
||||||
},
|
|
||||||
"logFile": "无默认,非必须,如果需要存储日志文件时使用,保存格式为:a/b/c/20060102150405.txt,将生成:a/b/c/年月日时分秒.txt,按需设置",
|
|
||||||
"logLevel": "默认0,必须,0关闭,1打印,日志等级",
|
|
||||||
"mode": "默认0,非必须,0生产模式,1,测试模式,2开发模式,3内嵌代码模式,在开发模式下会显示更多的数据用于开发测试,并能够辅助研发,自动生成配置文件、代码等功能,web无缓存,数据库不启用缓存",
|
|
||||||
"modeRouterStrict": "默认false,必须,路由严格模式false,为大小写忽略必须匹配,true必须大小写匹配",
|
|
||||||
"port": "默认80,必须,web服务开启Http端口,0为不启用http服务,默认80",
|
|
||||||
"sessionName": "默认HOTIME,必须,设置session的cookie名",
|
|
||||||
"tlsCert": "默认空,非必须,https证书",
|
|
||||||
"tlsKey": "默认空,非必须,https密钥",
|
|
||||||
"tlsPort": "默认空,非必须,web服务https端口,0为不启用https服务",
|
|
||||||
"tpt": "默认tpt,必须,web静态文件目录,默认为程序目录下tpt目录",
|
|
||||||
"webConnectLogFile": "无默认,非必须,webConnectLogShow开启之后才能使用,如果需要存储日志文件时使用,保存格式为:a/b/c/20060102150405.txt,将生成:a/b/c/年月日时分秒.txt,按需设置",
|
|
||||||
"webConnectLogShow": "默认true,非必须,访问日志如果需要web访问链接、访问ip、访问时间打印,false为关闭true开启此功能"
|
|
||||||
}
|
|
||||||
422
config/rule.json
422
config/rule.json
@ -1,422 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
252
db/README.md
252
db/README.md
@ -1,252 +0,0 @@
|
|||||||
# HoTimeDB ORM 文档集合
|
|
||||||
|
|
||||||
这是HoTimeDB ORM框架的完整文档集合,包含使用说明、API参考、示例代码和测试数据。
|
|
||||||
|
|
||||||
## ⚠️ 重要更新说明
|
|
||||||
|
|
||||||
**语法修正通知**:经过对源码的深入分析,发现HoTimeDB的条件查询语法有特定规则:
|
|
||||||
- ✅ **单个条件**:可以直接写在Map中
|
|
||||||
- ⚠️ **多个条件**:必须使用`AND`或`OR`包装
|
|
||||||
- 📝 所有文档和示例代码已按正确语法更新
|
|
||||||
|
|
||||||
## 📚 文档列表
|
|
||||||
|
|
||||||
### 1. [HoTimeDB_使用说明.md](./HoTimeDB_使用说明.md)
|
|
||||||
**完整使用说明书** - 详细的功能介绍和使用指南
|
|
||||||
- 🚀 快速开始
|
|
||||||
- ⚙️ 数据库配置
|
|
||||||
- 🔧 基本操作 (CRUD)
|
|
||||||
- 🔗 链式查询构建器
|
|
||||||
- 🔍 条件查询语法
|
|
||||||
- 🔄 JOIN操作
|
|
||||||
- 📄 分页查询
|
|
||||||
- 📊 聚合函数
|
|
||||||
- 🔐 事务处理
|
|
||||||
- 💾 缓存机制
|
|
||||||
- ⚡ 高级特性
|
|
||||||
|
|
||||||
### 2. [HoTimeDB_API参考.md](./HoTimeDB_API参考.md)
|
|
||||||
**快速API参考手册** - 开发时的速查手册
|
|
||||||
- 📖 基本方法
|
|
||||||
- 🔧 CRUD操作
|
|
||||||
- 📊 聚合函数
|
|
||||||
- 📄 分页查询
|
|
||||||
- 🔍 条件语法参考
|
|
||||||
- 🔗 JOIN语法
|
|
||||||
- 🔐 事务处理
|
|
||||||
- 🛠️ 工具方法
|
|
||||||
|
|
||||||
### 3. [示例代码文件](../examples/hotimedb_examples.go)
|
|
||||||
**完整示例代码集合** - 可运行的实际应用示例(语法已修正)
|
|
||||||
- 🏗️ 基本初始化和配置
|
|
||||||
- 📝 基本CRUD操作
|
|
||||||
- 🔗 链式查询操作
|
|
||||||
- 🤝 JOIN查询操作
|
|
||||||
- 🔍 条件查询语法
|
|
||||||
- 📄 分页查询
|
|
||||||
- 📊 聚合函数查询
|
|
||||||
- 🔐 事务处理
|
|
||||||
- 💾 缓存机制
|
|
||||||
- 🔧 原生SQL执行
|
|
||||||
- 🚨 错误处理和调试
|
|
||||||
- ⚡ 性能优化技巧
|
|
||||||
- 🎯 完整应用示例
|
|
||||||
|
|
||||||
### 4. [test_tables.sql](./test_tables.sql)
|
|
||||||
**测试数据库结构** - 快速搭建测试环境
|
|
||||||
- 🏗️ 完整的表结构定义
|
|
||||||
- 📊 测试数据插入
|
|
||||||
- 🔍 索引优化
|
|
||||||
- 👁️ 视图示例
|
|
||||||
- 🔧 存储过程示例
|
|
||||||
|
|
||||||
## 🎯 核心特性
|
|
||||||
|
|
||||||
### 🌟 主要优势
|
|
||||||
- **类Medoo语法**: 参考PHP Medoo设计,语法简洁易懂
|
|
||||||
- **链式查询**: 支持流畅的链式查询构建器
|
|
||||||
- **条件丰富**: 支持丰富的条件查询语法
|
|
||||||
- **事务支持**: 完整的事务处理机制
|
|
||||||
- **缓存集成**: 内置查询结果缓存
|
|
||||||
- **读写分离**: 支持主从数据库配置
|
|
||||||
- **类型安全**: 基于Golang的强类型系统
|
|
||||||
|
|
||||||
### 🔧 支持的数据库
|
|
||||||
- ✅ MySQL
|
|
||||||
- ✅ SQLite
|
|
||||||
- ✅ 其他标准SQL数据库
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 1. 安装依赖
|
|
||||||
```bash
|
|
||||||
go mod init your-project
|
|
||||||
go get github.com/go-sql-driver/mysql
|
|
||||||
go get github.com/sirupsen/logrus
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 创建测试数据库
|
|
||||||
```bash
|
|
||||||
mysql -u root -p < test_tables.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 基本使用
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"code.hoteas.com/golang/hotime/db"
|
|
||||||
"code.hoteas.com/golang/hotime/common"
|
|
||||||
"database/sql"
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
database := &db.HoTimeDB{
|
|
||||||
Prefix: "app_",
|
|
||||||
Mode: 2, // 开发模式
|
|
||||||
}
|
|
||||||
|
|
||||||
database.SetConnect(func() (master, slave *sql.DB) {
|
|
||||||
master, _ = sql.Open("mysql", "user:pass@tcp(localhost:3306)/dbname")
|
|
||||||
return master, master
|
|
||||||
})
|
|
||||||
|
|
||||||
// 链式查询(链式语法支持单独Where然后用And添加条件)
|
|
||||||
users := database.Table("user").
|
|
||||||
Where("status", 1). // 链式中可以单独Where
|
|
||||||
And("age[>]", 18). // 用And添加更多条件
|
|
||||||
Order("created_time DESC").
|
|
||||||
Limit(0, 10).
|
|
||||||
Select("id,name,email")
|
|
||||||
|
|
||||||
// 或者使用传统语法(多个条件必须用AND包装)
|
|
||||||
users2 := database.Select("user", "id,name,email", common.Map{
|
|
||||||
"AND": common.Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
},
|
|
||||||
"ORDER": "created_time DESC",
|
|
||||||
"LIMIT": []int{0, 10},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚠️ 重要语法规则
|
|
||||||
|
|
||||||
**条件查询语法规则:**
|
|
||||||
- ✅ **单个条件**:可以直接写在Map中
|
|
||||||
- ✅ **多个条件**:必须使用`AND`或`OR`包装
|
|
||||||
- ✅ **特殊参数**:`ORDER`、`GROUP`、`LIMIT`与条件同级
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ✅ 正确:单个条件
|
|
||||||
Map{"status": 1}
|
|
||||||
|
|
||||||
// ✅ 正确:多个条件用AND包装
|
|
||||||
Map{
|
|
||||||
"AND": Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18,
|
|
||||||
},
|
|
||||||
"ORDER": "id DESC",
|
|
||||||
}
|
|
||||||
|
|
||||||
// ❌ 错误:多个条件不用AND包装
|
|
||||||
Map{
|
|
||||||
"status": 1,
|
|
||||||
"age[>]": 18, // 不支持!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 条件查询语法速查
|
|
||||||
|
|
||||||
| 语法 | SQL | 说明 |
|
|
||||||
|------|-----|------|
|
|
||||||
| `"field": value` | `field = ?` | 等于 |
|
|
||||||
| `"field[!]": value` | `field != ?` | 不等于 |
|
|
||||||
| `"field[>]": value` | `field > ?` | 大于 |
|
|
||||||
| `"field[>=]": value` | `field >= ?` | 大于等于 |
|
|
||||||
| `"field[<]": value` | `field < ?` | 小于 |
|
|
||||||
| `"field[<=]": value` | `field <= ?` | 小于等于 |
|
|
||||||
| `"field[~]": "keyword"` | `field LIKE '%keyword%'` | 包含 |
|
|
||||||
| `"field[<>]": [min, max]` | `field BETWEEN ? AND ?` | 区间内 |
|
|
||||||
| `"field": [v1, v2, v3]` | `field IN (?, ?, ?)` | 在集合中 |
|
|
||||||
| `"field": nil` | `field IS NULL` | 为空 |
|
|
||||||
| `"field[#]": "NOW()"` | `field = NOW()` | 直接SQL |
|
|
||||||
|
|
||||||
## 🔗 JOIN语法速查
|
|
||||||
|
|
||||||
| 语法 | SQL | 说明 |
|
|
||||||
|------|-----|------|
|
|
||||||
| `"[>]table"` | `LEFT JOIN` | 左连接 |
|
|
||||||
| `"[<]table"` | `RIGHT JOIN` | 右连接 |
|
|
||||||
| `"[><]table"` | `INNER JOIN` | 内连接 |
|
|
||||||
| `"[<>]table"` | `FULL JOIN` | 全连接 |
|
|
||||||
|
|
||||||
## 🛠️ 链式方法速查
|
|
||||||
|
|
||||||
```go
|
|
||||||
db.Table("table") // 指定表名
|
|
||||||
.Where(key, value) // WHERE条件
|
|
||||||
.And(key, value) // AND条件
|
|
||||||
.Or(map) // OR条件
|
|
||||||
.LeftJoin(table, on) // LEFT JOIN
|
|
||||||
.Order(fields...) // ORDER BY
|
|
||||||
.Group(fields...) // GROUP BY
|
|
||||||
.Limit(offset, limit) // LIMIT
|
|
||||||
.Page(page, pageSize) // 分页
|
|
||||||
.Select(fields...) // 查询
|
|
||||||
.Get(fields...) // 获取单条
|
|
||||||
.Count() // 计数
|
|
||||||
.Update(data) // 更新
|
|
||||||
.Delete() // 删除
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚡ 性能优化建议
|
|
||||||
|
|
||||||
### 🔍 查询优化
|
|
||||||
- 使用合适的索引字段作为查询条件
|
|
||||||
- IN查询会自动优化为BETWEEN(连续数字)
|
|
||||||
- 避免SELECT *,指定需要的字段
|
|
||||||
- 合理使用LIMIT限制结果集大小
|
|
||||||
|
|
||||||
### 💾 缓存使用
|
|
||||||
- 查询结果会自动缓存
|
|
||||||
- 增删改操作会自动清除缓存
|
|
||||||
- `cached`表不参与缓存
|
|
||||||
|
|
||||||
### 🔐 事务处理
|
|
||||||
- 批量操作使用事务提高性能
|
|
||||||
- 事务中避免长时间操作
|
|
||||||
- 合理设置事务隔离级别
|
|
||||||
|
|
||||||
## 🚨 注意事项
|
|
||||||
|
|
||||||
### 🔒 安全相关
|
|
||||||
- 使用参数化查询防止SQL注入
|
|
||||||
- `[#]`语法需要注意防止注入
|
|
||||||
- 敏感数据加密存储
|
|
||||||
|
|
||||||
### 🎯 最佳实践
|
|
||||||
- 开发时设置`Mode = 2`便于调试
|
|
||||||
- 生产环境设置`Mode = 0`
|
|
||||||
- 合理设置表前缀
|
|
||||||
- 定期检查慢查询日志
|
|
||||||
|
|
||||||
## 🤝 与PHP Medoo的差异
|
|
||||||
|
|
||||||
1. **类型系统**: 使用`common.Map`和`common.Slice`
|
|
||||||
2. **错误处理**: Golang风格的错误处理
|
|
||||||
3. **链式调用**: 提供更丰富的链式API
|
|
||||||
4. **缓存集成**: 内置缓存功能
|
|
||||||
5. **并发安全**: 需要注意并发使用
|
|
||||||
|
|
||||||
## 📞 技术支持
|
|
||||||
|
|
||||||
- 📧 查看源码:`hotimedb.go`
|
|
||||||
- 📖 参考文档:本目录下的各个文档文件
|
|
||||||
- 🔧 示例代码:运行`HoTimeDB_示例代码.go`中的示例
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**HoTimeDB ORM框架 - 让数据库操作更简单!** 🎉
|
|
||||||
|
|
||||||
> 本文档基于HoTimeDB源码分析生成,参考了PHP Medoo的设计理念,并根据Golang语言特性进行了优化。
|
|
||||||
131
db/crud.go
131
db/crud.go
@ -87,17 +87,26 @@ 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" {
|
||||||
query += " " + qu[intQs].(string)
|
// 字段列表字符串,使用处理器处理 table.column 格式
|
||||||
|
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, ".") {
|
||||||
query += " " + k + " "
|
// 处理 table.column 格式
|
||||||
|
query += " " + processor.ProcessFieldList(k) + " "
|
||||||
} else {
|
} else {
|
||||||
query += " `" + k + "` "
|
// 单独的列名
|
||||||
|
query += " " + processor.ProcessColumnNoPrefix(k) + " "
|
||||||
}
|
}
|
||||||
|
|
||||||
if i+1 != len(data) {
|
if i+1 != len(data) {
|
||||||
@ -109,11 +118,8 @@ func (that *HoTimeDB) Select(table string, qu ...interface{}) []Map {
|
|||||||
query += " *"
|
query += " *"
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(table, ".") && !strings.Contains(table, " AS ") {
|
// 处理表名(添加前缀和正确的引号)
|
||||||
query += " FROM `" + that.Prefix + table + "` "
|
query += " FROM " + processor.ProcessTableName(table) + " "
|
||||||
} else {
|
|
||||||
query += " FROM " + that.Prefix + table + " "
|
|
||||||
}
|
|
||||||
|
|
||||||
if join {
|
if join {
|
||||||
query += that.buildJoin(qu[0])
|
query += that.buildJoin(qu[0])
|
||||||
@ -157,6 +163,7 @@ 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)
|
||||||
@ -184,36 +191,34 @@ 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 = "`" + table + "`"
|
table = processor.ProcessTableName(table)
|
||||||
}
|
// 处理 ON 条件中的 table.column
|
||||||
query += " LEFT JOIN " + table + " ON " + v.(string) + " "
|
onCondition := processor.ProcessConditionString(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)
|
||||||
if !strings.Contains(table, " ") {
|
table = processor.ProcessTableName(table)
|
||||||
table = "`" + table + "`"
|
onCondition := processor.ProcessConditionString(v.(string))
|
||||||
}
|
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)
|
||||||
if !strings.Contains(table, " ") {
|
table = processor.ProcessTableName(table)
|
||||||
table = "`" + table + "`"
|
onCondition := processor.ProcessConditionString(v.(string))
|
||||||
}
|
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)
|
||||||
if !strings.Contains(table, " ") {
|
table = processor.ProcessTableName(table)
|
||||||
table = "`" + table + "`"
|
onCondition := processor.ProcessConditionString(v.(string))
|
||||||
}
|
query += " INNER JOIN " + table + " ON " + onCondition + " "
|
||||||
query += " INNER JOIN " + table + " ON " + v.(string) + " "
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,15 +228,16 @@ 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) == 1 {
|
if len(qu) == 0 {
|
||||||
|
// 没有参数时,添加默认字段和 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
|
||||||
@ -249,6 +255,7 @@ 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
|
||||||
@ -261,25 +268,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 += "`" + k + "`,"
|
queryString += processor.ProcessColumnNoPrefix(k) + ","
|
||||||
valueString += vstr + ","
|
valueString += vstr + ","
|
||||||
} else {
|
} else {
|
||||||
queryString += "`" + k + "`) "
|
queryString += processor.ProcessColumnNoPrefix(k) + ") "
|
||||||
valueString += vstr + ");"
|
valueString += vstr + ");"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
values = append(values, v)
|
values = append(values, v)
|
||||||
if tempLen < lens {
|
if tempLen < lens {
|
||||||
queryString += "`" + k + "`,"
|
queryString += processor.ProcessColumnNoPrefix(k) + ","
|
||||||
valueString += "?,"
|
valueString += "?,"
|
||||||
} else {
|
} else {
|
||||||
queryString += "`" + k + "`) "
|
queryString += processor.ProcessColumnNoPrefix(k) + ") "
|
||||||
valueString += "?);"
|
valueString += "?);"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query := "INSERT INTO `" + that.Prefix + table + "` " + queryString + "VALUES" + valueString
|
query := "INSERT INTO " + processor.ProcessTableName(table) + " " + queryString + "VALUES" + valueString
|
||||||
|
|
||||||
res, err := that.Exec(query, values...)
|
res, err := that.Exec(query, values...)
|
||||||
|
|
||||||
@ -300,19 +307,19 @@ func (that *HoTimeDB) Insert(table string, data map[string]interface{}) int64 {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchInsert 批量插入数据
|
// Inserts 批量插入数据
|
||||||
// table: 表名
|
// table: 表名
|
||||||
// dataList: 数据列表,每个元素是一个 Map
|
// dataList: 数据列表,每个元素是一个 Map
|
||||||
// 返回受影响的行数
|
// 返回受影响的行数
|
||||||
//
|
//
|
||||||
// 示例:
|
// 示例:
|
||||||
//
|
//
|
||||||
// affected := db.BatchInsert("user", []Map{
|
// affected := db.Inserts("user", []Map{
|
||||||
// {"name": "张三", "age": 25, "email": "zhang@example.com"},
|
// {"name": "张三", "age": 25, "email": "zhang@example.com"},
|
||||||
// {"name": "李四", "age": 30, "email": "li@example.com"},
|
// {"name": "李四", "age": 30, "email": "li@example.com"},
|
||||||
// {"name": "王五", "age": 28, "email": "wang@example.com"},
|
// {"name": "王五", "age": 28, "email": "wang@example.com"},
|
||||||
// })
|
// })
|
||||||
func (that *HoTimeDB) BatchInsert(table string, dataList []Map) int64 {
|
func (that *HoTimeDB) Inserts(table string, dataList []Map) int64 {
|
||||||
if len(dataList) == 0 {
|
if len(dataList) == 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -333,10 +340,12 @@ func (that *HoTimeDB) BatchInsert(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] = "`" + col + "`"
|
quotedCols[i] = processor.ProcessColumnNoPrefix(col)
|
||||||
}
|
}
|
||||||
colStr := strings.Join(quotedCols, ", ")
|
colStr := strings.Join(quotedCols, ", ")
|
||||||
|
|
||||||
@ -367,7 +376,7 @@ func (that *HoTimeDB) BatchInsert(table string, dataList []Map) int64 {
|
|||||||
placeholders[i] = "(" + strings.Join(rowPlaceholders, ", ") + ")"
|
placeholders[i] = "(" + strings.Join(rowPlaceholders, ", ") + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
query := "INSERT INTO `" + that.Prefix + table + "` (" + colStr + ") VALUES " + strings.Join(placeholders, ", ")
|
query := "INSERT INTO " + processor.ProcessTableName(table) + " (" + colStr + ") VALUES " + strings.Join(placeholders, ", ")
|
||||||
|
|
||||||
res, err := that.Exec(query, values...)
|
res, err := that.Exec(query, values...)
|
||||||
|
|
||||||
@ -494,11 +503,12 @@ 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] = "`" + col + "`"
|
quotedCols[i] = processor.ProcessColumnNoPrefix(col)
|
||||||
if raw, ok := rawValues[col]; ok {
|
if raw, ok := rawValues[col]; ok {
|
||||||
valueParts[i] = raw
|
valueParts[i] = raw
|
||||||
} else {
|
} else {
|
||||||
@ -508,14 +518,15 @@ 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] = "`" + col + "` = " + raw
|
updateParts[i] = quotedCol + " = " + raw
|
||||||
} else {
|
} else {
|
||||||
updateParts[i] = "`" + col + "` = VALUES(`" + col + "`)"
|
updateParts[i] = quotedCol + " = VALUES(" + quotedCol + ")"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "INSERT INTO `" + that.Prefix + table + "` (" + strings.Join(quotedCols, ", ") +
|
return "INSERT INTO " + processor.ProcessTableName(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, ", ")
|
||||||
}
|
}
|
||||||
@ -524,12 +535,14 @@ 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] = "\"" + col + "\""
|
quotedCols[i] = dialect.QuoteIdentifier(col)
|
||||||
if raw, ok := rawValues[col]; ok {
|
if raw, ok := rawValues[col]; ok {
|
||||||
valueParts[i] = raw
|
valueParts[i] = raw
|
||||||
} else {
|
} else {
|
||||||
@ -540,19 +553,20 @@ 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] = "\"" + key + "\""
|
quotedUniqueKeys[i] = dialect.QuoteIdentifier(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] = "\"" + col + "\" = " + raw
|
updateParts[i] = quotedCol + " = " + raw
|
||||||
} else {
|
} else {
|
||||||
updateParts[i] = "\"" + col + "\" = EXCLUDED.\"" + col + "\""
|
updateParts[i] = quotedCol + " = EXCLUDED." + quotedCol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "INSERT INTO \"" + that.Prefix + table + "\" (" + strings.Join(quotedCols, ", ") +
|
return "INSERT INTO " + processor.ProcessTableName(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, ", ")
|
||||||
@ -562,11 +576,13 @@ 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] = "\"" + col + "\""
|
quotedCols[i] = dialect.QuoteIdentifier(col)
|
||||||
if raw, ok := rawValues[col]; ok {
|
if raw, ok := rawValues[col]; ok {
|
||||||
valueParts[i] = raw
|
valueParts[i] = raw
|
||||||
} else {
|
} else {
|
||||||
@ -576,19 +592,20 @@ 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] = "\"" + key + "\""
|
quotedUniqueKeys[i] = dialect.QuoteIdentifier(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] = "\"" + col + "\" = " + raw
|
updateParts[i] = quotedCol + " = " + raw
|
||||||
} else {
|
} else {
|
||||||
updateParts[i] = "\"" + col + "\" = excluded.\"" + col + "\""
|
updateParts[i] = quotedCol + " = excluded." + quotedCol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "INSERT INTO \"" + that.Prefix + table + "\" (" + strings.Join(quotedCols, ", ") +
|
return "INSERT INTO " + processor.ProcessTableName(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, ", ")
|
||||||
@ -596,7 +613,8 @@ 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 {
|
||||||
query := "UPDATE `" + that.Prefix + table + "` SET "
|
processor := that.GetProcessor()
|
||||||
|
query := "UPDATE " + processor.ProcessTableName(table) + " SET "
|
||||||
qs := make([]interface{}, 0)
|
qs := make([]interface{}, 0)
|
||||||
tp := len(data)
|
tp := len(data)
|
||||||
|
|
||||||
@ -608,7 +626,7 @@ func (that *HoTimeDB) Update(table string, data Map, where Map) int64 {
|
|||||||
} else {
|
} else {
|
||||||
qs = append(qs, v)
|
qs = append(qs, v)
|
||||||
}
|
}
|
||||||
query += "`" + k + "`=" + vstr + " "
|
query += processor.ProcessColumnNoPrefix(k) + "=" + vstr + " "
|
||||||
if tp--; tp != 0 {
|
if tp--; tp != 0 {
|
||||||
query += ", "
|
query += ", "
|
||||||
}
|
}
|
||||||
@ -638,7 +656,8 @@ 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 {
|
||||||
query := "DELETE FROM `" + that.Prefix + table + "` "
|
processor := that.GetProcessor()
|
||||||
|
query := "DELETE FROM " + processor.ProcessTableName(table) + " "
|
||||||
|
|
||||||
temp, resWhere := that.where(data)
|
temp, resWhere := that.where(data)
|
||||||
query += temp + ";"
|
query += temp + ";"
|
||||||
|
|||||||
49
db/db.go
49
db/db.go
@ -4,10 +4,12 @@ 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 数据库操作核心结构体
|
||||||
@ -98,3 +100,48 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@ -14,6 +14,15 @@ 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...
|
||||||
@ -54,6 +63,16 @@ 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 "?"
|
||||||
}
|
}
|
||||||
@ -121,6 +140,16 @@ 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)
|
||||||
}
|
}
|
||||||
@ -192,6 +221,16 @@ 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 "?"
|
||||||
}
|
}
|
||||||
|
|||||||
441
db/dialect_test.go
Normal file
441
db/dialect_test.go
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
267
db/identifier.go
Normal file
267
db/identifier.go
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
205
db/query.go
205
db/query.go
@ -1,12 +1,13 @@
|
|||||||
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 哈希(用于缓存)
|
||||||
@ -23,6 +24,9 @@ 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
|
||||||
@ -82,6 +86,9 @@ 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
|
||||||
@ -155,6 +162,202 @@ 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)
|
||||||
|
|||||||
@ -1,332 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
119
db/where.go
119
db/where.go
@ -70,8 +70,8 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
|
|||||||
}
|
}
|
||||||
sort.Strings(testQu)
|
sort.Strings(testQu)
|
||||||
|
|
||||||
// 追踪普通条件数量,用于自动添加 AND
|
// 追踪条件数量,用于自动添加 AND
|
||||||
normalCondCount := 0
|
condCount := 0
|
||||||
|
|
||||||
for _, k := range testQu {
|
for _, k := range testQu {
|
||||||
v := data[k]
|
v := data[k]
|
||||||
@ -79,8 +79,16 @@ func (that *HoTimeDB) where(data Map) (string, []interface{}) {
|
|||||||
// 检查是否是 AND/OR 条件关键字
|
// 检查是否是 AND/OR 条件关键字
|
||||||
if isConditionKey(k) {
|
if isConditionKey(k) {
|
||||||
tw, ts := that.cond(strings.ToUpper(k), v.(Map))
|
tw, ts := that.cond(strings.ToUpper(k), v.(Map))
|
||||||
where += tw
|
if tw != "" && strings.TrimSpace(tw) != "" {
|
||||||
|
// 与前面的条件用 AND 连接
|
||||||
|
if condCount > 0 {
|
||||||
|
where += " AND "
|
||||||
|
}
|
||||||
|
// 用括号包裹 OR/AND 组条件
|
||||||
|
where += "(" + strings.TrimSpace(tw) + ")"
|
||||||
|
condCount++
|
||||||
res = append(res, ts...)
|
res = append(res, ts...)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,37 +98,61 @@ 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 normalCondCount > 0 {
|
if condCount > 0 {
|
||||||
where += " AND "
|
where += " AND "
|
||||||
}
|
}
|
||||||
where += tv
|
where += tv
|
||||||
normalCondCount++
|
condCount++
|
||||||
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(where, v) == 0 {
|
if strings.Index(trimmedWhere, v) == 0 {
|
||||||
hasWhere = false
|
hasWhere = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasWhere {
|
if hasWhere {
|
||||||
where = " WHERE " + where + " "
|
where = " WHERE " + trimmedWhere + " "
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 没有实际条件内容,重置 where
|
||||||
|
where = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理特殊字符(按固定顺序:GROUP, HAVING, ORDER, LIMIT, OFFSET)
|
// 处理特殊字符(按固定顺序:GROUP, HAVING, ORDER, LIMIT, OFFSET)
|
||||||
@ -182,6 +214,7 @@ 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)
|
||||||
@ -195,73 +228,53 @@ 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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(k) + " "
|
||||||
k = "`" + k + "` "
|
|
||||||
}
|
|
||||||
where += " " + k + "!=" + ObjToStr(v) + " "
|
where += " " + k + "!=" + ObjToStr(v) + " "
|
||||||
case "[!#]":
|
case "[!#]":
|
||||||
k = strings.Replace(k, "[!#]", "", -1)
|
k = strings.Replace(k, "[!#]", "", -1)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(k) + " "
|
||||||
k = "`" + k + "` "
|
|
||||||
}
|
|
||||||
where += " " + k + "!=" + ObjToStr(v) + " "
|
where += " " + k + "!=" + ObjToStr(v) + " "
|
||||||
case "[~]":
|
case "[~]":
|
||||||
k = strings.Replace(k, "[~]", "", -1)
|
k = strings.Replace(k, "[~]", "", -1)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(k) + " "
|
||||||
k = "`" + k + "` "
|
|
||||||
}
|
|
||||||
where += k + " LIKE ? "
|
where += k + " LIKE ? "
|
||||||
res = append(res, v)
|
res = append(res, v)
|
||||||
default:
|
default:
|
||||||
@ -272,32 +285,24 @@ 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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(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)
|
||||||
if !strings.Contains(k, ".") {
|
k = processor.ProcessColumn(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])
|
||||||
@ -315,13 +320,14 @@ 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{}) {
|
||||||
if !strings.Contains(k, ".") {
|
processor := that.GetProcessor()
|
||||||
k = "`" + k + "` "
|
k = processor.ProcessColumn(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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,15 +349,16 @@ 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{}) {
|
||||||
if !strings.Contains(k, ".") {
|
processor := that.GetProcessor()
|
||||||
k = "`" + k + "` "
|
k = processor.ProcessColumn(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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
757
docs/CodeConfig_代码生成配置规范.md
Normal file
757
docs/CodeConfig_代码生成配置规范.md
Normal file
@ -0,0 +1,757 @@
|
|||||||
|
# 代码生成配置规范
|
||||||
|
|
||||||
|
本文档详细说明 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
|
||||||
468
docs/CodeGen_使用说明.md
Normal file
468
docs/CodeGen_使用说明.md
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
# HoTime 代码生成器使用说明
|
||||||
|
|
||||||
|
`code` 包提供了 HoTime 框架的自动代码生成功能,能够根据数据库表结构自动生成 CRUD 接口代码和配置文件。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [功能概述](#功能概述)
|
||||||
|
- [配置说明](#配置说明)
|
||||||
|
- [使用方法](#使用方法)
|
||||||
|
- [生成规则](#生成规则)
|
||||||
|
- [自定义规则](#自定义规则)
|
||||||
|
- [生成的代码结构](#生成的代码结构)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
代码生成器可以:
|
||||||
|
|
||||||
|
1. **自动读取数据库表结构** - 支持 MySQL 和 SQLite
|
||||||
|
2. **生成 CRUD 接口** - 增删改查、搜索、分页
|
||||||
|
3. **生成配置文件** - 表字段配置、菜单配置、权限配置
|
||||||
|
4. **智能字段识别** - 根据字段名自动识别类型和权限
|
||||||
|
5. **支持表关联** - 自动识别外键关系
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
在 `config.json` 中配置代码生成:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codeConfig": [
|
||||||
|
{
|
||||||
|
"table": "admin",
|
||||||
|
"config": "config/admin.json",
|
||||||
|
"configDB": "config/adminDB.json",
|
||||||
|
"rule": "config/rule.json",
|
||||||
|
"name": "",
|
||||||
|
"mode": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置项说明
|
||||||
|
|
||||||
|
| 配置项 | 必须 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `table` | ✅ | 用户表名,用于权限控制的基准表 |
|
||||||
|
| `config` | ✅ | 接口描述配置文件路径 |
|
||||||
|
| `configDB` | ❌ | 数据库结构配置输出路径,有则每次自动生成 |
|
||||||
|
| `rule` | ❌ | 字段规则配置文件,无则使用默认规则 |
|
||||||
|
| `name` | ❌ | 生成代码的包名和目录名,空则使用内嵌模式 |
|
||||||
|
| `mode` | ❌ | 0=内嵌代码模式,1=生成代码模式 |
|
||||||
|
|
||||||
|
### 运行模式
|
||||||
|
|
||||||
|
- **mode=0(内嵌模式)**:不生成独立代码文件,使用框架内置的通用控制器
|
||||||
|
- **mode=1(生成模式)**:为每张表生成独立的 Go 控制器文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 基础配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": 2,
|
||||||
|
"codeConfig": [
|
||||||
|
{
|
||||||
|
"table": "admin",
|
||||||
|
"config": "config/admin.json",
|
||||||
|
"rule": "config/rule.json",
|
||||||
|
"mode": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启动应用
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime"
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := Init("config/config.json")
|
||||||
|
|
||||||
|
// 代码生成器在 Init 时自动执行
|
||||||
|
// 会读取数据库结构并生成配置
|
||||||
|
|
||||||
|
app.Run(Router{
|
||||||
|
// 路由配置
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 开发模式
|
||||||
|
|
||||||
|
在 `config.json` 中设置 `"mode": 2`(开发模式)时:
|
||||||
|
|
||||||
|
- 自动读取数据库表结构
|
||||||
|
- 自动生成/更新配置文件
|
||||||
|
- 自动生成代码(如果 codeConfig.mode=1)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 生成规则
|
||||||
|
|
||||||
|
### 默认字段规则
|
||||||
|
|
||||||
|
代码生成器内置了一套默认的字段识别规则:
|
||||||
|
|
||||||
|
| 字段名 | 列表显示 | 新增 | 编辑 | 详情 | 类型 |
|
||||||
|
|--------|----------|------|------|------|------|
|
||||||
|
| `id` | ✅ | ❌ | ❌ | ✅ | number |
|
||||||
|
| `name` | ✅ | ✅ | ✅ | ✅ | text |
|
||||||
|
| `status` | ✅ | ✅ | ✅ | ✅ | select |
|
||||||
|
| `create_time` | ❌ | ❌ | ❌ | ✅ | time |
|
||||||
|
| `modify_time` | ✅ | ❌ | ❌ | ✅ | time |
|
||||||
|
| `password` | ❌ | ✅ | ✅ | ❌ | password |
|
||||||
|
| `image/img/avatar` | ❌ | ✅ | ✅ | ✅ | image |
|
||||||
|
| `file` | ❌ | ✅ | ✅ | ✅ | file |
|
||||||
|
| `content/info` | ❌ | ✅ | ✅ | ✅ | textArea |
|
||||||
|
| `parent_id` | ✅ | ✅ | ✅ | ✅ | number |
|
||||||
|
| `parent_ids/index` | ❌ | ❌ | ❌ | ❌ | index |
|
||||||
|
| `delete` | ❌ | ❌ | ❌ | ❌ | - |
|
||||||
|
|
||||||
|
### 数据类型映射
|
||||||
|
|
||||||
|
数据库字段类型自动映射:
|
||||||
|
|
||||||
|
| 数据库类型 | 生成类型 |
|
||||||
|
|------------|----------|
|
||||||
|
| `int`, `integer`, `float`, `double`, `decimal` | number |
|
||||||
|
| `char`, `varchar`, `text`, `blob` | text |
|
||||||
|
| `date`, `datetime`, `time`, `timestamp`, `year` | time |
|
||||||
|
|
||||||
|
### 字段备注解析
|
||||||
|
|
||||||
|
支持从数据库字段备注中提取信息:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 字段备注格式: 标签名:选项1-名称1,选项2-名称2 {提示信息}
|
||||||
|
-- 例如:
|
||||||
|
status TINYINT COMMENT '状态:0-禁用,1-启用 {用户账号状态}'
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"label": "状态",
|
||||||
|
"type": "select",
|
||||||
|
"ps": "用户账号状态",
|
||||||
|
"options": [
|
||||||
|
{"name": "禁用", "value": "0"},
|
||||||
|
{"name": "启用", "value": "1"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 自定义规则
|
||||||
|
|
||||||
|
### rule.json 配置
|
||||||
|
|
||||||
|
创建 `config/rule.json` 自定义字段规则:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"list": true,
|
||||||
|
"add": false,
|
||||||
|
"edit": false,
|
||||||
|
"info": true,
|
||||||
|
"must": false,
|
||||||
|
"strict": true,
|
||||||
|
"type": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"list": true,
|
||||||
|
"add": true,
|
||||||
|
"edit": true,
|
||||||
|
"info": true,
|
||||||
|
"must": false,
|
||||||
|
"strict": false,
|
||||||
|
"type": "select"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user.special_field",
|
||||||
|
"list": true,
|
||||||
|
"add": true,
|
||||||
|
"edit": true,
|
||||||
|
"info": true,
|
||||||
|
"type": "text",
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 规则字段说明
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `name` | 字段名,支持 `表名.字段名` 格式精确匹配 |
|
||||||
|
| `list` | 是否在列表中显示 |
|
||||||
|
| `add` | 是否在新增表单中显示 |
|
||||||
|
| `edit` | 是否在编辑表单中显示 |
|
||||||
|
| `info` | 是否在详情中显示 |
|
||||||
|
| `must` | 是否必填 |
|
||||||
|
| `strict` | 是否严格匹配字段名(false 则模糊匹配) |
|
||||||
|
| `type` | 字段类型(覆盖自动识别) |
|
||||||
|
|
||||||
|
### 字段类型
|
||||||
|
|
||||||
|
| 类型 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `text` | 普通文本输入 |
|
||||||
|
| `textArea` | 多行文本 |
|
||||||
|
| `number` | 数字输入 |
|
||||||
|
| `select` | 下拉选择 |
|
||||||
|
| `time` | 时间选择器 |
|
||||||
|
| `unixTime` | Unix 时间戳 |
|
||||||
|
| `image` | 图片上传 |
|
||||||
|
| `file` | 文件上传 |
|
||||||
|
| `password` | 密码输入 |
|
||||||
|
| `money` | 金额(带格式化) |
|
||||||
|
| `index` | 索引字段(不显示) |
|
||||||
|
| `tree` | 树形选择 |
|
||||||
|
| `form` | 表单配置 |
|
||||||
|
| `auth` | 权限配置 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 生成的代码结构
|
||||||
|
|
||||||
|
### 内嵌模式 (mode=0)
|
||||||
|
|
||||||
|
不生成代码文件,使用框架内置控制器,只生成配置文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── admin.json # 接口配置
|
||||||
|
├── adminDB.json # 数据库结构配置(可选)
|
||||||
|
└── rule.json # 字段规则
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生成模式 (mode=1)
|
||||||
|
|
||||||
|
生成独立的控制器代码:
|
||||||
|
|
||||||
|
```
|
||||||
|
admin/ # 生成的包目录
|
||||||
|
├── init.go # 包初始化和路由注册
|
||||||
|
├── user.go # user 表控制器
|
||||||
|
├── role.go # role 表控制器
|
||||||
|
└── ... # 其他表控制器
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生成的控制器结构
|
||||||
|
|
||||||
|
```go
|
||||||
|
package admin
|
||||||
|
|
||||||
|
var userCtr = Ctr{
|
||||||
|
"info": func(that *Context) {
|
||||||
|
// 查询单条记录
|
||||||
|
},
|
||||||
|
"add": func(that *Context) {
|
||||||
|
// 新增记录
|
||||||
|
},
|
||||||
|
"update": func(that *Context) {
|
||||||
|
// 更新记录
|
||||||
|
},
|
||||||
|
"remove": func(that *Context) {
|
||||||
|
// 删除记录
|
||||||
|
},
|
||||||
|
"search": func(that *Context) {
|
||||||
|
// 搜索列表(分页)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置文件结构
|
||||||
|
|
||||||
|
### admin.json 示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"label": "管理平台",
|
||||||
|
"menus": [
|
||||||
|
{
|
||||||
|
"label": "系统管理",
|
||||||
|
"name": "sys",
|
||||||
|
"icon": "Setting",
|
||||||
|
"menus": [
|
||||||
|
{
|
||||||
|
"label": "用户管理",
|
||||||
|
"table": "user",
|
||||||
|
"auth": ["show", "add", "delete", "edit", "info", "download"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "角色管理",
|
||||||
|
"table": "role",
|
||||||
|
"auth": ["show", "add", "delete", "edit", "info"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tables": {
|
||||||
|
"user": {
|
||||||
|
"label": "用户",
|
||||||
|
"table": "user",
|
||||||
|
"auth": ["show", "add", "delete", "edit", "info", "download"],
|
||||||
|
"columns": [
|
||||||
|
{"name": "id", "type": "number", "label": "ID"},
|
||||||
|
{"name": "name", "type": "text", "label": "用户名"},
|
||||||
|
{"name": "status", "type": "select", "label": "状态",
|
||||||
|
"options": [{"name": "禁用", "value": "0"}, {"name": "启用", "value": "1"}]}
|
||||||
|
],
|
||||||
|
"search": [
|
||||||
|
{"type": "search", "name": "keyword", "label": "请输入关键词"},
|
||||||
|
{"type": "search", "name": "daterange", "label": "时间段"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 外键关联
|
||||||
|
|
||||||
|
### 自动识别
|
||||||
|
|
||||||
|
代码生成器会自动识别 `_id` 结尾的字段作为外键:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- user 表
|
||||||
|
CREATE TABLE user (
|
||||||
|
id INT PRIMARY KEY,
|
||||||
|
name VARCHAR(50),
|
||||||
|
role_id INT, -- 自动关联 role 表
|
||||||
|
org_id INT -- 自动关联 org 表
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的配置会包含 `link` 和 `value` 字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "role_id",
|
||||||
|
"type": "number",
|
||||||
|
"label": "角色",
|
||||||
|
"link": "role",
|
||||||
|
"value": "name"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 树形结构
|
||||||
|
|
||||||
|
`parent_id` 字段会被识别为树形结构的父级关联:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "parent_id",
|
||||||
|
"type": "number",
|
||||||
|
"label": "上级",
|
||||||
|
"link": "org",
|
||||||
|
"value": "name"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 权限控制
|
||||||
|
|
||||||
|
### 数据权限
|
||||||
|
|
||||||
|
配置 `flow` 实现数据权限控制:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"flow": {
|
||||||
|
"order": {
|
||||||
|
"table": "order",
|
||||||
|
"stop": false,
|
||||||
|
"sql": {
|
||||||
|
"user_id": "id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `stop`: 是否禁止修改该表
|
||||||
|
- `sql`: 数据过滤条件,`user_id = 当前用户.id`
|
||||||
|
|
||||||
|
### 操作权限
|
||||||
|
|
||||||
|
每张表可配置的权限:
|
||||||
|
|
||||||
|
| 权限 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `show` | 查看列表 |
|
||||||
|
| `add` | 新增 |
|
||||||
|
| `edit` | 编辑 |
|
||||||
|
| `delete` | 删除 |
|
||||||
|
| `info` | 查看详情 |
|
||||||
|
| `download` | 下载导出 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 开发流程
|
||||||
|
|
||||||
|
1. 设置 `config.json` 中 `mode: 2`(开发模式)
|
||||||
|
2. 设计数据库表结构,添加字段备注
|
||||||
|
3. 启动应用,自动生成配置
|
||||||
|
4. 检查生成的配置文件,按需调整
|
||||||
|
5. 生产环境改为 `mode: 0`
|
||||||
|
|
||||||
|
### 2. 字段命名规范
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 推荐的命名方式
|
||||||
|
id -- 主键
|
||||||
|
name -- 名称
|
||||||
|
status -- 状态(自动识别为 select)
|
||||||
|
create_time -- 创建时间
|
||||||
|
modify_time -- 修改时间
|
||||||
|
xxx_id -- 外键关联
|
||||||
|
parent_id -- 树形结构父级
|
||||||
|
avatar -- 头像(自动识别为 image)
|
||||||
|
content -- 内容(自动识别为 textArea)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 自定义扩展
|
||||||
|
|
||||||
|
如果默认规则不满足需求,可以:
|
||||||
|
|
||||||
|
1. 修改 `rule.json` 添加自定义规则
|
||||||
|
2. 使用 `mode=1` 生成代码后手动修改
|
||||||
|
3. 在生成的配置文件中直接调整字段属性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [快速上手指南](QUICKSTART.md)
|
||||||
|
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md)
|
||||||
|
- [Common 工具类使用说明](Common_工具类使用说明.md)
|
||||||
484
docs/Common_工具类使用说明.md
Normal file
484
docs/Common_工具类使用说明.md
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
# HoTime Common 工具类使用说明
|
||||||
|
|
||||||
|
`common` 包提供了 HoTime 框架的核心数据类型和工具函数,包括 `Map`、`Slice`、`Obj` 类型及丰富的类型转换函数。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [核心数据类型](#核心数据类型)
|
||||||
|
- [Map 类型](#map-类型)
|
||||||
|
- [Slice 类型](#slice-类型)
|
||||||
|
- [Obj 类型](#obj-类型)
|
||||||
|
- [类型转换函数](#类型转换函数)
|
||||||
|
- [工具函数](#工具函数)
|
||||||
|
- [错误处理](#错误处理)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心数据类型
|
||||||
|
|
||||||
|
### Map 类型
|
||||||
|
|
||||||
|
`Map` 是 `map[string]interface{}` 的别名,提供了丰富的链式调用方法。
|
||||||
|
|
||||||
|
```go
|
||||||
|
import . "code.hoteas.com/golang/hotime/common"
|
||||||
|
|
||||||
|
// 创建 Map
|
||||||
|
data := Map{
|
||||||
|
"name": "张三",
|
||||||
|
"age": 25,
|
||||||
|
"score": 98.5,
|
||||||
|
"active": true,
|
||||||
|
"tags": Slice{"Go", "Web"},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取值方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 获取字符串
|
||||||
|
name := data.GetString("name") // "张三"
|
||||||
|
|
||||||
|
// 获取整数
|
||||||
|
age := data.GetInt("age") // 25
|
||||||
|
age64 := data.GetInt64("age") // int64(25)
|
||||||
|
|
||||||
|
// 获取浮点数
|
||||||
|
score := data.GetFloat64("score") // 98.5
|
||||||
|
|
||||||
|
// 获取布尔值
|
||||||
|
active := data.GetBool("active") // true
|
||||||
|
|
||||||
|
// 获取嵌套 Map
|
||||||
|
info := data.GetMap("info") // 返回 Map 类型
|
||||||
|
|
||||||
|
// 获取 Slice
|
||||||
|
tags := data.GetSlice("tags") // 返回 Slice 类型
|
||||||
|
|
||||||
|
// 获取时间
|
||||||
|
createTime := data.GetTime("create_time") // 返回 *time.Time
|
||||||
|
|
||||||
|
// 获取原始值
|
||||||
|
raw := data.Get("name") // interface{}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 向上取整方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 向上取整获取整数
|
||||||
|
ceilInt := data.GetCeilInt("score") // 99
|
||||||
|
ceilInt64 := data.GetCeilInt64("score") // int64(99)
|
||||||
|
ceilFloat := data.GetCeilFloat64("score") // 99.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 操作方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 添加/修改值
|
||||||
|
data.Put("email", "test@example.com")
|
||||||
|
|
||||||
|
// 删除值
|
||||||
|
data.Delete("email")
|
||||||
|
|
||||||
|
// 转换为 JSON 字符串
|
||||||
|
jsonStr := data.ToJsonString()
|
||||||
|
|
||||||
|
// 从 JSON 字符串解析
|
||||||
|
data.JsonToMap(`{"key": "value"}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 有序遍历
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 按 key 字母顺序遍历
|
||||||
|
data.RangeSort(func(k string, v interface{}) bool {
|
||||||
|
fmt.Printf("%s: %v\n", k, v)
|
||||||
|
return false // 返回 true 则终止遍历
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 转换为结构体
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
Name string
|
||||||
|
Age int64
|
||||||
|
Score float64
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
data.ToStruct(&user) // 传入指针,字段名首字母大写匹配
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Slice 类型
|
||||||
|
|
||||||
|
`Slice` 是 `[]interface{}` 的别名,提供类似 Map 的链式调用方法。
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建 Slice
|
||||||
|
list := Slice{
|
||||||
|
Map{"id": 1, "name": "Alice"},
|
||||||
|
Map{"id": 2, "name": "Bob"},
|
||||||
|
"text",
|
||||||
|
123,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取值方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 按索引获取值(类型转换)
|
||||||
|
str := list.GetString(2) // "text"
|
||||||
|
num := list.GetInt(3) // 123
|
||||||
|
num64 := list.GetInt64(3) // int64(123)
|
||||||
|
f := list.GetFloat64(3) // 123.0
|
||||||
|
b := list.GetBool(3) // true (非0为true)
|
||||||
|
|
||||||
|
// 获取嵌套类型
|
||||||
|
item := list.GetMap(0) // Map{"id": 1, "name": "Alice"}
|
||||||
|
subList := list.GetSlice(0) // 尝试转换为 Slice
|
||||||
|
|
||||||
|
// 获取原始值
|
||||||
|
raw := list.Get(0) // interface{}
|
||||||
|
|
||||||
|
// 获取时间
|
||||||
|
t := list.GetTime(0) // *time.Time
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 向上取整方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
ceilInt := list.GetCeilInt(3)
|
||||||
|
ceilInt64 := list.GetCeilInt64(3)
|
||||||
|
ceilFloat := list.GetCeilFloat64(3)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 操作方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 修改指定位置的值
|
||||||
|
list.Put(0, "new value")
|
||||||
|
|
||||||
|
// 转换为 JSON 字符串
|
||||||
|
jsonStr := list.ToJsonString()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Obj 类型
|
||||||
|
|
||||||
|
`Obj` 是一个通用的对象包装器,用于链式类型转换,常用于 `Context` 方法的返回值。
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Obj struct {
|
||||||
|
Data interface{} // 原始数据
|
||||||
|
Error // 错误信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
obj := &Obj{Data: "123"}
|
||||||
|
|
||||||
|
// 链式类型转换
|
||||||
|
i := obj.ToInt() // 123
|
||||||
|
i64 := obj.ToInt64() // int64(123)
|
||||||
|
f := obj.ToFloat64() // 123.0
|
||||||
|
s := obj.ToStr() // "123"
|
||||||
|
b := obj.ToBool() // true
|
||||||
|
|
||||||
|
// 复杂类型转换
|
||||||
|
m := obj.ToMap() // 尝试转换为 Map
|
||||||
|
sl := obj.ToSlice() // 尝试转换为 Slice
|
||||||
|
arr := obj.ToMapArray() // 转换为 []Map
|
||||||
|
|
||||||
|
// 获取原始值
|
||||||
|
raw := obj.ToObj() // interface{}
|
||||||
|
|
||||||
|
// 获取时间
|
||||||
|
t := obj.ToTime() // *time.Time
|
||||||
|
|
||||||
|
// 向上取整
|
||||||
|
ceil := obj.ToCeilInt()
|
||||||
|
ceil64 := obj.ToCeilInt64()
|
||||||
|
ceilF := obj.ToCeilFloat64()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 在 Context 中的应用
|
||||||
|
|
||||||
|
```go
|
||||||
|
func handler(that *Context) {
|
||||||
|
// ReqData 返回 *Obj,支持链式调用
|
||||||
|
userId := that.ReqData("user_id").ToInt()
|
||||||
|
name := that.ReqData("name").ToStr()
|
||||||
|
|
||||||
|
// Session 也返回 *Obj
|
||||||
|
adminId := that.Session("admin_id").ToInt64()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 类型转换函数
|
||||||
|
|
||||||
|
`common` 包提供了一系列全局类型转换函数。
|
||||||
|
|
||||||
|
### 基础转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 转字符串
|
||||||
|
str := ObjToStr(123) // "123"
|
||||||
|
str := ObjToStr(3.14) // "3.14"
|
||||||
|
str := ObjToStr(Map{"a": 1}) // JSON 格式字符串
|
||||||
|
|
||||||
|
// 转整数
|
||||||
|
i := ObjToInt("123") // 123
|
||||||
|
i64 := ObjToInt64("123") // int64(123)
|
||||||
|
|
||||||
|
// 转浮点数
|
||||||
|
f := ObjToFloat64("3.14") // 3.14
|
||||||
|
|
||||||
|
// 转布尔
|
||||||
|
b := ObjToBool(1) // true
|
||||||
|
b := ObjToBool(0) // false
|
||||||
|
|
||||||
|
// 转 Map
|
||||||
|
m := ObjToMap(`{"a": 1}`) // Map{"a": 1}
|
||||||
|
m := ObjToMap(someStruct) // 结构体转 Map
|
||||||
|
|
||||||
|
// 转 Slice
|
||||||
|
s := ObjToSlice(`[1, 2, 3]`) // Slice{1, 2, 3}
|
||||||
|
|
||||||
|
// 转 []Map
|
||||||
|
arr := ObjToMapArray(slice) // []Map
|
||||||
|
```
|
||||||
|
|
||||||
|
### 向上取整转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 向上取整后转整数
|
||||||
|
ceil := ObjToCeilInt(3.2) // 4
|
||||||
|
ceil64 := ObjToCeilInt64(3.2) // int64(4)
|
||||||
|
ceilF := ObjToCeilFloat64(3.2) // 4.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 时间转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 自动识别多种格式
|
||||||
|
t := ObjToTime("2024-01-15 10:30:00") // *time.Time
|
||||||
|
t := ObjToTime("2024-01-15") // *time.Time
|
||||||
|
t := ObjToTime(1705298400) // Unix 秒
|
||||||
|
t := ObjToTime(1705298400000) // Unix 毫秒
|
||||||
|
t := ObjToTime(1705298400000000) // Unix 微秒
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字符串转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 字符串转 Map
|
||||||
|
m := StrToMap(`{"key": "value"}`)
|
||||||
|
|
||||||
|
// 字符串转 Slice
|
||||||
|
s := StrToSlice(`[1, 2, 3]`)
|
||||||
|
|
||||||
|
// 字符串转 int
|
||||||
|
i, err := StrToInt("123")
|
||||||
|
|
||||||
|
// 字符串数组格式转换
|
||||||
|
jsonArr := StrArrayToJsonStr("a1,a2,a3") // "[a1,a2,a3]"
|
||||||
|
strArr := JsonStrToStrArray("[a1,a2,a3]") // ",a1,a2,a3,"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
|
||||||
|
所有转换函数支持可选的错误参数:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var e Error
|
||||||
|
i := ObjToInt("abc", &e)
|
||||||
|
if e.GetError() != nil {
|
||||||
|
// 处理转换错误
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 工具函数
|
||||||
|
|
||||||
|
### 字符串处理
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 字符串截取(支持中文)
|
||||||
|
str := Substr("Hello世界", 0, 7) // "Hello世"
|
||||||
|
str := Substr("Hello", -2, 2) // "lo" (负数从末尾计算)
|
||||||
|
|
||||||
|
// 首字母大写
|
||||||
|
upper := StrFirstToUpper("hello") // "Hello"
|
||||||
|
|
||||||
|
// 查找最后出现位置
|
||||||
|
idx := IndexLastStr("a.b.c", ".") // 3
|
||||||
|
|
||||||
|
// 字符串相似度(Levenshtein 距离)
|
||||||
|
dist := StrLd("hello", "hallo", true) // 1 (忽略大小写)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 时间处理
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 时间转字符串
|
||||||
|
str := Time2Str(time.Now()) // "2024-01-15 10:30:00"
|
||||||
|
str := Time2Str(time.Now(), 1) // "2024-01"
|
||||||
|
str := Time2Str(time.Now(), 2) // "2024-01-15"
|
||||||
|
str := Time2Str(time.Now(), 3) // "2024-01-15 10"
|
||||||
|
str := Time2Str(time.Now(), 4) // "2024-01-15 10:30"
|
||||||
|
str := Time2Str(time.Now(), 5) // "2024-01-15 10:30:00"
|
||||||
|
|
||||||
|
// 特殊格式
|
||||||
|
str := Time2Str(time.Now(), 12) // "01-15"
|
||||||
|
str := Time2Str(time.Now(), 14) // "01-15 10:30"
|
||||||
|
str := Time2Str(time.Now(), 34) // "10:30"
|
||||||
|
str := Time2Str(time.Now(), 35) // "10:30:00"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 加密与随机
|
||||||
|
|
||||||
|
```go
|
||||||
|
// MD5 加密
|
||||||
|
hash := Md5("password") // 32位小写MD5
|
||||||
|
|
||||||
|
// 随机数
|
||||||
|
r := Rand(3) // 3位随机数 (0-999)
|
||||||
|
r := RandX(10, 100) // 10-100之间的随机数
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数学计算
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 四舍五入保留小数
|
||||||
|
f := Round(3.14159, 2) // 3.14
|
||||||
|
f := Round(3.145, 2) // 3.15
|
||||||
|
```
|
||||||
|
|
||||||
|
### 深拷贝
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 深拷贝 Map/Slice(递归复制)
|
||||||
|
original := Map{"a": Map{"b": 1}}
|
||||||
|
copied := DeepCopyMap(original).(Map)
|
||||||
|
|
||||||
|
// 修改副本不影响原始数据
|
||||||
|
copied.GetMap("a")["b"] = 2
|
||||||
|
// original["a"]["b"] 仍然是 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### Error 类型
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Error struct {
|
||||||
|
Logger *logrus.Logger // 可选的日志记录器
|
||||||
|
error // 内嵌错误
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
var e Error
|
||||||
|
|
||||||
|
// 设置错误
|
||||||
|
e.SetError(errors.New("something wrong"))
|
||||||
|
|
||||||
|
// 获取错误
|
||||||
|
if err := e.GetError(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配合日志自动记录
|
||||||
|
e.Logger = logrusLogger
|
||||||
|
e.SetError(errors.New("will be logged"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在类型转换中使用
|
||||||
|
|
||||||
|
```go
|
||||||
|
var e Error
|
||||||
|
data := Map{"count": "abc"}
|
||||||
|
|
||||||
|
count := data.GetInt("count", &e)
|
||||||
|
if e.GetError() != nil {
|
||||||
|
// 转换失败,count = 0
|
||||||
|
fmt.Println("转换失败:", e.GetError())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 链式调用处理请求数据
|
||||||
|
|
||||||
|
```go
|
||||||
|
func handler(that *Context) {
|
||||||
|
// 推荐:使用 Obj 链式调用
|
||||||
|
userId := that.ReqData("user_id").ToInt()
|
||||||
|
page := that.ReqData("page").ToInt()
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 Map 数据
|
||||||
|
user := that.Db.Get("user", "*", Map{"id": userId})
|
||||||
|
if user != nil {
|
||||||
|
name := user.GetString("name")
|
||||||
|
age := user.GetInt("age")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安全的类型转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 带错误检查的转换
|
||||||
|
var e Error
|
||||||
|
data := someMap.GetInt("key", &e)
|
||||||
|
if e.GetError() != nil {
|
||||||
|
// 使用默认值
|
||||||
|
data = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单场景直接转换(失败返回零值)
|
||||||
|
data := someMap.GetInt("key") // 失败返回 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 处理数据库查询结果
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 查询返回 Map
|
||||||
|
user := that.Db.Get("user", "*", Map{"id": 1})
|
||||||
|
if user != nil {
|
||||||
|
name := user.GetString("name")
|
||||||
|
createTime := user.GetTime("create_time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询返回 []Map
|
||||||
|
users := that.Db.Select("user", "*", Map{"status": 1})
|
||||||
|
for _, u := range users {
|
||||||
|
fmt.Printf("ID: %d, Name: %s\n", u.GetInt("id"), u.GetString("name"))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [快速上手指南](QUICKSTART.md)
|
||||||
|
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md)
|
||||||
|
- [代码生成器使用说明](CodeGen_使用说明.md)
|
||||||
408
docs/DatabaseDesign_数据库设计规范.md
Normal file
408
docs/DatabaseDesign_数据库设计规范.md
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
# 数据库设计规范
|
||||||
|
|
||||||
|
本文档定义了 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
|
||||||
|
- [ ] 选择字段注释格式正确(`标签:值-名称`)
|
||||||
@ -98,17 +98,17 @@ id := database.Insert("table", dataMap)
|
|||||||
// 返回新插入记录的ID
|
// 返回新插入记录的ID
|
||||||
```
|
```
|
||||||
|
|
||||||
### 批量插入 (BatchInsert) - 新增
|
### 批量插入 (Inserts) - 新增
|
||||||
```go
|
```go
|
||||||
// 使用 []Map 格式,更直观简洁
|
// 使用 []Map 格式,更直观简洁
|
||||||
affected := database.BatchInsert("table", []Map{
|
affected := database.Inserts("table", []Map{
|
||||||
{"col1": "val1", "col2": "val2", "col3": "val3"},
|
{"col1": "val1", "col2": "val2", "col3": "val3"},
|
||||||
{"col1": "val4", "col2": "val5", "col3": "val6"},
|
{"col1": "val4", "col2": "val5", "col3": "val6"},
|
||||||
})
|
})
|
||||||
// 返回受影响的行数
|
// 返回受影响的行数
|
||||||
|
|
||||||
// 支持 [#] 标记直接 SQL
|
// 支持 [#] 标记直接 SQL
|
||||||
affected := database.BatchInsert("log", []Map{
|
affected := database.Inserts("log", []Map{
|
||||||
{"user_id": 1, "created_time[#]": "NOW()"},
|
{"user_id": 1, "created_time[#]": "NOW()"},
|
||||||
{"user_id": 2, "created_time[#]": "NOW()"},
|
{"user_id": 2, "created_time[#]": "NOW()"},
|
||||||
})
|
})
|
||||||
@ -465,7 +465,7 @@ stats := database.Select("order",
|
|||||||
### 批量操作
|
### 批量操作
|
||||||
```go
|
```go
|
||||||
// 批量插入(使用 []Map 格式)
|
// 批量插入(使用 []Map 格式)
|
||||||
affected := database.BatchInsert("user", []Map{
|
affected := database.Inserts("user", []Map{
|
||||||
{"name": "用户1", "email": "user1@example.com", "status": 1},
|
{"name": "用户1", "email": "user1@example.com", "status": 1},
|
||||||
{"name": "用户2", "email": "user2@example.com", "status": 1},
|
{"name": "用户2", "email": "user2@example.com", "status": 1},
|
||||||
{"name": "用户3", "email": "user3@example.com", "status": 1},
|
{"name": "用户3", "email": "user3@example.com", "status": 1},
|
||||||
@ -511,3 +511,6 @@ result := database.Table("order").
|
|||||||
|
|
||||||
*快速参考版本: 2.0*
|
*快速参考版本: 2.0*
|
||||||
*更新日期: 2026年1月*
|
*更新日期: 2026年1月*
|
||||||
|
|
||||||
|
**详细说明:**
|
||||||
|
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md) - 完整教程
|
||||||
@ -12,7 +12,7 @@ HoTimeDB是一个基于Golang实现的轻量级ORM框架,参考PHP Medoo设计
|
|||||||
- [查询(Select)](#查询select)
|
- [查询(Select)](#查询select)
|
||||||
- [获取单条记录(Get)](#获取单条记录get)
|
- [获取单条记录(Get)](#获取单条记录get)
|
||||||
- [插入(Insert)](#插入insert)
|
- [插入(Insert)](#插入insert)
|
||||||
- [批量插入(BatchInsert)](#批量插入batchinsert)
|
- [批量插入(Inserts)](#批量插入Inserts)
|
||||||
- [更新(Update)](#更新update)
|
- [更新(Update)](#更新update)
|
||||||
- [Upsert操作](#upsert操作)
|
- [Upsert操作](#upsert操作)
|
||||||
- [删除(Delete)](#删除delete)
|
- [删除(Delete)](#删除delete)
|
||||||
@ -223,11 +223,11 @@ id := database.Insert("user", common.Map{
|
|||||||
fmt.Println("插入的用户ID:", id)
|
fmt.Println("插入的用户ID:", id)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 批量插入(BatchInsert)
|
### 批量插入(Inserts)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// 批量插入多条记录(使用 []Map 格式,更直观)
|
// 批量插入多条记录(使用 []Map 格式,更直观)
|
||||||
affected := database.BatchInsert("user", []common.Map{
|
affected := database.Inserts("user", []common.Map{
|
||||||
{"name": "张三", "email": "zhang@example.com", "age": 25},
|
{"name": "张三", "email": "zhang@example.com", "age": 25},
|
||||||
{"name": "李四", "email": "li@example.com", "age": 30},
|
{"name": "李四", "email": "li@example.com", "age": 30},
|
||||||
{"name": "王五", "email": "wang@example.com", "age": 28},
|
{"name": "王五", "email": "wang@example.com", "age": 28},
|
||||||
@ -237,7 +237,7 @@ affected := database.BatchInsert("user", []common.Map{
|
|||||||
fmt.Printf("批量插入 %d 条记录\n", affected)
|
fmt.Printf("批量插入 %d 条记录\n", affected)
|
||||||
|
|
||||||
// 支持 [#] 标记直接插入 SQL 表达式
|
// 支持 [#] 标记直接插入 SQL 表达式
|
||||||
affected := database.BatchInsert("log", []common.Map{
|
affected := database.Inserts("log", []common.Map{
|
||||||
{"user_id": 1, "action": "login", "created_time[#]": "NOW()"},
|
{"user_id": 1, "action": "login", "created_time[#]": "NOW()"},
|
||||||
{"user_id": 2, "action": "logout", "created_time[#]": "NOW()"},
|
{"user_id": 2, "action": "logout", "created_time[#]": "NOW()"},
|
||||||
})
|
})
|
||||||
@ -808,7 +808,7 @@ A1: 不再需要!现在多条件会自动用 AND 连接。当然,使用 `AND
|
|||||||
A2: 在 `Action` 函数中返回 `false` 即可触发回滚,所有操作都会被撤销。
|
A2: 在 `Action` 函数中返回 `false` 即可触发回滚,所有操作都会被撤销。
|
||||||
|
|
||||||
### Q3: 缓存何时会被清除?
|
### Q3: 缓存何时会被清除?
|
||||||
A3: 执行 `Insert`、`Update`、`Delete`、`Upsert`、`BatchInsert` 操作时会自动清除对应表的缓存。
|
A3: 执行 `Insert`、`Update`、`Delete`、`Upsert`、`Inserts` 操作时会自动清除对应表的缓存。
|
||||||
|
|
||||||
### Q4: 如何执行复杂的原生SQL?
|
### Q4: 如何执行复杂的原生SQL?
|
||||||
A4: 使用 `Query` 方法执行查询,使用 `Exec` 方法执行更新操作。
|
A4: 使用 `Query` 方法执行查询,使用 `Exec` 方法执行更新操作。
|
||||||
@ -828,3 +828,6 @@ A7: 框架会自动处理差异(占位符、引号等),代码无需修改
|
|||||||
*最后更新: 2026年1月*
|
*最后更新: 2026年1月*
|
||||||
|
|
||||||
> 本文档基于HoTimeDB源码分析生成,如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念,但根据Golang语言特性进行了适配和优化。
|
> 本文档基于HoTimeDB源码分析生成,如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念,但根据Golang语言特性进行了适配和优化。
|
||||||
|
|
||||||
|
**更多参考:**
|
||||||
|
- [HoTimeDB API 参考](HoTimeDB_API参考.md) - API 速查手册
|
||||||
529
docs/QUICKSTART.md
Normal file
529
docs/QUICKSTART.md
Normal file
@ -0,0 +1,529 @@
|
|||||||
|
# HoTime 快速上手指南
|
||||||
|
|
||||||
|
5 分钟入门 HoTime 框架。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get code.hoteas.com/golang/hotime
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最小示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime"
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
appIns := Init("config/config.json")
|
||||||
|
|
||||||
|
appIns.Run(Router{
|
||||||
|
"app": {
|
||||||
|
"test": {
|
||||||
|
"hello": func(that *Context) {
|
||||||
|
that.Display(0, Map{"message": "Hello World"})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
访问: `http://localhost:8081/app/test/hello`
|
||||||
|
|
||||||
|
## 配置文件
|
||||||
|
|
||||||
|
创建 `config/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"port": "8081",
|
||||||
|
"mode": 2,
|
||||||
|
"sessionName": "HOTIME",
|
||||||
|
"tpt": "tpt",
|
||||||
|
"defFile": ["index.html", "index.htm"],
|
||||||
|
"db": {
|
||||||
|
"mysql": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "3306",
|
||||||
|
"name": "your_database",
|
||||||
|
"user": "root",
|
||||||
|
"password": "your_password",
|
||||||
|
"prefix": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cache": {
|
||||||
|
"memory": {
|
||||||
|
"db": true,
|
||||||
|
"session": true,
|
||||||
|
"timeout": 7200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置项说明
|
||||||
|
|
||||||
|
| 配置项 | 默认值 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| `port` | 80 | HTTP 服务端口,0 为不启用 |
|
||||||
|
| `tlsPort` | - | HTTPS 端口,需配合 tlsCert/tlsKey |
|
||||||
|
| `tlsCert` | - | HTTPS 证书路径 |
|
||||||
|
| `tlsKey` | - | HTTPS 密钥路径 |
|
||||||
|
| `mode` | 0 | 0=生产, 1=测试, 2=开发(输出SQL) |
|
||||||
|
| `tpt` | tpt | 静态文件目录 |
|
||||||
|
| `sessionName` | HOTIME | Session Cookie 名称 |
|
||||||
|
| `modeRouterStrict` | false | 路由大小写敏感,false=忽略大小写 |
|
||||||
|
| `crossDomain` | - | 跨域设置,空=不开启,auto=智能开启,或指定域名 |
|
||||||
|
| `logFile` | - | 日志文件路径,如 `logs/20060102.txt` |
|
||||||
|
| `logLevel` | 0 | 日志等级,0=关闭,1=打印 |
|
||||||
|
| `webConnectLogShow` | true | 是否显示访问日志 |
|
||||||
|
| `defFile` | ["index.html"] | 目录默认访问文件 |
|
||||||
|
|
||||||
|
### 数据库配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"db": {
|
||||||
|
"mysql": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": "3306",
|
||||||
|
"name": "database_name",
|
||||||
|
"user": "root",
|
||||||
|
"password": "password",
|
||||||
|
"prefix": "app_",
|
||||||
|
"slave": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": "3306",
|
||||||
|
"name": "database_name",
|
||||||
|
"user": "root",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sqlite": {
|
||||||
|
"path": "config/data.db",
|
||||||
|
"prefix": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> MySQL 配置 `slave` 项即启用主从读写分离
|
||||||
|
|
||||||
|
### 缓存配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cache": {
|
||||||
|
"memory": {
|
||||||
|
"db": true,
|
||||||
|
"session": true,
|
||||||
|
"timeout": 7200
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 6379,
|
||||||
|
"password": "",
|
||||||
|
"db": true,
|
||||||
|
"session": true,
|
||||||
|
"timeout": 1296000
|
||||||
|
},
|
||||||
|
"db": {
|
||||||
|
"db": true,
|
||||||
|
"session": true,
|
||||||
|
"timeout": 2592000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
缓存优先级: **Memory > Redis > DB**,自动穿透与回填
|
||||||
|
|
||||||
|
### 错误码配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"1": "内部系统异常",
|
||||||
|
"2": "访问权限异常",
|
||||||
|
"3": "请求参数异常",
|
||||||
|
"4": "数据处理异常",
|
||||||
|
"5": "数据结果异常"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 自定义错误码建议从 10 开始
|
||||||
|
|
||||||
|
## 路由系统
|
||||||
|
|
||||||
|
HoTime 使用三层路由结构:`模块/控制器/方法`
|
||||||
|
|
||||||
|
```go
|
||||||
|
appIns.Run(Router{
|
||||||
|
"模块名": {
|
||||||
|
"控制器名": {
|
||||||
|
"方法名": func(that *Context) {
|
||||||
|
// 处理逻辑
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 路由路径
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 获取路由信息
|
||||||
|
module := that.RouterString[0] // 模块
|
||||||
|
controller := that.RouterString[1] // 控制器
|
||||||
|
action := that.RouterString[2] // 方法
|
||||||
|
|
||||||
|
// 完整请求路径
|
||||||
|
fullPath := that.HandlerStr // 如 /app/user/login
|
||||||
|
```
|
||||||
|
|
||||||
|
## 请求参数获取
|
||||||
|
|
||||||
|
### 新版推荐方法(支持链式调用)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 获取 URL 查询参数 (?id=1)
|
||||||
|
id := that.ReqParam("id").ToInt()
|
||||||
|
name := that.ReqParam("name").ToStr()
|
||||||
|
|
||||||
|
// 获取表单参数 (POST form-data / x-www-form-urlencoded)
|
||||||
|
username := that.ReqForm("username").ToStr()
|
||||||
|
age := that.ReqForm("age").ToInt()
|
||||||
|
|
||||||
|
// 获取 JSON Body 参数 (POST application/json)
|
||||||
|
data := that.ReqJson("data").ToMap()
|
||||||
|
items := that.ReqJson("items").ToSlice()
|
||||||
|
|
||||||
|
// 统一获取(自动判断来源,优先级: JSON > Form > URL)
|
||||||
|
userId := that.ReqData("user_id").ToInt()
|
||||||
|
status := that.ReqData("status").ToStr()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 类型转换方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
obj := that.ReqData("key")
|
||||||
|
|
||||||
|
obj.ToStr() // 转字符串
|
||||||
|
obj.ToInt() // 转 int
|
||||||
|
obj.ToInt64() // 转 int64
|
||||||
|
obj.ToFloat64() // 转 float64
|
||||||
|
obj.ToBool() // 转 bool
|
||||||
|
obj.ToMap() // 转 Map
|
||||||
|
obj.ToSlice() // 转 Slice
|
||||||
|
obj.Data // 获取原始值(interface{})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文件上传
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 单文件上传
|
||||||
|
file, header, err := that.ReqFile("avatar")
|
||||||
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
// header.Filename - 文件名
|
||||||
|
// header.Size - 文件大小
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多文件上传(批量)
|
||||||
|
files, err := that.ReqFiles("images")
|
||||||
|
if err == nil {
|
||||||
|
for _, fh := range files {
|
||||||
|
file, _ := fh.Open()
|
||||||
|
defer file.Close()
|
||||||
|
// 处理每个文件
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 传统方法(兼容)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// GET/POST 参数
|
||||||
|
name := that.Req.FormValue("name")
|
||||||
|
|
||||||
|
// URL 参数
|
||||||
|
id := that.Req.URL.Query().Get("id")
|
||||||
|
|
||||||
|
// 请求头
|
||||||
|
token := that.Req.Header.Get("Authorization")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应数据
|
||||||
|
|
||||||
|
### Display 方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 成功响应 (status=0)
|
||||||
|
that.Display(0, Map{"user": user, "token": token})
|
||||||
|
// 输出: {"status":0, "result":{"user":..., "token":...}}
|
||||||
|
|
||||||
|
// 错误响应 (status>0)
|
||||||
|
that.Display(1, "系统内部错误")
|
||||||
|
// 输出: {"status":1, "result":{"type":"内部系统异常", "msg":"系统内部错误"}, "error":{...}}
|
||||||
|
|
||||||
|
that.Display(2, "请先登录")
|
||||||
|
// 输出: {"status":2, "result":{"type":"访问权限异常", "msg":"请先登录"}, "error":{...}}
|
||||||
|
|
||||||
|
that.Display(3, "参数不能为空")
|
||||||
|
// 输出: {"status":3, "result":{"type":"请求参数异常", "msg":"参数不能为空"}, "error":{...}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码含义
|
||||||
|
|
||||||
|
| 错误码 | 类型 | 使用场景 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| 0 | 成功 | 请求成功 |
|
||||||
|
| 1 | 内部系统异常 | 环境配置、文件权限等基础运行环境错误 |
|
||||||
|
| 2 | 访问权限异常 | 未登录或登录异常 |
|
||||||
|
| 3 | 请求参数异常 | 参数不足、类型错误等 |
|
||||||
|
| 4 | 数据处理异常 | 数据库操作或第三方请求返回异常 |
|
||||||
|
| 5 | 数据结果异常 | 无法返回要求的格式 |
|
||||||
|
|
||||||
|
### 自定义响应
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 自定义 Header
|
||||||
|
that.Resp.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// 直接写入
|
||||||
|
that.Resp.Write([]byte("raw data"))
|
||||||
|
|
||||||
|
// 自定义响应函数
|
||||||
|
that.RespFunc = func() {
|
||||||
|
// 自定义响应逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 中间件
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 全局中间件(请求拦截)
|
||||||
|
appIns.SetConnectListener(func(that *Context) bool {
|
||||||
|
// 放行登录接口
|
||||||
|
if len(that.RouterString) >= 3 && that.RouterString[2] == "login" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
if that.Session("user_id").Data == nil {
|
||||||
|
that.Display(2, "请先登录")
|
||||||
|
return true // 返回 true 终止请求
|
||||||
|
}
|
||||||
|
return false // 返回 false 继续处理
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session 与缓存
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Session 操作
|
||||||
|
that.Session("user_id", 123) // 设置
|
||||||
|
userId := that.Session("user_id") // 获取 *Obj
|
||||||
|
that.Session("user_id", nil) // 删除
|
||||||
|
|
||||||
|
// 链式获取
|
||||||
|
id := that.Session("user_id").ToInt64()
|
||||||
|
name := that.Session("username").ToStr()
|
||||||
|
|
||||||
|
// 通用缓存
|
||||||
|
that.Cache("key", "value") // 设置
|
||||||
|
data := that.Cache("key") // 获取
|
||||||
|
that.Cache("key", nil) // 删除
|
||||||
|
```
|
||||||
|
|
||||||
|
三级缓存自动运作:**Memory → Redis → Database**
|
||||||
|
|
||||||
|
## 数据库操作(简要)
|
||||||
|
|
||||||
|
### 基础 CRUD
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 查询列表
|
||||||
|
users := that.Db.Select("user", "*", Map{"status": 1})
|
||||||
|
|
||||||
|
// 查询单条
|
||||||
|
user := that.Db.Get("user", "*", Map{"id": 1})
|
||||||
|
|
||||||
|
// 插入
|
||||||
|
id := that.Db.Insert("user", Map{"name": "test", "age": 18})
|
||||||
|
|
||||||
|
// 批量插入
|
||||||
|
affected := that.Db.Inserts("user", []Map{
|
||||||
|
{"name": "user1", "age": 20},
|
||||||
|
{"name": "user2", "age": 25},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
rows := that.Db.Update("user", Map{"name": "new"}, Map{"id": 1})
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
rows := that.Db.Delete("user", Map{"id": 1})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 链式查询
|
||||||
|
|
||||||
|
```go
|
||||||
|
users := that.Db.Table("user").
|
||||||
|
LeftJoin("order", "user.id=order.user_id").
|
||||||
|
Where("status", 1).
|
||||||
|
And("age[>]", 18).
|
||||||
|
Order("id DESC").
|
||||||
|
Page(1, 10).
|
||||||
|
Select("*")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 条件语法速查
|
||||||
|
|
||||||
|
| 语法 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `key` | 等于 | `"id": 1` |
|
||||||
|
| `key[>]` | 大于 | `"age[>]": 18` |
|
||||||
|
| `key[<]` | 小于 | `"age[<]": 60` |
|
||||||
|
| `key[>=]` | 大于等于 | `"age[>=]": 18` |
|
||||||
|
| `key[<=]` | 小于等于 | `"age[<=]": 60` |
|
||||||
|
| `key[!]` | 不等于 | `"status[!]": 0` |
|
||||||
|
| `key[~]` | LIKE | `"name[~]": "test"` |
|
||||||
|
| `key[<>]` | BETWEEN | `"age[<>]": Slice{18, 60}` |
|
||||||
|
| `key` | IN | `"id": Slice{1, 2, 3}` |
|
||||||
|
|
||||||
|
### 事务
|
||||||
|
|
||||||
|
```go
|
||||||
|
success := that.Db.Action(func(tx db.HoTimeDB) bool {
|
||||||
|
tx.Update("user", Map{"balance[#]": "balance - 100"}, Map{"id": 1})
|
||||||
|
tx.Insert("order", Map{"user_id": 1, "amount": 100})
|
||||||
|
return true // 返回 true 提交,false 回滚
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
> **更多数据库操作**:参见 [HoTimeDB 使用说明](HoTimeDB_使用说明.md)
|
||||||
|
|
||||||
|
## 日志记录
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建操作日志(自动插入 logs 表)
|
||||||
|
that.Log = Map{
|
||||||
|
"type": "login",
|
||||||
|
"action": "用户登录",
|
||||||
|
"data": Map{"phone": phone},
|
||||||
|
}
|
||||||
|
// 框架会自动添加 time, admin_id/user_id, ip 等字段
|
||||||
|
```
|
||||||
|
|
||||||
|
## 扩展功能
|
||||||
|
|
||||||
|
| 功能 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 微信支付/公众号/小程序 | `dri/wechat/` | 微信全套 SDK |
|
||||||
|
| 阿里云服务 | `dri/aliyun/` | 企业认证等 |
|
||||||
|
| 腾讯云服务 | `dri/tencent/` | 企业认证等 |
|
||||||
|
| 文件上传 | `dri/upload/` | 文件上传处理 |
|
||||||
|
| 文件下载 | `dri/download/` | 文件下载处理 |
|
||||||
|
| MongoDB | `dri/mongodb/` | MongoDB 驱动 |
|
||||||
|
| RSA 加解密 | `dri/rsa/` | RSA 加解密工具 |
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "code.hoteas.com/golang/hotime"
|
||||||
|
. "code.hoteas.com/golang/hotime/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
appIns := Init("config/config.json")
|
||||||
|
|
||||||
|
// 登录检查中间件
|
||||||
|
appIns.SetConnectListener(func(that *Context) bool {
|
||||||
|
// 放行登录接口
|
||||||
|
if len(that.RouterString) >= 3 && that.RouterString[2] == "login" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if that.Session("user_id").Data == nil {
|
||||||
|
that.Display(2, "请先登录")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
appIns.Run(Router{
|
||||||
|
"api": {
|
||||||
|
"user": {
|
||||||
|
"login": func(that *Context) {
|
||||||
|
phone := that.ReqData("phone").ToStr()
|
||||||
|
password := that.ReqData("password").ToStr()
|
||||||
|
|
||||||
|
if phone == "" || password == "" {
|
||||||
|
that.Display(3, "手机号和密码不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := that.Db.Get("user", "*", Map{
|
||||||
|
"phone": phone,
|
||||||
|
"password": Md5(password),
|
||||||
|
})
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
that.Display(3, "账号或密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
that.Session("user_id", user.GetInt64("id"))
|
||||||
|
that.Display(0, Map{"user": user})
|
||||||
|
},
|
||||||
|
|
||||||
|
"info": func(that *Context) {
|
||||||
|
userId := that.Session("user_id").ToInt64()
|
||||||
|
user := that.Db.Get("user", "*", Map{"id": userId})
|
||||||
|
that.Display(0, Map{"user": user})
|
||||||
|
},
|
||||||
|
|
||||||
|
"list": func(that *Context) {
|
||||||
|
page := that.ReqData("page").ToInt()
|
||||||
|
if page == 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
users := that.Db.Table("user").
|
||||||
|
Where("status", 1).
|
||||||
|
Order("id DESC").
|
||||||
|
Page(page, 10).
|
||||||
|
Select("id,name,phone,created_at")
|
||||||
|
|
||||||
|
total := that.Db.Count("user", Map{"status": 1})
|
||||||
|
|
||||||
|
that.Display(0, Map{
|
||||||
|
"list": users,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
"logout": func(that *Context) {
|
||||||
|
that.Session("user_id", nil)
|
||||||
|
that.Display(0, "退出成功")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**下一步**:
|
||||||
|
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md) - 完整数据库教程
|
||||||
|
- [HoTimeDB API 参考](HoTimeDB_API参考.md) - API 速查手册
|
||||||
183
docs/ROADMAP_改进规划.md
Normal file
183
docs/ROADMAP_改进规划.md
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
# 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 | 初始版本,记录分析结果和待改进项 |
|
||||||
199
example/main.go
199
example/main.go
@ -1,43 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
. "code.hoteas.com/golang/hotime"
|
||||||
"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
|
||||||
@ -49,7 +21,6 @@ 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)
|
||||||
@ -73,7 +44,7 @@ func main() {
|
|||||||
results["6_pagination"] = testPagination(that)
|
results["6_pagination"] = testPagination(that)
|
||||||
|
|
||||||
// 7. 批量插入测试
|
// 7. 批量插入测试
|
||||||
results["7_batch_insert"] = testBatchInsert(that)
|
results["7_batch_insert"] = testInserts(that)
|
||||||
|
|
||||||
// 8. Upsert 测试
|
// 8. Upsert 测试
|
||||||
results["8_upsert"] = testUpsert(that)
|
results["8_upsert"] = testUpsert(that)
|
||||||
@ -84,16 +55,13 @@ 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})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -108,7 +76,6 @@ 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})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -119,7 +86,7 @@ func main() {
|
|||||||
"join": func(that *Context) { that.Display(0, testJoinQuery(that)) },
|
"join": func(that *Context) { that.Display(0, testJoinQuery(that)) },
|
||||||
"aggregate": func(that *Context) { that.Display(0, testAggregate(that)) },
|
"aggregate": func(that *Context) { that.Display(0, testAggregate(that)) },
|
||||||
"pagination": func(that *Context) { that.Display(0, testPagination(that)) },
|
"pagination": func(that *Context) { that.Display(0, testPagination(that)) },
|
||||||
"batch": func(that *Context) { that.Display(0, testBatchInsert(that)) },
|
"batch": func(that *Context) { that.Display(0, testInserts(that)) },
|
||||||
"upsert": func(that *Context) { that.Display(0, testUpsert(that)) },
|
"upsert": func(that *Context) { that.Display(0, testUpsert(that)) },
|
||||||
"transaction": func(that *Context) { that.Display(0, testTransaction(that)) },
|
"transaction": func(that *Context) { that.Display(0, testTransaction(that)) },
|
||||||
"rawsql": func(that *Context) { that.Display(0, testRawSQL(that)) },
|
"rawsql": func(that *Context) { that.Display(0, testRawSQL(that)) },
|
||||||
@ -140,15 +107,9 @@ func initTestTables(that *Context) {
|
|||||||
create_time DATETIME
|
create_time DATETIME
|
||||||
)`)
|
)`)
|
||||||
|
|
||||||
// 检查 admin 表数据
|
// 检查 admin 表数据(确认表存在)
|
||||||
adminCount := that.Db.Count("admin")
|
_ = that.Db.Count("admin")
|
||||||
articleCount := that.Db.Count("article")
|
_ = that.Db.Count("article")
|
||||||
|
|
||||||
debugLog("main.go:init", "MySQL数据库初始化检查完成", Map{
|
|
||||||
"adminCount": adminCount,
|
|
||||||
"articleCount": articleCount,
|
|
||||||
"dbType": "MySQL",
|
|
||||||
}, "INIT")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 1. 基础 CRUD 测试 ====================
|
// ==================== 1. 基础 CRUD 测试 ====================
|
||||||
@ -156,8 +117,6 @@ 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{
|
||||||
@ -173,7 +132,6 @@ 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 测试
|
||||||
@ -181,7 +139,6 @@ 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 测试 - 单条件
|
||||||
@ -189,7 +146,6 @@ 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)
|
||||||
@ -203,7 +159,6 @@ 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 测试
|
||||||
@ -214,7 +169,6 @@ 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 表
|
||||||
@ -229,7 +183,6 @@ 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
|
||||||
@ -242,14 +195,11 @@ 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 不等于 ([!])
|
||||||
@ -258,7 +208,6 @@ 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 大于 ([>]) 和 小于 ([<])
|
||||||
@ -270,7 +219,6 @@ 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 大于等于 ([>=]) 和 小于等于 ([<=])
|
||||||
@ -282,7 +230,6 @@ 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 模糊查询 ([~])
|
||||||
@ -291,7 +238,6 @@ 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 右模糊 ([~!])
|
||||||
@ -299,7 +245,6 @@ 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 ([<>])
|
||||||
@ -308,7 +253,6 @@ 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 ([><])
|
||||||
@ -316,7 +260,6 @@ 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 查询
|
||||||
@ -325,7 +268,6 @@ 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 ([!])
|
||||||
@ -333,7 +275,6 @@ 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
|
||||||
@ -341,7 +282,6 @@ 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 ([!])
|
||||||
@ -349,7 +289,6 @@ 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 片段)
|
||||||
@ -361,7 +300,6 @@ 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 条件
|
||||||
@ -375,7 +313,6 @@ 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 条件
|
||||||
@ -389,7 +326,6 @@ 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 条件
|
||||||
@ -407,7 +343,6 @@ 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
|
||||||
@ -420,8 +355,6 @@ 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").
|
||||||
@ -429,7 +362,6 @@ 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 条件
|
||||||
@ -441,7 +373,6 @@ 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 条件
|
||||||
@ -455,7 +386,6 @@ 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
|
||||||
@ -467,7 +397,6 @@ 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
|
||||||
@ -478,7 +407,6 @@ 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 单条
|
||||||
@ -488,7 +416,6 @@ 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
|
||||||
@ -498,7 +425,6 @@ 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 分页
|
||||||
@ -509,7 +435,6 @@ 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 分组
|
||||||
@ -520,7 +445,6 @@ 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
|
||||||
@ -537,7 +461,6 @@ 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
|
||||||
@ -550,8 +473,6 @@ 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").
|
||||||
@ -562,7 +483,6 @@ 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 语法
|
||||||
@ -579,7 +499,6 @@ 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
|
||||||
@ -593,7 +512,6 @@ 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
|
||||||
@ -605,7 +523,6 @@ 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
|
||||||
@ -618,14 +535,11 @@ 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 带条件
|
||||||
@ -633,7 +547,6 @@ 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 求和
|
||||||
@ -641,7 +554,6 @@ 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 平均值
|
||||||
@ -649,7 +561,6 @@ 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 最大值
|
||||||
@ -657,7 +568,6 @@ 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 最小值
|
||||||
@ -665,7 +575,6 @@ 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 分组统计
|
||||||
@ -680,7 +589,6 @@ 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
|
||||||
@ -693,8 +601,6 @@ 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{
|
||||||
@ -703,7 +609,6 @@ 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 第二页
|
||||||
@ -714,7 +619,6 @@ 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 链式分页
|
||||||
@ -726,7 +630,6 @@ 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 偏移
|
||||||
@ -739,7 +642,6 @@ 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
|
||||||
@ -748,16 +650,14 @@ func testPagination(that *Context) Map {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 7. 批量插入测试 ====================
|
// ==================== 7. 批量插入测试 ====================
|
||||||
func testBatchInsert(that *Context) Map {
|
func testInserts(that *Context) Map {
|
||||||
result := Map{"name": "批量插入测试", "tests": Slice{}}
|
result := Map{"name": "批量插入测试", "tests": Slice{}}
|
||||||
tests := Slice{}
|
tests := Slice{}
|
||||||
|
|
||||||
debugLog("main.go:697", "开始批量插入测试 (MySQL)", nil, "H7_BATCH")
|
|
||||||
|
|
||||||
// 7.1 批量插入
|
// 7.1 批量插入
|
||||||
test1 := Map{"name": "BatchInsert 批量插入"}
|
test1 := Map{"name": "Inserts 批量插入"}
|
||||||
timestamp := time.Now().UnixNano()
|
timestamp := time.Now().UnixNano()
|
||||||
affected1 := that.Db.BatchInsert("test_batch", []Map{
|
affected1 := that.Db.Inserts("test_batch", []Map{
|
||||||
{"name": fmt.Sprintf("批量测试1_%d", timestamp), "title": "标题1", "state": 1},
|
{"name": fmt.Sprintf("批量测试1_%d", timestamp), "title": "标题1", "state": 1},
|
||||||
{"name": fmt.Sprintf("批量测试2_%d", timestamp), "title": "标题2", "state": 1},
|
{"name": fmt.Sprintf("批量测试2_%d", timestamp), "title": "标题2", "state": 1},
|
||||||
{"name": fmt.Sprintf("批量测试3_%d", timestamp), "title": "标题3", "state": 1},
|
{"name": fmt.Sprintf("批量测试3_%d", timestamp), "title": "标题3", "state": 1},
|
||||||
@ -765,19 +665,17 @@ func testBatchInsert(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": "BatchInsert 带 [#] 标记"}
|
test2 := Map{"name": "Inserts 带 [#] 标记"}
|
||||||
timestamp2 := time.Now().UnixNano()
|
timestamp2 := time.Now().UnixNano()
|
||||||
affected2 := that.Db.BatchInsert("test_batch", []Map{
|
affected2 := that.Db.Inserts("test_batch", []Map{
|
||||||
{"name": fmt.Sprintf("带时间测试1_%d", timestamp2), "title": "标题带时间1", "state": 1, "create_time[#]": "NOW()"},
|
{"name": fmt.Sprintf("带时间测试1_%d", timestamp2), "title": "标题带时间1", "state": 1, "create_time[#]": "NOW()"},
|
||||||
{"name": fmt.Sprintf("带时间测试2_%d", timestamp2), "title": "标题带时间2", "state": 1, "create_time[#]": "NOW()"},
|
{"name": fmt.Sprintf("带时间测试2_%d", timestamp2), "title": "标题带时间2", "state": 1, "create_time[#]": "NOW()"},
|
||||||
})
|
})
|
||||||
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)
|
||||||
|
|
||||||
// 清理测试数据
|
// 清理测试数据
|
||||||
@ -794,8 +692,6 @@ 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 表测试 Upsert(MySQL ON DUPLICATE KEY UPDATE)
|
// 使用 admin 表测试 Upsert(MySQL 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)
|
||||||
@ -819,7 +715,6 @@ 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 更新已存在记录
|
||||||
@ -840,7 +735,6 @@ 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)
|
||||||
|
|
||||||
// 验证更新结果
|
// 验证更新结果
|
||||||
@ -860,8 +754,6 @@ 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()
|
||||||
@ -875,7 +767,6 @@ 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
|
||||||
})
|
})
|
||||||
@ -884,7 +775,6 @@ 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 事务回滚
|
||||||
@ -893,13 +783,12 @@ func testTransaction(that *Context) Map {
|
|||||||
|
|
||||||
success2 := that.Db.Action(func(tx HoTimeDB) bool {
|
success2 := that.Db.Action(func(tx HoTimeDB) bool {
|
||||||
// 插入记录
|
// 插入记录
|
||||||
recordId := tx.Insert("test_batch", Map{
|
_ = 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
|
||||||
@ -909,7 +798,6 @@ 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)
|
||||||
|
|
||||||
// 清理测试数据
|
// 清理测试数据
|
||||||
@ -925,14 +813,11 @@ 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 表
|
||||||
@ -953,9 +838,67 @@ 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
|
||||||
|
|||||||
Binary file not shown.
@ -766,12 +766,12 @@ correctUsers4 := db.Select("user", "*", common.Map{
|
|||||||
_ = db
|
_ = db
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchInsertExample 批量插入示例
|
// InsertsExample 批量插入示例
|
||||||
func BatchInsertExample(database *db.HoTimeDB) {
|
func InsertsExample(database *db.HoTimeDB) {
|
||||||
fmt.Println("\n=== 批量插入示例 ===")
|
fmt.Println("\n=== 批量插入示例 ===")
|
||||||
|
|
||||||
// 批量插入用户(使用 []Map 格式)
|
// 批量插入用户(使用 []Map 格式)
|
||||||
affected := database.BatchInsert("user", []common.Map{
|
affected := database.Inserts("user", []common.Map{
|
||||||
{"name": "批量用户1", "email": "batch1@example.com", "age": 25, "status": 1},
|
{"name": "批量用户1", "email": "batch1@example.com", "age": 25, "status": 1},
|
||||||
{"name": "批量用户2", "email": "batch2@example.com", "age": 30, "status": 1},
|
{"name": "批量用户2", "email": "batch2@example.com", "age": 30, "status": 1},
|
||||||
{"name": "批量用户3", "email": "batch3@example.com", "age": 28, "status": 1},
|
{"name": "批量用户3", "email": "batch3@example.com", "age": 28, "status": 1},
|
||||||
@ -779,7 +779,7 @@ func BatchInsertExample(database *db.HoTimeDB) {
|
|||||||
fmt.Printf("批量插入了 %d 条用户记录\n", affected)
|
fmt.Printf("批量插入了 %d 条用户记录\n", affected)
|
||||||
|
|
||||||
// 批量插入日志(使用 [#] 标记直接 SQL)
|
// 批量插入日志(使用 [#] 标记直接 SQL)
|
||||||
logAffected := database.BatchInsert("log", []common.Map{
|
logAffected := database.Inserts("log", []common.Map{
|
||||||
{"user_id": 1, "action": "login", "ip": "192.168.1.1", "created_time[#]": "NOW()"},
|
{"user_id": 1, "action": "login", "ip": "192.168.1.1", "created_time[#]": "NOW()"},
|
||||||
{"user_id": 2, "action": "logout", "ip": "192.168.1.2", "created_time[#]": "NOW()"},
|
{"user_id": 2, "action": "logout", "ip": "192.168.1.2", "created_time[#]": "NOW()"},
|
||||||
{"user_id": 3, "action": "view", "ip": "192.168.1.3", "created_time[#]": "NOW()"},
|
{"user_id": 3, "action": "view", "ip": "192.168.1.3", "created_time[#]": "NOW()"},
|
||||||
@ -838,7 +838,7 @@ func RunAllFixedExamples() {
|
|||||||
fmt.Println("所有示例代码已修正完毕,语法正确!")
|
fmt.Println("所有示例代码已修正完毕,语法正确!")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("新增功能示例说明:")
|
fmt.Println("新增功能示例说明:")
|
||||||
fmt.Println(" - BatchInsertExample(db): 批量插入示例,使用 []Map 格式")
|
fmt.Println(" - InsertsExample(db): 批量插入示例,使用 []Map 格式")
|
||||||
fmt.Println(" - UpsertExample(db): 插入或更新示例")
|
fmt.Println(" - UpsertExample(db): 插入或更新示例")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
110
log/logrus.go
110
log/logrus.go
@ -2,12 +2,13 @@ package log
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetLog(path string, showCodeLine bool) *log.Logger {
|
func GetLog(path string, showCodeLine bool) *log.Logger {
|
||||||
@ -73,41 +74,98 @@ func (that *MyHook) Fire(entry *log.Entry) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对caller进行递归查询, 直到找到非logrus包产生的第一个调用.
|
// 最大框架层数限制 - 超过这个层数后不再跳过,防止误过滤应用层
|
||||||
// 因为filename我获取到了上层目录名, 因此所有logrus包的调用的文件名都是 logrus/...
|
const maxFrameworkDepth = 10
|
||||||
// 因此通过排除logrus开头的文件名, 就可以排除所有logrus包的自己的函数调用
|
|
||||||
func findCaller(skip int) string {
|
// isHoTimeFrameworkFile 判断是否是 HoTime 框架文件
|
||||||
file := ""
|
// 更精确的匹配:只有明确属于框架的文件才会被跳过
|
||||||
line := 0
|
func isHoTimeFrameworkFile(file string) bool {
|
||||||
for i := 0; i < 10; i++ {
|
// 1. logrus 日志库内部文件
|
||||||
file, line = getCaller(skip + i)
|
if strings.HasPrefix(file, "logrus/") {
|
||||||
if !strings.HasPrefix(file, "logrus") {
|
return true
|
||||||
j := 0
|
|
||||||
for true {
|
|
||||||
j++
|
|
||||||
if file == "common/error.go" {
|
|
||||||
file, line = getCaller(skip + i + j)
|
|
||||||
}
|
}
|
||||||
if file == "db/hotimedb.go" {
|
|
||||||
file, line = getCaller(skip + i + j)
|
// 2. Go 运行时文件
|
||||||
|
if strings.HasPrefix(file, "runtime/") {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
if file == "code/makecode.go" {
|
|
||||||
file, line = getCaller(skip + i + j)
|
// 3. HoTime 框架核心文件 - 通过包含 "hotime" 或框架特有文件名来识别
|
||||||
|
// 检查路径中是否包含 hotime 框架标识
|
||||||
|
lowerFile := strings.ToLower(file)
|
||||||
|
if strings.Contains(lowerFile, "hotime") {
|
||||||
|
// 是 hotime 框架的一部分,检查是否是核心模块
|
||||||
|
frameworkDirs := []string{"/db/", "/common/", "/code/", "/cache/", "/log/", "/dri/"}
|
||||||
|
for _, dir := range frameworkDirs {
|
||||||
|
if strings.Contains(file, dir) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
if strings.Index(file, "common/") == 0 {
|
|
||||||
file, line = getCaller(skip + i + j)
|
|
||||||
}
|
}
|
||||||
if strings.Contains(file, "application.go") {
|
// 框架核心文件(在 hotime 根目录下的 .go 文件)
|
||||||
file, line = getCaller(skip + i + j)
|
if strings.HasSuffix(file, "application.go") ||
|
||||||
}
|
strings.HasSuffix(file, "context.go") ||
|
||||||
if j == 5 {
|
strings.HasSuffix(file, "session.go") ||
|
||||||
break
|
strings.HasSuffix(file, "const.go") ||
|
||||||
|
strings.HasSuffix(file, "type.go") ||
|
||||||
|
strings.HasSuffix(file, "var.go") ||
|
||||||
|
strings.HasSuffix(file, "mime.go") {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 直接匹配框架核心目录(用于没有完整路径的情况)
|
||||||
|
// 只匹配 "db/xxx.go" 这种在框架核心目录下的文件
|
||||||
|
frameworkCoreDirs := []string{"db/", "common/", "code/", "cache/"}
|
||||||
|
for _, dir := range frameworkCoreDirs {
|
||||||
|
if strings.HasPrefix(file, dir) {
|
||||||
|
// 额外检查:确保不是用户项目中同名目录
|
||||||
|
// 框架文件通常有特定的文件名
|
||||||
|
frameworkFiles := []string{
|
||||||
|
"query.go", "crud.go", "where.go", "builder.go", "db.go",
|
||||||
|
"dialect.go", "aggregate.go", "transaction.go", "identifier.go",
|
||||||
|
"error.go", "func.go", "map.go", "obj.go", "slice.go",
|
||||||
|
"makecode.go", "template.go", "config.go",
|
||||||
|
"cache.go", "cache_db.go", "cache_memory.go", "cache_redis.go",
|
||||||
|
}
|
||||||
|
for _, f := range frameworkFiles {
|
||||||
|
if strings.HasSuffix(file, f) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对caller进行递归查询, 直到找到非框架层产生的第一个调用.
|
||||||
|
// 遍历调用栈,跳过框架层文件,找到应用层代码
|
||||||
|
// 使用层数限制确保不会误过滤应用层同名目录
|
||||||
|
func findCaller(skip int) string {
|
||||||
|
frameworkCount := 0 // 连续框架层计数
|
||||||
|
|
||||||
|
// 遍历调用栈,找到第一个非框架文件
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
file, line := getCaller(skip + i)
|
||||||
|
if file == "" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isHoTimeFrameworkFile(file) {
|
||||||
|
frameworkCount++
|
||||||
|
// 层数限制:如果已经跳过太多层,停止跳过
|
||||||
|
if frameworkCount >= maxFrameworkDepth {
|
||||||
|
return fmt.Sprintf("%s:%d", file, line)
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到非框架文件,返回应用层代码位置
|
||||||
|
return fmt.Sprintf("%s:%d", file, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找不到应用层,返回最初的调用者
|
||||||
|
file, line := getCaller(skip)
|
||||||
return fmt.Sprintf("%s:%d", file, line)
|
return fmt.Sprintf("%s:%d", file, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
var.go
1
var.go
@ -107,6 +107,7 @@ 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": "默认服务ip:127.0.0.1,必须,如果需要使用redis服务时配置,",
|
"host": "默认服务ip:127.0.0.1,必须,如果需要使用redis服务时配置,",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user