diff --git a/.cursor/plans/缓存数据库表重构_73959142.plan.md b/.cursor/plans/缓存数据库表重构_73959142.plan.md new file mode 100644 index 0000000..2d56520 --- /dev/null +++ b/.cursor/plans/缓存数据库表重构_73959142.plan.md @@ -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,插入历史表 +- 删除:不记录历史 +- 历史表仅日志记录,无读取接口,人工在数据库操作 \ No newline at end of file diff --git a/.cursor/plans/规范文档创建_6d183386.plan.md b/.cursor/plans/规范文档创建_6d183386.plan.md new file mode 100644 index 0000000..ac4a382 --- /dev/null +++ b/.cursor/plans/规范文档创建_6d183386.plan.md @@ -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): 字段规则示例 \ No newline at end of file diff --git a/README.md b/README.md index b03d781..6ca03e0 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ | [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) | 待改进项、设计思考、版本迭代规划 | ## 安装 diff --git a/cache/cache.go b/cache/cache.go index be4b364..bbb8b01 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -265,9 +265,13 @@ func (that *HoTimeCache) Init(config Map, hotimeDb HoTimeDBInterface, err ...*Er } that.Config["db"] = db - that.dbCache = &CacheDb{TimeOut: db.GetCeilInt64("timeout"), - DbSet: db.GetBool("db"), SessionSet: db.GetBool("session"), - Db: hotimeDb} + that.dbCache = &CacheDb{ + TimeOut: db.GetCeilInt64("timeout"), + DbSet: db.GetBool("db"), + SessionSet: db.GetBool("session"), + HistorySet: db.GetBool("history"), + Db: hotimeDb, + } if err[0] != nil { that.dbCache.SetError(err[0]) diff --git a/cache/cache_db.go b/cache/cache_db.go index 958c210..0e9e0f2 100644 --- a/cache/cache_db.go +++ b/cache/cache_db.go @@ -1,11 +1,38 @@ package cache import ( - . "code.hoteas.com/golang/hotime/common" "database/sql" "encoding/json" + "fmt" + "os" "strings" "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 { @@ -24,6 +51,7 @@ type CacheDb struct { TimeOut int64 DbSet bool SessionSet bool + HistorySet bool // 是否开启历史记录 Db HoTimeDBInterface *Error ContextBase @@ -31,143 +59,468 @@ type CacheDb struct { } func (that *CacheDb) GetError() *Error { - return that.Error - } func (that *CacheDb) SetError(err *Error) { 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() { if that.isInit { 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 { - return - } - 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'") - if len(res) != 0 { - 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 - } + // #region agent log + 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) } - 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 - } - + // 检查并迁移旧 cached 表 + oldTableName := that.Db.GetPrefix() + "cached" + if that.tableExists(oldTableName) { + that.migrateFromCached(dbType, oldTableName, tableName) } + // 检查并创建历史表(开启历史记录时) + historyTableExists := that.tableExists(historyTableName) + // #region agent log + debugLog("F", "cache_db.go:initDbTable", "history table check", map[string]interface{}{ + "HistorySet": that.HistorySet, "historyTableName": historyTableName, "exists": historyTableExists, + "shouldCreate": that.HistorySet && !historyTableExists, + }) + // #endregion + if that.HistorySet && !historyTableExists { + that.createHistoryTable(dbType, historyTableName) + // #region agent log + debugLog("F", "cache_db.go:initDbTable", "createHistoryTable called", map[string]interface{}{ + "dbType": dbType, "historyTableName": historyTableName, + }) + // #endregion + } + + that.isInit = true } -// 获取Cache键只能为string类型 -func (that *CacheDb) get(key string) interface{} { +// tableExists 检查表是否存在 +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 { 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 } - data := Map{} - data.JsonToMap(cached.GetString("value")) - - return data.Get("data") -} - -// 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}) + // 直接解析 value,不再需要 {"data": value} 包装 + valueStr := cached.GetString("value") + if valueStr == "" { + return nil } - //随机执行删除命令 + 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 { - 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) { - + tableName := that.getTableName() del := strings.Index(key, "*") - //如果通配删除 + // 如果通配删除 if del != -1 { key = Substr(key, 0, del) - that.Db.Delete("cached", Map{"key": key + "%"}) - + that.Db.Delete(tableName, Map{"key[~]": key + "%"}) } 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 { - 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 { return &Obj{Data: that.get(key)} } - tim := time.Now().Unix() + // 删除缓存 if len(data) == 1 && data[0] == nil { that.delete(key) return &Obj{Data: nil} } + // 计算过期时间 + var timeout int64 if len(data) == 1 { - if that.TimeOut == 0 { - //that.Time = Config.GetInt64("cacheLongTime") + // 使用配置的 TimeOut,如果为 0 则使用默认值 + timeout = that.TimeOut + if timeout == 0 { + timeout = DefaultCacheTimeout } - tim += that.TimeOut - } - if len(data) == 2 { + } else if len(data) >= 2 { + // 使用指定的超时时间 that.SetError(nil) - tempt := ObjToInt64(data[1], that.Error) - - if tempt > tim { - tim = tempt - } else if that.GetError() == nil { - - tim = tim + tempt + tempTimeout := ObjToInt64(data[1], that.Error) + if that.GetError() == nil && tempTimeout > 0 { + timeout = tempTimeout + } else { + timeout = that.TimeOut + if timeout == 0 { + 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} } diff --git a/code/makecode.go b/code/makecode.go index 1670f5f..209e1ee 100644 --- a/code/makecode.go +++ b/code/makecode.go @@ -120,9 +120,9 @@ func (that *MakeCode) Db2JSON(db *db.HoTimeDB, config Map) { } //idSlice=append(idSlice,nowTables) for _, v := range nowTables { - if v.GetString("name") == "cached" { - continue - } + // if v.GetString("name") == "cached" { + // continue + // } if that.TableConfig.GetMap(v.GetString("name")) == nil { if v.GetString("label") == "" { v["label"] = v.GetString("name") diff --git a/docs/CodeConfig_代码生成配置规范.md b/docs/CodeConfig_代码生成配置规范.md new file mode 100644 index 0000000..3f70176 --- /dev/null +++ b/docs/CodeConfig_代码生成配置规范.md @@ -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 diff --git a/docs/DatabaseDesign_数据库设计规范.md b/docs/DatabaseDesign_数据库设计规范.md new file mode 100644 index 0000000..b516865 --- /dev/null +++ b/docs/DatabaseDesign_数据库设计规范.md @@ -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 +- [ ] 选择字段注释格式正确(`标签:值-名称`) diff --git a/docs/ROADMAP_改进规划.md b/docs/ROADMAP_改进规划.md new file mode 100644 index 0000000..7878afa --- /dev/null +++ b/docs/ROADMAP_改进规划.md @@ -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 | 初始版本,记录分析结果和待改进项 | diff --git a/var.go b/var.go index 0ba2954..3094eb0 100644 --- a/var.go +++ b/var.go @@ -107,6 +107,7 @@ var ConfigNote = Map{ "timeout": "默认60 * 60 * 24 * 30,非必须,过期时间,超时自动删除", "db": "默认false,非必须,缓存数据库,启用后能减少数据库的读写压力", "session": "默认true,非必须,缓存web session,同时缓存session保持的用户缓存", + "history": "默认false,非必须,是否开启缓存历史记录,开启后每次新增/修改缓存都会记录到历史表,历史表一旦创建不会自动删除", }, "redis": Map{ "host": "默认服务ip:127.0.0.1,必须,如果需要使用redis服务时配置,",