diff --git a/.gitignore b/.gitignore index 7360a18..5dfad93 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.exe /example/config /.cursor/*.log +*.sql diff --git a/cache/cache_db.go b/cache/cache_db.go index 322158a..7050cb1 100644 --- a/cache/cache_db.go +++ b/cache/cache_db.go @@ -3,33 +3,12 @@ package cache import ( "database/sql" "encoding/json" - "os" "strings" "time" . "code.hoteas.com/golang/hotime/common" ) -// debugLogDb 写入调试日志 -func debugLogDb(hypothesisId, location, message string, data map[string]interface{}) { - // #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 { - logEntry, _ := json.Marshal(map[string]interface{}{ - "sessionId": "cache-db-debug", - "runId": "test-run", - "hypothesisId": hypothesisId, - "location": location, - "message": message, - "data": data, - "timestamp": time.Now().UnixMilli(), - }) - logFile.Write(append(logEntry, '\n')) - logFile.Close() - } - // #endregion -} - // 表名常量 const ( CacheTableName = "hotime_cache" @@ -614,13 +593,6 @@ func (that *CacheDb) CachesGet(keys []string) Map { return result } - // #region agent log - debugLogDb("E", "cache_db.go:CachesGet:start", "CacheDb.CachesGet开始", map[string]interface{}{ - "keys_count": len(keys), - "keys": keys, - }) - // #endregion - tableName := that.getTableName() nowTime := Time2Str(time.Now()) @@ -630,36 +602,18 @@ func (that *CacheDb) CachesGet(keys []string) Map { "end_time[>]": nowTime, }) - // #region agent log - debugLogDb("E", "cache_db.go:CachesGet:afterSelect", "DB Select完成", map[string]interface{}{ - "table": tableName, - "now_time": nowTime, - "found_rows": len(cachedList), - }) - // #endregion - for _, cached := range cachedList { valueStr := cached.GetString("value") if valueStr != "" { data, err := JsonToObj(valueStr) if err == nil { result[cached.GetString("key")] = data - } else { - // #region agent log - debugLogDb("E", "cache_db.go:CachesGet:unmarshalError", "JSON解析失败", map[string]interface{}{ - "key": cached.GetString("key"), - "error": err.Error(), - }) - // #endregion } } } // 兼容模式:新表没有的 key,回退读取老表 if that.isCompatibleMode() { - // #region agent log - debugLogDb("E", "cache_db.go:CachesGet:compatMode", "兼容模式检查老表", nil) - // #endregion for _, key := range keys { if _, exists := result[key]; !exists { legacyData := that.getLegacy(key) @@ -670,12 +624,6 @@ func (that *CacheDb) CachesGet(keys []string) Map { } } - // #region agent log - debugLogDb("E", "cache_db.go:CachesGet:end", "CacheDb.CachesGet完成", map[string]interface{}{ - "result_count": len(result), - }) - // #endregion - return result } @@ -688,12 +636,6 @@ func (that *CacheDb) CachesSet(data Map, timeout ...int64) { return } - // #region agent log - debugLogDb("A", "cache_db.go:CachesSet:start", "CacheDb.CachesSet开始", map[string]interface{}{ - "data_count": len(data), - }) - // #endregion - // 计算过期时间 var tim int64 if len(timeout) > 0 && timeout[0] > 0 { @@ -710,13 +652,6 @@ func (that *CacheDb) CachesSet(data Map, timeout ...int64) { for key, value := range data { that.set(key, value, endTime) } - - // #region agent log - debugLogDb("A", "cache_db.go:CachesSet:end", "CacheDb.CachesSet完成", map[string]interface{}{ - "data_count": len(data), - "end_time": Time2Str(endTime), - }) - // #endregion } // CachesDelete 批量删除缓存 diff --git a/docs/CodeGen_使用说明.md b/docs/CodeGen_使用说明.md index 22e61d2..771ab1a 100644 --- a/docs/CodeGen_使用说明.md +++ b/docs/CodeGen_使用说明.md @@ -17,7 +17,7 @@ 代码生成器可以: -1. **自动读取数据库表结构** - 支持 MySQL 和 SQLite +1. **自动读取数据库表结构** - 支持 MySQL、SQLite 和达梦 DM8 2. **生成 CRUD 接口** - 增删改查、搜索、分页 3. **生成配置文件** - 表字段配置、菜单配置、权限配置 4. **智能字段识别** - 根据字段名自动识别类型和权限 @@ -459,6 +459,23 @@ content -- 内容(自动识别为 textArea) 2. 使用 `mode=1` 生成代码后手动修改 3. 在生成的配置文件中直接调整字段属性 +### 4. 达梦 DM8 使用注意 + +代码生成器已原生支持达梦 DM8,会自动读取 DM 数据库的表结构和字段备注。使用时注意: + +- 表注释(COMMENT)在 DM 中通过 `COMMENT ON TABLE` 语句单独设置,建表后需额外执行 +- 字段备注同样通过 `COMMENT ON COLUMN` 设置 +- 由于 DM 的 `USER_TAB_COLUMNS` 视图字段名称与 MySQL `information_schema` 不同,代码生成器已做兼容处理 + +```sql +-- DM8 设置表备注(替代 MySQL 的 COMMENT='...' 语法) +COMMENT ON TABLE "user" IS '用户管理'; + +-- DM8 设置字段备注 +COMMENT ON COLUMN "user"."state" IS '状态:0-正常,1-异常,2-隐藏'; +COMMENT ON COLUMN "user"."name" IS '用户名'; +``` + --- ## 相关文档 diff --git a/docs/DatabaseDesign_数据库设计规范.md b/docs/DatabaseDesign_数据库设计规范.md index b516865..b88a491 100644 --- a/docs/DatabaseDesign_数据库设计规范.md +++ b/docs/DatabaseDesign_数据库设计规范.md @@ -191,6 +191,7 @@ 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` | +| 达梦 DM8 | TIMESTAMP | `2024-01-15 10:30:00` | --- @@ -324,6 +325,14 @@ SQLite 不支持字段备注(COMMENT),代码生成器会使用字段名作 "modify_time" TIMESTAMP DEFAULT NULL, -- 变更时间 ``` +### 达梦 DM8 + +```sql +"state" INT DEFAULT 0, -- 状态:0-正常,1-异常,2-隐藏 +"create_time" TIMESTAMP DEFAULT NULL, -- 创建日期 +"modify_time" TIMESTAMP DEFAULT NULL, -- 变更时间 +``` + --- ## 完整建表示例 @@ -392,6 +401,56 @@ CREATE TABLE "user" ( ); ``` +### 达梦 DM8 示例 + +达梦建表有以下关键差异: +- 自增主键使用 `IDENTITY(1,1)`(对应 MySQL 的 `AUTO_INCREMENT`) +- 长文本使用 `CLOB`(对应 MySQL 的 `LONGTEXT`) +- 标识符建议用**双引号包裹小写名称**,与框架保持一致 +- `admin`、`user`、`order` 等是 DM 保留字,必须加双引号 + +```sql +-- 用户表 +CREATE TABLE "user" ( + "id" INT IDENTITY(1,1) PRIMARY KEY, + "name" VARCHAR(50) DEFAULT NULL, + "phone" VARCHAR(20) DEFAULT NULL, + "password" VARCHAR(64) DEFAULT NULL, + "org_id" INT DEFAULT NULL, + "role_id" INT DEFAULT NULL, + "avatar" VARCHAR(255) DEFAULT NULL, + "sex" INT DEFAULT 0, + "state" INT DEFAULT 0, + "create_time" TIMESTAMP DEFAULT NULL, + "modify_time" TIMESTAMP DEFAULT NULL +); + +-- 组织表(树形结构) +CREATE TABLE "org" ( + "id" INT IDENTITY(1,1) PRIMARY KEY, + "name" VARCHAR(100) DEFAULT NULL, + "parent_id" INT DEFAULT NULL, + "parent_ids" VARCHAR(255) DEFAULT NULL, + "level" INT DEFAULT 0, + "sort" INT DEFAULT 0, + "state" INT DEFAULT 0, + "create_time" TIMESTAMP DEFAULT NULL, + "modify_time" TIMESTAMP DEFAULT NULL +); + +-- 用户组织关联表 +CREATE TABLE "user_org" ( + "id" INT IDENTITY(1,1) PRIMARY KEY, + "user_id" INT DEFAULT NULL, + "org_id" INT DEFAULT NULL, + "state" INT DEFAULT 0, + "create_time" TIMESTAMP DEFAULT NULL, + "modify_time" TIMESTAMP DEFAULT NULL +); +``` + +> **注意**:达梦中 DDL 的表名/列名大小写敏感(双引号包裹时),建议统一使用小写,与框架自动生成的 SQL 保持一致。 + --- ## 规范检查清单 @@ -406,3 +465,11 @@ CREATE TABLE "user" ( - [ ] 树形表包含 parent_id、parent_ids、level - [ ] 所有表包含 state、create_time、modify_time - [ ] 选择字段注释格式正确(`标签:值-名称`) + +### 达梦 DM8 额外检查项 + +- [ ] 建表时标识符使用**双引号包裹小写名称**(如 `"user"`、`"id"`) +- [ ] 保留字(`admin`、`user`、`order`、`key` 等)必须加双引号 +- [ ] 自增主键使用 `IDENTITY(1,1)` 而非 `AUTO_INCREMENT` +- [ ] 长文本字段使用 `CLOB` 而非 `LONGTEXT` +- [ ] 检测表是否存在时用 `COUNT(*)` 查询,不用 `USER_TABLES`(存在 schema 错配问题) diff --git a/docs/HoTimeDB_API参考.md b/docs/HoTimeDB_API参考.md index 3e2d19d..49f2ea6 100644 --- a/docs/HoTimeDB_API参考.md +++ b/docs/HoTimeDB_API参考.md @@ -107,10 +107,11 @@ affected := database.Inserts("table", []Map{ }) // 返回受影响的行数 -// 支持 [#] 标记直接 SQL +// 推荐:使用服务器时间(Time2Str),避免数据库时区差异 +now := Time2Str(time.Now()) affected := database.Inserts("log", []Map{ - {"user_id": 1, "created_time[#]": "NOW()"}, - {"user_id": 2, "created_time[#]": "NOW()"}, + {"user_id": 1, "created_time": now}, + {"user_id": 2, "created_time": now}, }) ``` @@ -223,10 +224,20 @@ data := database.Page(page, pageSize).PageSelect("table", "fields", whereMap) ### 直接SQL | 写法 | SQL | 说明 | |------|-----|------| -| `"field[#]": "NOW()"` | `field = NOW()` | 直接SQL函数 | +| `"field[#]": "balance + 1"` | `field = balance + 1` | 直接SQL表达式(数值运算等) | | `"[##]": "a > b"` | `a > b` | 直接SQL片段 | | `"field[#!]": "1"` | `field != 1` | 不等于(不参数化) | +> **时间字段推荐写法**:不要用 `"[#]": "NOW()"`,改用 Go 侧传值: +> ```go +> // 赋值当前时间 +> "create_time": Time2Str(time.Now()) +> +> // 时间范围查询(一天前) +> "create_time[>]": Time2Str(time.Now().AddDate(0, 0, -1)) +> ``` +> 原因:`NOW()` 使用数据库时区;`Time2Str(time.Now())` 使用应用服务器时区,行为稳定且跨数据库兼容。 + ## 逻辑连接符 ### AND 条件 @@ -391,6 +402,61 @@ database := &db.HoTimeDB{ // - Upsert: ON DUPLICATE KEY -> ON CONFLICT ``` +## 达梦数据库(DM8)支持 + +### 连接配置 + +```go +import ( + _ "gitee.com/chunanyong/dm" // vendor 已内置,无需额外安装 +) + +database := &db.HoTimeDB{ + Type: "dm", // 或 "dameng" +} + +database.SetConnect(func(err ...*common.Error) (master, slave *sql.DB) { + // schema= 指定当前会话默认搜索 schema + dsn := "dm://SYSDBA:password@127.0.0.1:5236?schema=TEST" + master, _ = sql.Open("dm", dsn) + return master, master +}) +``` + +### 各数据库差异对比 + +| 特性 | MySQL | PostgreSQL | 达梦 DM8 | +|------|-------|------------|---------| +| 标识符引号 | \`name\` | "name" | "name" | +| 占位符 | ? | $1, $2... | ? | +| Upsert | ON DUPLICATE KEY UPDATE | ON CONFLICT DO UPDATE | MERGE INTO...USING | +| 自增列 | AUTO_INCREMENT | SERIAL | IDENTITY(1,1) | +| 分页 | LIMIT m, n | LIMIT n OFFSET m | LIMIT m, n 或 LIMIT n OFFSET m | + +框架自动处理所有差异,业务代码无需修改。 + +### 达梦注意事项速查 + +| 场景 | 说明 | +|------|------| +| 标识符大小写 | 双引号内大小写敏感,框架统一使用小写 | +| 保留字 | `admin`/`user`/`order` 等框架自动加双引号,原生SQL需手动处理 | +| 表存在检测 | 用 `COUNT(*)` 代替 `USER_TABLES`(schema 错配问题) | +| `Insert` 返回ID | 原生驱动已支持 `LastInsertId()`,与 MySQL 一致 | +| 长文本类型 | DM 用 `CLOB`(对应 MySQL 的 `LONGTEXT`) | +| 时间赋值 | **推荐统一用** `Time2Str(time.Now())` 传服务器时间,避免 `NOW()` 时区差异及跨库函数不兼容 | + +### 原生 SQL 中的保留字 + +```go +// 达梦中 admin、user 等是保留字,原生 SQL 必须加双引号 +results := database.Query( + `SELECT "id", "name" FROM "admin" WHERE "state" = ?`, 1) + +// ORM 方法无需处理,框架自动加引号 +results := database.Select("admin", "*", common.Map{"state": 1}) +``` + ## 错误处理 ```go // 检查最后的错误 @@ -509,8 +575,8 @@ result := database.Table("order"). --- -*快速参考版本: 2.0* -*更新日期: 2026年1月* +*快速参考版本: 2.1* +*更新日期: 2026年3月* **详细说明:** - [HoTimeDB 使用说明](HoTimeDB_使用说明.md) - 完整教程 diff --git a/docs/HoTimeDB_使用说明.md b/docs/HoTimeDB_使用说明.md index fd0aa8c..fd04f4d 100644 --- a/docs/HoTimeDB_使用说明.md +++ b/docs/HoTimeDB_使用说明.md @@ -2,7 +2,7 @@ ## 概述 -HoTimeDB是一个基于Golang实现的轻量级ORM框架,参考PHP Medoo设计,提供简洁的数据库操作接口。支持MySQL、SQLite、PostgreSQL等数据库,并集成了缓存、事务、链式查询等功能。 +HoTimeDB是一个基于Golang实现的轻量级ORM框架,参考PHP Medoo设计,提供简洁的数据库操作接口。支持MySQL、SQLite、PostgreSQL、达梦(DM8)等数据库,并集成了缓存、事务、链式查询等功能。 ## 目录 @@ -24,6 +24,7 @@ HoTimeDB是一个基于Golang实现的轻量级ORM框架,参考PHP Medoo设计 - [事务处理](#事务处理) - [缓存机制](#缓存机制) - [PostgreSQL支持](#postgresql支持) +- [达梦数据库支持](#达梦数据库支持) - [高级特性](#高级特性) ## 快速开始 @@ -48,7 +49,7 @@ func createConnection() (master, slave *sql.DB) { // 初始化HoTimeDB database := &db.HoTimeDB{ - Type: "mysql", // 可选:mysql, sqlite3, postgres + Type: "mysql", // 可选:mysql, sqlite3, postgres, dm } database.SetConnect(createConnection) ``` @@ -64,7 +65,7 @@ type HoTimeDB struct { DBName string *cache.HoTimeCache Log *logrus.Logger - Type string // 数据库类型:mysql, sqlite3, postgres + Type string // 数据库类型:mysql, sqlite3, postgres, dm Prefix string // 表前缀 LastQuery string // 最后执行的SQL LastData []interface{} // 最后的参数 @@ -212,17 +213,27 @@ user := database.Get("user", "id,name,email", common.Map{ ```go // 基本插入 id := database.Insert("user", common.Map{ - "name": "张三", - "email": "zhangsan@example.com", - "age": 25, - "status": 1, - "created_time[#]": "NOW()", // [#]表示直接插入SQL函数 + "name": "张三", + "email": "zhangsan@example.com", + "age": 25, + "status": 1, + "created_time": Time2Str(time.Now()), // 推荐:使用服务器时间,避免数据库时区问题 }) // 返回插入的ID fmt.Println("插入的用户ID:", id) ``` +> **时间字段最佳实践**:建议使用 `Time2Str(time.Now())` 传入服务器时间,而非 `"[#]": "NOW()"` 让数据库执行 `NOW()`。 +> 原因:`NOW()` 使用数据库服务器时间,当应用服务器与数据库服务器时区不同时会产生偏差;而 `Time2Str(time.Now())` 始终使用应用服务器时间,行为一致,且跨数据库(MySQL / 达梦 / PostgreSQL)完全兼容。 +> +> ```go +> import ( +> "time" +> . "code.hoteas.com/golang/hotime/common" // Time2Str 在 common 包中 +> ) +> ``` + ### 批量插入(Inserts) ```go @@ -236,10 +247,11 @@ affected := database.Inserts("user", []common.Map{ fmt.Printf("批量插入 %d 条记录\n", affected) -// 支持 [#] 标记直接插入 SQL 表达式 +// 推荐:使用服务器时间(Time2Str)而非数据库函数 NOW() +now := Time2Str(time.Now()) affected := database.Inserts("log", []common.Map{ - {"user_id": 1, "action": "login", "created_time[#]": "NOW()"}, - {"user_id": 2, "action": "logout", "created_time[#]": "NOW()"}, + {"user_id": 1, "action": "login", "created_time": now}, + {"user_id": 2, "action": "logout", "created_time": now}, }) ``` @@ -248,9 +260,9 @@ affected := database.Inserts("log", []common.Map{ ```go // 基本更新 affected := database.Update("user", common.Map{ - "name": "李四", - "email": "lisi@example.com", - "updated_time[#]": "NOW()", + "name": "李四", + "email": "lisi@example.com", + "modify_time": Time2Str(time.Now()), // 推荐:服务器时间 }, common.Map{ "id": 1, }) @@ -291,6 +303,12 @@ affected := database.Upsert("user", // INSERT INTO "user" (id,name,email,login_count) VALUES ($1,$2,$3,$4) // ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, email=EXCLUDED.email, login_count=EXCLUDED.login_count +// 达梦 DM8 生成: +// MERGE INTO "user" USING (SELECT ? AS "id", ? AS "name", ...) src +// ON ("user"."id" = src."id") +// WHEN MATCHED THEN UPDATE SET "name"=src."name", "email"=src."email", ... +// WHEN NOT MATCHED THEN INSERT ("id","name","email","login_count") VALUES (src."id", ...) + // 使用 [#] 标记直接 SQL 更新 affected := database.Upsert("user", common.Map{ @@ -349,8 +367,8 @@ user := database.Table("user"). affected := database.Table("user"). Where("id", 1). Update(common.Map{ - "name": "新名称", - "updated_time[#]": "NOW()", + "name": "新名称", + "modify_time": Time2Str(time.Now()), }) // 链式删除 @@ -458,14 +476,14 @@ HoTimeDB支持丰富的条件查询语法,类似于Medoo: ### 直接SQL ```go -// 直接插入SQL表达式(注意防注入) -"created_time[#]": "> DATE_SUB(NOW(), INTERVAL 1 DAY)" - -// 字段直接赋值(不使用参数化查询) -"update_time[#]": "NOW()" - -// 直接SQL片段 +// 直接SQL片段(通常用于复杂条件,时间推荐用 Go 计算) "[##]": "user.status = 1 AND user.level > 0" + +// 时间范围条件:推荐在 Go 侧计算,避免跨库 NOW() 差异 +"created_time[>]": Time2Str(time.Now().AddDate(0, 0, -1)) // 一天前 + +// 如必须用数据库函数(不跨库时可用,但不推荐) +"update_time[#]": "NOW()" ``` ## JOIN操作 @@ -637,10 +655,10 @@ success := database.Action(func(tx db.HoTimeDB) bool { // 创建订单 orderId := tx.Insert("order", common.Map{ - "user_id": 1, - "amount": 100, - "status": "paid", - "created_time[#]": "NOW()", + "user_id": 1, + "amount": 100, + "status": "paid", + "created_time": Time2Str(time.Now()), }) if orderId == 0 { @@ -721,6 +739,182 @@ database.SetConnect(func(err ...*common.Error) (master, slave *sql.DB) { 所有这些差异由框架自动处理,无需手动调整代码。 +## 达梦数据库支持 + +HoTimeDB 原生支持**达梦数据库 DM8**,驱动已内置于 `vendor/gitee.com/chunanyong/dm`,无需额外安装。 + +### 达梦配置 + +在 `config.json` 中配置: + +```json +{ + "db": { + "dm": { + "host": "127.0.0.1", + "port": "5236", + "user": "SYSDBA", + "password": "your_password", + "name": "TEST", + "prefix": "" + } + } +} +``` + +代码配置方式: + +```go +import ( + "code.hoteas.com/golang/hotime/db" + _ "gitee.com/chunanyong/dm" // 达梦驱动(已在 vendor 中) +) + +database := &db.HoTimeDB{ + Type: "dm", // 或 "dameng" + Prefix: "", // 表前缀(可选) +} + +database.SetConnect(func(err ...*common.Error) (master, slave *sql.DB) { + // name 字段即为 schema(默认搜索路径) + dsn := "dm://SYSDBA:your_password@127.0.0.1:5236?schema=TEST" + master, _ = sql.Open("dm", dsn) + return master, master +}) +``` + +> **schema 说明**:`?schema=TEST` 将当前会话的默认搜索 schema 设置为 `TEST`。 +> 所有不带 schema 前缀的表名均在该 schema 下查找和创建。 +> 建议为应用创建独立的专用 schema,避免直接使用 `SYSDBA` 默认 schema。 + +### 与 MySQL/PostgreSQL 的差异对比 + +| 特性 | MySQL | PostgreSQL | 达梦 DM8 | +|------|-------|------------|---------| +| 标识符引号 | \`name\` | "name" | "name" | +| 占位符 | ? | $1, $2... | ? | +| Upsert | ON DUPLICATE KEY UPDATE | ON CONFLICT DO UPDATE | MERGE INTO ... USING | +| 自增列 | AUTO_INCREMENT | SERIAL | IDENTITY(1,1) | +| 文本类型 | TEXT | TEXT | TEXT / CLOB | +| 时间赋值(推荐) | `Time2Str(time.Now())` | `Time2Str(time.Now())` | `Time2Str(time.Now())` | +| 时间函数(不推荐) | `NOW()` | `NOW()` | `NOW()` | +| 分页语法 | LIMIT m, n | LIMIT n OFFSET m | LIMIT m, n 或 LIMIT n OFFSET m | + +所有这些差异由框架自动处理,业务代码无需修改。 + +### Upsert 在达梦中的行为 + +达梦不支持 MySQL 的 `ON DUPLICATE KEY UPDATE` 和 PostgreSQL 的 `ON CONFLICT`,框架会自动生成 `MERGE INTO` 语句: + +```go +// 调用方式与其他数据库完全一致 +affected := database.Upsert("admin", + common.Map{ + "name": "张三", + "phone": "13800000001", + "password": "abc123", + "role_id": 1, + }, + common.Slice{"phone"}, // 唯一键(冲突检测) + common.Slice{"name", "password", "role_id"}, // 冲突时更新的字段 +) + +// 达梦自动生成: +// MERGE INTO "admin" USING (SELECT ? AS "name", ? AS "phone", ...) src +// ON ("admin"."phone" = src."phone") +// WHEN MATCHED THEN UPDATE SET "name" = src."name", ... +// WHEN NOT MATCHED THEN INSERT (...) VALUES (src....) +``` + +### 达梦注意事项 + +#### 1. 标识符大小写敏感 + +达梦对双引号包裹的标识符大小写敏感。框架统一使用**小写**创建和访问表/列,确保一致性: + +```sql +-- ✅ 正确:框架自动生成,小写标识符 +SELECT * FROM "admin" WHERE "id" = ? + +-- ❌ 错误:大小写不一致会导致 "无效的表或视图名" 错误 +SELECT * FROM "ADMIN" WHERE "ID" = ? +``` + +> 使用 DM 管理工具手动创建表时,建议使用双引号包裹小写名称,与框架保持一致。 + +#### 2. 保留字冲突 + +达梦有大量保留字(如 `admin`、`user`、`order`、`key`、`value` 等),直接使用这些名称作为表名或列名会报语法错误。 +**框架已自动为所有标识符加双引号**,一般情况下无需手动处理。 + +如果在原生 SQL(`Query`/`Exec`)中使用保留字,需要手动加引号: + +```go +// 原生 SQL 中需要手动加双引号 +results := database.Query(`SELECT "id", "name" FROM "admin" WHERE "state" = ?`, 1) +``` + +#### 3. schema 与 USER_TABLES 的关系 + +连接达梦时使用 `?schema=TEST`,DM 的 `USER_TABLES` 视图只显示当前**登录用户**(如 SYSDBA)所属 schema 的表,而非当前会话 schema 的表。 + +框架内部已改用 `COUNT(*)` 方式直接探查表可访问性,避免 schema 错配问题。 + +如果在自定义代码中检测表是否存在,**不要用 USER_TABLES**,改用: + +```go +// ✅ 推荐:直接探查表可访问性 +res := database.Query(`SELECT COUNT(*) as cnt FROM "your_table"`) +exists := len(res) > 0 + +// ❌ 不推荐:USER_TABLES 可能显示当前会话 schema 之外的表 +res := database.Query(`SELECT TABLE_NAME FROM USER_TABLES WHERE TABLE_NAME='your_table'`) +``` + +#### 4. DDL 建表建议 + +手动建表时,列定义与 MySQL 的区别: + +```sql +-- 达梦建表示例 +CREATE TABLE "admin" ( + "id" INT IDENTITY(1,1) PRIMARY KEY, -- 自增(MySQL: AUTO_INCREMENT) + "name" VARCHAR(100), + "phone" VARCHAR(20), + "content" CLOB, -- 长文本(MySQL: TEXT/LONGTEXT) + "create_time" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "modify_time" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "uk_admin_phone" UNIQUE ("phone") +); + +-- 索引 +CREATE INDEX "idx_admin_state" ON "admin" ("state"); +``` + +#### 5. 日期时间函数差异 + +部分日期函数在达梦中语法不同,使用 `[##]` 直接SQL时需注意: + +| 功能 | 推荐(Go侧) | MySQL `[##]`/原生SQL | 达梦 DM8 `[##]`/原生SQL | +|------|------------|---------------------|----------------------| +| 当前时间赋值 | `Time2Str(time.Now())` ✅ | `NOW()` | `NOW()` | +| 一天前(时间范围查询) | `Time2Str(time.Now().AddDate(0,0,-1))` ✅ | `DATE_SUB(NOW(), INTERVAL 1 DAY)` | `DATEADD(DAY, -1, NOW())` | +| 时间戳转字符串 | `Time2Str(time.Unix(ts, 0))` ✅ | `FROM_UNIXTIME(ts)` | 不支持,需 Go 侧处理 | + +> **强烈推荐**:时间字段赋值和时间范围计算都在 Go 侧完成(使用 `Time2Str`、`time.Now().Add()` 等),传入参数化的字符串值。这样完全规避数据库时区设置差异,也无需关心各数据库函数的语法不同。 + +#### 6. 使用 IN 查询时的注意事项 + +IN 查询在达梦中与 MySQL 行为一致,框架已统一处理: + +```go +// 正常使用,框架自动展开 +results := database.Select("article", "*", common.Map{ + "id": []int{1, 2, 3}, +}) +// 生成: WHERE "id" IN (?, ?, ?) +``` + ## 高级特性 ### 调试模式 @@ -758,8 +952,8 @@ database.SetConnect(createConnection) // 执行查询SQL results := database.Query("SELECT * FROM user WHERE age > ? AND status = ?", 18, 1) -// 执行更新SQL -result, err := database.Exec("UPDATE user SET last_login = NOW() WHERE id = ?", 1) +// 执行更新SQL(原生 SQL 中也建议使用服务器时间) +result, err := database.Exec("UPDATE user SET last_login = ? WHERE id = ?", Time2Str(time.Now()), 1) if err.GetError() == nil { affected, _ := result.RowsAffected() fmt.Println("影响行数:", affected) @@ -783,7 +977,7 @@ if err.GetError() == nil { | `[~~]` | 手动LIKE | `"name[~~]": "%张%"` | `name LIKE '%张%'` | | `[<>]` | BETWEEN | `"age[<>]": [18,60]` | `age BETWEEN 18 AND 60` | | `[><]` | NOT BETWEEN | `"age[><]": [18,25]` | `age NOT BETWEEN 18 AND 25` | -| `[#]` | 直接SQL | `"time[#]": "NOW()"` | `time = NOW()` | +| `[#]` | 直接SQL表达式 | `"balance[#]": "balance + 1"` | `balance = balance + 1` | | `[##]` | SQL片段 | `"[##]": "a > b"` | `a > b` | | `[#!]` | 不等于直接SQL | `"status[#!]": "1"` | `status != 1` | | `[!#]` | 不等于直接SQL | `"status[!#]": "1"` | `status != 1` | @@ -822,12 +1016,28 @@ A6: 使用 `nil` 作为值,查询时使用 `"field": nil` 表示 `IS NULL`。 ### Q7: PostgreSQL 和 MySQL 语法有区别吗? A7: 框架会自动处理差异(占位符、引号等),代码无需修改。 +### Q8: 达梦数据库和 MySQL 的代码使用方式一样吗? +A8: 基本一样。切换数据库类型只需修改 `Type` 字段和连接字符串,业务查询代码完全不用改。唯一需要注意的是: +- 原生 SQL(`Query`/`Exec`)中的标识符需要手动加双引号 +- `MERGE INTO` 等 DM 特有 DDL 需要按 DM 语法书写 +- 检测表是否存在时,不要用 `USER_TABLES`,改用直接 `COUNT(*)` 查询 + +### Q9: 表名或列名用了达梦保留字怎么办? +A9: 框架所有自动生成的 SQL 都会对标识符加双引号,无需手动处理。但在原生 SQL 中使用保留字(如 `admin`、`user`、`order`)时,必须手动加双引号,否则会报语法错误。 + +### Q10: 达梦数据库中 INSERT 后如何获取自增 ID? +A10: 框架的 `Insert` 方法已支持从达梦原生驱动获取 `LastInsertId`,与 MySQL 使用方式完全一致: +```go +id := database.Insert("user", common.Map{"name": "张三"}) +fmt.Println("新插入的ID:", id) +``` + --- -*文档版本: 2.0* -*最后更新: 2026年1月* +*文档版本: 2.1* +*最后更新: 2026年3月* -> 本文档基于HoTimeDB源码分析生成,如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念,但根据Golang语言特性进行了适配和优化。 +> 本文档基于HoTimeDB源码分析生成,如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念,根据Golang语言特性进行了适配和优化,并新增了对达梦(DM8)国产数据库的完整支持。 **更多参考:** - [HoTimeDB API 参考](HoTimeDB_API参考.md) - API 速查手册 diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index e596ad5..efb6581 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -86,6 +86,8 @@ func main() { ### 数据库配置 +支持的数据库类型:`mysql`、`sqlite`、`postgres`(PostgreSQL)、`dm`(达梦 DM8)。 + ```json { "db": { @@ -107,12 +109,21 @@ func main() { "sqlite": { "path": "config/data.db", "prefix": "" + }, + "dm": { + "host": "127.0.0.1", + "port": "5236", + "name": "TEST", + "user": "SYSDBA", + "password": "your_password", + "prefix": "" } } } ``` -> MySQL 配置 `slave` 项即启用主从读写分离 +> MySQL 配置 `slave` 项即启用主从读写分离 +> 达梦 DM8 的 `name` 字段对应 schema(搜索路径),驱动已内置于 `vendor/gitee.com/chunanyong/dm`,无需额外安装 ### 缓存配置 diff --git a/docs/ROADMAP_改进规划.md b/docs/ROADMAP_改进规划.md index b22aab3..0aed6e4 100644 --- a/docs/ROADMAP_改进规划.md +++ b/docs/ROADMAP_改进规划.md @@ -276,3 +276,4 @@ a.JSON(Map{"phone": "138..."}).Post("正常注册", 0, Map{"id": notEmpty}) // |------|------| | 2026-01-24 | 初始版本,记录分析结果和待改进项 | | 2026-03-15 | 新增测试框架响应字段断言改进规划(七) | +| 2026-03-20 | 完成达梦(DM8)数据库完整支持:Dialect 适配(MERGE INTO Upsert、双引号标识符)、LastInsertId、保留字自动引号、schema 正确处理、代码生成器 DM 兼容、缓存表自动建表、测试框架 DM 适配 | diff --git a/docs/mysql2dm.py b/docs/mysql2dm.py new file mode 100644 index 0000000..793ed7e --- /dev/null +++ b/docs/mysql2dm.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +MySQL 8.0 dump 转达梦 DM8 SQL 转换脚本 +兼容 HoTime 框架达梦适配规范 +""" + +import re +import sys +import os +from typing import Optional + +INPUT_FILE = os.path.join(os.path.dirname(__file__), "zwbg_2026-03-20_11-00-59_mysql_data_zhqCt.sql") +OUTPUT_FILE = os.path.join(os.path.dirname(__file__), "zwbg_dm8.sql") + + +# ───────────────────────────────────────────────────────────────────────────── +# 工具函数 +# ───────────────────────────────────────────────────────────────────────────── + +def backtick_to_doublequote(s: str) -> str: + """把反引号标识符改为双引号,并强制小写""" + def replacer(m): + return '"' + m.group(1).lower() + '"' + return re.sub(r'`([^`]+)`', replacer, s) + + +def convert_col_type(coldef: str) -> str: + """转换列的数据类型(不含 COMMENT)""" + # tinyint -> INT(先处理,否则会被 int 规则误匹配) + coldef = re.sub(r'\btinyint(?:\s*\(\s*\d+\s*\))?\b', 'INT', coldef, flags=re.I) + # bigint -> BIGINT + coldef = re.sub(r'\bbigint(?:\s*\(\s*\d+\s*\))?\b', 'BIGINT', coldef, flags=re.I) + # int unsigned / int(N) -> INT + coldef = re.sub(r'\bint\s+unsigned(?:\s*\(\s*\d+\s*\))?\b', 'INT', coldef, flags=re.I) + coldef = re.sub(r'\bint(?:\s*\(\s*\d+\s*\))?\b', 'INT', coldef, flags=re.I) + # datetime -> TIMESTAMP + coldef = re.sub(r'\bdatetime\b', 'TIMESTAMP', coldef, flags=re.I) + # text / longtext / mediumtext -> CLOB + coldef = re.sub(r'\b(?:long|medium)?text\b', 'CLOB', coldef, flags=re.I) + # 去掉 CHARACTER SET ... / COLLATE ... 修饰 + coldef = re.sub(r'\bCHARACTER\s+SET\s+\S+', '', coldef, flags=re.I) + coldef = re.sub(r'\bCOLLATE\s+\S+', '', coldef, flags=re.I) + # 去掉列级 AUTO_INCREMENT(由外层统一处理自增列声明) + coldef = re.sub(r'\bAUTO_INCREMENT\b', '', coldef, flags=re.I) + # 压缩多余空格 + coldef = re.sub(r' +', ' ', coldef).strip() + return coldef + + +def extract_comment(line: str) -> tuple[str, Optional[str]]: + """提取行尾 COMMENT '...' 并返回 (去掉comment的行, comment文本 or None)""" + m = re.search(r"\bCOMMENT\s+'((?:[^'\\]|\\.)*)'\s*,?\s*$", line, re.I) + if m: + comment_text = m.group(1) + # 转义单引号 + comment_text = comment_text.replace("''", "'") + line_clean = line[:m.start()].rstrip().rstrip(',') + return line_clean, comment_text + return line, None + + +# ───────────────────────────────────────────────────────────────────────────── +# 主解析器状态机 +# ───────────────────────────────────────────────────────────────────────────── + +class Table: + def __init__(self, name: str, table_comment: Optional[str]): + self.name = name # 原始小写名 + self.table_comment = table_comment + self.col_defs: list[str] = [] # 已转换的列定义(不含索引) + self.col_comments: dict[str, str] = {} # col_name -> comment + self.pk_cols: list[str] = [] # PRIMARY KEY 列 + self.unique_keys: list[tuple[str, list[str]]] = [] # (key_name, cols) + self.normal_keys: list[tuple[str, list[str]]] = [] # (key_name, cols) + self.identity_col: Optional[str] = None # 自增列名 + self.auto_increment_start: int = 1 # 表级 AUTO_INCREMENT 值 + self.inserts: list[str] = [] # INSERT 语句列表 + + def qname(self) -> str: + """双引号包裹小写表名""" + return f'"{self.name}"' + + +def parse_key_cols(col_str: str) -> list[str]: + """解析索引列列表,如 `col1`,`col2` -> ['col1','col2']""" + cols = [] + for part in col_str.split(','): + part = part.strip() + # 去掉长度修饰 `col`(10) + part = re.sub(r'\(\d+\)$', '', part.strip()) + m = re.match(r'[`"\']?([^`"\'()\s]+)[`"\']?', part) + if m: + cols.append(m.group(1).lower()) + return cols + + +def parse_mysql_dump(sql_text: str) -> list[Table]: + """解析 MySQL dump,提取所有表的结构和数据""" + tables: list[Table] = [] + current_table: Optional[Table] = None + in_create = False + + lines = sql_text.splitlines() + i = 0 + while i < len(lines): + raw = lines[i] + stripped = raw.strip() + + # 跳过 MySQL 条件注释行和空行 + if re.match(r'^/\*!', stripped) or stripped == '' or stripped.startswith('-- '): + i += 1 + continue + + # DROP TABLE IF EXISTS + m = re.match(r'DROP TABLE IF EXISTS `([^`]+)`', stripped, re.I) + if m: + i += 1 + continue + + # CREATE TABLE 开始 + m = re.match(r'CREATE TABLE `([^`]+)`\s*\(', stripped, re.I) + if m: + tname = m.group(1).lower() + current_table = Table(tname, None) + in_create = True + i += 1 + continue + + # CREATE TABLE 结束行:) ENGINE=... COMMENT='...' + if in_create and stripped.startswith(')'): + in_create = False + # 提取表注释 + tc = re.search(r"COMMENT\s*=\s*'((?:[^'\\]|\\.)*)'", stripped, re.I) + if tc: + current_table.table_comment = tc.group(1) + # 提取 AUTO_INCREMENT=N + ai = re.search(r'AUTO_INCREMENT=(\d+)', stripped, re.I) + if ai: + current_table.auto_increment_start = int(ai.group(1)) + tables.append(current_table) + current_table = None + i += 1 + continue + + # CREATE TABLE 内部行 + if in_create and current_table is not None: + # 去掉行尾逗号暂时,用于判断 + col_line = stripped + + # PRIMARY KEY + m = re.match(r'PRIMARY KEY\s*\((.+)\)', col_line, re.I) + if m: + current_table.pk_cols = parse_key_cols(m.group(1)) + i += 1 + continue + + # UNIQUE KEY + m = re.match(r'UNIQUE KEY\s*`([^`]+)`\s*\((.+?)\)', col_line, re.I) + if m: + kname = m.group(1).lower() + kcols = parse_key_cols(m.group(2)) + current_table.unique_keys.append((kname, kcols)) + i += 1 + continue + + # KEY (普通索引) + m = re.match(r'KEY\s*`([^`]+)`\s*\((.+?)\)', col_line, re.I) + if m: + kname = m.group(1).lower() + kcols = parse_key_cols(m.group(2)) + current_table.normal_keys.append((kname, kcols)) + i += 1 + continue + + # CONSTRAINT ... PRIMARY KEY (复合pk in some versions) + m = re.match(r'CONSTRAINT\s*`[^`]+`\s*PRIMARY KEY\s*\((.+)\)', col_line, re.I) + if m: + current_table.pk_cols = parse_key_cols(m.group(1)) + i += 1 + continue + + # CONSTRAINT ... UNIQUE KEY + m = re.match(r'CONSTRAINT\s*`([^`]+)`\s*UNIQUE\s*(?:KEY)?\s*\((.+?)\)', col_line, re.I) + if m: + kname = m.group(1).lower() + kcols = parse_key_cols(m.group(2)) + current_table.unique_keys.append((kname, kcols)) + i += 1 + continue + + # 列定义 + m = re.match(r'`([^`]+)`\s+(.+)', col_line) + if m: + col_name = m.group(1).lower() + col_rest = m.group(2) + + # 提取 COMMENT + comment_m = re.search(r"\bCOMMENT\s+'((?:[^'\\]|\\.)*)'\s*,?\s*$", col_rest, re.I) + comment_text = None + if comment_m: + comment_text = comment_m.group(1) + col_rest = col_rest[:comment_m.start()].rstrip().rstrip(',') + + # 检测是否是自增列 + is_identity = bool(re.search(r'\bAUTO_INCREMENT\b', col_rest, re.I)) + + # 类型转换 + col_rest = convert_col_type(col_rest) + + # 去掉行尾逗号 + col_rest = col_rest.rstrip(',').strip() + + if is_identity: + # 替换成 IDENTITY(1,1),保留 NOT NULL + # 先提取基础类型 + type_m = re.match(r'((?:BIG)?INT)\s+(.*)', col_rest, re.I) + if type_m: + base_type = type_m.group(1).upper() + rest_attrs = type_m.group(2).strip() + # 去掉 NOT NULL(IDENTITY 列隐含 NOT NULL) + rest_attrs = re.sub(r'\bNOT\s+NULL\b', '', rest_attrs, flags=re.I).strip() + col_rest = f"{base_type} IDENTITY(1,1) {rest_attrs}".strip() + else: + col_rest = re.sub(r'\bNOT\s+NULL\b', '', col_rest, flags=re.I).strip() + col_rest = f"INT IDENTITY(1,1) {col_rest}".strip() + current_table.identity_col = col_name + + col_def = f' "{col_name}" {col_rest}' + current_table.col_defs.append(col_def) + if comment_text is not None: + current_table.col_comments[col_name] = comment_text + i += 1 + continue + + i += 1 + continue + + # INSERT INTO `tablename` VALUES (...) + m = re.match(r'INSERT INTO `([^`]+)`\s+VALUES\s+', stripped, re.I) + if m: + tname = m.group(1).lower() + # 找到对应的 table 对象 + tbl = next((t for t in tables if t.name == tname), None) + if tbl is not None: + # 转换反引号 -> 双引号 + insert_line = re.sub(r'`([^`]+)`', lambda mm: f'"{mm.group(1).lower()}"', stripped) + # 去掉行尾分号后的 /*!...*/ + insert_line = re.sub(r'\s*/\*!.*?\*/\s*', ' ', insert_line) + insert_line = insert_line.strip().rstrip(';') + ';' + tbl.inserts.append(insert_line) + i += 1 + continue + + i += 1 + + return tables + + +# ───────────────────────────────────────────────────────────────────────────── +# 输出生成 +# ───────────────────────────────────────────────────────────────────────────── + +def gen_drop(tbl: Table) -> str: + return f'DROP TABLE IF EXISTS {tbl.qname()};\n' + + +def gen_create(tbl: Table) -> str: + lines = [] + col_parts = list(tbl.col_defs) + + # PRIMARY KEY 约束 + if tbl.pk_cols: + pk_str = ', '.join(f'"{c}"' for c in tbl.pk_cols) + col_parts.append(f' PRIMARY KEY ({pk_str})') + + create = f'CREATE TABLE {tbl.qname()} (\n' + create += ',\n'.join(col_parts) + create += '\n);\n' + return create + + +def gen_comments(tbl: Table) -> str: + out = '' + if tbl.table_comment: + tc = tbl.table_comment.replace("'", "''") + out += f"COMMENT ON TABLE {tbl.qname()} IS '{tc}';\n" + for col, comment in tbl.col_comments.items(): + cc = comment.replace("'", "''") + out += f"COMMENT ON COLUMN {tbl.qname()}.\"{ col}\" IS '{cc}';\n" + return out + + +def gen_indexes(tbl: Table) -> str: + out = '' + for kname, kcols in tbl.unique_keys: + cols_str = ', '.join(f'"{c}"' for c in kcols) + out += f'CREATE UNIQUE INDEX "{kname}" ON {tbl.qname()} ({cols_str});\n' + for kname, kcols in tbl.normal_keys: + cols_str = ', '.join(f'"{c}"' for c in kcols) + out += f'CREATE INDEX "{kname}" ON {tbl.qname()} ({cols_str});\n' + return out + + +def gen_inserts(tbl: Table) -> str: + if not tbl.inserts: + return '' + out = '' + if tbl.identity_col: + out += f'SET IDENTITY_INSERT {tbl.qname()} ON;\n' + for ins in tbl.inserts: + out += ins + '\n' + if tbl.identity_col: + out += f'SET IDENTITY_INSERT {tbl.qname()} OFF;\n' + # 重置自增起始值,取 auto_increment_start(MySQL 导出时已包含下一个可用值) + next_val = tbl.auto_increment_start + if next_val > 1: + out += ( + f'ALTER TABLE {tbl.qname()} ALTER COLUMN "{tbl.identity_col}" ' + f'SET GENERATED BY DEFAULT AS IDENTITY (START WITH {next_val});\n' + ) + return out + + +def generate_dm8_sql(tables: list[Table]) -> str: + header = """\ +-- ============================================================================= +-- 达梦 DM8 数据库初始化脚本 +-- 源文件: zwbg_2026-03-20_11-00-59_mysql_data_zhqCt.sql (MySQL 8.0) +-- 目标数据库: 达梦 DM8 +-- 兼容框架: HoTime (标识符统一小写双引号, IDENTITY 自增, TIMESTAMP 时间) +-- 生成时间: 2026-03-20 +-- ============================================================================= + +""" + footer = "\n-- 脚本执行完毕\n" + + parts = [header] + for tbl in tables: + parts.append(f'-- ---- 表: {tbl.name} ----\n') + parts.append(gen_drop(tbl)) + parts.append(gen_create(tbl)) + comments = gen_comments(tbl) + if comments: + parts.append(comments) + indexes = gen_indexes(tbl) + if indexes: + parts.append(indexes) + inserts = gen_inserts(tbl) + if inserts: + parts.append(inserts) + parts.append('\n') + parts.append(footer) + return ''.join(parts) + + +# ───────────────────────────────────────────────────────────────────────────── +# 入口 +# ───────────────────────────────────────────────────────────────────────────── + +def main(): + print(f"读取源文件: {INPUT_FILE}") + with open(INPUT_FILE, 'r', encoding='utf-8') as f: + sql_text = f.read() + + print("解析 MySQL dump...") + tables = parse_mysql_dump(sql_text) + print(f" 共解析到 {len(tables)} 张表: {', '.join(t.name for t in tables)}") + + print("生成达梦 DM8 SQL...") + dm8_sql = generate_dm8_sql(tables) + + print(f"写出目标文件: {OUTPUT_FILE}") + with open(OUTPUT_FILE, 'w', encoding='utf-8') as f: + f.write(dm8_sql) + + size_kb = os.path.getsize(OUTPUT_FILE) / 1024 + print(f"完成! 文件大小: {size_kb:.1f} KB") + + +if __name__ == '__main__': + main() diff --git a/example/app/mysql.go b/example/app/mysql.go index 19442ff..54b0fc4 100644 --- a/example/app/mysql.go +++ b/example/app/mysql.go @@ -27,7 +27,7 @@ var MysqlCtr = Ctr{ return } columns := that.Db.Query("DESCRIBE " + tableName) - data := that.Db.Select(tableName, Map{"LIMIT": 10}) + data := that.Db.Select(tableName, "*", Map{"LIMIT": 10}) that.Display(0, Map{"table": tableName, "columns": columns, "sample_data": data}) }, diff --git a/example/tpt/swagger/app/api-spec.json b/example/tpt/swagger/app/api-spec.json index 66200db..e606728 100644 --- a/example/tpt/swagger/app/api-spec.json +++ b/example/tpt/swagger/app/api-spec.json @@ -165,7 +165,7 @@ "key_in_new_table": false, "name": "4. 测试兼容模式老表回退读取", "result": false, - "test_key": "test_compat_fallback_1773974669147574100" + "test_key": "test_compat_fallback_1773975517169601300" }, { "name": "5. 测试兼容模式写新删老", @@ -183,7 +183,7 @@ "result": false } ], - "timestamp": "2026-03-20 10:44:29" + "timestamp": "2026-03-20 10:58:37" }, "status": 0 } @@ -705,19 +705,19 @@ "success": true, "tests": [ { - "adminId": 5, - "lastQuery": "INSERT INTO \"admin\" (\"phone\",\"state\",\"password\",\"role_id\",\"title\",\"create_time\",\"modify_time\",\"name\") VALUES (?,?,?,?,?,NOW(),NOW(),?);", + "adminId": 10, + "lastQuery": "INSERT INTO \"admin\" (\"name\",\"phone\",\"state\",\"password\",\"role_id\",\"title\",\"create_time\",\"modify_time\") VALUES (?,?,?,?,?,?,NOW(),NOW());", "name": "Insert 插入测试 (admin表)", "result": true }, { "admin": { - "create_time": "2026-03-20T10:44:32.462878+08:00", - "id": 5, - "modify_time": "2026-03-20T10:44:32.46288+08:00", - "name": "测试管理员_1773974673", + "create_time": "2026-03-20T10:58:29.604213+08:00", + "id": 10, + "modify_time": "2026-03-20T10:58:29.604215+08:00", + "name": "测试管理员_1773975510", "password": "test123456", - "phone": "13873974673", + "phone": "13873975510", "role_id": 1, "state": 1, "title": "测试职位" @@ -1147,7 +1147,7 @@ "tests": [ { "affected": 1, - "lastQuery": "MERGE INTO \"admin\" USING (SELECT ? AS \"state\", ? AS \"password\", ? AS \"role_id\", ? AS \"title\", NOW() AS \"create_time\", NOW() AS \"modify_time\", ? AS \"name\", ? AS \"phone\") src ON (\"admin\".\"phone\" = src.\"phone\") WHEN MATCHED THEN UPDATE SET \"name\" = src.\"name\", \"state\" = src.\"state\", \"title\" = src.\"title\", \"modify_time\" = NOW() WHEN NOT MATCHED THEN INSERT (\"state\", \"password\", \"role_id\", \"title\", \"create_time\", \"modify_time\", \"name\", \"phone\") VALUES (src.\"state\", src.\"password\", src.\"role_id\", src.\"title\", src.\"create_time\", src.\"modify_time\", src.\"name\", src.\"phone\")", + "lastQuery": "MERGE INTO \"admin\" USING (SELECT ? AS \"title\", NOW() AS \"create_time\", NOW() AS \"modify_time\", ? AS \"name\", ? AS \"phone\", ? AS \"state\", ? AS \"password\", ? AS \"role_id\") src ON (\"admin\".\"phone\" = src.\"phone\") WHEN MATCHED THEN UPDATE SET \"name\" = src.\"name\", \"state\" = src.\"state\", \"title\" = src.\"title\", \"modify_time\" = NOW() WHEN NOT MATCHED THEN INSERT (\"title\", \"create_time\", \"modify_time\", \"name\", \"phone\", \"state\", \"password\", \"role_id\") VALUES (src.\"title\", src.\"create_time\", src.\"modify_time\", src.\"name\", src.\"phone\", src.\"state\", src.\"password\", src.\"role_id\")", "name": "Upsert 插入新记录 (admin表)", "result": true }, @@ -1156,12 +1156,12 @@ "name": "Upsert 更新已存在记录", "result": true, "updatedAdmin": { - "create_time": "2026-03-20T10:44:36.561713+08:00", - "id": 6, - "modify_time": "2026-03-20T10:44:36.618708+08:00", + "create_time": "2026-03-20T10:58:33.501828+08:00", + "id": 11, + "modify_time": "2026-03-20T10:58:33.566436+08:00", "name": "Upsert更新后管理员", "password": "test123", - "phone": "19973974677", + "phone": "19973975514", "role_id": 2, "state": 1, "title": "更新后职位" @@ -1453,19 +1453,19 @@ "success": true, "tests": [ { - "adminId": 7, - "lastQuery": "INSERT INTO \"admin\" (\"title\",\"create_time\",\"modify_time\",\"name\",\"phone\",\"state\",\"password\",\"role_id\") VALUES (?,NOW(),NOW(),?,?,?,?,?);", + "adminId": 9, + "lastQuery": "INSERT INTO \"admin\" (\"password\",\"role_id\",\"title\",\"create_time\",\"modify_time\",\"name\",\"phone\",\"state\") VALUES (?,?,?,NOW(),NOW(),?,?,?);", "name": "Insert 插入测试 (admin表)", "result": true }, { "admin": { - "create_time": "2026-03-20T10:44:37.425673+08:00", - "id": 7, - "modify_time": "2026-03-20T10:44:37.425675+08:00", - "name": "测试管理员_1773974678", + "create_time": "2026-03-20T10:58:25.844257+08:00", + "id": 9, + "modify_time": "2026-03-20T10:58:25.844258+08:00", + "name": "测试管理员_1773975506", "password": "test123456", - "phone": "13873974678", + "phone": "13873975506", "role_id": 1, "state": 1, "title": "测试职位" @@ -1717,7 +1717,7 @@ "tests": [ { "affected": 1, - "lastQuery": "MERGE INTO \"admin\" USING (SELECT NOW() AS \"modify_time\", ? AS \"name\", ? AS \"phone\", ? AS \"state\", ? AS \"password\", ? AS \"role_id\", ? AS \"title\", NOW() AS \"create_time\") src ON (\"admin\".\"phone\" = src.\"phone\") WHEN MATCHED THEN UPDATE SET \"name\" = src.\"name\", \"state\" = src.\"state\", \"title\" = src.\"title\", \"modify_time\" = NOW() WHEN NOT MATCHED THEN INSERT (\"modify_time\", \"name\", \"phone\", \"state\", \"password\", \"role_id\", \"title\", \"create_time\") VALUES (src.\"modify_time\", src.\"name\", src.\"phone\", src.\"state\", src.\"password\", src.\"role_id\", src.\"title\", src.\"create_time\")", + "lastQuery": "MERGE INTO \"admin\" USING (SELECT NOW() AS \"create_time\", NOW() AS \"modify_time\", ? AS \"name\", ? AS \"phone\", ? AS \"state\", ? AS \"password\", ? AS \"role_id\", ? AS \"title\") src ON (\"admin\".\"phone\" = src.\"phone\") WHEN MATCHED THEN UPDATE SET \"name\" = src.\"name\", \"state\" = src.\"state\", \"title\" = src.\"title\", \"modify_time\" = NOW() WHEN NOT MATCHED THEN INSERT (\"create_time\", \"modify_time\", \"name\", \"phone\", \"state\", \"password\", \"role_id\", \"title\") VALUES (src.\"create_time\", src.\"modify_time\", src.\"name\", src.\"phone\", src.\"state\", src.\"password\", src.\"role_id\", src.\"title\")", "name": "Upsert 插入新记录 (admin表)", "result": true }, @@ -1726,12 +1726,12 @@ "name": "Upsert 更新已存在记录", "result": true, "updatedAdmin": { - "create_time": "2026-03-20T10:44:29.219142+08:00", - "id": 4, - "modify_time": "2026-03-20T10:44:29.279349+08:00", + "create_time": "2026-03-20T10:58:25.400239+08:00", + "id": 8, + "modify_time": "2026-03-20T10:58:25.461693+08:00", "name": "Upsert更新后管理员", "password": "test123", - "phone": "19973974670", + "phone": "19973975506", "role_id": 2, "state": 1, "title": "更新后职位"