Compare commits

...

267 Commits

Author SHA1 Message Date
hoteas 3d83c41905 feat(cache): 增加批量操作支持以提升性能
- 在 HoTimeCache 中新增 SessionsGet、SessionsSet 和 SessionsDelete 方法,支持批量获取、设置和删除 Session 缓存
- 优化缓存逻辑,减少数据库写入次数,提升性能
- 更新文档,详细说明批量操作的使用方法和性能对比
- 添加调试日志记录,便于追踪批量操作的执行情况
2026-01-30 17:51:43 +08:00
hoteas 27300025f3 refactor(cache): 移除调试日志并优化缓存逻辑
- 删除 CacheDb 结构体中的 debugLog 调用,清理冗余的调试日志代码
- 优化历史表的创建逻辑,简化条件检查
- 更新缓存的获取和设置方法,确保逻辑清晰且高效
- 维护代码整洁性,提升可读性和可维护性
2026-01-25 23:15:32 +08:00
hoteas 230dfc5a2b feat(cache): 增加历史记录功能和数据库设计规范
- 在 README.md 中新增数据库设计规范、代码生成配置规范和改进规划的文档链接
- 在 CacheDb 结构体中添加历史记录开关 HistorySet
- 实现历史记录的写入逻辑,记录每次缓存的新增和修改
- 更新缓存的获取和设置方法,支持历史记录的管理
- 优化数据库表的初始化和迁移逻辑,确保历史表的创建与管理
2026-01-25 05:14:18 +08:00
hoteas f4760c5d3e refactor(log): 优化日志调用栈查找逻辑
- 添加最大框架层数限制防止误过滤应用层
- 新增 isHoTimeFrameworkFile 函数精确识别框架文件
- 实现更准确的框架文件过滤机制
- 替换原有的简单前缀匹配为复合条件判断
- 添加框架核心目录和文件的精确匹配规则
- 改进调用栈遍历算法提高查找准确性
2026-01-23 02:47:22 +08:00
hoteas d1b905b780 test(db): 添加 OR 条件处理的单元测试并修复 WHERE 子句逻辑
- 添加了 TestWhereWithORCondition 测试函数验证 OR 条件的括号包裹
- 修复了 where.go 中条件计数变量名称从 normalCondCount 改为 condCount
- 实现了 OR/AND 组条件的括号包裹逻辑确保 SQL 语法正确
- 添加了空条件检查避免生成无效的 SQL 片段
- 更新了 .gitignore 文件添加日志文件忽略规则
2026-01-23 01:51:35 +08:00
hoteas 8dac2aff66 docs(guides): 完善框架文档并新增代码生成器说明
- 添加代码生成器完整使用说明文档,包含配置规则和自定义规则
- 新增 Common 工具类使用说明,介绍 Map/Slice/Obj 类型及转换函数
- 更新 QUICKSTART 文档中的配置项说明和数据库配置示例
- 完善请求参数获取方法,添加新版链式调用推荐用法
- 更新响应数据处理说明,包含错误码含义和自定义响应方法
- 优化中间件和 Session 操作的代码示例
- 修正路由路径参数获取的安全检查逻辑
- 更新 README 添加新文档链接索引
2026-01-22 22:13:05 +08:00
hoteas cf64276ab1 refactor(db): 重命名批量插入方法并更新文档
- 将 BatchInsert 方法重命名为 Inserts,以更好地反映其功能
- 更新示例代码和文档,确保使用新方法名
- 删除过时的文档文件,整合 HoTimeDB 使用说明和 API 参考
- 优化 README.md,增强框架特性和安装说明的清晰度
2026-01-22 20:32:29 +08:00
hoteas 29a3b6095d feat(db): 实现多数据库方言与自动表前缀支持
- 扩展 Dialect 接口添加 QuoteIdentifier 和 QuoteChar 方法
- 实现 IdentifierProcessor 结构体处理标识符转换
- 添加系统数据库列表避免对 INFORMATION_SCHEMA 等添加前缀
- 支持 database.table 格式的表名处理和前缀添加
- 在 CRUD 方法中集成新的标识符处理器
- 修改 WHERE 条件处理逻辑支持自动前缀
- 更新链式构建器的 JOIN 方法处理表名和条件
- 保持完全向后兼容性支持现有写法
2026-01-22 09:52:43 +08:00
hoteas 650fafad1a refactor(db): 重构数据库查询构建器以支持多数据库方言和标识符处理
- 实现了标识符处理器,统一处理表名、字段名的前缀添加和引号转换
- 添加对 MySQL、PostgreSQL、SQLite 三种数据库方言的支持
- 引入 ProcessTableName、ProcessColumn、ProcessConditionString 等方法处理标识符
- 为 HoTimeDB 添加 T() 和 C() 辅助方法用于手动构建 SQL 查询
- 重构 CRUD 操作中的表名和字段名处理逻辑,统一使用标识符处理器
- 添加完整的单元测试验证不同数据库方言下的标识符处理功能
- 优化 JOIN 操作中表名和条件字符串的处理方式
2026-01-22 09:32:01 +08:00
hoteas 6164dfe9bf feat(db): 实现多数据库方言与表名前缀支持功能
- 扩展 Dialect 接口,添加 QuoteIdentifier 和 QuoteChar 方法
- 新建 identifier.go,实现 IdentifierProcessor 及智能解析逻辑
- 在 db.go 中集成处理器,添加 T() 和 C() 辅助方法
- 修改 crud.go 中 Select/Insert/Update/Delete/buildJoin 等方法
- 修改 where.go 中 varCond 等条件处理方法
- 检查并更新 builder.go 相关功能
- 编写测试用例验证多数据库和前缀功能
- 移除调试日志文件以完成开发任务
2026-01-22 09:18:45 +08:00
hoteas 5ba883cd6b chore(example): 清理调试日志并优化测试代码
- 移除大量 debugLog 调试日志调用
- 修改数据库计数检查代码,使用下划线忽略返回值
- 更新事务测试中的变量声明,统一使用下划线忽略不需要的返回值
- 添加注释说明 admin 表数据检查的目的
- 移除条件查询语法测试中的冗余调试日志
- 优化链式查询测试中的调试信息
- 清理 JOIN 查询测试部分的日志输出
- 移除聚合函数测试中的调试日志
- 删除分页查询测试中的多余日志
- 清理批量插入测试中的调试信息
- 优化 upsert 测试部分的日志输出
- 移除事务测试中的冗余调试日志
- 删除原生 SQL 测试中的大量调试日志
- 移除 IN/NOT IN 数组测试中的 region 注释块
2026-01-22 07:25:41 +08:00
hoteas c2955d2500 feat(db): 实现数据库查询中的数组参数展开和空数组处理
- 在 Get 方法中添加无参数时的默认字段和 LIMIT 1 处理
- 实现 expandArrayPlaceholder 方法,自动展开 IN (?) 和 NOT IN (?) 中的数组参数
- 为空数组的 IN 条件生成 1=0 永假条件,NOT IN 生成 1=1 永真条件
- 在 queryWithRetry 和 execWithRetry 中集成数组占位符预处理
- 修复 where.go 中空切片条件的处理逻辑
- 添加完整的 IN/NOT IN 数组查询测试用例
- 更新 .gitignore 规则格式
2026-01-22 07:16:42 +08:00
hoteas 0e1775f72b remove(config): 移除配置相关JSON文件
- 删除 app.json 应用配置文件
- 删除 config.json 系统配置文件
- 删除 configNote.json 配置注释文件
- 删除 rule.json 规则配置文件
2026-01-22 06:12:26 +08:00
hoteas 1a9e7e19b7 chore(project): 更新 .gitignore 文件
- 添加 /example/config 到忽略列表中
- 防止配置文件被提交到版本控制中
2026-01-22 06:10:51 +08:00
hoteas 4828f3625c chore(config): 更新 .gitignore 配置
- 移除 /example/config/app.json 的忽略规则
- 移除 /example/config/ 目录的忽略规则
- 保留其他现有忽略配置不变
2026-01-22 06:09:19 +08:00
hoteas 0318c95055 refactor(db): 重构数据库构建器以支持JOIN参数传递并删除示例配置文件
- 修改 Get 方法以根据是否存在 JOIN 来构建参数结构
- 修改 Count 方法以根据是否存在 JOIN 来传递参数
- 修改 Select 方法以根据是否存在 JOIN 来构建参数结构
- 在 where.go 中添加对 [##] 键的支持以直接添加 SQL 片段
- 删除 example/config/admin.json 配置文件
- 删除 example/config/config.json 配置文件
- 删除 example/config/configNote.json 配置文件
- 删除 example/config/rule.json 配置文件
- 删除 example/benchmark_test.go 压测文件
- 重构 example/main.go 文件,添加完整的 HoTimeDB 功能测试套件
- 添加调试日志功能以跟踪数据库操作流程
2026-01-22 05:31:17 +08:00
hoteas f2f1fcc9aa feat(request): 实现请求参数获取方法
- 完成 ReqParam/ReqParams 方法实现,用于获取 URL 参数并返回 *Obj
- 完成 ReqForm/ReqForms 方法实现,用于获取表单数据并返回 *Obj
- 完成 ReqJson/ReqJsons 方法实现,用于获取 JSON Body 并返回 *Obj
- 完成 ReqFile/ReqFiles 方法实现,用于获取上传文件
- 完成 ReqData/ReqDatas 方法实现,用于统一封装请求数据获取并返回 *Obj
- 更新计划文件状态,标记所有相关功能模块为已完成
2026-01-22 04:59:53 +08:00
hoteas b755519fc6 feat(cache): 优化缓存系统并重构数据库连接管理
- 将Redis连接方式改为连接池模式,提升连接复用效率
- 修复缓存注释错误,统一标识数据库缓存逻辑
- 添加数据检索结果非空验证,避免空指针异常
- 在数据库操作中添加读写锁保护,确保并发安全性
- 实现数据库查询和执行操作的重试机制,增强稳定性
- 更新配置文件中的缓存和数据库设置,优化缓存策略
- 重构README文档,补充框架特性和性能测试数据
- 添加示例路由配置,完善快速入门指南
2026-01-22 04:36:52 +08:00
hoteas 3455fb0a1c style(cache): 格式化代码注释中的空格并添加 DISTINCT 选项
- 修复了缓存相关文件中注释开头缺少空格的问题
- 在 db/hotimedb.go 中为 vcond 数组添加了 "DISTINCT" 选项
- 注释掉了 session.go 中的并发控制代码以简化实现
2026-01-22 02:44:53 +08:00
hoteas 5bb9ed77b8 优化数据库连接配置,增强性能和稳定性 2025-08-07 17:51:31 +08:00
hoteas 678686fd48 refactor(dri/baidu):优化导入路径和代码结构
- 更新了导入路径,使用了新的 "code.hoteas.com/golang/hotime/common" 路径
- 删除了未使用的变量和导入语句,简化了代码结构
2025-04-11 02:09:57 +08:00
hoteas 9766648536 refactor(cache): 重构 CacheMemory 实现
- 使用 sync.Map替代自定义 Map 结构- 优化缓存过期处理逻辑
- 改进通配符删除功能
- 随机触发缓存清理机制
2025-04-10 22:09:53 +08:00
hoteas 2cf20e7206 增加数据库must设置 2023-04-06 11:37:03 +08:00
hoteas 8cac1f5393 修复部分bug,增加DB SUM函数,以及优化IN性能,增加分析接口 2023-03-22 03:17:33 +08:00
hoteas 8337cdec0c 修复部分bug,增加DB SUM函数,以及优化IN性能,增加分析接口 2023-03-10 15:22:03 +08:00
hoteas 46cecc47ea 修复部分bug 2022-12-31 22:28:12 +08:00
hoteas 7535300107 修复部分bug 2022-11-17 17:17:07 +08:00
hoteas be41a70c76 修复部分bug 2022-11-14 16:49:37 +08:00
hoteas 5b407824a5 修复部分bug 2022-11-14 10:38:15 +08:00
hoteas 56f66fcaed 修复部分bug 2022-11-08 16:05:47 +08:00
hoteas a298cb3d52 修复部分bug 2022-11-07 01:41:22 +08:00
hoteas 68f2c0fd8f 修复部分bug 2022-11-07 01:40:24 +08:00
hoteas d314891126 修复部分bug 2022-11-06 06:37:44 +08:00
hoteas c0ae31f499 修复部分bug 2022-11-05 01:55:11 +08:00
hoteas 25edb6b5b7 修复部分bug 2022-11-04 03:22:00 +08:00
hoteas ead64dc776 修复bug 2022-11-04 02:55:58 +08:00
hoteas 876e3b1f45 优化性能 2022-11-04 02:34:48 +08:00
hoteas d6e940803d 优化性能 2022-11-04 02:25:33 +08:00
hoteas 203e72f8a2 优化性能 2022-11-04 02:12:07 +08:00
hoteas 541c82410e 修正bug 2022-10-21 04:28:51 +08:00
hoteas b680112fdb 修正bug 2022-10-20 15:49:49 +08:00
hoteas 4ab5ab9ff4 时间由指针改回对象 2022-10-20 09:08:23 +08:00
hoteas b425024359 升级流程引擎配置文件版本 2022-10-20 09:00:44 +08:00
hoteas 78337c14ec 升级流程引擎配置文件版本 2022-10-19 21:32:34 +08:00
hoteas cdba8af129 升级流程引擎配置文件版本 2022-10-19 21:29:38 +08:00
hoteas 9a7426180d 增加单表某字段权限及类型控制功能 2022-10-19 03:37:52 +08:00
hoteas b7131603c4 增加备注功能 2022-10-08 17:32:37 +08:00
hoteas 2f3a5a0a59 增加壁纸及壁纸地址缓存 2022-10-08 16:52:42 +08:00
hoteas c2468a7389 增加两个参数 2022-09-16 07:10:45 +08:00
hoteas b6ba3486d6 IP获取真实条件修改 2022-09-02 00:04:23 +08:00
hoteas f59e909d30 修复下载插件bug 2022-08-31 19:15:47 +08:00
hoteas 3bb47de45a 修复下载插件bug 2022-08-31 19:10:41 +08:00
hoteas c71973850e 修复下载插件bug 2022-08-31 19:10:08 +08:00
hoteas 65f062b860 数据库操作bug 2022-08-31 11:20:58 +08:00
hoteas 7b502fed36 数据库操作bug 2022-08-31 11:14:45 +08:00
hoteas fd6b15bdaf 修正查询bug 2022-08-30 06:21:46 +08:00
hoteas f31bf24ec2 增加mongodb驱动,简单版本 2022-08-30 04:44:37 +08:00
hoteas 30edb4d9fa 增加mongodb驱动,简单版本 2022-08-30 04:05:17 +08:00
hoteas a32c3a1589 增加mongodb驱动,简单版本 2022-08-30 03:30:27 +08:00
hoteas 127e027940 修复重复执行context.View()的bug 2022-08-25 14:06:31 +08:00
hoteas 2ee0838d8a 树状结构优化 2022-08-25 13:06:26 +08:00
hoteas 72c23499f2 树状结构优化 2022-08-25 13:02:44 +08:00
hoteas 96d8868694 数据库错误操作优化 2022-08-25 05:01:50 +08:00
hoteas 1533a57cb8 数据库错误操作优化 2022-08-25 03:48:09 +08:00
hoteas 615f52d2e4 数据库错误操作优化 2022-08-25 03:44:58 +08:00
hoteas f7dfd4ec77 增加记录IP 2022-08-21 03:49:56 +08:00
hoteas 2276aa12ef Merge remote-tracking branch 'origin/master' 2022-08-20 22:38:40 +08:00
hoteas 6505594fd0 增加记录IP 2022-08-20 22:38:15 +08:00
hoteas c55e26021b 数据库错误操作优化 2022-08-20 11:20:03 +08:00
hoteas 78420a692f 检索升级 2022-08-19 17:58:18 +08:00
hoteas 6d9af149ad 数据库错误操作优化 2022-08-18 13:40:02 +08:00
hoteas 253cad35ef 数据库错误操作优化 2022-08-18 13:28:02 +08:00
hoteas 47c8736f34 数据库错误操作优化 2022-08-18 12:33:01 +08:00
hoteas dcce8ace9e 数据库事务操作优化 2022-08-18 11:36:23 +08:00
hoteas 125ccc5c3b 数据库操作优化 2022-08-17 17:10:12 +08:00
hoteas 432d59d483 数据库操作优化 2022-08-17 16:42:46 +08:00
hoteas 60f222b011 数据库操作优化 2022-08-17 16:35:52 +08:00
hoteas 7a8b86ed12 数据库操作优化,优化系统管理后台代码生成逻辑 2022-08-17 16:02:36 +08:00
hoteas b940afa6b0 数据库操作优化 2022-08-15 05:59:06 +08:00
hoteas 8afd00ff86 修复登录bug 2022-08-11 10:41:35 +08:00
hoteas 8992fd6d3a 解决BUG 2022-08-11 10:18:48 +08:00
hoteas 524a892480 修复bug 2022-08-08 04:34:26 +08:00
hoteas c420e23edb 修复bug 2022-08-08 02:37:15 +08:00
hoteas 7419e03d67 解决bug 2022-08-04 12:14:18 +08:00
hoteas 9425544d1c 修复bug 2022-08-04 02:41:41 +08:00
hoteas a8d78832df 修复配置bug 2022-08-03 12:07:55 +08:00
hoteas adfb2236dc V2.0封测 2022-08-02 03:02:57 +08:00
hoteas 6034598bb4 权限管理优化 2022-08-01 18:48:08 +08:00
hoteas 6fe44cb1cb 权限管理优化 2022-08-01 16:45:24 +08:00
hoteas 39d67d1775 增加导出功能 2022-08-01 03:40:09 +08:00
hoteas a8f9f12bb8 大岗山水电站初版完成 2022-07-28 12:08:47 +08:00
hoteas 2c1abb1d11 大岗山水电站初版完成 2022-07-28 11:08:51 +08:00
hoteas 21e0219568 V2半封测 2022-07-27 17:09:56 +08:00
hoteas 952c3a1243 技术性调整 2022-07-26 17:37:17 +08:00
hoteas 33cd7ebd6b 技术性调整 2022-07-25 05:44:38 +08:00
hoteas be02b3418d 技术性调整 2022-07-25 05:37:19 +08:00
hoteas f390617167 技术性调整 2022-07-24 00:00:36 +08:00
hoteas f7410200e3 增加权限管理 2022-07-21 00:24:39 +08:00
hoteas 9cfdfbac78 增加权限管理 2022-07-18 19:45:06 +08:00
hoteas b015e78fa0 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	example/config/configNote.json
#	var.go
2022-07-18 19:44:52 +08:00
hoteas bb9092c3e5 增加权限管理 2022-07-18 19:44:00 +08:00
hoteas cc1bad8d3f 技术性调整 2022-07-18 08:46:09 +08:00
hoteas 8f7380d796 优化日志及config 2022-07-11 19:13:20 +08:00
hoteas f37b9aee70 优化日志及config 2022-07-11 11:07:17 +08:00
hoteas 1ab9027c93 技术性调整 2022-07-08 09:55:02 +08:00
hoteas 96d341aaab 修复数据库及时间bug 2022-06-21 00:06:34 +08:00
hoteas 1f25f511cc 增加mysql断线重连优化 2022-06-14 11:29:54 +08:00
hoteas 07c7a628d1 增加time兼容性 2022-06-14 09:52:49 +08:00
hoteas d7a59845eb 接口层复用 2022-06-09 04:05:50 +08:00
hoteas 98619083a1 接口层复用 2022-06-09 03:54:33 +08:00
hoteas c8a9038efe 数据库where增加[##]操作 2022-05-27 15:00:44 +08:00
hoteas b638d8bd65 增加vendor 2022-05-24 13:49:25 +08:00
hoteas 77753a123f 删除example代码 2022-05-19 15:45:51 +08:00
zhoupengwei 2c9d475a45 导出功能修改关联salesman关联关系 2022-05-18 17:52:49 +08:00
zhoupengwei 3540209b26 科创券优化 2022-05-17 12:31:47 +08:00
zhoupengwei 797681d76c 添source_type字段(券的来源:1-vip获得,2-认证获得,3-拉新获得),用来判断券的来源,
userinfo接口返回企业统一社会信用代码和营业执照路径
2022-05-13 18:43:35 +08:00
zhoupengwei 0ec83f9a95 科服券列表+领券,企业认证,购买vip领券 2022-05-13 15:50:45 +08:00
zhoupengwei ec798bd624 科服券列表+领券,企业认证,购买vip领券 2022-05-13 15:45:50 +08:00
zhoupengwei 6b57201e59 Merge branch 'zct-v2-dengluyang' of https://code.hoteas.com/golang/hotime into zhoupengwei 2022-05-13 15:34:53 +08:00
zhoupengwei 4e35dfe6ed 科服券列表+领券,企业认证,购买vip领券 2022-05-13 15:34:15 +08:00
hoteas 970bdb0157 增加time类型直接转换 2022-05-13 15:31:55 +08:00
zhoupengwei d656faf915 Merge branch 'zct-v2-dengluyang' of https://code.hoteas.com/golang/hotime into zhoupengwei 2022-05-13 09:36:46 +08:00
hoteas 2ecc70288e 打印影响延迟优化 2022-05-13 09:30:09 +08:00
hoteas ce95cdc021 打印影响延迟优化 2022-05-12 18:27:01 +08:00
zhoupengwei 22130c7afe vip当日购买信息导出excel的sql调整,添加新字段:用户关联parentid时间,用户关联salesmanid时间 2022-05-11 18:19:30 +08:00
zhoupengwei d85c65faef Merge branch 'zct-v2-dengluyang' of https://code.hoteas.com/golang/hotime into zhoupengwei 2022-05-11 18:17:40 +08:00
hoteas c9f33eefec 打印影响延迟优化 2022-05-11 14:55:28 +08:00
hoteas 1826e76a34 打印影响延迟优化 2022-05-11 13:56:53 +08:00
zhoupengwei b9a6b7af22 Merge branch 'zct-v2-dengluyang' of https://code.hoteas.com/golang/hotime into zhoupengwei 2022-05-11 09:51:35 +08:00
hoteas 95bfea4feb 对政策匹配进行优化 2022-05-11 00:44:52 +08:00
zhoupengwei e4bb8ac94a vip当日购买信息导出excel 2022-05-10 19:47:20 +08:00
zhoupengwei d7a49bf56d vip当日购买信息导出excel 2022-05-10 19:45:00 +08:00
zhoupengwei 3d1a1670cc Merge branch 'zct-v2-dengluyang' of https://code.hoteas.com/golang/hotime into zhoupengwei 2022-05-10 19:43:36 +08:00
hoteas 7de6a79611 对政策匹配进行优化 2022-05-10 12:54:24 +08:00
zhoupengwei 83198b39cb Merge branch 'zct-v2-dengluyang' of https://code.hoteas.com/golang/hotime into zhoupengwei
 Conflicts:
	example/app/declare.go
	example/provider/company.go
2022-05-10 09:43:03 +08:00
hoteas db0724f09b 对政策匹配进行优化 2022-05-09 19:19:15 +08:00
hoteas c1b013c926 对政策匹配进行优化 2022-05-09 15:07:01 +08:00
zhoupengwei 40bc84ea3d Merge branch 'zct-v2' of https://code.hoteas.com/golang/hotime into zhoupengwei 2022-05-07 20:56:36 +08:00
zhoupengwei e977a97013 Merge pull request '修复 bug 客户管理领域信息没存起来,高层次人才单选的形式存储的' (#1) from liyun into zct-v2
Reviewed-on: #1
2022-05-07 12:55:28 +00:00
michael 9330f7f5ea 修复 bug 客户管理领域信息没存起来,高层次人才单选的形式存储的 2022-05-07 20:47:58 +08:00
zhoupengwei d20bcc162c Merge branch 'zct-v2' of https://code.hoteas.com/golang/hotime into zhoupengwei 2022-05-07 17:11:27 +08:00
hoteas b48ad793da 修正企服商那边不显示名称问题 2022-05-07 13:55:57 +08:00
hoteas 3754734130 临时增加state为1企服商列表不显示企业 2022-05-07 11:08:11 +08:00
hoteas 918b88332b 增加限制条件 2022-05-07 11:02:12 +08:00
hoteas 9ba182d65c 增加限制条件 2022-05-06 13:08:13 +08:00
hoteas 8cf54ce25b 增加限制条件 2022-05-06 11:35:40 +08:00
hoteas 22d00739ff 增加join的orm兼容性 2022-05-05 18:33:27 +08:00
hoteas 3b2a317d2b 增加U盘登录 2022-05-05 16:07:01 +08:00
zhoupengwei afa9b173ba Merge branch 'zct-v2' of https://code.hoteas.com/golang/hotime into zhoupengwei 2022-05-05 10:44:10 +08:00
hoteas 53f24c033c 初步版本修正 2022-05-05 00:29:30 +08:00
zhoupengwei 1ffcaaef8e 只在main.go中加了双//,测试是否能够提交成功 2022-05-04 21:50:52 +08:00
hoteas 62ba00270a 调优 2022-05-03 22:40:02 +08:00
hoteas fe31335cf5 验证数据 2022-05-03 15:17:27 +08:00
hoteas b709b58fef 基础整理 2022-05-03 09:41:18 +08:00
hoteas 76b220aa5d 基础整理 2022-05-03 08:09:25 +08:00
hoteas dbf8f91696 基础整理 2022-05-02 15:42:54 +08:00
hoteas 335e8730da 用户登录 2022-05-02 15:39:50 +08:00
hoteas cbed318832 科创地图打包 2022-04-29 17:58:14 +08:00
hoteas 31a6568cbe 科创地图打包 2022-04-29 03:08:32 +08:00
hoteas 2aa86361e1 增加普通时间类型,以及增加富文本类型 2022-04-28 14:44:56 +08:00
hoteas 2a21dad90a Merge branch 'zct-wechat' into zct-manage
# Conflicts:
#	example/config/config.json
#	example/main.go
#	example/zct_manage.exe
2022-04-26 09:24:51 +08:00
hoteas bae9f60012 增加普通时间类型,以及增加富文本类型 2022-04-25 20:08:16 +08:00
hoteas 79d1189803 增加普通时间类型,以及增加富文本类型 2022-04-25 19:58:49 +08:00
hoteas c3d0c8921b 系统优化 2022-04-22 15:48:17 +08:00
hoteas 1da98a058b 系统优化 2022-04-21 02:20:37 +08:00
hoteas 152386c5a0 系统优化 2022-04-21 02:20:27 +08:00
hoteas 5e2845d3a1 系统优化 2022-04-20 22:49:07 +08:00
hoteas bab49c9a66 系统优化 2022-04-20 22:47:58 +08:00
hoteas 1d3bfcaee5 系统优化 2022-04-20 18:36:05 +08:00
hoteas a37a33321f 系统优化 2022-04-18 06:28:01 +08:00
hoteas 48042ecce4 系统优化 2022-03-29 05:31:02 +08:00
hoteas 5d8227d8bb 初步完成 2022-03-28 04:52:10 +08:00
hoteas 38d3d77c59 初步完成 2022-03-28 01:14:09 +08:00
hoteas fec195b3a1 框架优化 2022-03-26 16:53:40 +08:00
hoteas 5945ed64c6 框架优化 2022-03-16 09:58:24 +08:00
hoteas ce515d991a 框架优化 2022-03-14 16:48:19 +08:00
hoteas cb04dc6c34 框架优化 2022-03-13 17:02:19 +08:00
hoteas 17905c2d21 框架优化 2022-03-13 05:13:32 +08:00
hoteas b7c243823a 框架优化 2022-03-13 05:06:28 +08:00
hoteas ebea14c74f Merge remote-tracking branch 'origin/master' into rfcb
# Conflicts:
#	code.go
2022-03-13 02:16:31 +08:00
hoteas d6ef790010 优化整体 2022-03-13 02:00:40 +08:00
hoteas 5c64580378 优化整体 2022-03-13 01:48:54 +08:00
hoteas e49164fa81 使用go mod 2022-03-13 01:12:29 +08:00
hoteas 8265e24add 使用go mod 2022-03-13 01:00:59 +08:00
hoteas 283985df52 删除无关代码 2022-03-13 00:26:01 +08:00
hoteas 95adca04bd 删除无关代码 2022-03-13 00:24:36 +08:00
hoteas 2e65138222 hotimego V1.0.1 2022-03-12 23:57:48 +08:00
hoteas 87039524a7 政企超链接开始集成 2022-03-12 23:52:42 +08:00
hoteas 66cd21c35f 政企超链接开始集成 2022-03-12 23:52:25 +08:00
hoteas 7e64131931 政企超链接开始集成 2022-03-06 23:55:14 +08:00
hoteas e30d40cfbb 政企超链接开始集成 2022-03-04 00:06:26 +08:00
hoteas 751ed0003d 政企超链接开始集成 2022-03-04 00:06:04 +08:00
hoteas 6d9f89a1d4 政企超链接开始集成 2022-03-03 21:23:57 +08:00
hoteas bc329be03b 政企超链接开始集成 2022-03-02 18:19:15 +08:00
hoteas 4597a0a50d 政企超链接开始集成 2022-03-02 12:14:07 +08:00
hoteas 3dda86d170 政企超链接开始集成 2022-02-28 19:37:52 +08:00
hoteas d847da7591 政企超链接开始集成 2022-02-28 19:08:48 +08:00
hoteas e1eca90835 政企超链接开始集成 2022-02-28 09:59:05 +08:00
hoteas 9283b8c284 政企超链接开始集成 2022-02-28 08:53:38 +08:00
hoteas d8d9afa4d2 政企超链接开始集成 2022-02-24 06:26:36 +08:00
hoteas 4d3829c345 简阳版本 2022-02-19 21:38:35 +08:00
hoteas a23037a437 简阳版本 2022-02-19 21:38:17 +08:00
hoteas 181295e54e 更新研发 2022-02-17 04:10:53 +08:00
hoteas 8cc2499e21 更新研发 2022-02-14 19:11:38 +08:00
hoteas a2c49452b4 更新研发 2022-02-14 19:10:58 +08:00
hoteas ebf0df5ca8 更新研发 2022-01-27 23:41:40 +08:00
hoteas 73bf691ccc 更新研发 2022-01-26 11:45:17 +08:00
hoteas b2e9701826 更新研发 2022-01-23 05:13:19 +08:00
hoteas 789b0a14d1 更新研发 2022-01-22 16:12:02 +08:00
hoteas 3ee64fcd8c 更新研发 2022-01-20 07:15:14 +08:00
hoteas 527503b0d9 企业画像 2022-01-19 05:38:49 +08:00
hoteas a95b2fccd7 更新研发 2022-01-17 04:47:39 +08:00
hoteas 7e2abb43e3 权限模块开发 2022-01-12 17:33:20 +08:00
hoteas 48fd753a96 权限模块开发 2022-01-10 13:23:39 +08:00
hoteas 136232dd57 修复太过于严格的bug 2022-01-03 01:42:35 +08:00
hoteas 008f19355c 增加自适应模式 2021-12-31 02:38:51 +08:00
hoteas 1fc0a5bbbc 增加自适应模式 2021-12-30 22:19:52 +08:00
hoteas ce099b8fc8 增加自适应模式 2021-12-29 00:46:07 +08:00
hoteas 9c00ac6ba1 增加自适应模式 2021-12-28 09:26:26 +08:00
hoteas a5c002bbce 增加自适应模式 2021-12-27 21:30:33 +08:00
hoteas cf50a699a6 增加自适应模式 2021-12-27 21:30:24 +08:00
hoteas 662003c0ca 增加自适应模式 2021-12-27 20:40:16 +08:00
hoteas f24de2562a 优化系统 2021-12-27 18:21:41 +08:00
hoteas b84bd7e16f 优化系统 2021-12-27 14:09:49 +08:00
hoteas 2fb6abf53a Merge branch 'master' into myhs
# Conflicts:
#	example/admin/init.go
#	example/bzyy.exe
#	example/main.go
2021-12-27 14:02:39 +08:00
hoteas 2eab29f428 优化系统 2021-12-27 14:01:04 +08:00
hoteas 84ee0d259f 优化系统 2021-12-27 14:00:08 +08:00
hoteas eccab42fc8 优化系统 2021-12-18 00:07:35 +08:00
hoteas 1465ba36d3 优化系统 2021-12-17 14:37:41 +08:00
hoteas 8380b097b2 调试 2021-12-17 01:21:08 +08:00
hoteas 6b8f901163 调试 2021-12-16 13:40:19 +08:00
hoteas bb16f0a75b 调试 2021-12-16 13:24:31 +08:00
hoteas 84dbc5d71e 出入库完成 2021-12-16 10:49:35 +08:00
hoteas 166eeee64c 出入库完成 2021-12-15 09:24:41 +08:00
hoteas 7a649fd652 出入库完成 2021-12-15 05:56:31 +08:00
hoteas 94ae568526 书写前端接口 2021-12-12 04:22:30 +08:00
hoteas 9ec597f26c 书写前端接口 2021-12-11 23:53:47 +08:00
hoteas 388bc5006d 书写前端代码 2021-12-11 17:59:02 +08:00
hoteas 0e33c2ad9d ocr增加 2021-12-09 11:16:15 +08:00
hoteas af726fbcfb ocr增加 2021-12-09 10:58:53 +08:00
hoteas 1e82a5964d 优化makecode 2021-11-29 03:27:07 +08:00
hoteas d62ba8b0cd 优化makecode 2021-11-23 18:31:00 +08:00
hoteas 320ebe25ec 优化makecode 2021-11-18 12:22:14 +08:00
hoteas 68257d1742 绵阳宏升代码优化 2021-11-15 03:35:13 +08:00
hoteas 80d167cb1b 绵阳宏升代码优化 2021-11-15 03:34:45 +08:00
hoteas cd911e8fa4 巴中代码提交 2021-11-15 03:32:50 +08:00
hoteas faa6fcb00c 初次提交 2021-11-15 03:30:21 +08:00
hoteas 836c1f461f 初次提交 2021-11-15 03:28:14 +08:00
hoteas 97b2f54383 初次提交 2021-11-15 03:26:54 +08:00
hoteas 9d5f8438c5 初次提交 2021-11-13 21:06:29 +08:00
hoteas 3f7963e7cf 初次提交 2021-11-08 09:16:28 +08:00
hoteas 269df85d27 初次提交 2021-11-08 09:15:52 +08:00
hoteas d28c6863e4 初次提交 2021-11-08 06:50:37 +08:00
hoteas bba4990ccc 完善访问日志 2021-11-08 06:49:34 +08:00
hoteas c57afd6a5d 修复短信发送bug 2021-10-27 00:27:24 +08:00
hoteas 25f355c8d0 增加天府通办登录 2021-10-25 23:18:27 +08:00
hoteas b1b0673e69 修复bug 2021-10-21 23:20:58 +08:00
hoteas 72c086d726 修复bug 2021-10-21 23:19:32 +08:00
hoteas 3810ba6c0b 修复bug 2021-10-21 22:52:40 +08:00
hoteas 5c775b4539 修复bug 2021-10-18 04:49:55 +08:00
hoteas deb3e54cc3 修复bug 2021-09-25 04:36:07 +08:00
hoteas d1a8f6c668 短信变更 2021-09-24 15:01:03 +08:00
hoteas 2291ba7402 巴中恩阳适配 2021-09-24 10:49:07 +08:00
hoteas 20390091f2 巴中恩阳适配 2021-09-17 04:45:43 +08:00
hoteas 123d819b0f 开始新增代码生成配置2.0功能,配置文件格式规定完成 2021-09-17 04:00:49 +08:00
hoteas 30e275a27d 开始新增代码生成配置2.0功能,配置文件格式规定完成 2021-09-16 04:33:10 +08:00
hoteas 1a53f587e8 增加表关联新建功能,同时修复数据库bug 2021-09-12 06:14:01 +08:00
1353 changed files with 678033 additions and 2752 deletions
@@ -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()` 正确性
@@ -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,插入历史表
- 删除:不记录历史
- 历史表仅日志记录,无读取接口,人工在数据库操作
@@ -0,0 +1,221 @@
---
name: 缓存表模式配置
overview: 为 CacheDb 添加 mode 配置项,支持 "new"(默认,只用新表)和 "compatible"(写新读老)两种模式,并更新配置说明。
todos:
- id: add-mode-field
content: 在 CacheDb 结构体中添加 Mode 字段
status: completed
- id: modify-init
content: 修改 initDbTable,根据 Mode 决定是否迁移删除老表
status: completed
dependencies:
- add-mode-field
- id: add-legacy-get
content: 添加 getLegacy 方法读取老表数据(unix时间戳格式)
status: completed
dependencies:
- add-mode-field
- id: modify-get
content: 修改 get 方法,compatible 模式下回退读取老表
status: completed
dependencies:
- add-legacy-get
- id: modify-set
content: 修改 set 方法,compatible 模式下写新表后删除老表同key记录
status: completed
dependencies:
- add-mode-field
- id: modify-delete
content: 修改 delete 方法,compatible 模式下同时删除新表和老表
status: completed
dependencies:
- add-mode-field
- id: update-cache-init
content: 在 cache.go Init 方法中读取 mode 配置
status: completed
dependencies:
- add-mode-field
- id: update-config-note
content: 在 var.go ConfigNote 中添加 mode 配置说明
status: completed
- id: add-cache-test
content: 在 example/main.go 中添加缓存测试路由
status: completed
dependencies:
- modify-get
- modify-set
- modify-delete
- update-cache-init
- id: todo-1769763169689-k7t9twp5t
content: |
QUICKSTART.md 更新:缓存配置部分需要添加 mode 和 history 配置说明
status: pending
---
# 缓存表模式配置实现计划
## 需求概述
在 [`cache/cache_db.go`](cache/cache_db.go) 中实现两种缓存表模式:
- **new**(默认):只使用新的 `hotime_cache` 表,自动迁移老表数据
- **compatible**:写入新表,读取时先查新表再查老表,老数据自然过期消亡
## 实现步骤
### 1. 修改 CacheDb 结构体
在 [`cache/cache_db.go`](cache/cache_db.go) 中添加 `Mode` 字段:
```go
type CacheDb struct {
TimeOut int64
DbSet bool
SessionSet bool
HistorySet bool
Mode string // "new"(默认) 或 "compatible"
Db HoTimeDBInterface
// ...
}
```
### 2. 修改初始化逻辑 initDbTable
- **new 模式**:创建新表、迁移老表数据,**但不删除老表**(删除交给用户手动操作,更安全)
- **compatible 模式**:创建新表,不迁移也不删除老表
两种模式都不自动删除老表,避免自动删除造成数据丢失风险
### 3. 修改 get 方法
- **new 模式**:只从新表读取
- **compatible 模式**:先从新表读取,如果没有再从老表读取
需要新增 `getLegacy` 方法来读取老表数据,该方法需要:
1. 查询老表数据
2. 检查 `endtime`unix 时间戳)是否过期
3. 如果过期:删除该条记录,返回 nil
4. 如果未过期:返回数据
### 4. 修改 set 方法(写新删老)
- **new 模式**:只写新表(老表保留但不再管理)
- **compatible 模式**:写入新表 + 删除老表中相同 key 的记录
这样可以主动加速老数据消亡,而不是等自然过期。
### 5. 修改 delete 方法
- **new 模式**:只删除新表(老表保留但不再管理)
- **compatible 模式**:同时删除新表和老表中的 key
这是必要的,否则删除新表后,下次读取会回退读到老表的数据,造成"删不掉"的问题。
需要新增 `deleteLegacy` 方法处理老表删除逻辑
### 6. 更新缓存初始化
在 [`cache/cache.go`](cache/cache.go) 的 `Init` 方法中读取 `mode` 配置:
```go
that.dbCache = &CacheDb{
// ...
Mode: db.GetString("mode"), // 读取 mode 配置
}
```
### 7. 更新配置说明
在 [`var.go`](var.go) 的 `ConfigNote` 中添加 `mode` 配置说明:
```go
"db": Map{
// ...
"mode": "默认new,非必须,new为只使用新表(自动迁移老数据),compatible为兼容模式(写新表读老表,老数据自然过期)",
}
```
### 8. 编写测试
在 [`example/main.go`](example/main.go) 中添加缓存测试路由,测试覆盖:
**new 模式测试:**
- 基础读写:set/get/delete 正常工作
- 过期测试:设置短过期时间,验证过期后读取返回 nil
- 数据迁移:验证老表数据能正确迁移到新表
- 老表保留:验证迁移后老表仍存在(不自动删除)
**compatible 模式测试:**
- 新表读写:优先从新表读取
- 老表回退:新表没有时从老表读取
- 过期检测:读取老表过期数据时返回 nil 并删除该记录
- 写新删老:写入新表后老表同 key 记录被删除
- 删除双表:删除操作同时删除新表和老表记录
- 通配删除:`key*` 格式删除测试
**边界情况测试:**
- 空值处理:nil 值的 set/get
- 不存在的 key 读取
- 重复 set 同一个 key
- 超时时间参数测试(默认/自定义)
## 架构图
```mermaid
flowchart TD
subgraph Config[配置]
ModeNew["mode: new (默认)"]
ModeCompat["mode: compatible"]
end
subgraph NewMode[new模式]
N1[初始化] --> N2[创建新表]
N2 --> N3{老表存在?}
N3 -->|是| N4[迁移数据到新表]
N4 --> N5[保留老表由人工删除]
N3 -->|否| N6[完成]
N5 --> N6
NR[读取] --> NR1[只查询新表]
NW[写入] --> NW1[只写入新表]
ND[删除] --> ND1[只删除新表记录]
end
subgraph CompatMode[compatible模式]
C1[初始化] --> C2[创建新表]
C2 --> C3[保留老表]
CR[读取] --> CR1[查询新表]
CR1 -->|未找到| CR2[查询老表]
CR2 --> CR3{过期?}
CR3 -->|是| CR4[删除老表记录]
CR4 --> CR5[返回nil]
CR3 -->|否| CR6[返回数据]
CW[写入] --> CW1[写入新表]
CW1 --> CW2[删除老表同key]
CD[删除] --> CD1[删除新表记录]
CD1 --> CD2[删除老表记录]
end
```
## 文件修改列表
| 文件 | 修改内容 |
|------|----------|
| [`cache/cache_db.go`](cache/cache_db.go) | 添加 Mode 字段、修改 initDbTable、添加 getLegacy(含过期检测删除)、修改 get、修改 set(写新删老)、修改 delete |
| [`cache/cache.go`](cache/cache.go) | 读取 mode 配置 |
| [`var.go`](var.go) | ConfigNote 添加 mode 说明 |
| [`example/main.go`](example/main.go) | 添加缓存测试路由 |
| [`example/config/config.json`](example/config/config.json) | 可选:添加 mode 配置示例 |
@@ -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): 字段规则示例
@@ -0,0 +1,168 @@
---
name: 集成请求参数获取方法
overview: 在 context.go 中为 Context 结构体添加五个请求参数获取方法:ReqData(统一获取)、ReqDataParamsURL参数)、ReqDataJsonJSON Body)、ReqDataForm(表单数据)、ReqFile(获取上传文件)。
todos:
- id: add-imports
content: 在 context.go 中添加 bytes、io、mime/multipart 包的导入
status: completed
- id: impl-params
content: 实现 ReqParam/ReqParams 方法(获取 URL 参数,返回 *Obj)
status: completed
dependencies:
- add-imports
- id: impl-form
content: 实现 ReqForm/ReqForms 方法(获取表单数据,返回 *Obj)
status: completed
dependencies:
- add-imports
- id: impl-json
content: 实现 ReqJson/ReqJsons 方法(获取 JSON Body,返回 *Obj
status: completed
dependencies:
- add-imports
- id: impl-file
content: 实现 ReqFile/ReqFiles 方法(获取上传文件)
status: completed
dependencies:
- add-imports
- id: impl-reqdata
content: 实现 ReqData/ReqDatas 方法(统一获取,返回 *Obj)
status: completed
dependencies:
- impl-params
- impl-form
- impl-json
---
# 集成请求参数获取方法
## 实现位置
在 [`context.go`](context.go) 中添加请求参数获取方法,**风格与 `Session("key")` 保持一致,返回 `*Obj` 支持链式调用**。
## 新增方法
### 1. ReqParam - 获取 URL 查询参数(返回 *Obj)
```go
// 获取单个参数,支持链式调用
func (that *Context) ReqParam(key string) *Obj {
// that.ReqParam("id").ToStr()
// that.ReqParam("id").ToInt()
}
// 获取所有 URL 参数
func (that *Context) ReqParams() Map
```
### 2. ReqForm - 获取表单数据(返回 *Obj)
```go
// 获取单个表单字段
func (that *Context) ReqForm(key string) *Obj {
// that.ReqForm("name").ToStr()
}
// 获取所有表单数据
func (that *Context) ReqForms() Map
```
### 3. ReqJson - 获取 JSON Body(返回 *Obj
```go
// 获取 JSON 中的单个字段
func (that *Context) ReqJson(key string) *Obj {
// that.ReqJson("data").ToMap()
// that.ReqJson("count").ToInt()
}
// 获取完整 JSON Body
func (that *Context) ReqJsons() Map
```
### 4. ReqFile - 获取上传文件
```go
func (that *Context) ReqFile(name string) (multipart.File, *multipart.FileHeader, error)
func (that *Context) ReqFiles(name string) ([]*multipart.FileHeader, error)
```
### 5. ReqData - 统一获取参数(返回 *Obj)
```go
// 统一获取(JSON > Form > URL),支持链式调用
func (that *Context) ReqData(key string) *Obj {
// that.ReqData("id").ToStr()
}
// 获取所有合并后的参数
func (that *Context) ReqDatas() Map
```
## 需要的导入
```go
import (
"bytes"
"io"
"mime/multipart"
)
```
## 关键实现细节
1. **Body 只能读取一次的问题**:读取 Body 后需要用 `io.NopCloser(bytes.NewBuffer(body))` 恢复,以便后续代码(如其他中间件)还能再次读取
2. **废弃 API 替换**:使用 `io.ReadAll` 替代已废弃的 `ioutil.ReadAll`
3. **多值参数处理**:当同一参数有多个值时(如 `?id=1&id=2`),存储为 `Slice`;单值则直接存储字符串
## 使用示例
```go
appIns.Run(Router{
"app": {
"user": {
"info": func(that *Context) {
// 链式调用获取单个参数(类似 Session 风格)
id := that.ReqData("id").ToInt() // 统一获取
name := that.ReqParam("name").ToStr() // URL 参数
age := that.ReqForm("age").ToCeilInt() // 表单参数
data := that.ReqJson("profile").ToMap() // JSON 字段
// 获取所有参数(返回 Map
allParams := that.ReqDatas() // 合并后的所有参数
urlParams := that.ReqParams() // 所有 URL 参数
formData := that.ReqForms() // 所有表单数据
jsonBody := that.ReqJsons() // 完整 JSON Body
that.Display(0, Map{"id": id, "name": name})
},
"upload": func(that *Context) {
// 获取单个上传文件
file, header, err := that.ReqFile("avatar")
if err == nil {
defer file.Close()
// header.Filename - 文件名
// header.Size - 文件大小
}
// 获取多个同名上传文件
files, err := that.ReqFiles("images")
},
},
},
})
```
## 风格对比
| 旧方式(需要类型断言) | 新方式(链式调用) |
|------------------------|-------------------|
| `req["id"].(string) `| `that.ReqData("id").ToStr()` |
| `ObjToInt(req["id"]) `| `that.ReqData("id").ToInt()` |
| 需要手动处理 nil | `*Obj` 自动处理空值 |
+5
View File
@@ -0,0 +1,5 @@
{
"setup-worktree": [
"npm install"
]
}
+5 -1
View File
@@ -1,2 +1,6 @@
/.idea/*
.idea
.idea
/example/tpt/demo/
*.exe
/example/config
/.cursor/*.log
View File
+11 -11
View File
@@ -7,17 +7,17 @@ AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution
as defined by Sections 1 through 9 of that document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
@@ -26,31 +26,31 @@ or indirect, to cause the direction or management of such entity, whether
by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions
granted by that License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation
or translation of a Source form, including but not limited to compiled object
code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License, as indicated by a copyright notice that
is included in or attached to the work (an example is provided in the Appendix
below).
"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial revisions,
@@ -59,7 +59,7 @@ original work of authorship. For the purposes of that License, Derivative
Works shall not include works that remain separable from, or merely link (or
bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative
@@ -74,7 +74,7 @@ for the purpose of discussing and improving the Work, but excluding communicatio
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently incorporated
@@ -142,7 +142,7 @@ any additional terms or conditions. Notwithstanding the above, nothing herein
shall supersede or modify the terms of any separate license agreement you
may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names,
6. Trademarks. that License does not grant permission to use the trade names,
trademarks, service marks, or product names of the Licensor, except as required
for reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
+88 -5
View File
@@ -1,6 +1,89 @@
# hotime
golang web服务框架
支持数据库dbmysql、sqlite3
支持缓存cacheredismemory,数据库
自带工具类,上下文,以及session等功能
# HoTime
**高性能 Go Web 服务框架**
一个"小而全"的 Go Web 框架,内置 ORM、三级缓存、Session 管理,让你专注于业务逻辑。
## 核心特性
- **高性能** - 单机 10万+ QPS,支持百万级并发用户
- **内置 ORM** - 类 Medoo 语法,链式查询,支持 MySQL/SQLite/PostgreSQL
- **三级缓存** - Memory > Redis > DB,自动穿透与回填
- **Session 管理** - 内置会话管理,支持多种存储后端
- **代码生成** - 根据数据库表自动生成 CRUD 接口
- **开箱即用** - 微信支付/公众号/小程序、阿里云、腾讯云等 SDK 内置
## 文档
| 文档 | 说明 |
|------|------|
| [快速上手指南](docs/QUICKSTART.md) | 5 分钟入门,安装配置、路由、中间件、基础数据库操作 |
| [HoTimeDB 使用说明](docs/HoTimeDB_使用说明.md) | 完整数据库 ORM 教程 |
| [HoTimeDB API 参考](docs/HoTimeDB_API参考.md) | 数据库 API 速查手册 |
| [Common 工具类](docs/Common_工具类使用说明.md) | Map/Slice/Obj 类型、类型转换、工具函数 |
| [代码生成器](docs/CodeGen_使用说明.md) | 自动 CRUD 代码生成、配置规则 |
| [数据库设计规范](docs/DatabaseDesign_数据库设计规范.md) | 表命名、字段命名、时间类型、必有字段规范 |
| [代码生成配置规范](docs/CodeConfig_代码生成配置规范.md) | codeConfig、菜单权限、字段规则配置说明 |
| [改进规划](docs/ROADMAP_改进规划.md) | 待改进项、设计思考、版本迭代规划 |
## 安装
```bash
go get code.hoteas.com/golang/hotime
```
## 性能
| 并发数 | QPS | 成功率 | 平均延迟 |
|--------|-----|--------|----------|
| 500 | 99,960 | 100% | 5.0ms |
| **1000** | **102,489** | **100%** | **9.7ms** |
| 2000 | 75,801 | 99.99% | 26.2ms |
> 测试环境:24 核 CPUWindows 10Go 1.19.3
### 并发用户估算
| 使用场景 | 请求频率 | 可支持用户数 |
|----------|----------|--------------|
| 高频交互 | 1次/秒 | ~10万 |
| 活跃用户 | 1次/5秒 | ~50万 |
| 普通浏览 | 1次/10秒 | ~100万 |
## 框架对比
| 特性 | HoTime | Gin | Echo | Fiber |
|------|--------|-----|------|-------|
| 性能 | 100K QPS | 70K QPS | 70K QPS | 100K QPS |
| 内置ORM | ✅ | ❌ | ❌ | ❌ |
| 内置缓存 | ✅ 三级缓存 | ❌ | ❌ | ❌ |
| Session | ✅ 内置 | ❌ 需插件 | ❌ 需插件 | ❌ 需插件 |
| 代码生成 | ✅ | ❌ | ❌ | ❌ |
| 微信/支付集成 | ✅ 内置 | ❌ | ❌ | ❌ |
## 适用场景
| 场景 | 推荐度 | 说明 |
|------|--------|------|
| 中小型后台系统 | ⭐⭐⭐⭐⭐ | 完美适配,开发效率最高 |
| 微信小程序后端 | ⭐⭐⭐⭐⭐ | 内置微信 SDK |
| 快速原型开发 | ⭐⭐⭐⭐⭐ | 代码生成 + 全功能集成 |
| 高并发 API 服务 | ⭐⭐⭐⭐ | 性能足够 |
| 大型微服务 | ⭐⭐⭐ | 建议用 Gin/Echo |
## 扩展功能
- 微信支付/公众号/小程序 - `dri/wechat/`
- 阿里云服务 - `dri/aliyun/`
- 腾讯云服务 - `dri/tencent/`
- 文件上传下载 - `dri/upload/`, `dri/download/`
- MongoDB - `dri/mongodb/`
- RSA 加解密 - `dri/rsa/`
## License
MIT License
---
**HoTime** - 让 Go Web 开发更简单、更高效
+329 -66
View File
@@ -1,12 +1,13 @@
package hotime
import (
. "./cache"
"./code"
. "./common"
. "./db"
. "./log"
. "code.hoteas.com/golang/hotime/cache"
"code.hoteas.com/golang/hotime/code"
. "code.hoteas.com/golang/hotime/common"
. "code.hoteas.com/golang/hotime/db"
. "code.hoteas.com/golang/hotime/log"
"database/sql"
"fmt"
"github.com/sirupsen/logrus"
"io/ioutil"
"net/http"
@@ -15,19 +16,19 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
)
type Application struct {
*code.MakeCode
MakeCodeRouter map[string]*code.MakeCode
MethodRouter
Router
ContextBase
Error
Log *logrus.Logger
WebConnectLog *logrus.Logger
Port string //端口号
TLSPort string //ssl访问端口号
connectListener []func(this *Context) bool //所有的访问监听,true按原计划继续使用,false表示有监听器处理
connectListener []func(that *Context) bool //所有的访问监听,true按原计划继续使用,false表示有监听器处理
connectDbFunc func(err ...*Error) (master, slave *sql.DB)
configPath string
Config Map
@@ -63,15 +64,37 @@ func (that *Application) Run(router Router) {
//
//}
that.Router = router
//that.Router = router
if that.Router == nil {
that.Router = Router{}
}
for k, _ := range router {
v := router[k]
if that.Router[k] == nil {
that.Router[k] = v
}
//直达接口层复用
for k1, _ := range v {
v1 := v[k1]
if that.Router[k][k1] == nil {
that.Router[k][k1] = v1
}
for k2, _ := range v1 {
v2 := v1[k2]
that.Router[k][k1][k2] = v2
}
}
}
//重新设置MethodRouter//直达路由
that.MethodRouter = MethodRouter{}
modeRouterStrict := true
if that.Config.GetBool("modeRouterStrict") == false {
modeRouterStrict = false
}
if router != nil {
for pk, pv := range router {
if that.Router != nil {
for pk, pv := range that.Router {
if !modeRouterStrict {
pk = strings.ToLower(pk)
}
@@ -216,10 +239,8 @@ func (that *Application) SetConfig(configPath ...string) {
}
that.Log = GetLog(that.Config.GetString("logFile"), true)
that.Error = Error{Logger: that.Log}
if that.Config.Get("webConnectLogShow") == nil || that.Config.GetBool("webConnectLogShow") {
that.WebConnectLog = GetLog(that.Config.GetString("webConnectLogFile"), false)
if that.Error.GetError() != nil {
fmt.Println(that.Error.GetError().Error())
}
//文件如果损坏则不写入配置防止配置文件数据丢失
@@ -246,19 +267,25 @@ func (that *Application) SetConfig(configPath ...string) {
}
that.Log = GetLog(that.Config.GetString("logFile"), true)
that.Error = Error{Logger: that.Log}
if that.Config.Get("webConnectLogShow") == nil || that.Config.GetBool("webConnectLogShow") {
that.WebConnectLog = GetLog(that.Config.GetString("webConnectLogFile"), false)
}
}
// SetConnectListener 连接判断,返回true继续传输至控制层,false则停止传输
func (that *Application) SetConnectListener(lis func(this *Context) bool) {
// SetConnectListener 连接判断,返回false继续传输至控制层,true则停止传输
func (that *Application) SetConnectListener(lis func(that *Context) (isFinished bool)) {
that.connectListener = append(that.connectListener, lis)
}
//网络错误
//func (this *Application) session(w http.ResponseWriter, req *http.Request) {
//func (that *Application) session(w http.ResponseWriter, req *http.Request) {
//
//}
//序列化链接
// 序列化链接
func (that *Application) urlSer(url string) (string, []string) {
q := strings.Index(url, "?")
if q == -1 {
@@ -281,6 +308,7 @@ func (that *Application) urlSer(url string) (string, []string) {
//访问
func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
nowUnixTime := time.Now()
_, s := that.urlSer(req.RequestURI)
//获取cookie
@@ -292,21 +320,25 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
// 没有保存就生成随机的session
cookie, err := req.Cookie(that.Config.GetString("sessionName"))
sessionId := Md5(strconv.Itoa(Rand(10)))
token := req.FormValue("token")
needSetCookie := ""
token := req.Header.Get("Authorization")
if len(token) != 32 {
if err != nil || (len(token) == 32 && cookie.Value != token) {
if len(token) == 32 {
sessionId = token
token = req.FormValue("token")
}
//没有cookie或者cookie不等于token
//有token优先token
if len(token) == 32 {
sessionId = token
//没有token,则查阅session
if cookie == nil || cookie.Value != sessionId {
needSetCookie = sessionId
}
//没有跨域设置
if that.Config.GetString("crossDomain") == "" {
http.SetCookie(w, &http.Cookie{Name: that.Config.GetString("sessionName"), Value: sessionId, Path: "/"})
} else {
//跨域允许需要设置cookie的允许跨域https才有效果
w.Header().Set("Set-Cookie", that.Config.GetString("sessionName")+"="+sessionId+"; SameSite=None; Secure")
}
} else {
} else if err == nil && cookie.Value != "" {
sessionId = cookie.Value
//session也没有则判断是否创建cookie
} else {
needSetCookie = sessionId
}
unescapeUrl, err := url.QueryUnescape(req.RequestURI)
@@ -325,23 +357,43 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
context.HandlerStr, context.RouterString = that.urlSer(context.HandlerStr)
//跨域设置
that.crossDomain(&context)
//是否展示日志
if that.WebConnectLog != nil {
that.WebConnectLog.Infoln(Substr(context.Req.RemoteAddr, 0, strings.Index(context.Req.RemoteAddr, ":")), context.Req.Method, context.HandlerStr)
}
that.crossDomain(&context, needSetCookie)
defer func() {
//是否展示日志
if that.WebConnectLog != nil {
//负载均衡优化
ipStr := ""
if req.Header.Get("X-Forwarded-For") != "" {
ipStr = req.Header.Get("X-Forwarded-For")
} else if req.Header.Get("X-Real-IP") != "" {
ipStr = req.Header.Get("X-Real-IP")
}
//负载均衡优化
if ipStr == "" {
//RemoteAddr := that.Req.RemoteAddr
ipStr = Substr(context.Req.RemoteAddr, 0, strings.Index(context.Req.RemoteAddr, ":"))
}
that.WebConnectLog.Infoln(ipStr, context.Req.Method,
"time cost:", ObjToFloat64(time.Now().UnixNano()-nowUnixTime.UnixNano())/1000000.00, "ms",
"data length:", ObjToFloat64(context.DataSize)/1000.00, "KB", context.HandlerStr)
}
}()
//访问拦截true继续false暂停
connectListenerLen := len(that.connectListener)
if connectListenerLen != 0 {
for i := 0; i < connectListenerLen; i++ {
connectListenerLen := len(that.connectListener) - 1
if !that.connectListener[i](&context) {
context.View()
return
}
for true {
if connectListenerLen < 0 {
break
}
if that.connectListener[connectListenerLen](&context) {
context.View()
return
}
connectListenerLen--
}
//接口服务
@@ -394,8 +446,14 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
header.Set("Cache-Control", "no-cache")
}
if strings.Index(path, ".m3u8") != -1 {
header.Add("Content-Type", "audio/mpegurl")
t := strings.LastIndex(path, ".")
if t != -1 {
tt := path[t:]
if MimeMaps[tt] != "" {
header.Add("Content-Type", MimeMaps[tt])
}
}
//w.Write(data)
@@ -403,35 +461,68 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
}
func (that *Application) crossDomain(context *Context) {
func (that *Application) crossDomain(context *Context, sessionId string) {
//没有跨域设置
if context.Config.GetString("crossDomain") == "" {
if sessionId != "" {
http.SetCookie(context.Resp, &http.Cookie{Name: that.Config.GetString("sessionName"), Value: sessionId, Path: "/"})
//context.Resp.Header().Set("Set-Cookie", that.Config.GetString("sessionName")+"="+sessionId+"; Path=/; SameSite=None; Secure")
}
return
}
header := context.Resp.Header()
//header.Set("Access-Control-Allow-Origin", "*")
header.Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE")
header.Set("Access-Control-Allow-Credentials", "true")
header.Set("Access-Control-Expose-Headers", "*")
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token")
//不跨域,则不设置
remoteHost := context.Req.Host
if context.Config.GetString("port") == "80" || context.Config.GetString("port") == "443" {
remoteHost = remoteHost + ":" + context.Config.GetString("port")
}
if context.Config.GetString("crossDomain") != "auto" {
//不跨域,则不设置
if strings.Contains(context.Config.GetString("crossDomain"), remoteHost) {
if sessionId != "" {
http.SetCookie(context.Resp, &http.Cookie{Name: that.Config.GetString("sessionName"), Value: sessionId, Path: "/"})
}
return
}
header.Set("Access-Control-Allow-Origin", that.Config.GetString("crossDomain"))
// 后端设置,2592000单位秒,这里是30天
header.Set("Access-Control-Max-Age", "2592000")
//header.Set("Access-Control-Allow-Origin", "*")
header.Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE")
header.Set("Access-Control-Allow-Credentials", "true")
header.Set("Access-Control-Expose-Headers", "*")
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token,Authorization,Cookie,Set-Cookie")
if sessionId != "" {
//跨域允许需要设置cookie的允许跨域https才有效果
context.Resp.Header().Set("Set-Cookie", that.Config.GetString("sessionName")+"="+sessionId+"; Path=/; SameSite=None; Secure")
}
return
}
origin := context.Req.Header.Get("Origin")
if origin != "" {
header.Set("Access-Control-Allow-Origin", origin)
refer := context.Req.Header.Get("Referer")
if (origin != "" && strings.Contains(origin, remoteHost)) || strings.Contains(refer, remoteHost) {
if sessionId != "" {
//http.SetCookie(context.Resp, &http.Cookie{Name: that.Config.GetString("sessionName"), Value: sessionId, Path: "/"})
context.Resp.Header().Set("Set-Cookie", that.Config.GetString("sessionName")+"="+sessionId+"; Path=/; SameSite=None; Secure")
}
return
}
refer := context.Req.Header.Get("Referer")
if refer != "" {
if origin != "" {
header.Set("Access-Control-Allow-Origin", origin)
//return
} else if refer != "" {
tempInt := 0
lastInt := strings.IndexFunc(refer, func(r rune) bool {
if r == '/' && tempInt > 8 {
@@ -446,31 +537,88 @@ func (that *Application) crossDomain(context *Context) {
}
refer = Substr(refer, 0, lastInt)
header.Set("Access-Control-Allow-Origin", refer)
//header.Set("Access-Control-Allow-Origin", "*")
}
header.Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE")
header.Set("Access-Control-Allow-Credentials", "true")
header.Set("Access-Control-Expose-Headers", "*")
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token,Authorization,Cookie,Set-Cookie")
if sessionId != "" {
//跨域允许需要设置cookie的允许跨域https才有效果
context.Resp.Header().Set("Set-Cookie", that.Config.GetString("sessionName")+"="+sessionId+"; Path=/; SameSite=None; Secure")
}
}
//Init 初始化application
func Init(config string) Application {
// Init 初始化application
func Init(config string) *Application {
appIns := Application{}
//手动模式,
appIns.SetConfig(config)
SetDB(&appIns)
appIns.SetCache()
appIns.MakeCode = &code.MakeCode{}
codeConfig := appIns.Config.GetMap("codeConfig")
if codeConfig != nil {
codeConfig := appIns.Config.GetSlice("codeConfig")
if codeConfig != nil {
for k, _ := range codeConfig {
if appIns.Config.GetInt("mode") == 2{
appIns.MakeCode.Db2JSON(k, codeConfig.GetString(k), &appIns.Db)
}else{
appIns.MakeCode.Db2JSON(k, codeConfig.GetString(k), nil)
codeMake := codeConfig.GetMap(k)
if codeMake == nil {
continue
}
//codeMake["table"] = k
if appIns.MakeCodeRouter == nil {
appIns.MakeCodeRouter = map[string]*code.MakeCode{}
}
if codeMake.GetString("name") == "" {
codeMake["name"] = codeMake.GetString("table")
}
appIns.MakeCodeRouter[codeMake.GetString("name")] = &code.MakeCode{Error: appIns.Error}
appIns.MakeCodeRouter[codeMake.GetString("name")].Db2JSON(&appIns.Db, codeMake)
//接入动态代码层
if appIns.Router == nil {
appIns.Router = Router{}
}
//appIns.Router[codeMake.GetString("name")] = TptProject
appIns.Router[codeMake.GetString("name")] = Proj{}
for k2, _ := range TptProject {
if appIns.Router[codeMake.GetString("name")][k2] == nil {
appIns.Router[codeMake.GetString("name")][k2] = Ctr{}
}
for k3, _ := range TptProject[k2] {
v3 := TptProject[k2][k3]
appIns.Router[codeMake.GetString("name")][k2][k3] = v3
}
}
for k1, _ := range appIns.MakeCodeRouter[codeMake.GetString("name")].TableColumns {
if appIns.Router[codeMake.GetString("name")][k1] == nil {
appIns.Router[codeMake.GetString("name")][k1] = Ctr{}
}
for k2, _ := range appIns.Router[codeMake.GetString("name")]["hotimeCommon"] {
//golang毛病
v2 := appIns.Router[codeMake.GetString("name")]["hotimeCommon"][k2]
appIns.Router[codeMake.GetString("name")][k1][k2] = v2
}
}
setMakeCodeListener(codeMake.GetString("name"), &appIns)
}
}
return appIns
return &appIns
}
// SetDB 智能数据库设置
@@ -490,6 +638,8 @@ func SetMysqlDB(appIns *Application, config Map) {
appIns.Db.Type = "mysql"
appIns.Db.DBName = config.GetString("name")
appIns.Db.Prefix = config.GetString("prefix")
appIns.Db.Log = appIns.Log
appIns.Db.Mode = appIns.Config.GetCeilInt("mode")
appIns.SetConnectDB(func(err ...*Error) (master, slave *sql.DB) {
//master数据库配置
query := config.GetString("user") + ":" + config.GetString("password") +
@@ -519,6 +669,8 @@ func SetSqliteDB(appIns *Application, config Map) {
appIns.Db.Type = "sqlite"
appIns.Db.Prefix = config.GetString("prefix")
appIns.Db.Mode = appIns.Config.GetCeilInt("mode")
appIns.Db.Log = appIns.Log
appIns.SetConnectDB(func(err ...*Error) (master, slave *sql.DB) {
db, e := sql.Open("sqlite3", config.GetString("path"))
if e != nil && len(err) != 0 {
@@ -529,3 +681,114 @@ func SetSqliteDB(appIns *Application, config Map) {
return master, slave
})
}
func setMakeCodeListener(name string, appIns *Application) {
appIns.SetConnectListener(func(context *Context) (isFinished bool) {
codeIns := appIns.MakeCodeRouter[name]
if len(context.RouterString) < 2 || appIns.MakeCodeRouter[context.RouterString[0]] == nil {
return isFinished
}
if len(context.RouterString) > 1 && context.RouterString[0] == name {
if context.RouterString[1] == "hotime" && context.RouterString[2] == "login" {
return isFinished
}
if context.RouterString[1] == "hotime" && context.RouterString[2] == "logout" {
return isFinished
}
if context.RouterString[1] == "hotime" && context.RouterString[2] == "config" {
return isFinished
}
if context.RouterString[1] == "hotime" && context.RouterString[2] == "wallpaper" {
return isFinished
}
if context.Session(codeIns.FileConfig.GetString("table")+"_id").Data == nil {
context.Display(2, "你还没有登录")
return true
}
}
if len(context.RouterString) < 2 || len(context.RouterString) > 3 ||
!(context.Router[context.RouterString[0]] != nil &&
context.Router[context.RouterString[0]][context.RouterString[1]] != nil) {
return isFinished
}
//排除无效操作
if len(context.RouterString) == 2 &&
context.Req.Method != "GET" &&
context.Req.Method != "POST" {
return isFinished
}
//排除已有接口的无效操作
if len(context.RouterString) == 3 && context.Router[context.RouterString[0]] != nil && context.Router[context.RouterString[0]][context.RouterString[1]] != nil && context.Router[context.RouterString[0]][context.RouterString[1]][context.RouterString[2]] != nil {
return isFinished
}
//列表检索
if len(context.RouterString) == 2 &&
context.Req.Method == "GET" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["search"] == nil {
return isFinished
}
context.Router[context.RouterString[0]][context.RouterString[1]]["search"](context)
}
//新建
if len(context.RouterString) == 2 &&
context.Req.Method == "POST" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["add"] == nil {
return true
}
context.Router[context.RouterString[0]][context.RouterString[1]]["add"](context)
}
if len(context.RouterString) == 3 &&
context.Req.Method == "POST" {
return isFinished
}
//分析
if len(context.RouterString) == 3 && context.RouterString[2] == "analyse" &&
context.Req.Method == "GET" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["analyse"] == nil {
return isFinished
}
context.Router[context.RouterString[0]][context.RouterString[1]]["analyse"](context)
}
//查询单条
if len(context.RouterString) == 3 &&
context.Req.Method == "GET" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["info"] == nil {
return isFinished
}
context.Router[context.RouterString[0]][context.RouterString[1]]["info"](context)
}
//更新
if len(context.RouterString) == 3 &&
context.Req.Method == "PUT" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["update"] == nil {
return true
}
context.Router[context.RouterString[0]][context.RouterString[1]]["update"](context)
}
//移除
if len(context.RouterString) == 3 &&
context.Req.Method == "DELETE" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["remove"] == nil {
return true
}
context.Router[context.RouterString[0]][context.RouterString[1]]["remove"](context)
}
//context.View()
return true
})
}
+355 -28
View File
@@ -1,12 +1,38 @@
package cache
import (
. "../common"
"encoding/json"
"errors"
"os"
"time"
. "code.hoteas.com/golang/hotime/common"
)
const debugLogPath = `d:\work\hotimev1.5\.cursor\debug.log`
// debugLog 写入调试日志
func debugLog(hypothesisId, location, message string, data map[string]interface{}) {
// #region agent log
logFile, _ := os.OpenFile(debugLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if logFile != nil {
logEntry, _ := json.Marshal(map[string]interface{}{
"sessionId": "cache-debug",
"runId": "test-run",
"hypothesisId": hypothesisId,
"location": location,
"message": message,
"data": data,
"timestamp": time.Now().UnixMilli(),
})
logFile.Write(append(logEntry, '\n'))
logFile.Close()
}
// #endregion
}
// HoTimeCache 可配置memorydbredis,默认启用memory,默认优先级为memory>redis>db,memory与数据库缓存设置项一致,
//缓存数据填充会自动反方向反哺,加入memory缓存过期将自动从redis更新,但memory永远不会更新redis,如果是集群建议不要开启memory,配置即启用
// 缓存数据填充会自动反方向反哺,加入memory缓存过期将自动从redis更新,但memory永远不会更新redis,如果是集群建议不要开启memory,配置即启用
type HoTimeCache struct {
*Error
dbCache *CacheDb
@@ -21,7 +47,7 @@ func (that *HoTimeCache) Session(key string, data ...interface{}) *Obj {
//内存缓存有
if that.memoryCache != nil && that.memoryCache.SessionSet {
reData = that.memoryCache.Cache(key, data...)
if reData != nil {
if reData.Data != nil {
return reData
}
}
@@ -29,9 +55,9 @@ func (that *HoTimeCache) Session(key string, data ...interface{}) *Obj {
//redis缓存有
if that.redisCache != nil && that.redisCache.SessionSet {
reData = that.redisCache.Cache(key, data...)
if reData != nil {
if reData.Data != nil {
if that.memoryCache != nil && that.memoryCache.SessionSet {
that.memoryCache.Cache(key, reData)
that.memoryCache.Cache(key, reData.Data)
}
return reData
}
@@ -40,12 +66,12 @@ func (that *HoTimeCache) Session(key string, data ...interface{}) *Obj {
//db缓存有
if that.dbCache != nil && that.dbCache.SessionSet {
reData = that.dbCache.Cache(key, data...)
if reData != nil {
if reData.Data != nil {
if that.memoryCache != nil && that.memoryCache.SessionSet {
that.memoryCache.Cache(key, reData)
that.memoryCache.Cache(key, reData.Data)
}
if that.redisCache != nil && that.redisCache.SessionSet {
that.redisCache.Cache(key, reData)
that.redisCache.Cache(key, reData.Data)
}
return reData
@@ -76,7 +102,7 @@ func (that *HoTimeCache) Db(key string, data ...interface{}) *Obj {
//内存缓存有
if that.memoryCache != nil && that.memoryCache.DbSet {
reData = that.memoryCache.Cache(key, data...)
if reData != nil {
if reData.Data != nil {
return reData
}
}
@@ -84,24 +110,24 @@ func (that *HoTimeCache) Db(key string, data ...interface{}) *Obj {
//redis缓存有
if that.redisCache != nil && that.redisCache.DbSet {
reData = that.redisCache.Cache(key, data...)
if reData != nil {
if reData.Data != nil {
if that.memoryCache != nil && that.memoryCache.DbSet {
that.memoryCache.Cache(key, reData)
that.memoryCache.Cache(key, reData.Data)
}
return reData
}
}
//redis缓存有
//db缓存有
if that.dbCache != nil && that.dbCache.DbSet {
reData = that.dbCache.Cache(key, data...)
if reData != nil {
if reData.Data != nil {
if that.memoryCache != nil && that.memoryCache.DbSet {
that.memoryCache.Cache(key, reData)
that.memoryCache.Cache(key, reData.Data)
}
if that.redisCache != nil && that.redisCache.DbSet {
that.redisCache.Cache(key, reData)
that.redisCache.Cache(key, reData.Data)
}
return reData
@@ -119,7 +145,7 @@ func (that *HoTimeCache) Db(key string, data ...interface{}) *Obj {
if that.redisCache != nil && that.redisCache.DbSet {
reData = that.redisCache.Cache(key, data...)
}
//redis缓存有
//db缓存有
if that.dbCache != nil && that.dbCache.DbSet {
reData = that.dbCache.Cache(key, data...)
}
@@ -132,7 +158,7 @@ func (that *HoTimeCache) Cache(key string, data ...interface{}) *Obj {
//内存缓存有
if that.memoryCache != nil {
reData = that.memoryCache.Cache(key, data...)
if reData != nil {
if reData != nil && reData.Data != nil {
return reData
}
}
@@ -140,24 +166,23 @@ func (that *HoTimeCache) Cache(key string, data ...interface{}) *Obj {
//redis缓存有
if that.redisCache != nil {
reData = that.redisCache.Cache(key, data...)
if reData != nil {
if reData != nil && reData.Data != nil {
if that.memoryCache != nil {
that.memoryCache.Cache(key, reData)
that.memoryCache.Cache(key, reData.Data)
}
return reData
}
}
//redis缓存有
//db缓存有
if that.dbCache != nil {
reData = that.dbCache.Cache(key, data...)
if reData != nil {
if reData != nil && reData.Data != nil {
if that.memoryCache != nil {
that.memoryCache.Cache(key, reData)
that.memoryCache.Cache(key, reData.Data)
}
if that.redisCache != nil {
that.redisCache.Cache(key, reData)
that.redisCache.Cache(key, reData.Data)
}
return reData
}
@@ -174,13 +199,306 @@ func (that *HoTimeCache) Cache(key string, data ...interface{}) *Obj {
if that.redisCache != nil {
reData = that.redisCache.Cache(key, data...)
}
//redis缓存有
//db缓存有
if that.dbCache != nil {
reData = that.dbCache.Cache(key, data...)
}
return reData
}
// SessionsGet 批量获取 Session 缓存
// 返回 Mapkey 为缓存键,value 为缓存值
// 优先级:memory > redis > db,低优先级数据会反哺到高优先级缓存
func (that *HoTimeCache) SessionsGet(keys []string) Map {
if len(keys) == 0 {
return Map{}
}
// #region agent log
debugLog("D", "cache.go:SessionsGet:start", "SessionsGet开始", map[string]interface{}{
"keys_count": len(keys),
"has_memory": that.memoryCache != nil,
"has_redis": that.redisCache != nil,
"has_db": that.dbCache != nil,
})
// #endregion
result := make(Map, len(keys))
missingKeys := keys
// 从 memory 获取
if that.memoryCache != nil && that.memoryCache.SessionSet {
memResult := that.memoryCache.CachesGet(keys)
// #region agent log
debugLog("D", "cache.go:SessionsGet:memory", "从Memory获取", map[string]interface{}{
"found_count": len(memResult),
})
// #endregion
for k, v := range memResult {
result[k] = v
}
// 计算未命中的 keys
missingKeys = make([]string, 0)
for _, k := range keys {
if _, exists := result[k]; !exists {
missingKeys = append(missingKeys, k)
}
}
}
// 从 redis 获取未命中的
if len(missingKeys) > 0 && that.redisCache != nil && that.redisCache.SessionSet {
redisResult := that.redisCache.CachesGet(missingKeys)
// #region agent log
debugLog("D", "cache.go:SessionsGet:redis", "从Redis获取", map[string]interface{}{
"missing_count": len(missingKeys),
"found_count": len(redisResult),
})
// #endregion
// 反哺到 memory
if that.memoryCache != nil && that.memoryCache.SessionSet && len(redisResult) > 0 {
that.memoryCache.CachesSet(redisResult)
// #region agent log
debugLog("D", "cache.go:SessionsGet:backfill_redis_to_mem", "Redis数据反哺到Memory", map[string]interface{}{
"backfill_count": len(redisResult),
})
// #endregion
}
for k, v := range redisResult {
result[k] = v
}
// 更新未命中的 keys
newMissing := make([]string, 0)
for _, k := range missingKeys {
if _, exists := result[k]; !exists {
newMissing = append(newMissing, k)
}
}
missingKeys = newMissing
}
// 从 db 获取未命中的
if len(missingKeys) > 0 && that.dbCache != nil && that.dbCache.SessionSet {
dbResult := that.dbCache.CachesGet(missingKeys)
// #region agent log
debugLog("D", "cache.go:SessionsGet:db", "从DB获取", map[string]interface{}{
"missing_count": len(missingKeys),
"found_count": len(dbResult),
})
// #endregion
// 反哺到 memory 和 redis
if len(dbResult) > 0 {
if that.memoryCache != nil && that.memoryCache.SessionSet {
that.memoryCache.CachesSet(dbResult)
// #region agent log
debugLog("D", "cache.go:SessionsGet:backfill_db_to_mem", "DB数据反哺到Memory", map[string]interface{}{
"backfill_count": len(dbResult),
})
// #endregion
}
if that.redisCache != nil && that.redisCache.SessionSet {
that.redisCache.CachesSet(dbResult)
// #region agent log
debugLog("D", "cache.go:SessionsGet:backfill_db_to_redis", "DB数据反哺到Redis", map[string]interface{}{
"backfill_count": len(dbResult),
})
// #endregion
}
}
for k, v := range dbResult {
result[k] = v
}
}
// #region agent log
debugLog("D", "cache.go:SessionsGet:end", "SessionsGet完成", map[string]interface{}{
"total_found": len(result),
})
// #endregion
return result
}
// SessionsSet 批量设置 Session 缓存
// data: Mapkey 为缓存键,value 为缓存值
func (that *HoTimeCache) SessionsSet(data Map) {
if len(data) == 0 {
return
}
// #region agent log
debugLog("A", "cache.go:SessionsSet:start", "SessionsSet开始", map[string]interface{}{
"data_count": len(data),
"has_memory": that.memoryCache != nil,
"has_redis": that.redisCache != nil,
"has_db": that.dbCache != nil,
})
// #endregion
if that.memoryCache != nil && that.memoryCache.SessionSet {
that.memoryCache.CachesSet(data)
// #region agent log
debugLog("A", "cache.go:SessionsSet:memory", "写入Memory完成", map[string]interface{}{"count": len(data)})
// #endregion
}
if that.redisCache != nil && that.redisCache.SessionSet {
that.redisCache.CachesSet(data)
// #region agent log
debugLog("A", "cache.go:SessionsSet:redis", "写入Redis完成", map[string]interface{}{"count": len(data)})
// #endregion
}
if that.dbCache != nil && that.dbCache.SessionSet {
that.dbCache.CachesSet(data)
// #region agent log
debugLog("A", "cache.go:SessionsSet:db", "写入DB完成", map[string]interface{}{"count": len(data)})
// #endregion
}
// #region agent log
debugLog("A", "cache.go:SessionsSet:end", "SessionsSet完成", nil)
// #endregion
}
// SessionsDelete 批量删除 Session 缓存
func (that *HoTimeCache) SessionsDelete(keys []string) {
if len(keys) == 0 {
return
}
// #region agent log
debugLog("C", "cache.go:SessionsDelete:start", "SessionsDelete开始", map[string]interface{}{
"keys_count": len(keys),
"has_memory": that.memoryCache != nil,
"has_redis": that.redisCache != nil,
"has_db": that.dbCache != nil,
})
// #endregion
if that.memoryCache != nil && that.memoryCache.SessionSet {
that.memoryCache.CachesDelete(keys)
// #region agent log
debugLog("C", "cache.go:SessionsDelete:memory", "从Memory删除完成", map[string]interface{}{"count": len(keys)})
// #endregion
}
if that.redisCache != nil && that.redisCache.SessionSet {
that.redisCache.CachesDelete(keys)
// #region agent log
debugLog("C", "cache.go:SessionsDelete:redis", "从Redis删除完成", map[string]interface{}{"count": len(keys)})
// #endregion
}
if that.dbCache != nil && that.dbCache.SessionSet {
that.dbCache.CachesDelete(keys)
// #region agent log
debugLog("C", "cache.go:SessionsDelete:db", "从DB删除完成", map[string]interface{}{"count": len(keys)})
// #endregion
}
// #region agent log
debugLog("C", "cache.go:SessionsDelete:end", "SessionsDelete完成", nil)
// #endregion
}
// CachesGet 批量获取普通缓存
// 返回 Mapkey 为缓存键,value 为缓存值
// 优先级:memory > redis > db,低优先级数据会反哺到高优先级缓存
func (that *HoTimeCache) CachesGet(keys []string) Map {
if len(keys) == 0 {
return Map{}
}
result := make(Map, len(keys))
missingKeys := keys
// 从 memory 获取
if that.memoryCache != nil {
memResult := that.memoryCache.CachesGet(keys)
for k, v := range memResult {
result[k] = v
}
// 计算未命中的 keys
missingKeys = make([]string, 0)
for _, k := range keys {
if _, exists := result[k]; !exists {
missingKeys = append(missingKeys, k)
}
}
}
// 从 redis 获取未命中的
if len(missingKeys) > 0 && that.redisCache != nil {
redisResult := that.redisCache.CachesGet(missingKeys)
// 反哺到 memory
if that.memoryCache != nil && len(redisResult) > 0 {
that.memoryCache.CachesSet(redisResult)
}
for k, v := range redisResult {
result[k] = v
}
// 更新未命中的 keys
newMissing := make([]string, 0)
for _, k := range missingKeys {
if _, exists := result[k]; !exists {
newMissing = append(newMissing, k)
}
}
missingKeys = newMissing
}
// 从 db 获取未命中的
if len(missingKeys) > 0 && that.dbCache != nil {
dbResult := that.dbCache.CachesGet(missingKeys)
// 反哺到 memory 和 redis
if len(dbResult) > 0 {
if that.memoryCache != nil {
that.memoryCache.CachesSet(dbResult)
}
if that.redisCache != nil {
that.redisCache.CachesSet(dbResult)
}
}
for k, v := range dbResult {
result[k] = v
}
}
return result
}
// CachesSet 批量设置普通缓存
// data: Mapkey 为缓存键,value 为缓存值
func (that *HoTimeCache) CachesSet(data Map) {
if len(data) == 0 {
return
}
if that.memoryCache != nil {
that.memoryCache.CachesSet(data)
}
if that.redisCache != nil {
that.redisCache.CachesSet(data)
}
if that.dbCache != nil {
that.dbCache.CachesSet(data)
}
}
// CachesDelete 批量删除普通缓存
func (that *HoTimeCache) CachesDelete(keys []string) {
if len(keys) == 0 {
return
}
if that.memoryCache != nil {
that.memoryCache.CachesDelete(keys)
}
if that.redisCache != nil {
that.redisCache.CachesDelete(keys)
}
if that.dbCache != nil {
that.dbCache.CachesDelete(keys)
}
}
func (that *HoTimeCache) Init(config Map, hotimeDb HoTimeDBInterface, err ...*Error) {
//防止空数据问题
if config == nil {
@@ -263,11 +581,20 @@ func (that *HoTimeCache) Init(config Map, hotimeDb HoTimeDBInterface, err ...*Er
if db.Get("timeout") == nil {
db["timeout"] = 60 * 60 * 24 * 30
}
// mode 默认为 "compatible"(兼容模式,便于老系统平滑升级)
if db.Get("mode") == nil {
db["mode"] = CacheModeCompatible
}
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"),
Mode: db.GetString("mode"),
Db: hotimeDb,
}
if err[0] != nil {
that.dbCache.SetError(err[0])
+605 -85
View File
@@ -1,11 +1,43 @@
package cache
import (
. "../common"
"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"
CacheHistoryTableName = "hotime_cache_history"
LegacyCacheTableName = "cached" // 老版本缓存表名
DefaultCacheTimeout = 24 * 60 * 60 // 默认过期时间 24 小时
CacheModeNew = "new" // 新模式:只使用新表
CacheModeCompatible = "compatible" // 兼容模式:写新读老
)
type HoTimeDBInterface interface {
@@ -24,150 +56,638 @@ type CacheDb struct {
TimeOut int64
DbSet bool
SessionSet bool
HistorySet bool // 是否开启历史记录
Mode string // 缓存模式:"new"(默认,只用新表) 或 "compatible"(写新读老)
Db HoTimeDBInterface
*Error
ContextBase
isInit bool
}
func (this *CacheDb) GetError() *Error {
return this.Error
func (that *CacheDb) GetError() *Error {
return that.Error
}
func (this *CacheDb) SetError(err *Error) {
this.Error = err
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
}
// getLegacyTableName 获取带前缀的老版本缓存表名
func (that *CacheDb) getLegacyTableName() string {
return that.Db.GetPrefix() + LegacyCacheTableName
}
// isCompatibleMode 是否为兼容模式
func (that *CacheDb) isCompatibleMode() bool {
return that.Mode == CacheModeCompatible
}
// getEffectiveMode 获取有效模式(默认为 new)
func (that *CacheDb) getEffectiveMode() string {
if that.Mode == CacheModeCompatible {
return CacheModeCompatible
}
return CacheModeNew
}
// initDbTable 初始化数据库表
func (that *CacheDb) initDbTable() {
if that.isInit {
return
}
if that.Db.GetType() == "mysql" {
dbNames := that.Db.Query("SELECT DATABASE()")
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, `ckey` varchar(60) DEFAULT NULL, `cvalue` 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
}
dbType := that.Db.GetType()
tableName := that.getTableName()
historyTableName := that.getHistoryTableName()
legacyTableName := that.getLegacyTableName()
// 检查并创建主表
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,
"ckey" TEXT(60),
"cvalue" TEXT(2000),
"time" integer,
"endtime" integer
);`)
if e.GetError() == nil {
that.isInit = true
// 根据模式处理老表
// new 模式:迁移老表数据到新表(不删除老表,由人工删除)
// compatible 模式:不迁移,老表继续使用
if that.getEffectiveMode() == CacheModeNew {
if that.tableExists(legacyTableName) {
that.migrateFromCached(dbType, legacyTableName, tableName)
}
}
// compatible 模式不做任何处理,老表保留供读取
// 检查并创建历史表(开启历史记录时)
if that.HistorySet && !that.tableExists(historyTableName) {
that.createHistoryTable(dbType, historyTableName)
}
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{"ckey": 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 最大)
// 使用 INSERT IGNORE 避免重复 key 冲突
migrateSQL = "INSERT IGNORE 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 OR IGNORE 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 ` +
`ON CONFLICT ("key") DO NOTHING`
}
// 执行迁移,不删除老表(由人工确认后删除,更安全)
that.Db.Exec(migrateSQL)
}
// 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)
}
// getLegacy 从老表获取缓存(兼容模式使用)
// 老表结构:key, value, endtime(unix秒), time(纳秒时间戳)
func (that *CacheDb) getLegacy(key string) interface{} {
legacyTableName := that.getLegacyTableName()
// 检查老表是否存在
if !that.tableExists(legacyTableName) {
return nil
}
cached := that.Db.Get(legacyTableName, "*", Map{"key": key})
if cached == nil {
return nil
}
//data:=cacheMap[key];
if cached.GetInt64("endtime") <= time.Now().Unix() {
that.Db.Delete("cached", Map{"id": cached.GetString("id")})
// 检查过期时间(老表使用 unix 时间戳)
endTime := cached.GetInt64("endtime")
nowUnix := time.Now().Unix()
if endTime > 0 && endTime <= nowUnix {
// 已过期,删除该条记录
that.deleteLegacy(key)
return nil
}
data := Map{}
data.JsonToMap(cached.GetString("cvalue"))
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{"cvalue": string(bte), "time": time.Now().UnixNano(), "endtime": tim}, Map{"ckey": key})
if num == int64(0) {
that.Db.Insert("cached", Map{"cvalue": string(bte), "time": time.Now().UnixNano(), "endtime": tim, "ckey": key})
// 解析 value(老表 value 格式可能是 {"data": xxx} 或直接值)
valueStr := cached.GetString("value")
if valueStr == "" {
return nil
}
//随机执行删除命令
if Rand(1000) > 950 {
that.Db.Delete("cached", Map{"endtime[<]": time.Now().Unix()})
var data interface{}
err := json.Unmarshal([]byte(valueStr), &data)
if err != nil {
return nil
}
// 兼容老版本 {"data": xxx} 包装格式
if dataMap, ok := data.(map[string]interface{}); ok {
if innerData, exists := dataMap["data"]; exists {
return innerData
}
}
return data
}
func (that *CacheDb) delete(key string) {
// deleteLegacy 从老表删除缓存(兼容模式使用)
func (that *CacheDb) deleteLegacy(key string) {
legacyTableName := that.getLegacyTableName()
// 检查老表是否存在
if !that.tableExists(legacyTableName) {
return
}
del := strings.Index(key, "*")
//如果通配删除
// 如果通配删除
if del != -1 {
key = Substr(key, 0, del)
that.Db.Delete("cached", Map{"ckey": key + "%"})
keyPrefix := Substr(key, 0, del)
that.Db.Delete(legacyTableName, Map{"key[~]": keyPrefix + "%"})
} else {
that.Db.Delete("cached", Map{"ckey": key})
that.Db.Delete(legacyTableName, Map{"key": key})
}
}
func (that *CacheDb) Cache(key string, data ...interface{}) *Obj {
// get 获取缓存
func (that *CacheDb) get(key string) interface{} {
tableName := that.getTableName()
cached := that.Db.Get(tableName, "*", Map{"key": key})
if cached != nil {
// 使用字符串比较判断过期(ISO 格式天然支持)
endTime := cached.GetString("end_time")
nowTime := Time2Str(time.Now())
if endTime != "" && endTime <= nowTime {
// 惰性删除:过期只返回 nil,不立即删除
// 依赖随机清理批量删除过期数据
// 继续检查老表(如果是兼容模式)
} else {
// 直接解析 value,不再需要 {"data": value} 包装
valueStr := cached.GetString("value")
if valueStr != "" {
var data interface{}
err := json.Unmarshal([]byte(valueStr), &data)
if err == nil {
return data
}
}
}
}
// 兼容模式:新表没有数据时,回退读取老表
if that.isCompatibleMode() {
return that.getLegacy(key)
}
return nil
}
// 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,
})
}
}
// 写入历史记录
that.writeHistory(key)
// 兼容模式:写新表后删除老表同 key 记录(加速老数据消亡)
if that.isCompatibleMode() {
that.deleteLegacy(key)
}
// 随机执行删除过期数据命令(5% 概率)
if Rand(1000) > 950 {
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 {
keyPrefix := Substr(key, 0, del)
that.Db.Delete(tableName, Map{"key[~]": keyPrefix + "%"})
} else {
that.Db.Delete(tableName, Map{"key": key})
}
// 兼容模式:同时删除老表中的 key(避免回退读到老数据)
if that.isCompatibleMode() {
that.deleteLegacy(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()
// 获取缓存
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 {
that.SetError(nil)
tempt := ObjToInt64(data[1], that.Error)
if tempt > tim {
tim = tempt
} else if that.GetError() == nil {
tim = tim + tempt
} else if len(data) >= 2 {
// 使用指定的超时时间
var err Error
tempTimeout := ObjToInt64(data[1], &err)
if err.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}
}
// CachesGet 批量获取缓存(使用 IN 查询优化)
// 返回 Mapkey 为缓存键,value 为缓存值(不存在或过期的 key 不包含在结果中)
func (that *CacheDb) CachesGet(keys []string) Map {
that.initDbTable()
result := make(Map, len(keys))
if len(keys) == 0 {
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())
// 使用 IN 查询批量获取
cachedList := that.Db.Select(tableName, "*", Map{
"key": keys,
"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 != "" {
var data interface{}
err := json.Unmarshal([]byte(valueStr), &data)
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)
if legacyData != nil {
result[key] = legacyData
}
}
}
}
// #region agent log
debugLogDb("E", "cache_db.go:CachesGet:end", "CacheDb.CachesGet完成", map[string]interface{}{
"result_count": len(result),
})
// #endregion
return result
}
// CachesSet 批量设置缓存
// data: Mapkey 为缓存键,value 为缓存值
// timeout: 可选过期时间(秒),不传则使用默认超时时间
func (that *CacheDb) CachesSet(data Map, timeout ...int64) {
that.initDbTable()
if len(data) == 0 {
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 {
tim = timeout[0]
} else {
tim = that.TimeOut
if tim == 0 {
tim = DefaultCacheTimeout
}
}
endTime := time.Now().Add(time.Duration(tim) * time.Second)
// 逐个设置(保持事务一致性和历史记录)
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 批量删除缓存
func (that *CacheDb) CachesDelete(keys []string) {
that.initDbTable()
if len(keys) == 0 {
return
}
tableName := that.getTableName()
// 使用 IN 条件批量删除
that.Db.Delete(tableName, Map{"key": keys})
// 兼容模式:同时删除老表中的 keys
if that.isCompatibleMode() {
legacyTableName := that.getLegacyTableName()
if that.tableExists(legacyTableName) {
that.Db.Delete(legacyTableName, Map{"key": keys})
}
}
}
+109 -113
View File
@@ -1,158 +1,154 @@
package cache
import (
. "../common"
. "code.hoteas.com/golang/hotime/common"
"strings"
"sync"
"time"
)
// CacheMemory 基于 sync.Map 的缓存实现
type CacheMemory struct {
TimeOut int64
DbSet bool
SessionSet bool
Map
*Error
ContextBase
mutex *sync.RWMutex
cache sync.Map // 替代传统的 Map
}
func (this *CacheMemory) GetError() *Error {
func (that *CacheMemory) GetError() *Error {
return this.Error
return that.Error
}
func (this *CacheMemory) SetError(err *Error) {
this.Error = err
func (that *CacheMemory) SetError(err *Error) {
that.Error = err
}
func (c *CacheMemory) get(key string) (res *Obj) {
//获取Cache键只能为string类型
func (this *CacheMemory) get(key string) interface{} {
this.Error.SetError(nil)
if this.Map == nil {
this.Map = Map{}
res = &Obj{
Error: *c.Error,
}
value, ok := c.cache.Load(key)
if !ok {
return res // 缓存不存在
}
if this.Map[key] == nil {
return nil
}
data := this.Map.Get(key, this.Error).(cacheData)
if this.Error.GetError() != nil {
return nil
}
data := value.(cacheData)
// 检查是否过期
if data.time < time.Now().Unix() {
delete(this.Map, key)
return nil
c.cache.Delete(key) // 删除过期缓存
return res
}
return data.data
res.Data = data.data
return res
}
func (this *CacheMemory) refreshMap() {
func (c *CacheMemory) set(key string, value interface{}, expireAt int64) {
data := cacheData{
data: value,
time: expireAt,
}
c.cache.Store(key, data)
}
func (c *CacheMemory) delete(key string) {
if strings.Contains(key, "*") {
// 通配符删除
prefix := strings.TrimSuffix(key, "*")
c.cache.Range(func(k, v interface{}) bool {
if strings.HasPrefix(k.(string), prefix) {
c.cache.Delete(k)
}
return true
})
} else {
// 精确删除
c.cache.Delete(key)
}
}
func (c *CacheMemory) refreshMap() {
go func() {
this.mutex.Lock()
defer this.mutex.Unlock()
for key, v := range this.Map {
data := v.(cacheData)
if data.time <= time.Now().Unix() {
delete(this.Map, key)
now := time.Now().Unix()
c.cache.Range(func(key, value interface{}) bool {
data := value.(cacheData)
if data.time <= now {
c.cache.Delete(key) // 删除过期缓存
}
}
return true
})
}()
}
func (c *CacheMemory) Cache(key string, data ...interface{}) *Obj {
now := time.Now().Unix()
//key value ,时间为时间戳
func (this *CacheMemory) set(key string, value interface{}, time int64) {
this.Error.SetError(nil)
var data cacheData
if this.Map == nil {
this.Map = Map{}
// 随机触发刷新
if x := RandX(1, 100000); x > 99950 {
c.refreshMap()
}
dd := this.Map[key]
if dd == nil {
data = cacheData{}
} else {
data = dd.(cacheData)
}
data.time = time
data.data = value
this.Map.Put(key, data)
}
func (this *CacheMemory) delete(key string) {
del := strings.Index(key, "*")
//如果通配删除
if del != -1 {
key = Substr(key, 0, del)
for k, _ := range this.Map {
if strings.Index(k, key) != -1 {
delete(this.Map, k)
}
}
} else {
delete(this.Map, key)
}
}
func (this *CacheMemory) Cache(key string, data ...interface{}) *Obj {
x := RandX(1, 100000)
if x > 99950 {
this.refreshMap()
}
if this.mutex == nil {
this.mutex = &sync.RWMutex{}
}
reData := &Obj{Data: nil}
if len(data) == 0 {
this.mutex.RLock()
reData.Data = this.get(key)
this.mutex.RUnlock()
return reData
// 读操作
return c.get(key)
}
tim := time.Now().Unix()
if len(data) == 1 && data[0] == nil {
this.mutex.Lock()
this.delete(key)
this.mutex.Unlock()
return reData
// 删除操作
c.delete(key)
return nil
}
if len(data) == 1 {
tim = tim + this.TimeOut
}
// 写操作
expireAt := now + c.TimeOut
if len(data) == 2 {
this.Error.SetError(nil)
tempt := ObjToInt64(data[1], this.Error)
if tempt > tim {
tim = tempt
} else if this.Error.GetError() == nil {
tim = tim + tempt
if customExpire, ok := data[1].(int64); ok {
if customExpire > now {
expireAt = customExpire
} else {
expireAt = now + customExpire
}
}
}
this.mutex.Lock()
this.set(key, data[0], tim)
this.mutex.Unlock()
return reData
c.set(key, data[0], expireAt)
return nil
}
// CachesGet 批量获取缓存
// 返回 Mapkey 为缓存键,value 为缓存值(不存在或过期的 key 不包含在结果中)
func (c *CacheMemory) CachesGet(keys []string) Map {
result := make(Map, len(keys))
for _, key := range keys {
obj := c.get(key)
if obj != nil && obj.Data != nil {
result[key] = obj.Data
}
}
return result
}
// CachesSet 批量设置缓存
// data: Mapkey 为缓存键,value 为缓存值
// timeout: 可选过期时间(秒),不传则使用默认超时时间
func (c *CacheMemory) CachesSet(data Map, timeout ...int64) {
now := time.Now().Unix()
expireAt := now + c.TimeOut
if len(timeout) > 0 && timeout[0] > 0 {
if timeout[0] > now {
expireAt = timeout[0]
} else {
expireAt = now + timeout[0]
}
}
for key, value := range data {
c.set(key, value, expireAt)
}
}
// CachesDelete 批量删除缓存
func (c *CacheMemory) CachesDelete(keys []string) {
for _, key := range keys {
c.delete(key)
}
}
+204 -88
View File
@@ -1,9 +1,10 @@
package cache
import (
. "../common"
. "code.hoteas.com/golang/hotime/common"
"github.com/garyburd/redigo/redis"
"strings"
"sync"
"time"
)
@@ -14,157 +15,272 @@ type CacheRedis struct {
Host string
Pwd string
Port int64
conn redis.Conn
pool *redis.Pool
tag int64
ContextBase
*Error
initOnce sync.Once
}
func (this *CacheRedis) GetError() *Error {
func (that *CacheRedis) GetError() *Error {
return this.Error
return that.Error
}
func (this *CacheRedis) SetError(err *Error) {
this.Error = err
func (that *CacheRedis) SetError(err *Error) {
that.Error = err
}
//唯一标志
func (this *CacheRedis) GetTag() int64 {
// 唯一标志
func (that *CacheRedis) GetTag() int64 {
if this.tag == int64(0) {
this.tag = time.Now().UnixNano()
if that.tag == int64(0) {
that.tag = time.Now().UnixNano()
}
return this.tag
return that.tag
}
func (this *CacheRedis) reCon() bool {
var err error
this.conn, err = redis.Dial("tcp", this.Host+":"+ObjToStr(this.Port))
if err != nil {
this.conn = nil
this.Error.SetError(err)
return false
}
if this.Pwd != "" {
_, err = this.conn.Do("AUTH", this.Pwd)
if err != nil {
this.conn = nil
this.Error.SetError(err)
return false
// initPool 初始化连接池(只执行一次)
func (that *CacheRedis) initPool() {
that.initOnce.Do(func() {
that.pool = &redis.Pool{
MaxIdle: 10, // 最大空闲连接数
MaxActive: 100, // 最大活跃连接数,0表示无限制
IdleTimeout: 5 * time.Minute, // 空闲连接超时时间
Wait: true, // 当连接池耗尽时是否等待
Dial: func() (redis.Conn, error) {
conn, err := redis.Dial("tcp", that.Host+":"+ObjToStr(that.Port),
redis.DialConnectTimeout(5*time.Second),
redis.DialReadTimeout(3*time.Second),
redis.DialWriteTimeout(3*time.Second),
)
if err != nil {
return nil, err
}
if that.Pwd != "" {
if _, err := conn.Do("AUTH", that.Pwd); err != nil {
conn.Close()
return nil, err
}
}
return conn, nil
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if time.Since(t) < time.Minute {
return nil
}
_, err := c.Do("PING")
return err
},
}
}
return true
})
}
func (this *CacheRedis) del(key string) {
// getConn 从连接池获取连接
func (that *CacheRedis) getConn() redis.Conn {
that.initPool()
if that.pool == nil {
return nil
}
return that.pool.Get()
}
func (that *CacheRedis) del(key string) {
conn := that.getConn()
if conn == nil {
return
}
defer conn.Close()
del := strings.Index(key, "*")
if del != -1 {
val, err := redis.Strings(this.conn.Do("KEYS", key))
val, err := redis.Strings(conn.Do("KEYS", key))
if err != nil {
that.Error.SetError(err)
return
}
this.conn.Send("MULTI")
for i, _ := range val {
this.conn.Send("DEL", val[i])
if len(val) == 0 {
return
}
conn.Send("MULTI")
for i := range val {
conn.Send("DEL", val[i])
}
_, err = conn.Do("EXEC")
if err != nil {
that.Error.SetError(err)
}
this.conn.Do("EXEC")
} else {
_, err := this.conn.Do("DEL", key)
_, err := conn.Do("DEL", key)
if err != nil {
this.Error.SetError(err)
_, err = this.conn.Do("PING")
if err != nil {
if this.reCon() {
_, err = this.conn.Do("DEL", key)
}
}
that.Error.SetError(err)
}
}
}
//key value ,时间为时间戳
func (this *CacheRedis) set(key string, value string, time int64) {
_, err := this.conn.Do("SET", key, value, "EX", ObjToStr(time))
// key value ,时间为时间戳
func (that *CacheRedis) set(key string, value string, expireSeconds int64) {
conn := that.getConn()
if conn == nil {
return
}
defer conn.Close()
_, err := conn.Do("SET", key, value, "EX", ObjToStr(expireSeconds))
if err != nil {
this.Error.SetError(err)
_, err = this.conn.Do("PING")
if err != nil {
if this.reCon() {
_, err = this.conn.Do("SET", key, value, "EX", ObjToStr(time))
}
}
that.Error.SetError(err)
}
}
func (this *CacheRedis) get(key string) *Obj {
func (that *CacheRedis) get(key string) *Obj {
reData := &Obj{}
conn := that.getConn()
if conn == nil {
return reData
}
defer conn.Close()
var err error
reData.Data, err = redis.String(this.conn.Do("GET", key))
reData.Data, err = redis.String(conn.Do("GET", key))
if err != nil {
reData.Data = nil
if !strings.Contains(err.Error(), "nil returned") {
this.Error.SetError(err)
_, err = this.conn.Do("PING")
if err != nil {
if this.reCon() {
reData.Data, err = redis.String(this.conn.Do("GET", key))
}
}
return reData
that.Error.SetError(err)
}
}
return reData
}
func (this *CacheRedis) Cache(key string, data ...interface{}) *Obj {
func (that *CacheRedis) Cache(key string, data ...interface{}) *Obj {
reData := &Obj{}
if this.conn == nil {
re := this.reCon()
if !re {
return reData
}
}
//查询缓存
if len(data) == 0 {
reData = this.get(key)
reData = that.get(key)
return reData
}
tim := int64(0)
//删除缓存
if len(data) == 1 && data[0] == nil {
this.del(key)
that.del(key)
return reData
}
//添加缓存
if len(data) == 1 {
if this.TimeOut == 0 {
//this.Time = Config.GetInt64("cacheShortTime")
if that.TimeOut == 0 {
//that.Time = Config.GetInt64("cacheShortTime")
}
tim += this.TimeOut
tim += that.TimeOut
}
if len(data) == 2 {
this.Error.SetError(nil)
tempt := ObjToInt64(data[1], this.Error)
that.Error.SetError(nil)
tempt := ObjToInt64(data[1], that.Error)
if tempt > tim {
tim = tempt
} else if this.GetError() == nil {
} else if that.GetError() == nil {
tim = tim + tempt
}
}
this.set(key, ObjToStr(data[0]), tim)
that.set(key, ObjToStr(data[0]), tim)
return reData
}
// CachesGet 批量获取缓存(使用 Redis MGET 命令优化)
// 返回 Mapkey 为缓存键,value 为缓存值(不存在的 key 不包含在结果中)
func (that *CacheRedis) CachesGet(keys []string) Map {
result := make(Map, len(keys))
if len(keys) == 0 {
return result
}
conn := that.getConn()
if conn == nil {
return result
}
defer conn.Close()
// 构建 MGET 参数
args := make([]interface{}, len(keys))
for i, key := range keys {
args[i] = key
}
values, err := redis.Strings(conn.Do("MGET", args...))
if err != nil {
if !strings.Contains(err.Error(), "nil returned") {
that.Error.SetError(err)
}
return result
}
// 将结果映射回 Map
for i, value := range values {
if value != "" {
result[keys[i]] = value
}
}
return result
}
// CachesSet 批量设置缓存(使用 Redis pipeline 优化)
// data: Mapkey 为缓存键,value 为缓存值
// timeout: 可选过期时间(秒),不传则使用默认超时时间
func (that *CacheRedis) CachesSet(data Map, timeout ...int64) {
if len(data) == 0 {
return
}
conn := that.getConn()
if conn == nil {
return
}
defer conn.Close()
tim := that.TimeOut
if len(timeout) > 0 && timeout[0] > 0 {
if timeout[0] > tim {
tim = timeout[0]
} else {
tim = tim + timeout[0]
}
}
// 使用 pipeline 批量设置
conn.Send("MULTI")
for key, value := range data {
conn.Send("SET", key, ObjToStr(value), "EX", ObjToStr(tim))
}
_, err := conn.Do("EXEC")
if err != nil {
that.Error.SetError(err)
}
}
// CachesDelete 批量删除缓存(使用 Redis DEL 命令批量删除)
func (that *CacheRedis) CachesDelete(keys []string) {
if len(keys) == 0 {
return
}
conn := that.getConn()
if conn == nil {
return
}
defer conn.Close()
// 构建 DEL 参数
args := make([]interface{}, len(keys))
for i, key := range keys {
args[i] = key
}
_, err := conn.Do("DEL", args...)
if err != nil {
that.Error.SetError(err)
}
}
+6 -2
View File
@@ -1,7 +1,7 @@
package cache
import (
. "../common"
. "code.hoteas.com/golang/hotime/common"
)
type CacheIns interface {
@@ -11,9 +11,13 @@ type CacheIns interface {
GetError() *Error
SetError(err *Error)
Cache(key string, data ...interface{}) *Obj
// 批量操作
CachesGet(keys []string) Map // 批量获取
CachesSet(data Map, timeout ...int64) // 批量设置
CachesDelete(keys []string) // 批量删除
}
//单条缓存数据
// 单条缓存数据
type cacheData struct {
time int64
data interface{}
+1260
View File
File diff suppressed because it is too large Load Diff
+141 -46
View File
@@ -1,19 +1,34 @@
package code
import (
. "../common"
. "code.hoteas.com/golang/hotime/common"
)
var Config = Map{
"name": "HoTimeDashBoard",
"id": "2f92h3herh23rh2y8",
"name": "HoTimeDashBoard",
//"id": "2f92h3herh23rh2y8",
"label": "HoTime管理平台",
"stop": Slice{"role", "org"}, //不更新的,同时不允许修改用户自身对应的表数据
"labelConfig": Map{
"show": "开启",
"add": "添加",
"delete": "删除",
"edit": "编辑",
"info": "查看详情",
"download": "下载清单",
},
"menus": []Map{
{"label": "平台首页", "name": "HelloWorld", "icon": "el-icon-s-home"},
//{"label": "平台首页", "name": "HelloWorld", "icon": "el-icon-s-home"},
//{"label": "测试表格", "table": "table", "icon": "el-icon-suitcase"},
//{"label": "系统管理", "name": "setting", "icon": "el-icon-setting",
// "menus": []Map{
// {"label": "用户管理", "table": "user"},
// {"label": "用户管理", "table": "user",
// "default": {
// "path": "info",
// "id": "1"
// },
// "auth": ["show","edit","info","add","delete"],
// },
// {"label": "组织管理", "table": "organization"},
// {"label": "地区管理", "table": "area"},
// {"label": "角色管理", "table": "role"},
@@ -33,6 +48,7 @@ var ColumnDataType = map[string]string{
"float": "number",
"double": "number",
"decimal": "number",
"integer": "number", //sqlite3
"char": "text",
"text": "text",
"blob": "text",
@@ -43,51 +59,130 @@ var ColumnDataType = map[string]string{
}
type ColumnShow struct {
Name string
List bool
Edit bool
Info bool
Must bool
Name string //名称
List bool //列表权限
Edit bool //新增和编辑权限
Info bool //详情权限
Must bool //字段全匹配
Type string //空字符串表示
Strict bool //name严格匹配必须是这个词才行
}
var ColumnNameType = []ColumnShow{
//通用
{"idcard", false, true, true, false, "", false},
{"id", true, false, true, false, "", true},
//"sn"{true,true,true,""},
{"status", true, true, true, false, "select", false},
{"state", false, true, true, false, "select", false},
{"sex", true, true, true, false, "select", false},
{"delete", false, false, false, false, "", false},
var RuleConfig = []Map{
{"name": "idcard", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "id", "add": false, "list": true, "edit": false, "info": true, "must": false, "strict": true, "type": ""},
{"name": "sn", "add": false, "list": true, "edit": false, "info": true, "must": false, "strict": false, "type": ""},
{"name": "parent_ids", "add": false, "list": false, "edit": false, "info": false, "must": false, "strict": true, "type": "index"},
{"name": "index", "add": false, "list": false, "edit": false, "info": false, "must": false, "strict": true, "type": "index"},
{"lat", false, true, true, false, "", false},
{"lng", false, true, true, false, "", false},
{"latitude", false, true, true, false, "", false},
{"longitude", false, true, true, false, "", false},
{"name": "parent_id", "add": true, "list": true, "edit": true, "info": true, "must": false, "true": false, "type": ""},
{"index", false, false, false, false, "", false},
{"password", false, true, false, false, "password", false},
{"pwd", false, true, false, false, "password", false},
{"info", false, true, true, false, "", false},
{"version", false, false, false, false, "", false},
{"seq", false, true, true, false, "", false},
{"sort", false, true, true, false, "", false},
{"note", false, true, true, false, "", false},
{"description", false, true, true, false, "", false},
{"abstract", false, true, true, false, "", false},
{"content", false, true, true, false, "", false},
{"address", false, true, true, false, "", false},
{"full_name", false, true, true, false, "", false},
{"create_time", false, false, true, false, "", false},
{"modify_time", false, false, true, false, "", false},
{"image", false, true, true, false, "image", false},
{"img", false, true, true, false, "image", false},
{"avatar", false, true, true, false, "image", false},
{"file", false, true, true, false, "file", false},
{"age", false, true, true, false, "", false},
{"email", false, true, true, false, "", false},
{"time", true, false, true, false, "", false},
{"level", false, false, true, false, "", false},
{"name": "amount", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": true, "type": "money"},
{"name": "info", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "textArea"},
{"name": "status", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": "select"},
{"name": "state", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": "select"},
{"name": "sex", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": "select"},
{"name": "delete", "add": false, "list": false, "edit": false, "info": false, "must": false, "strict": false, "type": ""},
{"name": "lat", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "lng", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "latitude", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "longitude", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "password", "add": true, "list": false, "edit": true, "info": false, "must": false, "strict": false, "type": "password"},
{"name": "pwd", "add": true, "list": false, "edit": true, "info": false, "must": false, "strict": false, "type": "password"},
{"name": "version", "add": false, "list": false, "edit": false, "info": false, "must": false, "strict": false, "type": ""},
{"name": "seq", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "sort", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "note", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "description", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "abstract", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "content", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "textArea"},
{"name": "address", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "full_name", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "create_time", "add": false, "list": false, "edit": false, "info": true, "must": false, "strict": true, "type": "time"},
{"name": "modify_time", "add": false, "list": true, "edit": false, "info": true, "must": false, "strict": true, "type": "time"},
{"name": "image", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "image"},
{"name": "img", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "image"},
{"name": "avatar", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "image"},
{"name": "icon", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "image"},
{"name": "file", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "file"},
{"name": "age", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "email", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": ""},
{"name": "time", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": "time"},
{"name": "level", "add": false, "list": false, "edit": false, "info": true, "must": false, "strict": false, "type": ""},
{"name": "rule", "add": true, "list": true, "edit": true, "info": true, "must": false, "strict": false, "type": "form"},
{"name": "auth", "add": true, "list": false, "edit": true, "info": true, "must": false, "strict": false, "type": "auth"},
{"name": "table", "add": false, "list": true, "edit": false, "info": true, "must": false, "strict": false, "type": "table"},
{"name": "table_id", "add": false, "list": true, "edit": false, "info": true, "must": false, "strict": false, "type": "table_id"},
}
//var ColumnNameType = []ColumnShow{
// //通用
// {"idcard", false, true, true, false, "", false},
// {"id", true, false, true, false, "", true},
// {"sn", true, false, true, false, "", false},
// {"parent_ids", false, false, false, false, "index", true},
// {"parent_id", true, true, true, false, "", true},
// {"amount", true, true, true, false, "money", true},
// {"info", false, true, true, false, "textArea", false},
// //"sn"{true,true,true,""},
// {"status", true, true, true, false, "select", false},
// {"state", true, true, true, false, "select", false},
// {"sex", true, true, true, false, "select", false},
// {"delete", false, false, false, false, "", false},
//
// {"lat", false, true, true, false, "", false},
// {"lng", false, true, true, false, "", false},
// {"latitude", false, true, true, false, "", false},
// {"longitude", false, true, true, false, "", false},
//
// {"index", false, false, false, false, "index", false},
//
// {"password", false, true, false, false, "password", false},
// {"pwd", false, true, false, false, "password", false},
//
// {"version", false, false, false, false, "", false},
// {"seq", false, true, true, false, "", false},
// {"sort", false, true, true, false, "", false},
// {"note", false, true, true, false, "", false},
// {"description", false, true, true, false, "", false},
// {"abstract", false, true, true, false, "", false},
// {"content", false, true, true, false, "textArea", false},
// {"address", true, true, true, false, "", false},
// {"full_name", false, true, true, false, "", false},
// {"create_time", false, false, true, false, "time", true},
// {"modify_time", true, false, true, false, "time", true},
// {"image", false, true, true, false, "image", false},
// {"img", false, true, true, false, "image", false},
// {"icon", false, true, true, false, "image", false},
// {"avatar", false, true, true, false, "image", false},
// {"file", false, true, true, false, "file", false},
// {"age", false, true, true, false, "", false},
// {"email", false, true, true, false, "", false},
// {"time", true, true, true, false, "time", false},
// {"level", false, false, true, false, "", false},
// {"rule", true, true, true, false, "form", false},
// {"auth", false, true, true, false, "auth", true},
// {"table", true, false, true, false, "table", false},
// {"table_id", true, false, true, false, "table_id", false},
//}
+939 -328
View File
File diff suppressed because it is too large Load Diff
+134 -48
View File
@@ -3,8 +3,8 @@ package code
var InitTpt = `package {{name}}
import (
. "../../../hotime"
. "../../../hotime/common"
. "code.hoteas.com/golang/hotime"
. "code.hoteas.com/golang/hotime/common"
)
var ID = "{{id}}"
@@ -14,39 +14,84 @@ var Project = Proj{
//"user": UserCtr,
{{tablesCtr}}
"hotime":Ctr{
"login": func(this *Context) {
name:=this.Req.FormValue("name")
password:=this.Req.FormValue("password")
if name==""||password==""{
this.Display(3,"参数不足")
"login": func(that *Context) {
name := that.Req.FormValue("name")
password := that.Req.FormValue("password")
if name == "" || password == "" {
that.Display(3, "参数不足")
return
}
user:=this.Db.Get("{{name}}","*",Map{"AND":Map{"name":name,"password":Md5(password)}})
if user==nil{
this.Display(5,"登录失败")
user := that.Db.Get("admin", "*", Map{"AND": Map{"OR":Map{"name": name,"phone":name}, "password": Md5(password)}})
if user == nil {
that.Display(5, "登录失败")
return
}
this.Session("id",user.GetCeilInt("id"))
this.Session("name",name)
this.Display(0,"登录成功")
that.Session("admin_id", user.GetCeilInt("id"))
that.Session("admin_name", name)
that.Display(0, that.SessionId)
},
"logout": func(this *Context) {
this.Session("id",nil)
this.Session("name",nil)
this.Display(0,"退出登录成功")
"logout": func(that *Context) {
that.Session("admin_id", nil)
that.Session("admin_name", nil)
that.Display(0, "退出登录成功")
},
"info": func(that *Context) {
hotimeName := that.RouterString[0]
data := that.Db.Get("admin", "*", Map{"id": that.Session("admin_id").ToCeilInt()})
str, inData := that.MakeCodeRouter[hotimeName].Info("admin", data, that.Db)
where := Map{"id": that.Session("admin_id").ToCeilInt()}
if len(inData) ==1 {
inData["id"] =where["id"]
where = Map{"AND": inData}
}else if len(inData) >1 {
where["OR"]=inData
where = Map{"AND": where}
}
re := that.Db.Get("admin", str, where)
if re == nil {
that.Display(4, "找不到对应信息")
return
}
for k, v := range re {
column := that.MakeCodeRouter[hotimeName].TableColumns["admin"][k]
if column == nil {
continue
}
if (column["list"] == nil || column.GetBool("list")) && column.GetString("link") != "" {
re[column.GetString("link")] = that.Db.Get(column.GetString("link"),"id," +column.GetString("value"), Map{"id": v})
}
}
that.Display(0, re)
},
},
}
`
var CtrTpt = `package {{name}}
import (
. "../../../hotime"
. "../../../hotime/common"
. "code.hoteas.com/golang/hotime"
. "code.hoteas.com/golang/hotime/common"
"strings"
)
var {{table}}Ctr = Ctr{
"info": func(that *Context) {
re := that.Db.Get(that.RouterString[1], that.MakeCode.Info(that.RouterString[1]), Map{"id": that.RouterString[2]})
hotimeName := that.RouterString[0]
data := that.Db.Get("admin", "*", Map{"id": that.Session("admin_id").ToCeilInt()})
str, inData := that.MakeCodeRouter[hotimeName].Info(that.RouterString[1], data, that.Db)
where := Map{"id": that.RouterString[2]}
if len(inData) ==1 {
inData["id"] =where["id"]
where = Map{"AND": inData}
}else if len(inData) >1 {
where["OR"]=inData
where = Map{"AND": where}
}
re := that.Db.Get(that.RouterString[1], str, where)
if re == nil {
that.Display(4, "找不到对应信息")
@@ -54,39 +99,67 @@ var {{table}}Ctr = Ctr{
}
for k, v := range re {
column:=that.MakeCode.TableColumns[that.RouterString[1]][k]
if column==nil{
column := that.MakeCodeRouter[hotimeName].TableColumns[that.RouterString[1]][k]
if column == nil {
continue
}
if (column["list"]==nil||column.GetBool("list"))&&column.GetString("link")!=""{
re[column.GetString("link")] = that.Db.Get(column.GetString("link"), column.GetString("value"), Map{"id": v})
if (column["list"] == nil || column.GetBool("list")) && column.GetString("link") != "" {
re[column.GetString("link")] = that.Db.Get(column.GetString("link"), "id,"+column.GetString("value"), Map{"id": v})
}
}
that.Display(0, re)
},
"add": func(that *Context) {
inData := that.MakeCode.Add(that.RouterString[1], that.Req)
hotimeName := that.RouterString[0]
inData := that.MakeCodeRouter[hotimeName].Add(that.RouterString[1], that.Req)
if inData == nil {
that.Display(3, "请求参数不足")
return
}
re := that.Db.Insert(that.RouterString[1], inData)
if re == 0 {
that.Display(4, "无法插入对应数据")
return
}
//索引管理,便于检索以及权限
if inData.Get("parent_id") != nil && inData.GetString("index") != "" {
index := that.Db.Get(that.RouterString[1], "` + "`index`" + `", Map{"id": inData.Get("parent_id")})
inData["index"] = index.GetString("index")+ObjToStr(re)+","
that.Db.Update(that.RouterString[1],Map{"index":inData["index"]},Map{"id":re})
}else if inData.GetString("index") != ""{
inData["index"] = "," + ObjToStr(re) + ","
that.Db.Update(that.RouterString[1], Map{"index": inData["index"]}, Map{"id": re})
}
that.Display(0, re)
},
"update": func(that *Context) {
inData := that.MakeCode.Edit(that.RouterString[1], that.Req)
hotimeName := that.RouterString[0]
inData := that.MakeCodeRouter[hotimeName].Edit(that.RouterString[1], that.Req)
if inData == nil {
that.Display(3, "没有找到要更新的数据")
return
}
//索引管理,便于检索以及权限
if inData.Get("parent_id") != nil && inData.GetString("index") != "" {
Index := that.Db.Get(that.RouterString[1], "` + "`index`" + `", Map{"id": that.RouterString[2]})
parentIndex := that.Db.Get(that.RouterString[1], "` + "`index`" + `", Map{"id": inData.Get("parent_id")})
inData["index"] = parentIndex.GetString("index") + that.RouterString[2] + ","
childNodes := that.Db.Select(that.RouterString[1], "id,` + "`index`" + `", Map{"index[~]": "," + that.RouterString[2] + ","})
for _, v := range childNodes {
v["index"] = strings.Replace(v.GetString("index"), Index.GetString("index"), inData.GetString("index"), -1)
that.Db.Update(that.RouterString[1], Map{"index": v["index"]}, Map{"id": v.GetCeilInt("id")})
}
}
re := that.Db.Update(that.RouterString[1], inData, Map{"id": that.RouterString[2]})
if re == 0 {
@@ -97,54 +170,67 @@ var {{table}}Ctr = Ctr{
that.Display(0, re)
},
"remove": func(that *Context) {
re := that.Db.Delete(that.RouterString[1], Map{"id": that.RouterString[2]})
hotimeName := that.RouterString[0]
inData := that.MakeCodeRouter[hotimeName].Delete(that.RouterString[1], that.Req)
if inData == nil {
that.Display(3, "请求参数不足")
return
}
re :=int64(0)
//索引管理,便于检索以及权限
if inData.Get("parent_id") != nil && inData.GetSlice("index") != nil {
re=that.Db.Delete(that.RouterString[1], Map{"index[~]": "," + that.RouterString[2] + ","})
}else {
re=that.Db.Delete(that.RouterString[1], Map{"id": that.RouterString[2]})
}
if re == 0 {
that.Display(4, "删除数据失败")
return
}
that.Display(0, re)
that.Display(0, "删除成功")
},
"search": func(that *Context) {
hotimeName := that.RouterString[0]
data := that.Db.Get("admin", "*", Map{"id": that.Session("admin_id").ToCeilInt()})
columnStr, leftJoin, where := that.MakeCodeRouter[hotimeName].Search(that.RouterString[1], data, that.Req, that.Db)
columnStr,leftJoin,where := that.MakeCode.Search(that.RouterString[1], that.Req,that.Db)
page:=ObjToInt(that.Req.FormValue("page"))
pageSize:=ObjToInt(that.Req.FormValue("pageSize"))
page := ObjToInt(that.Req.FormValue("page"))
pageSize := ObjToInt(that.Req.FormValue("pageSize"))
if page<1{
page=1
if page < 1 {
page = 1
}
if pageSize<=0{
pageSize=20
if pageSize <= 0 {
pageSize = 20
}
count:=that.Db.Count(that.RouterString[1],leftJoin,where)
count := that.Db.Count(that.RouterString[1], leftJoin, where)
reData := that.Db.Page(page, pageSize).
PageSelect(that.RouterString[1],leftJoin,columnStr, where)
PageSelect(that.RouterString[1], leftJoin, columnStr, where)
for _, v := range reData {
for k, _ := range v {
column:=that.MakeCode.TableColumns[that.RouterString[1]][k]
if column==nil{
column := that.MakeCodeRouter[hotimeName].TableColumns[that.RouterString[1]][k]
if column == nil {
continue
}
if column["list"]!=false&&column["name"]=="parent_id"&&column.GetString("link")!=""{
if column["list"] != false && column["name"] == "parent_id" && column.GetString("link") != "" {
parentC := that.Db.Get(column.GetString("link"), column.GetString("value"), Map{"id": v.GetCeilInt(k)})
v[column.GetString("link")+"_"+column.GetString("name")+"_"+column.GetString("value")]=""
if parentC!=nil{
v[column.GetString("link")+"_"+column.GetString("name")+"_"+column.GetString("value")]=parentC.GetString(column.GetString("value"))
v[column.GetString("link")+"_"+column.GetString("name")+"_"+column.GetString("value")] = ""
if parentC != nil {
v[column.GetString("link")+"_"+column.GetString("name")+"_"+column.GetString("value")] = parentC.GetString(column.GetString("value"))
}
}
}
}
that.Display(0, Map{"count":count,"data":reData})
that.Display(0, Map{"count": count, "data": reData})
},
}
`
+5 -5
View File
@@ -8,11 +8,11 @@ type ContextBase struct {
tag string
}
//唯一标志
func (this *ContextBase) GetTag() string {
// 唯一标志
func (that *ContextBase) GetTag() string {
if this.tag == "" {
this.tag = ObjToStr(time.Now().Unix()) + ":" + ObjToStr(Random())
if that.tag == "" {
that.tag = ObjToStr(time.Now().Unix()) + ":" + ObjToStr(Random())
}
return this.tag
return that.tag
}
+7 -5
View File
@@ -2,26 +2,28 @@ package common
import (
"github.com/sirupsen/logrus"
"sync"
)
// Error 框架层处理错误
type Error struct {
Logger *logrus.Logger
error
mu sync.RWMutex
}
func (that *Error) GetError() error {
that.mu.RLock()
defer that.mu.RUnlock()
return that.error
}
func (that *Error) SetError(err error) {
that.mu.Lock()
that.error = err
that.mu.Unlock()
if that.Logger != nil && err != nil {
//that.Logger=log.GetLog("",false)
that.Logger.Warn(err)
}
return
}
+78 -5
View File
@@ -5,6 +5,7 @@ import (
"encoding/hex"
"math"
"strings"
"time"
)
//安全锁
@@ -36,6 +37,78 @@ func StrFirstToUpper(str string) string {
return strings.ToUpper(first) + other
}
// 时间转字符串,第二个参数支持1-5对应显示年月日时分秒
func Time2Str(t time.Time, qu ...interface{}) string {
if t.Unix() < 0 {
return ""
}
tp := 5
if len(qu) != 0 {
tp = (qu[0]).(int)
}
switch tp {
case 1:
return t.Format("2006-01")
case 2:
return t.Format("2006-01-02")
case 3:
return t.Format("2006-01-02 15")
case 4:
return t.Format("2006-01-02 15:04")
case 5:
return t.Format("2006-01-02 15:04:05")
case 12:
return t.Format("01-02")
case 14:
return t.Format("01-02 15:04")
case 15:
return t.Format("01-02 15:04:05")
case 34:
return t.Format("15:04")
case 35:
return t.Format("15:04:05")
}
return t.Format("2006-01-02 15:04:05")
}
// StrLd 相似度计算 ld compares two strings and returns the levenshtein distance between them.
func StrLd(s, t string, ignoreCase bool) int {
if ignoreCase {
s = strings.ToLower(s)
t = strings.ToLower(t)
}
d := make([][]int, len(s)+1)
for i := range d {
d[i] = make([]int, len(t)+1)
}
for i := range d {
d[i][0] = i
}
for j := range d[0] {
d[0][j] = j
}
for j := 1; j <= len(t); j++ {
for i := 1; i <= len(s); i++ {
if s[i-1] == t[j-1] {
d[i][j] = d[i-1][j-1]
} else {
min := d[i-1][j]
if d[i][j-1] < min {
min = d[i][j-1]
}
if d[i-1][j-1] < min {
min = d[i-1][j-1]
}
d[i][j] = min + 1
}
}
}
return d[len(s)][len(t)]
}
// Substr 字符串截取
func Substr(str string, start int, length int) string {
rs := []rune(str)
@@ -68,7 +141,7 @@ func Substr(str string, start int, length int) string {
}
// IndexLastStr 获取最后出现字符串的下标
//return 找不到返回 -1
// return 找不到返回 -1
func IndexLastStr(str, sep string) int {
sepSlice := []rune(sep)
strSlice := []rune(str)
@@ -106,7 +179,7 @@ func Md5(req string) string {
return hex.EncodeToString(cipherStr)
}
//随机数
// Rand 随机数
func Rand(count int) int {
res := Random()
for i := 0; i < count; i++ {
@@ -131,7 +204,7 @@ func Random() float64 {
}
//随机数范围
// RandX 随机数范围
func RandX(small int, max int) int {
res := 0
//随机对象
@@ -183,7 +256,7 @@ func RandX(small int, max int) int {
// GetDb()
//}
//复制返回数组
// DeepCopyMap 复制返回数组
func DeepCopyMap(value interface{}) interface{} {
if valueMap, ok := value.(Map); ok {
newMap := make(Map)
@@ -242,7 +315,7 @@ func DeepCopyMap(value interface{}) interface{} {
// }
//}
//浮点数四舍五入保留小数
// Round 浮点数四舍五入保留小数
func Round(f float64, n int) float64 {
pow10_n := math.Pow10(n)
return math.Trunc((f+0.5/pow10_n)*pow10_n) / pow10_n
+77 -51
View File
@@ -4,114 +4,140 @@ import (
"encoding/json"
"errors"
"reflect"
"sort"
"time"
)
//hotime的常用map
// hotime的常用map
type Map map[string]interface{}
//获取string
func (this Map) GetString(key string, err ...*Error) string {
// 获取string
func (that Map) GetString(key string, err ...*Error) string {
if len(err) != 0 {
err[0].SetError(nil)
}
return ObjToStr((this)[key])
return ObjToStr((that)[key])
}
func (this *Map) Pointer() *Map {
func (that *Map) Pointer() *Map {
return this
return that
}
//增加接口
func (this Map) Put(key string, value interface{}) {
//if this==nil{
// this=Map{}
// 增加接口
func (that Map) Put(key string, value interface{}) {
//if that==nil{
// that=Map{}
//}
this[key] = value
that[key] = value
}
//删除接口
func (this Map) Delete(key string) {
delete(this, key)
// 删除接口
func (that Map) Delete(key string) {
delete(that, key)
}
//获取Int
func (this Map) GetInt(key string, err ...*Error) int {
v := ObjToInt((this)[key], err...)
// 获取Int
func (that Map) GetInt(key string, err ...*Error) int {
v := ObjToInt((that)[key], err...)
return v
}
//获取Int
func (this Map) GetInt64(key string, err ...*Error) int64 {
v := ObjToInt64((this)[key], err...)
// 获取Int
func (that Map) GetInt64(key string, err ...*Error) int64 {
v := ObjToInt64((that)[key], err...)
return v
}
//获取向上取整Int64
func (this Map) GetCeilInt64(key string, err ...*Error) int64 {
v := ObjToCeilInt64((this)[key], err...)
// 获取向上取整Int64
func (that Map) GetCeilInt64(key string, err ...*Error) int64 {
v := ObjToCeilInt64((that)[key], err...)
return v
}
//获取向上取整Int
func (this Map) GetCeilInt(key string, err ...*Error) int {
v := ObjToCeilInt((this)[key], err...)
// 获取向上取整Int
func (that Map) GetCeilInt(key string, err ...*Error) int {
v := ObjToCeilInt((that)[key], err...)
return v
}
//获取向上取整float64
func (this Map) GetCeilFloat64(key string, err ...*Error) float64 {
v := ObjToCeilFloat64((this)[key], err...)
// 获取向上取整float64
func (that Map) GetCeilFloat64(key string, err ...*Error) float64 {
v := ObjToCeilFloat64((that)[key], err...)
return v
}
//获取Float64
func (this Map) GetFloat64(key string, err ...*Error) float64 {
// 获取Float64
func (that Map) GetFloat64(key string, err ...*Error) float64 {
v := ObjToFloat64((this)[key], err...)
v := ObjToFloat64((that)[key], err...)
return v
}
func (this Map) GetSlice(key string, err ...*Error) Slice {
func (that Map) GetSlice(key string, err ...*Error) Slice {
//var v Slice
v := ObjToSlice((this)[key], err...)
v := ObjToSlice((that)[key], err...)
return v
}
func (this Map) GetBool(key string, err ...*Error) bool {
func (that Map) GetBool(key string, err ...*Error) bool {
//var v Slice
v := ObjToBool((this)[key], err...)
v := ObjToBool((that)[key], err...)
return v
}
func (this Map) GetMap(key string, err ...*Error) Map {
func (that Map) GetTime(key string, err ...*Error) *time.Time {
v := ObjToTime((that)[key], err...)
return v
}
func (that Map) RangeSort(callback func(k string, v interface{}) (isEnd bool)) {
testQu := []string{}
//testQuData:= qu[0].(Map)
for key, _ := range that {
//fmt.Println(key, ":", value)
testQu = append(testQu, key)
}
sort.Strings(testQu)
for _, k := range testQu {
re := callback(k, that[k])
if re {
return
}
}
}
func (that Map) GetMap(key string, err ...*Error) Map {
//var data Slice
v := ObjToMap((this)[key], err...)
v := ObjToMap((that)[key], err...)
return v
}
func (this Map) Get(key string, err ...*Error) interface{} {
func (that Map) Get(key string, err ...*Error) interface{} {
if v, ok := (this)[key]; ok {
if v, ok := (that)[key]; ok {
return v
}
e := errors.New("没有存储key及对应的数据")
@@ -123,11 +149,11 @@ func (this Map) Get(key string, err ...*Error) interface{} {
return nil
}
//请传递指针过来
func (this Map) ToStruct(stct interface{}) {
// 请传递指针过来
func (that Map) ToStruct(stct interface{}) {
data := reflect.ValueOf(stct).Elem()
for k, v := range this {
for k, v := range that {
ks := StrFirstToUpper(k)
dkey := data.FieldByName(ks)
if !dkey.IsValid() {
@@ -135,13 +161,13 @@ func (this Map) ToStruct(stct interface{}) {
}
switch dkey.Type().String() {
case "int":
dkey.SetInt(this.GetInt64(k))
dkey.SetInt(that.GetInt64(k))
case "int64":
dkey.Set(reflect.ValueOf(this.GetInt64(k)))
dkey.Set(reflect.ValueOf(that.GetInt64(k)))
case "float64":
dkey.Set(reflect.ValueOf(this.GetFloat64(k)))
dkey.Set(reflect.ValueOf(that.GetFloat64(k)))
case "string":
dkey.Set(reflect.ValueOf(this.GetString(k)))
dkey.Set(reflect.ValueOf(that.GetString(k)))
case "interface{}":
dkey.Set(reflect.ValueOf(v))
}
@@ -149,13 +175,13 @@ func (this Map) ToStruct(stct interface{}) {
}
func (this Map) ToJsonString() string {
return ObjToStr(this)
func (that Map) ToJsonString() string {
return ObjToStr(that)
}
func (this Map) JsonToMap(jsonStr string, err ...*Error) {
e := json.Unmarshal([]byte(jsonStr), &this)
func (that Map) JsonToMap(jsonStr string, err ...*Error) {
e := json.Unmarshal([]byte(jsonStr), &that)
if e != nil && len(err) != 0 {
err[0].SetError(e)
}
+12 -3
View File
@@ -1,6 +1,8 @@
package common
//对象封装方便取用
import "time"
// 对象封装方便取用
type Obj struct {
Data interface{}
Error
@@ -18,6 +20,13 @@ func (that *Obj) ToInt(err ...Error) int {
return ObjToInt(that.Data, &that.Error)
}
func (that *Obj) ToTime(err ...Error) *time.Time {
if len(err) != 0 {
that.Error = err[0]
}
return ObjToTime(that.Data, &that.Error)
}
func (that *Obj) ToInt64(err ...Error) int64 {
if len(err) != 0 {
that.Error = err[0]
@@ -73,7 +82,7 @@ func (that *Obj) ToObj() interface{} {
return that.Data
}
//获取向上取整Int64
// 获取向上取整Int64
func (that *Obj) ToCeilInt64(err ...*Error) int64 {
if len(err) != 0 {
that.Error = *err[0]
@@ -83,7 +92,7 @@ func (that *Obj) ToCeilInt64(err ...*Error) int64 {
}
//获取向上取整Int
// 获取向上取整Int
func (that *Obj) ToCeilInt(err ...*Error) int {
if len(err) != 0 {
that.Error = *err[0]
+102 -10
View File
@@ -5,9 +5,11 @@ import (
"errors"
"math"
"strconv"
"strings"
"time"
)
//仅限于hotime.Slice
// 仅限于hotime.Slice
func ObjToMap(obj interface{}, e ...*Error) Map {
var err error
var v Map
@@ -58,7 +60,7 @@ func ObjToMapArray(obj interface{}, e ...*Error) []Map {
return res
}
//仅限于hotime.Slice
// 仅限于hotime.Slice
func ObjToSlice(obj interface{}, e ...*Error) Slice {
var err error
var v Slice
@@ -96,6 +98,87 @@ func ObjToSlice(obj interface{}, e ...*Error) Slice {
return v
}
func ObjToTime(obj interface{}, e ...*Error) *time.Time {
tInt := ObjToInt64(obj)
//字符串类型,只支持标准mysql datetime格式
if tInt == 0 {
tStr := ObjToStr(obj)
timeNewStr := ""
timeNewStrs := strings.Split(tStr, "-")
for _, v := range timeNewStrs {
if v == "" {
continue
}
if len(v) == 1 {
v = "0" + v
}
if timeNewStr == "" {
timeNewStr = v
continue
}
timeNewStr = timeNewStr + "-" + v
}
tStr = timeNewStr
if len(tStr) > 18 {
t, e := time.Parse("2006-01-02 15:04:05", tStr)
if e == nil {
return &t
}
} else if len(tStr) > 15 {
t, e := time.Parse("2006-01-02 15:04", tStr)
if e == nil {
return &t
}
} else if len(tStr) > 12 {
t, e := time.Parse("2006-01-02 15", tStr)
if e == nil {
return &t
}
} else if len(tStr) > 9 {
t, e := time.Parse("2006-01-02", tStr)
if e == nil {
return &t
}
} else if len(tStr) > 6 {
t, e := time.Parse("2006-01", tStr)
if e == nil {
return &t
}
}
}
//纳秒级别
if len(ObjToStr(tInt)) > 16 {
//t := time.Time{}.Add(time.Nanosecond * time.Duration(tInt))
t := time.UnixMicro(tInt / 1000)
return &t
//微秒级别
} else if len(ObjToStr(tInt)) > 13 {
//t := time.Time{}.Add(time.Microsecond * time.Duration(tInt))
t := time.UnixMicro(tInt)
return &t
//毫秒级别
} else if len(ObjToStr(tInt)) > 10 {
//t := time.Time{}.Add(time.Millisecond * time.Duration(tInt))
t := time.UnixMilli(tInt)
return &t
//秒级别
} else if len(ObjToStr(tInt)) > 9 {
//t := time.Time{}.Add(time.Second * time.Duration(tInt))
t := time.Unix(tInt, 0)
return &t
} else if len(ObjToStr(tInt)) > 3 {
t, e := time.Parse("2006", ObjToStr(tInt))
if e == nil {
return &t
}
}
return nil
}
func ObjToFloat64(obj interface{}, e ...*Error) float64 {
var err error
v := float64(0)
@@ -135,6 +218,15 @@ func ObjToFloat64(obj interface{}, e ...*Error) float64 {
err = errors.New("没有合适的转换对象!")
}
}
if math.IsNaN(v) {
err = errors.New("float64 is NaN")
v = 0
}
if math.IsInf(v, 0) {
err = errors.New("float64 is Inf")
v = 0
}
if len(e) != 0 {
e[0].SetError(err)
}
@@ -142,21 +234,21 @@ func ObjToFloat64(obj interface{}, e ...*Error) float64 {
return v
}
//向上取整
// 向上取整
func ObjToCeilInt64(obj interface{}, e ...*Error) int64 {
f := ObjToCeilFloat64(obj, e...)
return ObjToInt64(math.Ceil(f))
}
//向上取整
// 向上取整
func ObjToCeilFloat64(obj interface{}, e ...*Error) float64 {
f := ObjToFloat64(obj, e...)
return math.Ceil(f)
}
//向上取整
// 向上取整
func ObjToCeilInt(obj interface{}, e ...*Error) int {
f := ObjToCeilFloat64(obj, e...)
return ObjToInt(f)
@@ -268,7 +360,7 @@ func ObjToStr(obj interface{}) string {
return str
}
//转换为Map
// 转换为Map
func StrToMap(string string) Map {
data := Map{}
data.JsonToMap(string)
@@ -276,7 +368,7 @@ func StrToMap(string string) Map {
return data
}
//转换为Slice
// 转换为Slice
func StrToSlice(string string) Slice {
data := ObjToSlice(string)
@@ -284,7 +376,7 @@ func StrToSlice(string string) Slice {
return data
}
//字符串数组: a1,a2,a3转["a1","a2","a3"]
// 字符串数组: a1,a2,a3转["a1","a2","a3"]
func StrArrayToJsonStr(a string) string {
if len(a) > 2 {
@@ -302,7 +394,7 @@ func StrArrayToJsonStr(a string) string {
return a
}
//字符串数组: a1,a2,a3转["a1","a2","a3"]
// 字符串数组: a1,a2,a3转["a1","a2","a3"]
func JsonStrToStrArray(a string) string {
//a = strings.Replace(a, `"`, "", -1)
if len(a) != 0 {
@@ -312,7 +404,7 @@ func JsonStrToStrArray(a string) string {
return "," + a + ","
}
//字符串转int
// 字符串转int
func StrToInt(s string) (int, error) {
i, err := strconv.Atoi(s)
return i, err
+8
View File
@@ -2,6 +2,7 @@ package common
import (
"errors"
"time"
)
type Slice []interface{}
@@ -14,6 +15,13 @@ func (that Slice) GetString(key int, err ...*Error) string {
return ObjToStr((that)[key])
}
func (that Slice) GetTime(key int, err ...*Error) *time.Time {
v := ObjToTime((that)[key], err...)
return v
}
// GetInt 获取Int
func (that Slice) GetInt(key int, err ...*Error) int {
v := ObjToInt((that)[key], err...)
+142 -5
View File
@@ -1,24 +1,37 @@
package hotime
import (
. "./cache"
. "./common"
. "./db"
"bytes"
"encoding/json"
"io"
"mime/multipart"
"net/http"
"strings"
"sync"
"time"
. "code.hoteas.com/golang/hotime/common"
. "code.hoteas.com/golang/hotime/db"
)
type Context struct {
*Application
Resp http.ResponseWriter
Req *http.Request
Log Map //日志有则创建
RouterString []string
Config Map
Db *HoTimeDB
RespData Map
CacheIns
RespFunc func()
//CacheIns
SessionIns
DataSize int
HandlerStr string //复写请求url
// 请求参数缓存
reqJsonCache Map // JSON Body 缓存
reqMu sync.Once // 确保 JSON 只解析一次
}
// Mtd 唯一标志
@@ -29,7 +42,7 @@ func (that *Context) Mtd(router [3]string) Map {
return d
}
//打印
// 打印
func (that *Context) Display(statu int, data interface{}) {
resp := Map{"status": statu}
@@ -56,13 +69,137 @@ func (that *Context) Display(statu int, data interface{}) {
}
func (that *Context) View() {
if that.RespFunc != nil {
that.RespFunc()
}
if that.RespData == nil {
return
}
//创建日志
if that.Log != nil {
that.Log["time"] = time.Now().Format("2006-01-02 15:04")
if that.Session("admin_id").Data != nil {
that.Log["admin_id"] = that.Session("admin_id").ToCeilInt()
}
if that.Session("user_id").Data != nil {
that.Log["user_id"] = that.Session("user_id").ToCeilInt()
}
//负载均衡优化
ipStr := ""
if that.Req.Header.Get("X-Forwarded-For") != "" {
ipStr = that.Req.Header.Get("X-Forwarded-For")
} else if that.Req.Header.Get("X-Real-IP") != "" {
ipStr = that.Req.Header.Get("X-Real-IP")
}
//负载均衡优化
if ipStr == "" {
//RemoteAddr := that.Req.RemoteAddr
ipStr = Substr(that.Req.RemoteAddr, 0, strings.Index(that.Req.RemoteAddr, ":"))
}
that.Log["ip"] = ipStr
that.Db.Insert("logs", that.Log)
}
d, err := json.Marshal(that.RespData)
if err != nil {
that.Display(1, err.Error())
that.View()
return
}
that.DataSize = len(d)
that.RespData = nil
that.Resp.Write(d)
}
// ==================== 请求参数获取方法 ====================
// ReqParam 获取 URL 查询参数,返回 *Obj 支持链式调用
// 用法: that.ReqParam("id").ToInt()
func (that *Context) ReqParam(key string) *Obj {
v := that.Req.URL.Query().Get(key)
if v == "" {
return &Obj{Data: nil}
}
return &Obj{Data: v}
}
// ReqForm 获取表单参数,返回 *Obj 支持链式调用
// 用法: that.ReqForm("name").ToStr()
func (that *Context) ReqForm(key string) *Obj {
v := that.Req.FormValue(key)
if v == "" {
return &Obj{Data: nil}
}
return &Obj{Data: v}
}
// ReqJson 获取 JSON Body 中的字段,返回 *Obj 支持链式调用
// 用法: that.ReqJson("data").ToMap()
func (that *Context) ReqJson(key string) *Obj {
that.parseJsonBody()
if that.reqJsonCache == nil {
return &Obj{Data: nil}
}
return &Obj{Data: that.reqJsonCache.Get(key)}
}
// parseJsonBody 解析 JSON Body(只解析一次,并发安全)
func (that *Context) parseJsonBody() {
that.reqMu.Do(func() {
if that.Req.Body == nil {
return
}
body, err := io.ReadAll(that.Req.Body)
if err != nil || len(body) == 0 {
return
}
// 恢复 Body 以便后续代码可以再次读取
that.Req.Body = io.NopCloser(bytes.NewBuffer(body))
that.reqJsonCache = ObjToMap(string(body))
})
}
// ReqData 统一获取请求参数,优先级: JSON > Form > URL
// 用法: that.ReqData("id").ToInt()
func (that *Context) ReqData(key string) *Obj {
// 1. 优先从 JSON Body 获取
that.parseJsonBody()
if that.reqJsonCache != nil {
if v := that.reqJsonCache.Get(key); v != nil {
return &Obj{Data: v}
}
}
// 2. 其次从 Form 获取
if v := that.Req.FormValue(key); v != "" {
return &Obj{Data: v}
}
// 3. 最后从 URL 参数获取
if v := that.Req.URL.Query().Get(key); v != "" {
return &Obj{Data: v}
}
return &Obj{Data: nil}
}
// ReqFile 获取单个上传文件
// 用法: file, header, err := that.ReqFile("avatar")
func (that *Context) ReqFile(name string) (multipart.File, *multipart.FileHeader, error) {
return that.Req.FormFile(name)
}
// ReqFiles 获取多个同名上传文件(批量上传场景)
// 用法: files, err := that.ReqFiles("images")
func (that *Context) ReqFiles(name string) ([]*multipart.FileHeader, error) {
if that.Req.MultipartForm == nil {
if err := that.Req.ParseMultipartForm(32 << 20); err != nil {
return nil, err
}
}
if that.Req.MultipartForm == nil || that.Req.MultipartForm.File == nil {
return nil, http.ErrMissingFile
}
files, ok := that.Req.MultipartForm.File[name]
if !ok {
return nil, http.ErrMissingFile
}
return files, nil
}
+115
View File
@@ -0,0 +1,115 @@
package db
import (
. "code.hoteas.com/golang/hotime/common"
)
// Count 计数
func (that *HoTimeDB) Count(table string, qu ...interface{}) int {
var req = []interface{}{}
if len(qu) == 2 {
req = append(req, qu[0])
req = append(req, "COUNT(*)")
req = append(req, qu[1])
} else {
req = append(req, "COUNT(*)")
req = append(req, qu...)
}
data := that.Select(table, req...)
if len(data) == 0 {
return 0
}
res := ObjToStr(data[0]["COUNT(*)"])
count, _ := StrToInt(res)
return count
}
// Sum 求和
func (that *HoTimeDB) Sum(table string, column string, qu ...interface{}) float64 {
var req = []interface{}{}
if len(qu) == 2 {
req = append(req, qu[0])
req = append(req, "SUM("+column+")")
req = append(req, qu[1])
} else {
req = append(req, "SUM("+column+")")
req = append(req, qu...)
}
data := that.Select(table, req...)
if len(data) == 0 {
return 0
}
res := ObjToStr(data[0]["SUM("+column+")"])
sum := ObjToFloat64(res)
return sum
}
// Avg 平均值
func (that *HoTimeDB) Avg(table string, column string, qu ...interface{}) float64 {
var req = []interface{}{}
if len(qu) == 2 {
req = append(req, qu[0])
req = append(req, "AVG("+column+")")
req = append(req, qu[1])
} else {
req = append(req, "AVG("+column+")")
req = append(req, qu...)
}
data := that.Select(table, req...)
if len(data) == 0 {
return 0
}
res := ObjToStr(data[0]["AVG("+column+")"])
avg := ObjToFloat64(res)
return avg
}
// Max 最大值
func (that *HoTimeDB) Max(table string, column string, qu ...interface{}) float64 {
var req = []interface{}{}
if len(qu) == 2 {
req = append(req, qu[0])
req = append(req, "MAX("+column+")")
req = append(req, qu[1])
} else {
req = append(req, "MAX("+column+")")
req = append(req, qu...)
}
data := that.Select(table, req...)
if len(data) == 0 {
return 0
}
res := ObjToStr(data[0]["MAX("+column+")"])
max := ObjToFloat64(res)
return max
}
// Min 最小值
func (that *HoTimeDB) Min(table string, column string, qu ...interface{}) float64 {
var req = []interface{}{}
if len(qu) == 2 {
req = append(req, qu[0])
req = append(req, "MIN("+column+")")
req = append(req, qu[1])
} else {
req = append(req, "MIN("+column+")")
req = append(req, qu...)
}
data := that.Select(table, req...)
if len(data) == 0 {
return 0
}
res := ObjToStr(data[0]["MIN("+column+")"])
min := ObjToFloat64(res)
return min
}
+93
View File
@@ -0,0 +1,93 @@
package db
import (
. "code.hoteas.com/golang/hotime/common"
"os"
"strings"
)
// backupSave 保存备份
// path: 备份文件路径
// tt: 表名
// code: 备份类型 0=全部, 1=仅数据, 2=仅DDL
func (that *HoTimeDB) backupSave(path string, tt string, code int) {
fd, _ := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
defer fd.Close()
str := "\r\n"
if code == 0 || code == 2 {
str += that.backupDdl(tt)
}
if code == 0 || code == 1 {
str += "insert into `" + tt + "`\r\n\r\n("
str += that.backupCol(tt)
}
_, _ = fd.Write([]byte(str))
}
// backupDdl 备份表结构(DDL
func (that *HoTimeDB) backupDdl(tt string) string {
data := that.Query("show create table " + tt)
if len(data) == 0 {
return ""
}
return ObjToStr(data[0]["Create Table"]) + ";\r\n\r\n"
}
// backupCol 备份表数据
func (that *HoTimeDB) backupCol(tt string) string {
str := ""
data := that.Select(tt, "*")
lthData := len(data)
if lthData == 0 {
return str
}
lthCol := len(data[0])
col := make([]string, lthCol)
tempLthData := 0
for k := range data[0] {
if tempLthData == lthCol-1 {
str += "`" + k + "`) "
} else {
str += "`" + k + "`,"
}
col[tempLthData] = k
tempLthData++
}
str += " values"
for j := 0; j < lthData; j++ {
for m := 0; m < lthCol; m++ {
if m == 0 {
str += "("
}
v := "NULL"
if data[j][col[m]] != nil {
v = "'" + strings.Replace(ObjToStr(data[j][col[m]]), "'", `\'`, -1) + "'"
}
if m == lthCol-1 {
str += v + ") "
} else {
str += v + ","
}
}
if j == lthData-1 {
str += ";\r\n\r\n"
} else {
str += ",\r\n\r\n"
}
}
return str
}
+312
View File
@@ -0,0 +1,312 @@
package db
import (
. "code.hoteas.com/golang/hotime/common"
)
// HotimeDBBuilder 链式查询构建器
type HotimeDBBuilder struct {
HoTimeDB *HoTimeDB
table string
selects []interface{}
join Slice
where Map
lastWhere Map
page int
pageRow int
}
// Table 创建链式查询构建器
func (that *HoTimeDB) Table(table string) *HotimeDBBuilder {
return &HotimeDBBuilder{HoTimeDB: that, table: table, where: Map{}}
}
// Get 获取单条记录
func (that *HotimeDBBuilder) Get(qu ...interface{}) Map {
// 构建参数:根据是否有 JOIN 来决定参数结构
var args []interface{}
if len(that.join) > 0 {
// 有 JOIN 时:join, fields, where
if len(qu) > 0 {
args = append(args, that.join, qu[0], that.where)
} else {
args = append(args, that.join, "*", that.where)
}
} else {
// 无 JOIN 时:fields, where
if len(qu) > 0 {
args = append(args, qu[0], that.where)
} else {
args = append(args, "*", that.where)
}
}
return that.HoTimeDB.Get(that.table, args...)
}
// Count 统计数量
func (that *HotimeDBBuilder) Count() int {
// 构建参数:根据是否有 JOIN 来决定参数结构
if len(that.join) > 0 {
return that.HoTimeDB.Count(that.table, that.join, that.where)
}
return that.HoTimeDB.Count(that.table, that.where)
}
// Page 设置分页
func (that *HotimeDBBuilder) Page(page, pageRow int) *HotimeDBBuilder {
that.page = page
that.pageRow = pageRow
return that
}
// Select 查询多条记录
func (that *HotimeDBBuilder) Select(qu ...interface{}) []Map {
// 构建参数:根据是否有 JOIN 来决定参数结构
var args []interface{}
if len(that.join) > 0 {
// 有 JOIN 时:join, fields, where
if len(qu) > 0 {
args = append(args, that.join, qu[0], that.where)
} else {
args = append(args, that.join, "*", that.where)
}
} else {
// 无 JOIN 时:fields, where
if len(qu) > 0 {
args = append(args, qu[0], that.where)
} else {
args = append(args, "*", that.where)
}
}
if that.page != 0 {
return that.HoTimeDB.Page(that.page, that.pageRow).PageSelect(that.table, args...)
}
return that.HoTimeDB.Select(that.table, args...)
}
// Update 更新记录
func (that *HotimeDBBuilder) Update(qu ...interface{}) int64 {
lth := len(qu)
if lth == 0 {
return 0
}
data := Map{}
if lth == 1 {
data = ObjToMap(qu[0])
}
if lth > 1 {
for k := 1; k < lth; k++ {
data[ObjToStr(qu[k-1])] = qu[k]
k++
}
}
return that.HoTimeDB.Update(that.table, data, that.where)
}
// Delete 删除记录
func (that *HotimeDBBuilder) Delete() int64 {
return that.HoTimeDB.Delete(that.table, that.where)
}
// LeftJoin 左连接
func (that *HotimeDBBuilder) LeftJoin(table, joinStr string) *HotimeDBBuilder {
that.Join(Map{"[>]" + table: joinStr})
return that
}
// RightJoin 右连接
func (that *HotimeDBBuilder) RightJoin(table, joinStr string) *HotimeDBBuilder {
that.Join(Map{"[<]" + table: joinStr})
return that
}
// InnerJoin 内连接
func (that *HotimeDBBuilder) InnerJoin(table, joinStr string) *HotimeDBBuilder {
that.Join(Map{"[><]" + table: joinStr})
return that
}
// FullJoin 全连接
func (that *HotimeDBBuilder) FullJoin(table, joinStr string) *HotimeDBBuilder {
that.Join(Map{"[<>]" + table: joinStr})
return that
}
// Join 通用连接
func (that *HotimeDBBuilder) Join(qu ...interface{}) *HotimeDBBuilder {
lth := len(qu)
if lth == 0 {
return that
}
data := Map{}
if lth == 1 {
data = ObjToMap(qu[0])
}
if lth > 1 {
for k := 1; k < lth; k++ {
data[ObjToStr(qu[k-1])] = qu[k]
k++
}
}
if that.join == nil {
that.join = Slice{}
}
if data == nil {
return that
}
that.join = append(that.join, data)
return that
}
// And 添加 AND 条件
func (that *HotimeDBBuilder) And(qu ...interface{}) *HotimeDBBuilder {
lth := len(qu)
if lth == 0 {
return that
}
var where Map
if lth == 1 {
where = ObjToMap(qu[0])
}
if lth > 1 {
where = Map{}
for k := 1; k < lth; k++ {
where[ObjToStr(qu[k-1])] = qu[k]
k++
}
}
if where == nil {
return that
}
if that.lastWhere != nil {
that.lastWhere["AND"] = where
that.lastWhere = where
return that
}
that.lastWhere = where
that.where = Map{"AND": where}
return that
}
// Or 添加 OR 条件
func (that *HotimeDBBuilder) Or(qu ...interface{}) *HotimeDBBuilder {
lth := len(qu)
if lth == 0 {
return that
}
var where Map
if lth == 1 {
where = ObjToMap(qu[0])
}
if lth > 1 {
where = Map{}
for k := 1; k < lth; k++ {
where[ObjToStr(qu[k-1])] = qu[k]
k++
}
}
if where == nil {
return that
}
if that.lastWhere != nil {
that.lastWhere["OR"] = where
that.lastWhere = where
return that
}
that.lastWhere = where
that.where = Map{"Or": where}
return that
}
// Where 设置 WHERE 条件
func (that *HotimeDBBuilder) Where(qu ...interface{}) *HotimeDBBuilder {
lth := len(qu)
if lth == 0 {
return that
}
var where Map
if lth == 1 {
where = ObjToMap(qu[0])
}
if lth > 1 {
where = Map{}
for k := 1; k < lth; k++ {
where[ObjToStr(qu[k-1])] = qu[k]
k++
}
}
if where == nil {
return that
}
if that.lastWhere != nil {
that.lastWhere["AND"] = where
that.lastWhere = where
return that
}
that.lastWhere = where
that.where = Map{"AND": that.lastWhere}
return that
}
// From 设置表名
func (that *HotimeDBBuilder) From(table string) *HotimeDBBuilder {
that.table = table
return that
}
// Order 设置排序
func (that *HotimeDBBuilder) Order(qu ...interface{}) *HotimeDBBuilder {
that.where["ORDER"] = ObjToSlice(qu)
return that
}
// Limit 设置限制
func (that *HotimeDBBuilder) Limit(qu ...interface{}) *HotimeDBBuilder {
that.where["LIMIT"] = ObjToSlice(qu)
return that
}
// Group 设置分组
func (that *HotimeDBBuilder) Group(qu ...interface{}) *HotimeDBBuilder {
that.where["GROUP"] = ObjToSlice(qu)
return that
}
// Having 设置 HAVING 条件
func (that *HotimeDBBuilder) Having(qu ...interface{}) *HotimeDBBuilder {
lth := len(qu)
if lth == 0 {
return that
}
var having Map
if lth == 1 {
having = ObjToMap(qu[0])
}
if lth > 1 {
having = Map{}
for k := 1; k < lth; k++ {
having[ObjToStr(qu[k-1])] = qu[k]
k++
}
}
that.where["HAVING"] = having
return that
}
// Offset 设置偏移量
func (that *HotimeDBBuilder) Offset(offset int) *HotimeDBBuilder {
that.where["OFFSET"] = offset
return that
}
+679
View File
@@ -0,0 +1,679 @@
package db
import (
"reflect"
"sort"
"strings"
. "code.hoteas.com/golang/hotime/common"
)
// Page 设置分页参数
// page: 页码(从1开始)
// pageRow: 每页数量
func (that *HoTimeDB) Page(page, pageRow int) *HoTimeDB {
if page < 1 {
page = 1
}
if pageRow < 1 {
pageRow = 10
}
offset := (page - 1) * pageRow
that.limitMu.Lock()
that.limit = Slice{offset, pageRow}
that.limitMu.Unlock()
return that
}
// PageSelect 分页查询
func (that *HoTimeDB) PageSelect(table string, qu ...interface{}) []Map {
that.limitMu.Lock()
limit := that.limit
that.limit = nil // 使用后清空,避免影响下次调用
that.limitMu.Unlock()
if limit == nil {
return that.Select(table, qu...)
}
// 根据参数数量处理 LIMIT 注入
switch len(qu) {
case 0:
// PageSelect("user") -> 只有表名,添加 LIMIT
qu = append(qu, "*", Map{"LIMIT": limit})
case 1:
// PageSelect("user", "*") 或 PageSelect("user", Map{...})
if reflect.ValueOf(qu[0]).Kind() == reflect.Map {
// 是 where 条件
temp := DeepCopyMap(qu[0]).(Map)
temp["LIMIT"] = limit
qu[0] = temp
} else {
// 是字段选择
qu = append(qu, Map{"LIMIT": limit})
}
case 2:
// PageSelect("user", "*", Map{...}) 或 PageSelect("user", joinSlice, "*")
if reflect.ValueOf(qu[1]).Kind() == reflect.Map {
temp := DeepCopyMap(qu[1]).(Map)
temp["LIMIT"] = limit
qu[1] = temp
} else {
// join 模式,需要追加 where
qu = append(qu, Map{"LIMIT": limit})
}
case 3:
// PageSelect("user", joinSlice, "*", Map{...})
temp := DeepCopyMap(qu[2]).(Map)
temp["LIMIT"] = limit
qu[2] = temp
}
return that.Select(table, qu...)
}
// Select 查询多条记录
func (that *HoTimeDB) Select(table string, qu ...interface{}) []Map {
query := "SELECT"
where := Map{}
qs := make([]interface{}, 0)
intQs, intWhere := 0, 1
join := false
if len(qu) == 3 {
intQs = 1
intWhere = 2
join = true
}
processor := that.GetProcessor()
if len(qu) > 0 {
if reflect.ValueOf(qu[intQs]).Type().String() == "string" {
// 字段列表字符串,使用处理器处理 table.column 格式
fieldStr := qu[intQs].(string)
if fieldStr != "*" {
fieldStr = processor.ProcessFieldList(fieldStr)
}
query += " " + fieldStr
} else {
data := ObjToSlice(qu[intQs])
for i := 0; i < len(data); i++ {
k := data.GetString(i)
if strings.Contains(k, " AS ") || strings.Contains(k, ".") {
// 处理 table.column 格式
query += " " + processor.ProcessFieldList(k) + " "
} else {
// 单独的列名
query += " " + processor.ProcessColumnNoPrefix(k) + " "
}
if i+1 != len(data) {
query = query + ", "
}
}
}
} else {
query += " *"
}
// 处理表名(添加前缀和正确的引号)
query += " FROM " + processor.ProcessTableName(table) + " "
if join {
query += that.buildJoin(qu[0])
}
if len(qu) > 1 {
where = qu[intWhere].(Map)
}
temp, resWhere := that.where(where)
query += temp + ";"
qs = append(qs, resWhere...)
md5 := that.md5(query, qs...)
if that.HoTimeCache != nil && table != "cached" {
// 如果缓存有则从缓存取
cacheData := that.HoTimeCache.Db(table + ":" + md5)
if cacheData != nil && cacheData.Data != nil {
return cacheData.ToMapArray()
}
}
// 无缓存则数据库取
res := that.Query(query, qs...)
if res == nil {
res = []Map{}
}
// 缓存
if that.HoTimeCache != nil && table != "cached" {
_ = that.HoTimeCache.Db(table+":"+md5, res)
}
return res
}
// buildJoin 构建 JOIN 语句
func (that *HoTimeDB) buildJoin(joinData interface{}) string {
query := ""
var testQu = []string{}
testQuData := Map{}
processor := that.GetProcessor()
if reflect.ValueOf(joinData).Type().String() == "common.Map" {
testQuData = joinData.(Map)
for key := range testQuData {
testQu = append(testQu, key)
}
}
if reflect.ValueOf(joinData).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(joinData).Type().String(), "[]") {
qu0 := ObjToSlice(joinData)
for key := range qu0 {
v := qu0.GetMap(key)
for k1, v1 := range v {
testQu = append(testQu, k1)
testQuData[k1] = v1
}
}
}
sort.Strings(testQu)
for _, k := range testQu {
v := testQuData[k]
switch Substr(k, 0, 3) {
case "[>]":
func() {
table := Substr(k, 3, len(k)-3)
// 处理表名(添加前缀和正确的引号)
table = processor.ProcessTableName(table)
// 处理 ON 条件中的 table.column
onCondition := processor.ProcessConditionString(v.(string))
query += " LEFT JOIN " + table + " ON " + onCondition + " "
}()
case "[<]":
func() {
table := Substr(k, 3, len(k)-3)
table = processor.ProcessTableName(table)
onCondition := processor.ProcessConditionString(v.(string))
query += " RIGHT JOIN " + table + " ON " + onCondition + " "
}()
}
switch Substr(k, 0, 4) {
case "[<>]":
func() {
table := Substr(k, 4, len(k)-4)
table = processor.ProcessTableName(table)
onCondition := processor.ProcessConditionString(v.(string))
query += " FULL JOIN " + table + " ON " + onCondition + " "
}()
case "[><]":
func() {
table := Substr(k, 4, len(k)-4)
table = processor.ProcessTableName(table)
onCondition := processor.ProcessConditionString(v.(string))
query += " INNER JOIN " + table + " ON " + onCondition + " "
}()
}
}
return query
}
// Get 获取单条记录
func (that *HoTimeDB) Get(table string, qu ...interface{}) Map {
if len(qu) == 0 {
// 没有参数时,添加默认字段和 LIMIT
qu = append(qu, "*", Map{"LIMIT": 1})
} else if len(qu) == 1 {
qu = append(qu, Map{"LIMIT": 1})
} else if len(qu) == 2 {
temp := qu[1].(Map)
temp["LIMIT"] = 1
qu[1] = temp
} else if len(qu) == 3 {
temp := qu[2].(Map)
temp["LIMIT"] = 1
qu[2] = temp
}
data := that.Select(table, qu...)
if len(data) == 0 {
return nil
}
return data[0]
}
// Insert 插入新数据
func (that *HoTimeDB) Insert(table string, data map[string]interface{}) int64 {
values := make([]interface{}, 0)
queryString := " ("
valueString := " ("
processor := that.GetProcessor()
lens := len(data)
tempLen := 0
for k, v := range data {
tempLen++
vstr := "?"
if Substr(k, len(k)-3, 3) == "[#]" {
k = strings.Replace(k, "[#]", "", -1)
vstr = ObjToStr(v)
if tempLen < lens {
queryString += processor.ProcessColumnNoPrefix(k) + ","
valueString += vstr + ","
} else {
queryString += processor.ProcessColumnNoPrefix(k) + ") "
valueString += vstr + ");"
}
} else {
values = append(values, v)
if tempLen < lens {
queryString += processor.ProcessColumnNoPrefix(k) + ","
valueString += "?,"
} else {
queryString += processor.ProcessColumnNoPrefix(k) + ") "
valueString += "?);"
}
}
}
query := "INSERT INTO " + processor.ProcessTableName(table) + " " + queryString + "VALUES" + valueString
res, err := that.Exec(query, values...)
id := int64(0)
if err.GetError() == nil && res != nil {
id1, err := res.LastInsertId()
that.LastErr.SetError(err)
id = id1
}
// 如果插入成功,删除缓存
if id != 0 {
if that.HoTimeCache != nil && table != "cached" {
_ = that.HoTimeCache.Db(table+"*", nil)
}
}
return id
}
// Inserts 批量插入数据
// table: 表名
// dataList: 数据列表,每个元素是一个 Map
// 返回受影响的行数
//
// 示例:
//
// affected := db.Inserts("user", []Map{
// {"name": "张三", "age": 25, "email": "zhang@example.com"},
// {"name": "李四", "age": 30, "email": "li@example.com"},
// {"name": "王五", "age": 28, "email": "wang@example.com"},
// })
func (that *HoTimeDB) Inserts(table string, dataList []Map) int64 {
if len(dataList) == 0 {
return 0
}
// 从第一条数据提取所有列名(确保顺序一致)
columns := make([]string, 0)
rawValues := make(map[string]string) // 存储 [#] 标记的直接 SQL 值
for k := range dataList[0] {
realKey := k
if Substr(k, len(k)-3, 3) == "[#]" {
realKey = strings.Replace(k, "[#]", "", -1)
rawValues[realKey] = ObjToStr(dataList[0][k])
}
columns = append(columns, realKey)
}
// 排序列名以确保一致性
sort.Strings(columns)
processor := that.GetProcessor()
// 构建列名部分
quotedCols := make([]string, len(columns))
for i, col := range columns {
quotedCols[i] = processor.ProcessColumnNoPrefix(col)
}
colStr := strings.Join(quotedCols, ", ")
// 构建每行的占位符和值
placeholders := make([]string, len(dataList))
values := make([]interface{}, 0, len(dataList)*len(columns))
for i, data := range dataList {
rowPlaceholders := make([]string, len(columns))
for j, col := range columns {
// 检查是否有 [#] 标记
rawKey := col + "[#]"
if rawVal, ok := data[rawKey]; ok {
// 直接 SQL 表达式
rowPlaceholders[j] = ObjToStr(rawVal)
} else if _, isRaw := rawValues[col]; isRaw && i == 0 {
// 第一条数据中的 [#] 标记
rowPlaceholders[j] = rawValues[col]
} else if val, ok := data[col]; ok {
// 普通值
rowPlaceholders[j] = "?"
values = append(values, val)
} else {
// 字段不存在,使用 NULL
rowPlaceholders[j] = "NULL"
}
}
placeholders[i] = "(" + strings.Join(rowPlaceholders, ", ") + ")"
}
query := "INSERT INTO " + processor.ProcessTableName(table) + " (" + colStr + ") VALUES " + strings.Join(placeholders, ", ")
res, err := that.Exec(query, values...)
rows64 := int64(0)
if err.GetError() == nil && res != nil {
rows64, _ = res.RowsAffected()
}
// 如果插入成功,删除缓存
if rows64 != 0 {
if that.HoTimeCache != nil && table != "cached" {
_ = that.HoTimeCache.Db(table+"*", nil)
}
}
return rows64
}
// Upsert 插入或更新数据
// table: 表名
// data: 要插入的数据
// uniqueKeys: 唯一键字段(用于冲突检测),支持 Slice{"id"} 或 Slice{"col1", "col2"}
// updateColumns: 冲突时要更新的字段(如果为空,则更新所有非唯一键字段)
// 返回受影响的行数
//
// 示例:
//
// affected := db.Upsert("user",
// Map{"id": 1, "name": "张三", "email": "zhang@example.com"},
// Slice{"id"}, // 唯一键
// Slice{"name", "email"}, // 冲突时更新的字段
// )
func (that *HoTimeDB) Upsert(table string, data Map, uniqueKeys Slice, updateColumns ...interface{}) int64 {
if len(data) == 0 || len(uniqueKeys) == 0 {
return 0
}
// 转换 uniqueKeys 为 []string
uniqueKeyStrs := make([]string, len(uniqueKeys))
for i, uk := range uniqueKeys {
uniqueKeyStrs[i] = ObjToStr(uk)
}
// 转换 updateColumns 为 []string
var updateColumnStrs []string
if len(updateColumns) > 0 {
// 支持两种调用方式:Upsert(table, data, Slice{"id"}, Slice{"name"}) 或 Upsert(table, data, Slice{"id"}, "name", "email")
if slice, ok := updateColumns[0].(Slice); ok {
updateColumnStrs = make([]string, len(slice))
for i, col := range slice {
updateColumnStrs[i] = ObjToStr(col)
}
} else {
updateColumnStrs = make([]string, len(updateColumns))
for i, col := range updateColumns {
updateColumnStrs[i] = ObjToStr(col)
}
}
}
// 收集列和值
columns := make([]string, 0, len(data))
values := make([]interface{}, 0, len(data))
rawValues := make(map[string]string) // 存储 [#] 标记的直接 SQL 值
for k, v := range data {
if Substr(k, len(k)-3, 3) == "[#]" {
realKey := strings.Replace(k, "[#]", "", -1)
columns = append(columns, realKey)
rawValues[realKey] = ObjToStr(v)
} else {
columns = append(columns, k)
values = append(values, v)
}
}
// 如果没有指定更新字段,则更新所有非唯一键字段
if len(updateColumnStrs) == 0 {
uniqueKeySet := make(map[string]bool)
for _, uk := range uniqueKeyStrs {
uniqueKeySet[uk] = true
}
for _, col := range columns {
if !uniqueKeySet[col] {
updateColumnStrs = append(updateColumnStrs, col)
}
}
}
// 构建 SQL
var query string
dbType := that.Type
if dbType == "" {
dbType = "mysql"
}
switch dbType {
case "postgres", "postgresql":
query = that.buildPostgresUpsert(table, columns, uniqueKeyStrs, updateColumnStrs, rawValues)
case "sqlite3", "sqlite":
query = that.buildSQLiteUpsert(table, columns, uniqueKeyStrs, updateColumnStrs, rawValues)
default: // mysql
query = that.buildMySQLUpsert(table, columns, uniqueKeyStrs, updateColumnStrs, rawValues)
}
res, err := that.Exec(query, values...)
rows := int64(0)
if err.GetError() == nil && res != nil {
rows, _ = res.RowsAffected()
}
// 清除缓存
if rows != 0 {
if that.HoTimeCache != nil && table != "cached" {
_ = that.HoTimeCache.Db(table+"*", nil)
}
}
return rows
}
// buildMySQLUpsert 构建 MySQL 的 Upsert 语句
func (that *HoTimeDB) buildMySQLUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string {
// INSERT INTO table (col1, col2) VALUES (?, ?)
// ON DUPLICATE KEY UPDATE col1 = VALUES(col1), col2 = VALUES(col2)
processor := that.GetProcessor()
quotedCols := make([]string, len(columns))
valueParts := make([]string, len(columns))
for i, col := range columns {
quotedCols[i] = processor.ProcessColumnNoPrefix(col)
if raw, ok := rawValues[col]; ok {
valueParts[i] = raw
} else {
valueParts[i] = "?"
}
}
updateParts := make([]string, len(updateColumns))
for i, col := range updateColumns {
quotedCol := processor.ProcessColumnNoPrefix(col)
if raw, ok := rawValues[col]; ok {
updateParts[i] = quotedCol + " = " + raw
} else {
updateParts[i] = quotedCol + " = VALUES(" + quotedCol + ")"
}
}
return "INSERT INTO " + processor.ProcessTableName(table) + " (" + strings.Join(quotedCols, ", ") +
") VALUES (" + strings.Join(valueParts, ", ") +
") ON DUPLICATE KEY UPDATE " + strings.Join(updateParts, ", ")
}
// buildPostgresUpsert 构建 PostgreSQL 的 Upsert 语句
func (that *HoTimeDB) buildPostgresUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string {
// INSERT INTO table (col1, col2) VALUES ($1, $2)
// ON CONFLICT (unique_key) DO UPDATE SET col1 = EXCLUDED.col1
processor := that.GetProcessor()
dialect := that.GetDialect()
quotedCols := make([]string, len(columns))
valueParts := make([]string, len(columns))
paramIndex := 1
for i, col := range columns {
quotedCols[i] = dialect.QuoteIdentifier(col)
if raw, ok := rawValues[col]; ok {
valueParts[i] = raw
} else {
valueParts[i] = "$" + ObjToStr(paramIndex)
paramIndex++
}
}
quotedUniqueKeys := make([]string, len(uniqueKeys))
for i, key := range uniqueKeys {
quotedUniqueKeys[i] = dialect.QuoteIdentifier(key)
}
updateParts := make([]string, len(updateColumns))
for i, col := range updateColumns {
quotedCol := dialect.QuoteIdentifier(col)
if raw, ok := rawValues[col]; ok {
updateParts[i] = quotedCol + " = " + raw
} else {
updateParts[i] = quotedCol + " = EXCLUDED." + quotedCol
}
}
return "INSERT INTO " + processor.ProcessTableName(table) + " (" + strings.Join(quotedCols, ", ") +
") VALUES (" + strings.Join(valueParts, ", ") +
") ON CONFLICT (" + strings.Join(quotedUniqueKeys, ", ") +
") DO UPDATE SET " + strings.Join(updateParts, ", ")
}
// buildSQLiteUpsert 构建 SQLite 的 Upsert 语句
func (that *HoTimeDB) buildSQLiteUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string {
// INSERT INTO table (col1, col2) VALUES (?, ?)
// ON CONFLICT (unique_key) DO UPDATE SET col1 = excluded.col1
processor := that.GetProcessor()
dialect := that.GetDialect()
quotedCols := make([]string, len(columns))
valueParts := make([]string, len(columns))
for i, col := range columns {
quotedCols[i] = dialect.QuoteIdentifier(col)
if raw, ok := rawValues[col]; ok {
valueParts[i] = raw
} else {
valueParts[i] = "?"
}
}
quotedUniqueKeys := make([]string, len(uniqueKeys))
for i, key := range uniqueKeys {
quotedUniqueKeys[i] = dialect.QuoteIdentifier(key)
}
updateParts := make([]string, len(updateColumns))
for i, col := range updateColumns {
quotedCol := dialect.QuoteIdentifier(col)
if raw, ok := rawValues[col]; ok {
updateParts[i] = quotedCol + " = " + raw
} else {
updateParts[i] = quotedCol + " = excluded." + quotedCol
}
}
return "INSERT INTO " + processor.ProcessTableName(table) + " (" + strings.Join(quotedCols, ", ") +
") VALUES (" + strings.Join(valueParts, ", ") +
") ON CONFLICT (" + strings.Join(quotedUniqueKeys, ", ") +
") DO UPDATE SET " + strings.Join(updateParts, ", ")
}
// Update 更新数据
func (that *HoTimeDB) Update(table string, data Map, where Map) int64 {
processor := that.GetProcessor()
query := "UPDATE " + processor.ProcessTableName(table) + " SET "
qs := make([]interface{}, 0)
tp := len(data)
for k, v := range data {
vstr := "?"
if Substr(k, len(k)-3, 3) == "[#]" {
k = strings.Replace(k, "[#]", "", -1)
vstr = ObjToStr(v)
} else {
qs = append(qs, v)
}
query += processor.ProcessColumnNoPrefix(k) + "=" + vstr + " "
if tp--; tp != 0 {
query += ", "
}
}
temp, resWhere := that.where(where)
query += temp + ";"
qs = append(qs, resWhere...)
res, err := that.Exec(query, qs...)
rows := int64(0)
if err.GetError() == nil && res != nil {
rows, _ = res.RowsAffected()
}
// 如果更新成功,则删除缓存
if rows != 0 {
if that.HoTimeCache != nil && table != "cached" {
_ = that.HoTimeCache.Db(table+"*", nil)
}
}
return rows
}
// Delete 删除数据
func (that *HoTimeDB) Delete(table string, data map[string]interface{}) int64 {
processor := that.GetProcessor()
query := "DELETE FROM " + processor.ProcessTableName(table) + " "
temp, resWhere := that.where(data)
query += temp + ";"
res, err := that.Exec(query, resWhere...)
rows := int64(0)
if err.GetError() == nil && res != nil {
rows, _ = res.RowsAffected()
}
// 如果删除成功,删除对应缓存
if rows != 0 {
if that.HoTimeCache != nil && table != "cached" {
_ = that.HoTimeCache.Db(table+"*", nil)
}
}
return rows
}
+147
View File
@@ -0,0 +1,147 @@
package db
import (
"code.hoteas.com/golang/hotime/cache"
. "code.hoteas.com/golang/hotime/common"
"database/sql"
"strings"
"sync"
_ "github.com/go-sql-driver/mysql"
_ "github.com/mattn/go-sqlite3"
"github.com/sirupsen/logrus"
)
// HoTimeDB 数据库操作核心结构体
type HoTimeDB struct {
*sql.DB
ContextBase
DBName string
*cache.HoTimeCache
Log *logrus.Logger
Type string // 数据库类型: mysql, sqlite3, postgres
Prefix string
LastQuery string
LastData []interface{}
ConnectFunc func(err ...*Error) (*sql.DB, *sql.DB)
LastErr *Error
limit Slice
*sql.Tx //事务对象
SlaveDB *sql.DB // 从数据库
Mode int // mode为0生产模式,1为测试模式,2为开发模式
mu sync.RWMutex
limitMu sync.Mutex
Dialect Dialect // 数据库方言适配器
}
// SetConnect 设置数据库配置连接
func (that *HoTimeDB) SetConnect(connect func(err ...*Error) (master, slave *sql.DB), err ...*Error) {
that.ConnectFunc = connect
_ = that.InitDb(err...)
}
// InitDb 初始化数据库连接
func (that *HoTimeDB) InitDb(err ...*Error) *Error {
if len(err) != 0 {
that.LastErr = err[0]
}
that.DB, that.SlaveDB = that.ConnectFunc(that.LastErr)
if that.DB == nil {
return that.LastErr
}
e := that.DB.Ping()
that.LastErr.SetError(e)
if that.SlaveDB != nil {
e := that.SlaveDB.Ping()
that.LastErr.SetError(e)
}
// 根据数据库类型初始化方言适配器
if that.Dialect == nil {
that.initDialect()
}
return that.LastErr
}
// initDialect 根据数据库类型初始化方言
func (that *HoTimeDB) initDialect() {
switch that.Type {
case "postgres", "postgresql":
that.Dialect = &PostgreSQLDialect{}
case "sqlite3", "sqlite":
that.Dialect = &SQLiteDialect{}
default:
that.Dialect = &MySQLDialect{}
}
}
// GetDialect 获取当前方言适配器
func (that *HoTimeDB) GetDialect() Dialect {
if that.Dialect == nil {
that.initDialect()
}
return that.Dialect
}
// SetDialect 设置方言适配器
func (that *HoTimeDB) SetDialect(dialect Dialect) {
that.Dialect = dialect
}
// GetType 获取数据库类型
func (that *HoTimeDB) GetType() string {
return that.Type
}
// GetPrefix 获取表前缀
func (that *HoTimeDB) GetPrefix() string {
return that.Prefix
}
// GetProcessor 获取标识符处理器
// 用于处理表名、字段名的前缀添加和引号转换
func (that *HoTimeDB) GetProcessor() *IdentifierProcessor {
return NewIdentifierProcessor(that.GetDialect(), that.Prefix)
}
// T 辅助方法:获取带前缀和引号的表名
// 用于手动构建 SQL 时使用
// 示例: db.T("order") 返回 "`app_order`" (MySQL) 或 "\"app_order\"" (PostgreSQL)
func (that *HoTimeDB) T(table string) string {
return that.GetProcessor().ProcessTableName(table)
}
// C 辅助方法:获取带前缀和引号的 table.column
// 支持两种调用方式:
// - db.C("order", "name") 返回 "`app_order`.`name`"
// - db.C("order.name") 返回 "`app_order`.`name`"
func (that *HoTimeDB) C(args ...string) string {
if len(args) == 0 {
return ""
}
if len(args) == 1 {
return that.GetProcessor().ProcessColumn(args[0])
}
// 两个参数: table, column
dialect := that.GetDialect()
table := args[0]
column := args[1]
// 去除已有引号
table = trimQuotes(table)
column = trimQuotes(column)
return dialect.QuoteIdentifier(that.Prefix+table) + "." + dialect.QuoteIdentifier(column)
}
// trimQuotes 去除字符串两端的引号
func trimQuotes(s string) string {
s = strings.TrimSpace(s)
if len(s) >= 2 {
if (s[0] == '`' && s[len(s)-1] == '`') || (s[0] == '"' && s[len(s)-1] == '"') {
return s[1 : len(s)-1]
}
}
return s
}
+288
View File
@@ -0,0 +1,288 @@
package db
import (
"fmt"
"strings"
)
// Dialect 数据库方言接口
// 用于处理不同数据库之间的语法差异
type Dialect interface {
// Quote 对表名/字段名添加引号
// MySQL 使用反引号 `name`
// PostgreSQL 使用双引号 "name"
// SQLite 使用双引号或方括号 "name" 或 [name]
Quote(name string) string
// QuoteIdentifier 处理单个标识符(去除已有引号,添加正确引号)
// 输入可能带有反引号或双引号,会先去除再添加正确格式
QuoteIdentifier(name string) string
// QuoteChar 返回引号字符
// MySQL: `
// PostgreSQL/SQLite: "
QuoteChar() string
// Placeholder 生成占位符
// MySQL/SQLite 使用 ?
// PostgreSQL 使用 $1, $2, $3...
Placeholder(index int) string
// Placeholders 生成多个占位符,用逗号分隔
Placeholders(count int, startIndex int) string
// SupportsLastInsertId 是否支持 LastInsertId
// PostgreSQL 不支持,需要使用 RETURNING
SupportsLastInsertId() bool
// ReturningClause 生成 RETURNING 子句(用于 PostgreSQL
ReturningClause(column string) string
// UpsertSQL 生成 Upsert 语句
// MySQL: INSERT ... ON DUPLICATE KEY UPDATE ...
// PostgreSQL: INSERT ... ON CONFLICT ... DO UPDATE SET ...
// SQLite: INSERT OR REPLACE / INSERT ... ON CONFLICT ...
UpsertSQL(table string, columns []string, uniqueKeys []string, updateColumns []string) string
// GetName 获取方言名称
GetName() string
}
// MySQLDialect MySQL 方言实现
type MySQLDialect struct{}
func (d *MySQLDialect) GetName() string {
return "mysql"
}
func (d *MySQLDialect) Quote(name string) string {
// 如果已经包含点号(表.字段)或空格(别名),不添加引号
if strings.Contains(name, ".") || strings.Contains(name, " ") {
return name
}
return "`" + name + "`"
}
func (d *MySQLDialect) QuoteIdentifier(name string) string {
// 去除已有的引号(反引号和双引号)
name = strings.Trim(name, "`\"")
return "`" + name + "`"
}
func (d *MySQLDialect) QuoteChar() string {
return "`"
}
func (d *MySQLDialect) Placeholder(index int) string {
return "?"
}
func (d *MySQLDialect) Placeholders(count int, startIndex int) string {
if count <= 0 {
return ""
}
placeholders := make([]string, count)
for i := 0; i < count; i++ {
placeholders[i] = "?"
}
return strings.Join(placeholders, ",")
}
func (d *MySQLDialect) SupportsLastInsertId() bool {
return true
}
func (d *MySQLDialect) ReturningClause(column string) string {
return "" // MySQL 不支持 RETURNING
}
func (d *MySQLDialect) UpsertSQL(table string, columns []string, uniqueKeys []string, updateColumns []string) string {
// INSERT INTO table (col1, col2) VALUES (?, ?)
// ON DUPLICATE KEY UPDATE col1 = VALUES(col1), col2 = VALUES(col2)
quotedCols := make([]string, len(columns))
for i, col := range columns {
quotedCols[i] = d.Quote(col)
}
placeholders := d.Placeholders(len(columns), 1)
updateParts := make([]string, len(updateColumns))
for i, col := range updateColumns {
// 检查是否是 [#] 标记的直接 SQL
if strings.HasSuffix(col, "[#]") {
// 这种情况在调用处处理
updateParts[i] = col
} else {
quotedCol := d.Quote(col)
updateParts[i] = quotedCol + " = VALUES(" + quotedCol + ")"
}
}
return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s",
d.Quote(table),
strings.Join(quotedCols, ", "),
placeholders,
strings.Join(updateParts, ", "))
}
// PostgreSQLDialect PostgreSQL 方言实现
type PostgreSQLDialect struct{}
func (d *PostgreSQLDialect) GetName() string {
return "postgres"
}
func (d *PostgreSQLDialect) Quote(name string) string {
// 如果已经包含点号(表.字段)或空格(别名),不添加引号
if strings.Contains(name, ".") || strings.Contains(name, " ") {
return name
}
return "\"" + name + "\""
}
func (d *PostgreSQLDialect) QuoteIdentifier(name string) string {
// 去除已有的引号(反引号和双引号)
name = strings.Trim(name, "`\"")
return "\"" + name + "\""
}
func (d *PostgreSQLDialect) QuoteChar() string {
return "\""
}
func (d *PostgreSQLDialect) Placeholder(index int) string {
return fmt.Sprintf("$%d", index)
}
func (d *PostgreSQLDialect) Placeholders(count int, startIndex int) string {
if count <= 0 {
return ""
}
placeholders := make([]string, count)
for i := 0; i < count; i++ {
placeholders[i] = fmt.Sprintf("$%d", startIndex+i)
}
return strings.Join(placeholders, ",")
}
func (d *PostgreSQLDialect) SupportsLastInsertId() bool {
return false // PostgreSQL 需要使用 RETURNING
}
func (d *PostgreSQLDialect) ReturningClause(column string) string {
return " RETURNING " + d.Quote(column)
}
func (d *PostgreSQLDialect) UpsertSQL(table string, columns []string, uniqueKeys []string, updateColumns []string) string {
// INSERT INTO table (col1, col2) VALUES ($1, $2)
// ON CONFLICT (unique_key) DO UPDATE SET col1 = EXCLUDED.col1, col2 = EXCLUDED.col2
quotedCols := make([]string, len(columns))
for i, col := range columns {
quotedCols[i] = d.Quote(col)
}
placeholders := d.Placeholders(len(columns), 1)
quotedUniqueKeys := make([]string, len(uniqueKeys))
for i, key := range uniqueKeys {
quotedUniqueKeys[i] = d.Quote(key)
}
updateParts := make([]string, len(updateColumns))
for i, col := range updateColumns {
if strings.HasSuffix(col, "[#]") {
updateParts[i] = col
} else {
quotedCol := d.Quote(col)
updateParts[i] = quotedCol + " = EXCLUDED." + quotedCol
}
}
return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO UPDATE SET %s",
d.Quote(table),
strings.Join(quotedCols, ", "),
placeholders,
strings.Join(quotedUniqueKeys, ", "),
strings.Join(updateParts, ", "))
}
// SQLiteDialect SQLite 方言实现
type SQLiteDialect struct{}
func (d *SQLiteDialect) GetName() string {
return "sqlite3"
}
func (d *SQLiteDialect) Quote(name string) string {
// 如果已经包含点号(表.字段)或空格(别名),不添加引号
if strings.Contains(name, ".") || strings.Contains(name, " ") {
return name
}
return "\"" + name + "\""
}
func (d *SQLiteDialect) QuoteIdentifier(name string) string {
// 去除已有的引号(反引号和双引号)
name = strings.Trim(name, "`\"")
return "\"" + name + "\""
}
func (d *SQLiteDialect) QuoteChar() string {
return "\""
}
func (d *SQLiteDialect) Placeholder(index int) string {
return "?"
}
func (d *SQLiteDialect) Placeholders(count int, startIndex int) string {
if count <= 0 {
return ""
}
placeholders := make([]string, count)
for i := 0; i < count; i++ {
placeholders[i] = "?"
}
return strings.Join(placeholders, ",")
}
func (d *SQLiteDialect) SupportsLastInsertId() bool {
return true
}
func (d *SQLiteDialect) ReturningClause(column string) string {
return "" // SQLite 3.35+ 支持 RETURNING,但为兼容性暂不使用
}
func (d *SQLiteDialect) UpsertSQL(table string, columns []string, uniqueKeys []string, updateColumns []string) string {
// INSERT INTO table (col1, col2) VALUES (?, ?)
// ON CONFLICT (unique_key) DO UPDATE SET col1 = excluded.col1, col2 = excluded.col2
quotedCols := make([]string, len(columns))
for i, col := range columns {
quotedCols[i] = d.Quote(col)
}
placeholders := d.Placeholders(len(columns), 1)
quotedUniqueKeys := make([]string, len(uniqueKeys))
for i, key := range uniqueKeys {
quotedUniqueKeys[i] = d.Quote(key)
}
updateParts := make([]string, len(updateColumns))
for i, col := range updateColumns {
if strings.HasSuffix(col, "[#]") {
updateParts[i] = col
} else {
quotedCol := d.Quote(col)
updateParts[i] = quotedCol + " = excluded." + quotedCol
}
}
return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT (%s) DO UPDATE SET %s",
d.Quote(table),
strings.Join(quotedCols, ", "),
placeholders,
strings.Join(quotedUniqueKeys, ", "),
strings.Join(updateParts, ", "))
}
+441
View 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"
}
-1033
View File
File diff suppressed because it is too large Load Diff
+267
View 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
}
+391
View File
@@ -0,0 +1,391 @@
package db
import (
"database/sql"
"encoding/json"
"errors"
"reflect"
"strings"
. "code.hoteas.com/golang/hotime/common"
)
// md5 生成查询的 MD5 哈希(用于缓存)
func (that *HoTimeDB) md5(query string, args ...interface{}) string {
strByte, _ := json.Marshal(args)
str := Md5(query + ":" + string(strByte))
return str
}
// Query 执行查询 SQL
func (that *HoTimeDB) Query(query string, args ...interface{}) []Map {
return that.queryWithRetry(query, false, args...)
}
// queryWithRetry 内部查询方法,支持重试标记
func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interface{}) []Map {
// 预处理数组占位符 ?[]
query, args = that.expandArrayPlaceholder(query, args)
// 保存调试信息(加锁保护)
that.mu.Lock()
that.LastQuery = query
that.LastData = args
that.mu.Unlock()
defer func() {
if that.Mode != 0 {
that.mu.RLock()
that.Log.Info("SQL:"+that.LastQuery, " DATA:", that.LastData, " ERROR:", that.LastErr.GetError())
that.mu.RUnlock()
}
}()
var err error
var resl *sql.Rows
// 主从数据库切换,只有select语句有从数据库
db := that.DB
if that.SlaveDB != nil {
db = that.SlaveDB
}
if db == nil {
err = errors.New("没有初始化数据库")
that.LastErr.SetError(err)
return nil
}
// 处理参数中的 slice 类型
processedArgs := that.processArgs(args)
if that.Tx != nil {
resl, err = that.Tx.Query(query, processedArgs...)
} else {
resl, err = db.Query(query, processedArgs...)
}
that.LastErr.SetError(err)
if err != nil {
// 如果还没重试过,尝试 Ping 后重试一次
if !retried {
if pingErr := db.Ping(); pingErr == nil {
return that.queryWithRetry(query, true, args...)
}
}
return nil
}
return that.Row(resl)
}
// Exec 执行非查询 SQL
func (that *HoTimeDB) Exec(query string, args ...interface{}) (sql.Result, *Error) {
return that.execWithRetry(query, false, args...)
}
// execWithRetry 内部执行方法,支持重试标记
func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interface{}) (sql.Result, *Error) {
// 预处理数组占位符 ?[]
query, args = that.expandArrayPlaceholder(query, args)
// 保存调试信息(加锁保护)
that.mu.Lock()
that.LastQuery = query
that.LastData = args
that.mu.Unlock()
defer func() {
if that.Mode != 0 {
that.mu.RLock()
that.Log.Info("SQL: "+that.LastQuery, " DATA: ", that.LastData, " ERROR: ", that.LastErr.GetError())
that.mu.RUnlock()
}
}()
var e error
var resl sql.Result
if that.DB == nil {
err := errors.New("没有初始化数据库")
that.LastErr.SetError(err)
return nil, that.LastErr
}
// 处理参数中的 slice 类型
processedArgs := that.processArgs(args)
if that.Tx != nil {
resl, e = that.Tx.Exec(query, processedArgs...)
} else {
resl, e = that.DB.Exec(query, processedArgs...)
}
that.LastErr.SetError(e)
// 判断是否连接断开了,如果还没重试过,尝试重试一次
if e != nil {
if !retried {
if pingErr := that.DB.Ping(); pingErr == nil {
return that.execWithRetry(query, true, args...)
}
}
return resl, that.LastErr
}
return resl, that.LastErr
}
// processArgs 处理参数中的 slice 类型
func (that *HoTimeDB) processArgs(args []interface{}) []interface{} {
processedArgs := make([]interface{}, len(args))
copy(processedArgs, args)
for key := range processedArgs {
arg := processedArgs[key]
if arg == nil {
continue
}
argType := reflect.ValueOf(arg).Type().String()
if strings.Contains(argType, "[]") || strings.Contains(argType, "Slice") {
argLis := ObjToSlice(arg)
// 将slice转为逗号分割字符串
argStr := ""
for i := 0; i < len(argLis); i++ {
if i == len(argLis)-1 {
argStr += ObjToStr(argLis[i])
} else {
argStr += ObjToStr(argLis[i]) + ","
}
}
processedArgs[key] = argStr
}
}
return processedArgs
}
// expandArrayPlaceholder 展开 IN (?) / NOT IN (?) 中的数组参数
// 自动识别 IN/NOT IN (?) 模式,当参数是数组时展开为多个 ?
//
// 示例:
//
// db.Query("SELECT * FROM user WHERE id IN (?)", []int{1, 2, 3})
// // 展开为: SELECT * FROM user WHERE id IN (?, ?, ?) 参数: [1, 2, 3]
//
// db.Query("SELECT * FROM user WHERE id IN (?)", []int{})
// // 展开为: SELECT * FROM user WHERE 1=0 参数: [] (空集合的IN永假)
//
// db.Query("SELECT * FROM user WHERE id NOT IN (?)", []int{})
// // 展开为: SELECT * FROM user WHERE 1=1 参数: [] (空集合的NOT IN永真)
//
// db.Query("SELECT * FROM user WHERE id = ?", 1)
// // 保持不变: SELECT * FROM user WHERE id = ? 参数: [1]
func (that *HoTimeDB) expandArrayPlaceholder(query string, args []interface{}) (string, []interface{}) {
if len(args) == 0 || !strings.Contains(query, "?") {
return query, args
}
// 检查是否有数组参数
hasArray := false
for _, arg := range args {
if arg == nil {
continue
}
argType := reflect.ValueOf(arg).Type().String()
if strings.Contains(argType, "[]") || strings.Contains(argType, "Slice") {
hasArray = true
break
}
}
if !hasArray {
return query, args
}
newArgs := make([]interface{}, 0, len(args))
result := strings.Builder{}
argIndex := 0
for i := 0; i < len(query); i++ {
if query[i] == '?' && argIndex < len(args) {
arg := args[argIndex]
argIndex++
if arg == nil {
result.WriteByte('?')
newArgs = append(newArgs, nil)
continue
}
argType := reflect.ValueOf(arg).Type().String()
if strings.Contains(argType, "[]") || strings.Contains(argType, "Slice") {
// 是数组参数,检查是否在 IN (...) 或 NOT IN (...) 中
prevPart := result.String()
prevUpper := strings.ToUpper(prevPart)
// 查找最近的 NOT IN ( 模式
notInIndex := strings.LastIndex(prevUpper, " NOT IN (")
notInIndex2 := strings.LastIndex(prevUpper, " NOT IN(")
if notInIndex2 > notInIndex {
notInIndex = notInIndex2
}
// 查找最近的 IN ( 模式(但要排除 NOT IN 的情况)
inIndex := strings.LastIndex(prevUpper, " IN (")
inIndex2 := strings.LastIndex(prevUpper, " IN(")
if inIndex2 > inIndex {
inIndex = inIndex2
}
// 判断是 NOT IN 还是 IN
// 注意:" NOT IN (" 包含 " IN (",所以如果找到的 IN 位置在 NOT IN 范围内,应该优先判断为 NOT IN
isNotIn := false
matchIndex := -1
if notInIndex != -1 {
// 检查 inIndex 是否在 notInIndex 范围内(即 NOT IN 的 IN 部分)
// NOT IN ( 的 IN ( 部分从 notInIndex + 4 开始
if inIndex != -1 && inIndex >= notInIndex && inIndex <= notInIndex+5 {
// inIndex 是 NOT IN 的一部分,使用 NOT IN
isNotIn = true
matchIndex = notInIndex
} else if inIndex == -1 || notInIndex > inIndex {
// 没有独立的 IN,或 NOT IN 在 IN 之后
isNotIn = true
matchIndex = notInIndex
} else {
// 有独立的 IN 且在 NOT IN 之后
matchIndex = inIndex
}
} else if inIndex != -1 {
matchIndex = inIndex
}
// 检查 IN ( 后面是否只有空格(即当前 ? 紧跟在 IN ( 后面)
isInPattern := false
if matchIndex != -1 {
afterIn := prevPart[matchIndex:]
// 找到 ( 的位置
parenIdx := strings.Index(afterIn, "(")
if parenIdx != -1 {
afterParen := strings.TrimSpace(afterIn[parenIdx+1:])
if afterParen == "" {
isInPattern = true
}
}
}
if isInPattern {
// 在 IN (...) 或 NOT IN (...) 模式中
argList := ObjToSlice(arg)
if len(argList) == 0 {
// 空数组处理:需要找到字段名的开始位置
// 往前找最近的 AND/OR/WHERE/(,以确定条件的开始位置
truncateIndex := matchIndex
searchPart := prevUpper[:matchIndex]
// 找最近的分隔符位置
andIdx := strings.LastIndex(searchPart, " AND ")
orIdx := strings.LastIndex(searchPart, " OR ")
whereIdx := strings.LastIndex(searchPart, " WHERE ")
parenIdx := strings.LastIndex(searchPart, "(")
// 取最靠后的分隔符
sepIndex := -1
sepLen := 0
if andIdx > sepIndex {
sepIndex = andIdx
sepLen = 5 // " AND "
}
if orIdx > sepIndex {
sepIndex = orIdx
sepLen = 4 // " OR "
}
if whereIdx > sepIndex {
sepIndex = whereIdx
sepLen = 7 // " WHERE "
}
if parenIdx > sepIndex {
sepIndex = parenIdx
sepLen = 1 // "("
}
if sepIndex != -1 {
truncateIndex = sepIndex + sepLen
}
result.Reset()
result.WriteString(prevPart[:truncateIndex])
if isNotIn {
// NOT IN 空集合 = 永真
result.WriteString(" 1=1 ")
} else {
// IN 空集合 = 永假
result.WriteString(" 1=0 ")
}
// 跳过后面的 )
for j := i + 1; j < len(query); j++ {
if query[j] == ')' {
i = j
break
}
}
} else if len(argList) == 1 {
// 单元素数组
result.WriteByte('?')
newArgs = append(newArgs, argList[0])
} else {
// 多元素数组,展开为多个 ?
for j := 0; j < len(argList); j++ {
if j > 0 {
result.WriteString(", ")
}
result.WriteByte('?')
newArgs = append(newArgs, argList[j])
}
}
} else {
// 不在 IN 模式中,保持原有行为(数组会被 processArgs 转为逗号字符串)
result.WriteByte('?')
newArgs = append(newArgs, arg)
}
} else {
// 非数组参数
result.WriteByte('?')
newArgs = append(newArgs, arg)
}
} else {
result.WriteByte(query[i])
}
}
return result.String(), newArgs
}
// Row 数据库数据解析
func (that *HoTimeDB) Row(resl *sql.Rows) []Map {
dest := make([]Map, 0)
strs, _ := resl.Columns()
for i := 0; resl.Next(); i++ {
lis := make(Map, 0)
a := make([]interface{}, len(strs))
b := make([]interface{}, len(a))
for j := 0; j < len(a); j++ {
b[j] = &a[j]
}
err := resl.Scan(b...)
if err != nil {
that.LastErr.SetError(err)
return nil
}
for j := 0; j < len(a); j++ {
if a[j] != nil && reflect.ValueOf(a[j]).Type().String() == "[]uint8" {
lis[strs[j]] = string(a[j].([]byte))
} else {
lis[strs[j]] = a[j] // 取实际类型
}
}
dest = append(dest, lis)
}
return dest
}
+57
View File
@@ -0,0 +1,57 @@
package db
import (
"sync"
)
// Action 事务操作
// 如果 action 返回 true 则提交事务;返回 false 则回滚
func (that *HoTimeDB) Action(action func(db HoTimeDB) (isSuccess bool)) (isSuccess bool) {
db := HoTimeDB{
DB: that.DB,
ContextBase: that.ContextBase,
DBName: that.DBName,
HoTimeCache: that.HoTimeCache,
Log: that.Log,
Type: that.Type,
Prefix: that.Prefix,
LastQuery: that.LastQuery,
LastData: that.LastData,
ConnectFunc: that.ConnectFunc,
LastErr: that.LastErr,
limit: that.limit,
Tx: that.Tx,
SlaveDB: that.SlaveDB,
Mode: that.Mode,
Dialect: that.Dialect,
mu: sync.RWMutex{},
limitMu: sync.Mutex{},
}
tx, err := db.Begin()
if err != nil {
that.LastErr.SetError(err)
return isSuccess
}
db.Tx = tx
isSuccess = action(db)
if !isSuccess {
err = db.Tx.Rollback()
if err != nil {
that.LastErr.SetError(err)
return isSuccess
}
return isSuccess
}
err = db.Tx.Commit()
if err != nil {
that.LastErr.SetError(err)
return false
}
return true
}
+555
View File
@@ -0,0 +1,555 @@
package db
import (
. "code.hoteas.com/golang/hotime/common"
"reflect"
"sort"
"strings"
)
// 条件关键字
var condition = []string{"AND", "OR"}
// 特殊关键字(支持大小写)
var vcond = []string{"GROUP", "ORDER", "LIMIT", "DISTINCT", "HAVING", "OFFSET"}
// normalizeKey 标准化关键字(转大写)
func normalizeKey(k string) string {
upper := strings.ToUpper(k)
for _, v := range vcond {
if upper == v {
return v
}
}
for _, v := range condition {
if upper == v {
return v
}
}
return k
}
// isConditionKey 判断是否是条件关键字
func isConditionKey(k string) bool {
upper := strings.ToUpper(k)
for _, v := range condition {
if upper == v {
return true
}
}
return false
}
// isVcondKey 判断是否是特殊关键字
func isVcondKey(k string) bool {
upper := strings.ToUpper(k)
for _, v := range vcond {
if upper == v {
return true
}
}
return false
}
// where 语句解析
func (that *HoTimeDB) where(data Map) (string, []interface{}) {
where := ""
res := make([]interface{}, 0)
// 标准化 Map 的 key(大小写兼容)
normalizedData := Map{}
for k, v := range data {
normalizedData[normalizeKey(k)] = v
}
data = normalizedData
// 收集所有 key 并排序
testQu := []string{}
for key := range data {
testQu = append(testQu, key)
}
sort.Strings(testQu)
// 追踪条件数量,用于自动添加 AND
condCount := 0
for _, k := range testQu {
v := data[k]
// 检查是否是 AND/OR 条件关键字
if isConditionKey(k) {
tw, ts := that.cond(strings.ToUpper(k), v.(Map))
if tw != "" && strings.TrimSpace(tw) != "" {
// 与前面的条件用 AND 连接
if condCount > 0 {
where += " AND "
}
// 用括号包裹 OR/AND 组条件
where += "(" + strings.TrimSpace(tw) + ")"
condCount++
res = append(res, ts...)
}
continue
}
// 检查是否是特殊关键字(GROUP, ORDER, LIMIT 等)
if isVcondKey(k) {
continue // 特殊关键字在后面单独处理
}
// 处理普通条件字段
// 空切片的 IN 条件应该生成永假条件(1=0),而不是跳过
if v != nil && reflect.ValueOf(v).Type().String() == "common.Slice" && len(v.(Slice)) == 0 {
// 检查是否是 NOT IN(带 [!] 后缀)- NOT IN 空数组永真,跳过即可
if !strings.HasSuffix(k, "[!]") {
// IN 空数组 -> 生成永假条件
if condCount > 0 {
where += " AND "
}
where += "1=0 "
condCount++
}
continue
}
if v != nil && strings.Contains(reflect.ValueOf(v).Type().String(), "[]") && len(ObjToSlice(v)) == 0 {
// 检查是否是 NOT IN(带 [!] 后缀)- NOT IN 空数组永真,跳过即可
if !strings.HasSuffix(k, "[!]") {
// IN 空数组 -> 生成永假条件
if condCount > 0 {
where += " AND "
}
where += "1=0 "
condCount++
}
continue
}
tv, vv := that.varCond(k, v)
if tv != "" {
// 自动添加 AND 连接符
if condCount > 0 {
where += " AND "
}
where += tv
condCount++
res = append(res, vv...)
}
}
// 添加 WHERE 关键字
// 先去除首尾空格,检查是否有实际条件内容
trimmedWhere := strings.TrimSpace(where)
if len(trimmedWhere) != 0 {
hasWhere := true
for _, v := range vcond {
if strings.Index(trimmedWhere, v) == 0 {
hasWhere = false
}
}
if hasWhere {
where = " WHERE " + trimmedWhere + " "
}
} else {
// 没有实际条件内容,重置 where
where = ""
}
// 处理特殊字符(按固定顺序:GROUP, HAVING, ORDER, LIMIT, OFFSET
specialOrder := []string{"GROUP", "HAVING", "ORDER", "LIMIT", "OFFSET", "DISTINCT"}
for _, vcondKey := range specialOrder {
v, exists := data[vcondKey]
if !exists {
continue
}
switch vcondKey {
case "GROUP":
where += " GROUP BY "
where += that.formatVcondValue(v)
case "HAVING":
// HAVING 条件处理
if havingMap, ok := v.(Map); ok {
havingWhere, havingRes := that.cond("AND", havingMap)
if havingWhere != "" {
where += " HAVING " + strings.TrimSpace(havingWhere) + " "
res = append(res, havingRes...)
}
}
case "ORDER":
where += " ORDER BY "
where += that.formatVcondValue(v)
case "LIMIT":
where += " LIMIT "
where += that.formatVcondValue(v)
case "OFFSET":
where += " OFFSET " + ObjToStr(v) + " "
case "DISTINCT":
// DISTINCT 通常在 SELECT 中处理,这里暂时忽略
}
}
return where, res
}
// formatVcondValue 格式化特殊关键字的值
func (that *HoTimeDB) formatVcondValue(v interface{}) string {
result := ""
if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
vs := ObjToSlice(v)
for i := 0; i < len(vs); i++ {
result += " " + vs.GetString(i) + " "
if len(vs) != i+1 {
result += ", "
}
}
} else {
result += " " + ObjToStr(v) + " "
}
return result
}
// varCond 变量条件解析
func (that *HoTimeDB) varCond(k string, v interface{}) (string, []interface{}) {
where := ""
res := make([]interface{}, 0)
length := len(k)
processor := that.GetProcessor()
if k == "[#]" {
k = strings.Replace(k, "[#]", "", -1)
where += " " + ObjToStr(v) + " "
} else if k == "[##]" {
// 直接添加 SQL 片段(key 为 [##] 时)
where += " " + ObjToStr(v) + " "
} else if length > 0 && strings.Contains(k, "[") && k[length-1] == ']' {
def := false
switch Substr(k, length-3, 3) {
case "[>]":
k = strings.Replace(k, "[>]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + ">? "
res = append(res, v)
case "[<]":
k = strings.Replace(k, "[<]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + "<? "
res = append(res, v)
case "[!]":
k = strings.Replace(k, "[!]", "", -1)
k = processor.ProcessColumn(k) + " "
where, res = that.notIn(k, v, where, res)
case "[#]":
k = strings.Replace(k, "[#]", "", -1)
k = processor.ProcessColumn(k) + " "
where += " " + k + "=" + ObjToStr(v) + " "
case "[##]": // 直接添加value到sql,需要考虑防注入
where += " " + ObjToStr(v)
case "[#!]":
k = strings.Replace(k, "[#!]", "", -1)
k = processor.ProcessColumn(k) + " "
where += " " + k + "!=" + ObjToStr(v) + " "
case "[!#]":
k = strings.Replace(k, "[!#]", "", -1)
k = processor.ProcessColumn(k) + " "
where += " " + k + "!=" + ObjToStr(v) + " "
case "[~]":
k = strings.Replace(k, "[~]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " LIKE ? "
v = "%" + ObjToStr(v) + "%"
res = append(res, v)
case "[!~]": // 左边任意
k = strings.Replace(k, "[!~]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " LIKE ? "
v = "%" + ObjToStr(v) + ""
res = append(res, v)
case "[~!]": // 右边任意
k = strings.Replace(k, "[~!]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " LIKE ? "
v = ObjToStr(v) + "%"
res = append(res, v)
case "[~~]": // 手动任意
k = strings.Replace(k, "[~~]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " LIKE ? "
res = append(res, v)
default:
def = true
}
if def {
switch Substr(k, length-4, 4) {
case "[>=]":
k = strings.Replace(k, "[>=]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + ">=? "
res = append(res, v)
case "[<=]":
k = strings.Replace(k, "[<=]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + "<=? "
res = append(res, v)
case "[><]":
k = strings.Replace(k, "[><]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " NOT BETWEEN ? AND ? "
vs := ObjToSlice(v)
res = append(res, vs[0])
res = append(res, vs[1])
case "[<>]":
k = strings.Replace(k, "[<>]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " BETWEEN ? AND ? "
vs := ObjToSlice(v)
res = append(res, vs[0])
res = append(res, vs[1])
default:
where, res = that.handleDefaultCondition(k, v, where, res)
}
}
} else {
where, res = that.handlePlainField(k, v, where, res)
}
return where, res
}
// handleDefaultCondition 处理默认条件(带方括号但不是特殊操作符)
func (that *HoTimeDB) handleDefaultCondition(k string, v interface{}, where string, res []interface{}) (string, []interface{}) {
processor := that.GetProcessor()
k = processor.ProcessColumn(k) + " "
if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
vs := ObjToSlice(v)
if len(vs) == 0 {
// IN 空数组 -> 生成永假条件
where += "1=0 "
return where, res
}
if len(vs) == 1 {
where += k + "=? "
res = append(res, vs[0])
return where, res
}
// IN 优化:连续整数转为 BETWEEN
where, res = that.optimizeInCondition(k, vs, where, res)
} else {
where += k + "=? "
res = append(res, v)
}
return where, res
}
// handlePlainField 处理普通字段(无方括号)
func (that *HoTimeDB) handlePlainField(k string, v interface{}, where string, res []interface{}) (string, []interface{}) {
processor := that.GetProcessor()
k = processor.ProcessColumn(k) + " "
if v == nil {
where += k + " IS NULL "
} else if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
vs := ObjToSlice(v)
if len(vs) == 0 {
// IN 空数组 -> 生成永假条件
where += "1=0 "
return where, res
}
if len(vs) == 1 {
where += k + "=? "
res = append(res, vs[0])
return where, res
}
// IN 优化
where, res = that.optimizeInCondition(k, vs, where, res)
} else {
where += k + "=? "
res = append(res, v)
}
return where, res
}
// optimizeInCondition 优化 IN 条件(连续整数转为 BETWEEN)
func (that *HoTimeDB) optimizeInCondition(k string, vs Slice, where string, res []interface{}) (string, []interface{}) {
min := int64(0)
isMin := true
IsRange := true
num := int64(0)
isNum := true
where1 := ""
res1 := Slice{}
where2 := k + " IN ("
res2 := Slice{}
for kvs := 0; kvs <= len(vs); kvs++ {
vsv := int64(0)
if kvs < len(vs) {
vsv = vs.GetCeilInt64(kvs)
// 确保是全部是int类型
if ObjToStr(vsv) != vs.GetString(kvs) {
IsRange = false
break
}
}
if isNum {
isNum = false
num = vsv
} else {
num++
}
if isMin {
isMin = false
min = vsv
}
// 不等于则到了分路口
if num != vsv {
// between
if num-min > 1 {
if where1 != "" {
where1 += " OR " + k + " BETWEEN ? AND ? "
} else {
where1 += k + " BETWEEN ? AND ? "
}
res1 = append(res1, min)
res1 = append(res1, num-1)
} else {
where2 += "?,"
res2 = append(res2, min)
}
min = vsv
num = vsv
}
}
if IsRange {
where3 := ""
if where1 != "" {
where3 += where1
res = append(res, res1...)
}
if len(res2) == 1 {
if where3 == "" {
where3 += k + " = ? "
} else {
where3 += " OR " + k + " = ? "
}
res = append(res, res2...)
} else if len(res2) > 1 {
where2 = where2[:len(where2)-1]
if where3 == "" {
where3 += where2 + ")"
} else {
where3 += " OR " + where2 + ")"
}
res = append(res, res2...)
}
if where3 != "" {
where += "(" + where3 + ")"
}
return where, res
}
// 非连续整数,使用普通 IN
where += k + " IN ("
res = append(res, vs...)
for i := 0; i < len(vs); i++ {
if i+1 != len(vs) {
where += "?,"
} else {
where += "?) "
}
}
return where, res
}
// notIn NOT IN 条件处理
func (that *HoTimeDB) notIn(k string, v interface{}, where string, res []interface{}) (string, []interface{}) {
if v == nil {
where += k + " IS NOT NULL "
} else if reflect.ValueOf(v).Type().String() == "common.Slice" || strings.Contains(reflect.ValueOf(v).Type().String(), "[]") {
vs := ObjToSlice(v)
if len(vs) == 0 {
return where, res
}
where += k + " NOT IN ("
res = append(res, vs...)
for i := 0; i < len(vs); i++ {
if i+1 != len(vs) {
where += "?,"
} else {
where += "?) "
}
}
} else {
where += k + " !=? "
res = append(res, v)
}
return where, res
}
// cond 条件组合处理
func (that *HoTimeDB) cond(tag string, data Map) (string, []interface{}) {
where := " "
res := make([]interface{}, 0)
lens := len(data)
testQu := []string{}
for key := range data {
testQu = append(testQu, key)
}
sort.Strings(testQu)
for _, k := range testQu {
v := data[k]
x := 0
for i := 0; i < len(condition); i++ {
if condition[i] == strings.ToUpper(k) {
tw, ts := that.cond(strings.ToUpper(k), v.(Map))
if lens--; lens <= 0 {
where += "(" + tw + ") "
} else {
where += "(" + tw + ") " + tag + " "
}
res = append(res, ts...)
break
}
x++
}
if x == len(condition) {
tv, vv := that.varCond(k, v)
if tv == "" {
lens--
continue
}
res = append(res, vv...)
if lens--; lens <= 0 {
where += tv + ""
} else {
where += tv + " " + tag + " "
}
}
}
return where, res
}
+757
View 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
View 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
View 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)
@@ -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
- [ ] 选择字段注释格式正确(`标签:值-名称`
+516
View File
@@ -0,0 +1,516 @@
# HoTimeDB API 快速参考
## 条件查询语法规则
**新版本改进:**
- 多条件自动用 AND 连接,无需手动包装
- 关键字支持大小写(如 `LIMIT``limit` 都有效)
- 新增 `HAVING` 和独立 `OFFSET` 支持
```go
// ✅ 推荐:简化语法(多条件自动 AND)
Map{"status": 1, "age[>]": 18}
// 生成: WHERE `status`=? AND `age`>?
// ✅ 仍然支持:显式 AND 包装(向后兼容)
Map{
"AND": Map{
"status": 1,
"age[>]": 18,
},
}
// ✅ 混合条件和特殊关键字
Map{
"status": 1,
"age[>]": 18,
"ORDER": "id DESC", // 或 "order": "id DESC"
"LIMIT": 10, // 或 "limit": 10
}
```
## 基本方法
### 数据库连接
```go
database.SetConnect(func() (master, slave *sql.DB) { ... })
database.InitDb()
```
### 链式查询构建器
```go
// 创建查询构建器
builder := database.Table("tablename")
// 设置条件
builder.Where(key, value)
builder.And(key, value) builder.And(map)
builder.Or(key, value) builder.Or(map)
// JOIN操作
builder.LeftJoin(table, condition)
builder.RightJoin(table, condition)
builder.InnerJoin(table, condition)
builder.FullJoin(table, condition)
builder.Join(map) // 通用JOIN
// 排序和分组
builder.Order(fields...)
builder.Group(fields...)
builder.Limit(args...)
builder.Having(map) // 新增
// 分页
builder.Page(page, pageSize)
builder.Offset(offset) // 新增
// 执行查询
builder.Select(fields...) // 返回 []Map
builder.Get(fields...) // 返回 Map
builder.Count() // 返回 int
builder.Update(data) // 返回 int64
builder.Delete() // 返回 int64
```
## CRUD 操作
### 查询 (Select)
```go
// 基本查询
data := database.Select("table")
data := database.Select("table", "field1,field2")
data := database.Select("table", []string{"field1", "field2"})
data := database.Select("table", "*", whereMap)
// 带JOIN查询
data := database.Select("table", joinSlice, "fields", whereMap)
```
### 获取单条 (Get)
```go
// 自动添加 LIMIT 1
row := database.Get("table", "fields", whereMap)
```
### 插入 (Insert)
```go
id := database.Insert("table", dataMap)
// 返回新插入记录的ID
```
### 批量插入 (Inserts) - 新增
```go
// 使用 []Map 格式,更直观简洁
affected := database.Inserts("table", []Map{
{"col1": "val1", "col2": "val2", "col3": "val3"},
{"col1": "val4", "col2": "val5", "col3": "val6"},
})
// 返回受影响的行数
// 支持 [#] 标记直接 SQL
affected := database.Inserts("log", []Map{
{"user_id": 1, "created_time[#]": "NOW()"},
{"user_id": 2, "created_time[#]": "NOW()"},
})
```
### 更新 (Update)
```go
affected := database.Update("table", dataMap, whereMap)
// 返回受影响的行数
```
### Upsert - 新增
```go
// 使用 Slice 格式
affected := database.Upsert("table",
dataMap, // 插入数据
Slice{"unique_key"}, // 唯一键
Slice{"col1", "col2"}, // 冲突时更新的字段
)
// 也支持可变参数
affected := database.Upsert("table", dataMap, Slice{"id"}, "col1", "col2")
// 返回受影响的行数
```
### 删除 (Delete)
```go
affected := database.Delete("table", whereMap)
// 返回删除的行数
```
## 聚合函数
### 计数
```go
count := database.Count("table")
count := database.Count("table", whereMap)
count := database.Count("table", joinSlice, whereMap)
```
### 求和
```go
sum := database.Sum("table", "column")
sum := database.Sum("table", "column", whereMap)
```
### 平均值 - 新增
```go
avg := database.Avg("table", "column")
avg := database.Avg("table", "column", whereMap)
```
### 最大值 - 新增
```go
max := database.Max("table", "column")
max := database.Max("table", "column", whereMap)
```
### 最小值 - 新增
```go
min := database.Min("table", "column")
min := database.Min("table", "column", whereMap)
```
## 分页查询
```go
// 设置分页
database.Page(page, pageSize)
// 分页查询
data := database.Page(page, pageSize).PageSelect("table", "fields", whereMap)
```
## 条件语法参考
### 比较操作符
| 写法 | SQL | 说明 |
|------|-----|------|
| `"field": value` | `field = ?` | 等于 |
| `"field[!]": value` | `field != ?` | 不等于 |
| `"field[>]": value` | `field > ?` | 大于 |
| `"field[>=]": value` | `field >= ?` | 大于等于 |
| `"field[<]": value` | `field < ?` | 小于 |
| `"field[<=]": value` | `field <= ?` | 小于等于 |
### 模糊查询
| 写法 | SQL | 说明 |
|------|-----|------|
| `"field[~]": "keyword"` | `field LIKE '%keyword%'` | 包含 |
| `"field[~!]": "keyword"` | `field LIKE 'keyword%'` | 以...开头 |
| `"field[!~]": "keyword"` | `field LIKE '%keyword'` | 以...结尾 |
| `"field[~~]": "%keyword%"` | `field LIKE '%keyword%'` | 手动LIKE |
### 范围查询
| 写法 | SQL | 说明 |
|------|-----|------|
| `"field[<>]": [min, max]` | `field BETWEEN ? AND ?` | 区间内 |
| `"field[><]": [min, max]` | `field NOT BETWEEN ? AND ?` | 区间外 |
### 集合查询
| 写法 | SQL | 说明 |
|------|-----|------|
| `"field": [v1, v2, v3]` | `field IN (?, ?, ?)` | 在集合中 |
| `"field[!]": [v1, v2, v3]` | `field NOT IN (?, ?, ?)` | 不在集合中 |
### NULL查询
| 写法 | SQL | 说明 |
|------|-----|------|
| `"field": nil` | `field IS NULL` | 为空 |
| `"field[!]": nil` | `field IS NOT NULL` | 不为空 |
### 直接SQL
| 写法 | SQL | 说明 |
|------|-----|------|
| `"field[#]": "NOW()"` | `field = NOW()` | 直接SQL函数 |
| `"[##]": "a > b"` | `a > b` | 直接SQL片段 |
| `"field[#!]": "1"` | `field != 1` | 不等于(不参数化) |
## 逻辑连接符
### AND 条件
```go
// 简化语法(推荐)
whereMap := Map{
"status": 1,
"age[>]": 18,
}
// 生成: WHERE `status`=? AND `age`>?
// 显式 AND(向后兼容)
whereMap := Map{
"AND": Map{
"status": 1,
"age[>]": 18,
},
}
```
### OR 条件
```go
whereMap := Map{
"OR": Map{
"status": 1,
"type": 2,
},
}
```
### 嵌套条件
```go
whereMap := Map{
"AND": Map{
"status": 1,
"OR": Map{
"age[<]": 30,
"level[>]": 5,
},
},
}
```
## JOIN 语法
### 传统语法
```go
joinSlice := Slice{
Map{"[>]profile": "user.id = profile.user_id"}, // LEFT JOIN
Map{"[<]department": "user.dept_id = department.id"}, // RIGHT JOIN
Map{"[><]role": "user.role_id = role.id"}, // INNER JOIN
Map{"[<>]group": "user.group_id = group.id"}, // FULL JOIN
}
```
### 链式语法
```go
builder.LeftJoin("profile", "user.id = profile.user_id")
builder.RightJoin("department", "user.dept_id = department.id")
builder.InnerJoin("role", "user.role_id = role.id")
builder.FullJoin("group", "user.group_id = group.id")
```
## 特殊字段语法
### ORDER BY
```go
Map{
"ORDER": []string{"created_time DESC", "id ASC"},
}
// 或
Map{
"order": "created_time DESC", // 支持小写
}
```
### GROUP BY
```go
Map{
"GROUP": []string{"department", "level"},
}
// 或
Map{
"group": "department", // 支持小写
}
```
### HAVING - 新增
```go
Map{
"GROUP": "dept_id",
"HAVING": Map{
"COUNT(*).[>]": 5,
},
}
```
### LIMIT
```go
Map{
"LIMIT": []int{10, 20}, // offset 10, limit 20
}
// 或
Map{
"limit": 20, // limit 20,支持小写
}
```
### OFFSET - 新增
```go
Map{
"LIMIT": 10,
"OFFSET": 20, // 独立的 OFFSET
}
```
## 事务处理
```go
success := database.Action(func(tx HoTimeDB) bool {
// 在这里执行数据库操作
// 返回 true 提交事务
// 返回 false 回滚事务
id := tx.Insert("table", data)
if id == 0 {
return false // 回滚
}
affected := tx.Update("table2", data2, where2)
if affected == 0 {
return false // 回滚
}
return true // 提交
})
```
## 原生SQL执行
### 查询
```go
results := database.Query("SELECT * FROM user WHERE age > ?", 18)
```
### 执行
```go
result, err := database.Exec("UPDATE user SET status = ? WHERE id = ?", 1, 100)
affected, _ := result.RowsAffected()
```
## PostgreSQL 支持 - 新增
```go
// 配置 PostgreSQL
database := &db.HoTimeDB{
Type: "postgres", // 设置类型
}
// 框架自动处理差异:
// - 占位符: ? -> $1, $2, $3...
// - 引号: `name` -> "name"
// - Upsert: ON DUPLICATE KEY -> ON CONFLICT
```
## 错误处理
```go
// 检查最后的错误
if database.LastErr.GetError() != nil {
fmt.Println("错误:", database.LastErr.GetError())
}
// 查看最后执行的SQL
fmt.Println("SQL:", database.LastQuery)
fmt.Println("参数:", database.LastData)
```
## 工具方法
### 数据库信息
```go
prefix := database.GetPrefix() // 获取表前缀
dbType := database.GetType() // 获取数据库类型
dialect := database.GetDialect() // 获取方言适配器
```
### 设置模式
```go
database.Mode = 0 // 生产模式
database.Mode = 1 // 测试模式
database.Mode = 2 // 开发模式(输出SQL日志)
```
## 常用查询模式
### 分页列表查询
```go
// 获取总数
total := database.Count("user", Map{"status": 1})
// 分页数据
users := database.Table("user").
Where("status", 1).
Order("created_time DESC").
Page(page, pageSize).
Select("id,name,email,created_time")
// 计算分页信息
totalPages := (total + pageSize - 1) / pageSize
```
### 关联查询
```go
orders := database.Table("order").
LeftJoin("user", "order.user_id = user.id").
LeftJoin("product", "order.product_id = product.id").
Where("order.status", "paid").
Select(`
order.*,
user.name as user_name,
product.title as product_title
`)
```
### 统计查询
```go
stats := database.Select("order",
"user_id, COUNT(*) as order_count, SUM(amount) as total_amount",
Map{
"status": "paid",
"created_time[>]": "2023-01-01",
"GROUP": "user_id",
"ORDER": "total_amount DESC",
})
```
### 批量操作
```go
// 批量插入(使用 []Map 格式)
affected := database.Inserts("user", []Map{
{"name": "用户1", "email": "user1@example.com", "status": 1},
{"name": "用户2", "email": "user2@example.com", "status": 1},
{"name": "用户3", "email": "user3@example.com", "status": 1},
})
// Upsert(插入或更新,使用 Slice 格式)
affected := database.Upsert("user",
Map{"id": 1, "name": "新名称", "email": "new@example.com"},
Slice{"id"},
Slice{"name", "email"},
)
```
## 链式调用完整示例
```go
// 复杂查询链式调用
result := database.Table("order").
LeftJoin("user", "order.user_id = user.id").
LeftJoin("product", "order.product_id = product.id").
Where("order.status", "paid").
And("order.created_time[>]", "2023-01-01").
And(Map{
"OR": Map{
"user.level": "vip",
"order.amount[>]": 1000,
},
}).
Group("user.id").
Having(Map{"total_amount[>]": 500}).
Order("total_amount DESC").
Page(1, 20).
Select(`
user.id,
user.name,
user.email,
COUNT(order.id) as order_count,
SUM(order.amount) as total_amount
`)
```
---
*快速参考版本: 2.0*
*更新日期: 2026年1月*
**详细说明:**
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md) - 完整教程
+833
View File
@@ -0,0 +1,833 @@
# HoTimeDB ORM 使用说明书
## 概述
HoTimeDB是一个基于Golang实现的轻量级ORM框架,参考PHP Medoo设计,提供简洁的数据库操作接口。支持MySQL、SQLite、PostgreSQL等数据库,并集成了缓存、事务、链式查询等功能。
## 目录
- [快速开始](#快速开始)
- [数据库配置](#数据库配置)
- [基本操作](#基本操作)
- [查询(Select)](#查询select)
- [获取单条记录(Get)](#获取单条记录get)
- [插入(Insert)](#插入insert)
- [批量插入(Inserts)](#批量插入Inserts)
- [更新(Update)](#更新update)
- [Upsert操作](#upsert操作)
- [删除(Delete)](#删除delete)
- [链式查询构建器](#链式查询构建器)
- [条件查询语法](#条件查询语法)
- [JOIN操作](#join操作)
- [分页查询](#分页查询)
- [聚合函数](#聚合函数)
- [事务处理](#事务处理)
- [缓存机制](#缓存机制)
- [PostgreSQL支持](#postgresql支持)
- [高级特性](#高级特性)
## 快速开始
### 初始化数据库连接
```go
import (
"code.hoteas.com/golang/hotime/db"
"code.hoteas.com/golang/hotime/common"
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
// 创建连接函数
func createConnection() (master, slave *sql.DB) {
master, _ = sql.Open("mysql", "user:password@tcp(localhost:3306)/database")
// slave是可选的,用于读写分离
slave = master // 或者连接到从数据库
return
}
// 初始化HoTimeDB
database := &db.HoTimeDB{
Type: "mysql", // 可选:mysql, sqlite3, postgres
}
database.SetConnect(createConnection)
```
## 数据库配置
### 基本配置
```go
type HoTimeDB struct {
*sql.DB
ContextBase
DBName string
*cache.HoTimeCache
Log *logrus.Logger
Type string // 数据库类型:mysql, sqlite3, postgres
Prefix string // 表前缀
LastQuery string // 最后执行的SQL
LastData []interface{} // 最后的参数
ConnectFunc func(err ...*Error) (*sql.DB, *sql.DB)
LastErr *Error
limit Slice
*sql.Tx // 事务对象
SlaveDB *sql.DB // 从数据库
Mode int // 0生产模式,1测试模式,2开发模式
Dialect Dialect // 数据库方言适配器
}
```
### 设置表前缀
```go
database.Prefix = "app_"
```
### 设置运行模式
```go
database.Mode = 2 // 开发模式,会输出SQL日志
```
## 基本操作
### 查询(Select)
#### 基本查询
```go
// 查询所有字段
users := database.Select("user")
// 查询指定字段
users := database.Select("user", "id,name,email")
// 查询指定字段(数组形式)
users := database.Select("user", []string{"id", "name", "email"})
// 单条件查询
users := database.Select("user", "*", common.Map{
"status": 1,
})
// 多条件查询(自动用 AND 连接)
users := database.Select("user", "*", common.Map{
"status": 1,
"age[>]": 18,
})
// 生成: WHERE `status`=? AND `age`>?
```
#### 复杂条件查询
```go
// 简化语法:多条件自动用 AND 连接
users := database.Select("user", "*", common.Map{
"status": 1,
"age[>]": 18,
"name[~]": "张",
})
// 生成: WHERE `status`=? AND `age`>? AND `name` LIKE ?
// 显式 AND 条件(与上面等效)
users := database.Select("user", "*", common.Map{
"AND": common.Map{
"status": 1,
"age[>]": 18,
"name[~]": "张",
},
})
// OR 条件
users := database.Select("user", "*", common.Map{
"OR": common.Map{
"status": 1,
"type": 2,
},
})
// 混合条件(嵌套 AND/OR
users := database.Select("user", "*", common.Map{
"AND": common.Map{
"status": 1,
"OR": common.Map{
"age[<]": 30,
"level[>]": 5,
},
},
})
// 带 ORDER BY、LIMIT 等特殊条件(关键字支持大小写)
users := database.Select("user", "*", common.Map{
"status": 1,
"age[>]": 18,
"ORDER": "id DESC", // 或 "order": "id DESC"
"LIMIT": 10, // 或 "limit": 10
})
// 带多个特殊条件
users := database.Select("user", "*", common.Map{
"OR": common.Map{
"level": "vip",
"balance[>]": 1000,
},
"ORDER": []string{"created_time DESC", "id ASC"},
"GROUP": "department",
"LIMIT": []int{0, 20}, // offset 0, limit 20
})
// 使用 HAVING 过滤分组结果
users := database.Select("user", "dept_id, COUNT(*) as cnt", common.Map{
"GROUP": "dept_id",
"HAVING": common.Map{
"cnt[>]": 5,
},
})
// 使用独立 OFFSET
users := database.Select("user", "*", common.Map{
"status": 1,
"LIMIT": 10,
"OFFSET": 20,
})
```
### 获取单条记录(Get)
```go
// 获取单个用户
user := database.Get("user", "*", common.Map{
"id": 1,
})
// 获取指定字段
user := database.Get("user", "id,name,email", common.Map{
"status": 1,
})
```
### 插入(Insert)
```go
// 基本插入
id := database.Insert("user", common.Map{
"name": "张三",
"email": "zhangsan@example.com",
"age": 25,
"status": 1,
"created_time[#]": "NOW()", // [#]表示直接插入SQL函数
})
// 返回插入的ID
fmt.Println("插入的用户ID:", id)
```
### 批量插入(Inserts)
```go
// 批量插入多条记录(使用 []Map 格式,更直观)
affected := database.Inserts("user", []common.Map{
{"name": "张三", "email": "zhang@example.com", "age": 25},
{"name": "李四", "email": "li@example.com", "age": 30},
{"name": "王五", "email": "wang@example.com", "age": 28},
})
// 生成: INSERT INTO `user` (`age`, `email`, `name`) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
fmt.Printf("批量插入 %d 条记录\n", affected)
// 支持 [#] 标记直接插入 SQL 表达式
affected := database.Inserts("log", []common.Map{
{"user_id": 1, "action": "login", "created_time[#]": "NOW()"},
{"user_id": 2, "action": "logout", "created_time[#]": "NOW()"},
})
```
### 更新(Update)
```go
// 基本更新
affected := database.Update("user", common.Map{
"name": "李四",
"email": "lisi@example.com",
"updated_time[#]": "NOW()",
}, common.Map{
"id": 1,
})
// 条件更新(多条件自动 AND 连接)
affected := database.Update("user", common.Map{
"status": 0,
}, common.Map{
"age[<]": 18,
"status": 1,
})
fmt.Println("更新的记录数:", affected)
```
### Upsert操作
Upsert(插入或更新):如果记录存在则更新,不存在则插入。
```go
// Upsert 操作(使用 Slice 格式)
affected := database.Upsert("user",
common.Map{
"id": 1,
"name": "张三",
"email": "zhang@example.com",
"login_count": 1,
},
common.Slice{"id"}, // 唯一键(用于冲突检测)
common.Slice{"name", "email", "login_count"}, // 冲突时更新的字段
)
// MySQL 生成:
// INSERT INTO user (id,name,email,login_count) VALUES (?,?,?,?)
// ON DUPLICATE KEY UPDATE name=VALUES(name), email=VALUES(email), login_count=VALUES(login_count)
// PostgreSQL 生成:
// INSERT INTO "user" (id,name,email,login_count) VALUES ($1,$2,$3,$4)
// ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, email=EXCLUDED.email, login_count=EXCLUDED.login_count
// 使用 [#] 标记直接 SQL 更新
affected := database.Upsert("user",
common.Map{
"id": 1,
"name": "张三",
"login_count[#]": "login_count + 1", // 直接 SQL 表达式
},
common.Slice{"id"},
common.Slice{"name", "login_count"},
)
// 也支持可变参数形式
affected := database.Upsert("user",
common.Map{"id": 1, "name": "张三"},
common.Slice{"id"},
"name", "email", // 可变参数
)
```
### 删除(Delete)
```go
// 根据ID删除
affected := database.Delete("user", common.Map{
"id": 1,
})
// 条件删除(多条件自动 AND 连接)
affected := database.Delete("user", common.Map{
"status": 0,
"created_time[<]": "2023-01-01",
})
fmt.Println("删除的记录数:", affected)
```
## 链式查询构建器
HoTimeDB提供了链式查询构建器,让查询更加直观:
```go
// 基本链式查询
users := database.Table("user").
Where("status", 1).
And("age[>]", 18).
Order("created_time DESC").
Limit(10, 20). // offset, limit
Select()
// 链式获取单条记录
user := database.Table("user").
Where("id", 1).
Get()
// 链式更新
affected := database.Table("user").
Where("id", 1).
Update(common.Map{
"name": "新名称",
"updated_time[#]": "NOW()",
})
// 链式删除
affected := database.Table("user").
Where("status", 0).
Delete()
// 链式统计
count := database.Table("user").
Where("status", 1).
Count()
```
### 链式条件组合
```go
// 复杂条件组合
users := database.Table("user").
Where("status", 1).
And("age[>=]", 18).
Or(common.Map{
"level[>]": 5,
"vip": 1,
}).
Order("created_time DESC", "id ASC").
Group("department").
Having(common.Map{"COUNT(*).[>]": 5}). // 新增 HAVING 支持
Limit(0, 20).
Offset(10). // 新增独立 OFFSET 支持
Select("id,name,email,age")
```
## 条件查询语法
HoTimeDB支持丰富的条件查询语法,类似于Medoo:
### 基本比较
```go
// 等于
"id": 1
// 不等于
"id[!]": 1
// 大于
"age[>]": 18
// 大于等于
"age[>=]": 18
// 小于
"age[<]": 60
// 小于等于
"age[<=]": 60
```
### 模糊查询
```go
// LIKE %keyword%
"name[~]": "张"
// LIKE keyword% (右边任意)
"name[~!]": "张"
// LIKE %keyword (左边任意)
"name[!~]": "san"
// 手动LIKE(需要手动添加%
"name[~~]": "%张%"
```
### 区间查询
```go
// BETWEEN
"age[<>]": []int{18, 60}
// NOT BETWEEN
"age[><]": []int{18, 25}
```
### IN查询
```go
// IN
"id": []int{1, 2, 3, 4, 5}
// NOT IN
"id[!]": []int{1, 2, 3}
```
### NULL查询
```go
// IS NULL
"deleted_at": nil
// IS NOT NULL
"deleted_at[!]": nil
```
### 直接SQL
```go
// 直接插入SQL表达式(注意防注入)
"created_time[#]": "> DATE_SUB(NOW(), INTERVAL 1 DAY)"
// 字段直接赋值(不使用参数化查询)
"update_time[#]": "NOW()"
// 直接SQL片段
"[##]": "user.status = 1 AND user.level > 0"
```
## JOIN操作
### 链式JOIN
```go
// LEFT JOIN
users := database.Table("user").
LeftJoin("profile", "user.id = profile.user_id").
LeftJoin("department", "user.dept_id = department.id").
Where("user.status", 1).
Select("user.*, profile.avatar, department.name AS dept_name")
// RIGHT JOIN
users := database.Table("user").
RightJoin("order", "user.id = order.user_id").
Select()
// INNER JOIN
users := database.Table("user").
InnerJoin("profile", "user.id = profile.user_id").
Select()
// FULL JOIN
users := database.Table("user").
FullJoin("profile", "user.id = profile.user_id").
Select()
```
### 传统JOIN语法
```go
users := database.Select("user",
common.Slice{
common.Map{"[>]profile": "user.id = profile.user_id"},
common.Map{"[>]department": "user.dept_id = department.id"},
},
"user.*, profile.avatar, department.name AS dept_name",
common.Map{
"user.status": 1,
},
)
```
### JOIN类型说明
- `[>]`: LEFT JOIN
- `[<]`: RIGHT JOIN
- `[><]`: INNER JOIN
- `[<>]`: FULL JOIN
## 分页查询
### 基本分页
```go
// 设置分页:页码3,每页20条
users := database.Page(3, 20).PageSelect("user", "*", common.Map{
"status": 1,
})
// 链式分页
users := database.Table("user").
Where("status", 1).
Page(2, 15). // 第2页,每页15条
Select()
```
### 分页信息获取
```go
// 获取总数
total := database.Count("user", common.Map{
"status": 1,
})
// 计算分页信息
page := 2
pageSize := 20
offset := (page - 1) * pageSize
totalPages := (total + pageSize - 1) / pageSize
fmt.Printf("总记录数: %d, 总页数: %d, 当前页: %d\n", total, totalPages, page)
```
## 聚合函数
### 计数
```go
// 总数统计
total := database.Count("user")
// 条件统计
activeUsers := database.Count("user", common.Map{
"status": 1,
})
// JOIN统计
count := database.Count("user",
common.Slice{
common.Map{"[>]profile": "user.id = profile.user_id"},
},
common.Map{
"user.status": 1,
"profile.verified": 1,
},
)
```
### 求和
```go
// 基本求和
totalAmount := database.Sum("order", "amount")
// 条件求和
paidAmount := database.Sum("order", "amount", common.Map{
"status": "paid",
"created_time[>]": "2023-01-01",
})
```
### 平均值
```go
// 基本平均值
avgAge := database.Avg("user", "age")
// 条件平均值
avgAge := database.Avg("user", "age", common.Map{
"status": 1,
})
```
### 最大值/最小值
```go
// 最大值
maxAge := database.Max("user", "age")
// 最小值
minAge := database.Min("user", "age")
// 条件最大值
maxBalance := database.Max("user", "balance", common.Map{
"status": 1,
})
```
## 事务处理
```go
// 事务操作
success := database.Action(func(tx db.HoTimeDB) bool {
// 在事务中执行多个操作
// 扣减用户余额
affected1 := tx.Update("user", common.Map{
"balance[#]": "balance - 100",
}, common.Map{
"id": 1,
})
if affected1 == 0 {
return false // 回滚
}
// 创建订单
orderId := tx.Insert("order", common.Map{
"user_id": 1,
"amount": 100,
"status": "paid",
"created_time[#]": "NOW()",
})
if orderId == 0 {
return false // 回滚
}
return true // 提交
})
if success {
fmt.Println("事务执行成功")
} else {
fmt.Println("事务回滚")
fmt.Println("错误:", database.LastErr.GetError())
}
```
## 缓存机制
HoTimeDB集成了缓存功能,可以自动缓存查询结果:
### 缓存配置
```go
import "code.hoteas.com/golang/hotime/cache"
// 设置缓存
database.HoTimeCache = &cache.HoTimeCache{
// 缓存配置
}
```
### 缓存行为
- 查询操作会自动检查缓存
- 增删改操作会自动清除相关缓存
- 缓存键格式:`表名:查询MD5`
- `cached`表不会被缓存
### 缓存清理
```go
// 手动清除表缓存
database.HoTimeCache.Db("user*", nil) // 清除user表所有缓存
```
## PostgreSQL支持
HoTimeDB 支持 PostgreSQL 数据库,自动处理语法差异。
### PostgreSQL 配置
```go
import (
"code.hoteas.com/golang/hotime/db"
_ "github.com/lib/pq" // PostgreSQL 驱动
)
database := &db.HoTimeDB{
Type: "postgres", // 设置数据库类型为 postgres
Prefix: "app_",
}
database.SetConnect(func(err ...*common.Error) (master, slave *sql.DB) {
dsn := "host=localhost port=5432 user=postgres password=secret dbname=mydb sslmode=disable"
master, _ = sql.Open("postgres", dsn)
return master, master
})
```
### 主要差异
| 特性 | MySQL | PostgreSQL |
|------|-------|------------|
| 标识符引号 | \`name\` | "name" |
| 占位符 | ? | $1, $2, $3... |
| Upsert | ON DUPLICATE KEY UPDATE | ON CONFLICT DO UPDATE |
所有这些差异由框架自动处理,无需手动调整代码。
## 高级特性
### 调试模式
```go
// 设置调试模式
database.Mode = 2
// 查看最后执行的SQL
fmt.Println("最后的SQL:", database.LastQuery)
fmt.Println("参数:", database.LastData)
fmt.Println("错误:", database.LastErr.GetError())
```
### 主从分离
```go
func createConnection() (master, slave *sql.DB) {
// 主库连接
master, _ = sql.Open("mysql", "user:password@tcp(master:3306)/database")
// 从库连接
slave, _ = sql.Open("mysql", "user:password@tcp(slave:3306)/database")
return master, slave
}
database.SetConnect(createConnection)
// 查询会自动使用从库,增删改使用主库
```
### 原生SQL执行
```go
// 执行查询SQL
results := database.Query("SELECT * FROM user WHERE age > ? AND status = ?", 18, 1)
// 执行更新SQL
result, err := database.Exec("UPDATE user SET last_login = NOW() WHERE id = ?", 1)
if err.GetError() == nil {
affected, _ := result.RowsAffected()
fmt.Println("影响行数:", affected)
}
```
## 特殊语法详解
### 条件标记符说明
| 标记符 | 功能 | 示例 | 生成SQL |
|--------|------|------|---------|
| `[>]` | 大于 | `"age[>]": 18` | `age > 18` |
| `[<]` | 小于 | `"age[<]": 60` | `age < 60` |
| `[>=]` | 大于等于 | `"age[>=]": 18` | `age >= 18` |
| `[<=]` | 小于等于 | `"age[<=]": 60` | `age <= 60` |
| `[!]` | 不等于/NOT IN | `"id[!]": 1` | `id != 1` |
| `[~]` | LIKE模糊查询 | `"name[~]": "张"` | `name LIKE '%张%'` |
| `[!~]` | 左模糊 | `"name[!~]": "张"` | `name LIKE '%张'` |
| `[~!]` | 右模糊 | `"name[~!]": "张"` | `name LIKE '张%'` |
| `[~~]` | 手动LIKE | `"name[~~]": "%张%"` | `name LIKE '%张%'` |
| `[<>]` | BETWEEN | `"age[<>]": [18,60]` | `age BETWEEN 18 AND 60` |
| `[><]` | NOT BETWEEN | `"age[><]": [18,25]` | `age NOT BETWEEN 18 AND 25` |
| `[#]` | 直接SQL | `"time[#]": "NOW()"` | `time = NOW()` |
| `[##]` | SQL片段 | `"[##]": "a > b"` | `a > b` |
| `[#!]` | 不等于直接SQL | `"status[#!]": "1"` | `status != 1` |
| `[!#]` | 不等于直接SQL | `"status[!#]": "1"` | `status != 1` |
### 特殊关键字(支持大小写)
| 关键字 | 功能 | 示例 |
|--------|------|------|
| `ORDER` / `order` | 排序 | `"ORDER": "id DESC"` |
| `GROUP` / `group` | 分组 | `"GROUP": "dept_id"` |
| `LIMIT` / `limit` | 限制 | `"LIMIT": 10` |
| `OFFSET` / `offset` | 偏移 | `"OFFSET": 20` |
| `HAVING` / `having` | 分组过滤 | `"HAVING": Map{"cnt[>]": 5}` |
| `DISTINCT` / `distinct` | 去重 | 在 SELECT 中使用 |
## 常见问题
### Q1: 多条件查询需要用 AND 包装吗?
A1: 不再需要!现在多条件会自动用 AND 连接。当然,使用 `AND` 包装仍然有效(向后兼容)。
### Q2: 如何处理事务中的错误?
A2: 在 `Action` 函数中返回 `false` 即可触发回滚,所有操作都会被撤销。
### Q3: 缓存何时会被清除?
A3: 执行 `Insert``Update``Delete``Upsert``Inserts` 操作时会自动清除对应表的缓存。
### Q4: 如何执行复杂的原生SQL?
A4: 使用 `Query` 方法执行查询,使用 `Exec` 方法执行更新操作。
### Q5: 主从分离如何工作?
A5: 查询操作自动使用从库(如果配置了),增删改操作使用主库。
### Q6: 如何处理NULL值?
A6: 使用 `nil` 作为值,查询时使用 `"field": nil` 表示 `IS NULL`
### Q7: PostgreSQL 和 MySQL 语法有区别吗?
A7: 框架会自动处理差异(占位符、引号等),代码无需修改。
---
*文档版本: 2.0*
*最后更新: 2026年1月*
> 本文档基于HoTimeDB源码分析生成,如有疑问请参考源码实现。该ORM框架参考了PHP Medoo的设计理念,但根据Golang语言特性进行了适配和优化。
**更多参考:**
- [HoTimeDB API 参考](HoTimeDB_API参考.md) - API 速查手册
+604
View File
@@ -0,0 +1,604 @@
# 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,
"history": false,
"mode": "compatible"
}
}
}
```
缓存优先级: **Memory > Redis > DB**,自动穿透与回填
#### DB 缓存配置说明
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `db` | false | 是否缓存数据库查询 |
| `session` | true | 是否缓存 Session |
| `timeout` | 2592000 | 过期时间(秒) |
| `history` | false | 是否记录缓存历史,开启后每次新增/修改缓存都会记录到历史表 |
| `mode` | compatible | 缓存表模式,见下表 |
**缓存表模式 (mode)**
| 模式 | 说明 |
|------|------|
| `compatible` | **默认**。兼容模式:写新表,新表无数据时回退读老表;写入时自动删除老表同 key 记录;删除时同时删两表。适合从老版本平滑升级,老数据自然过期消亡 |
| `new` | 只使用新表 `hotime_cache`,启动时自动迁移老表 `cached` 数据。老表保留由人工删除,不再被读写 |
> **升级建议**:从老版本升级时,建议先使用 `compatible` 模式运行一段时间(让老数据自然过期),确认无问题后再切换到 `new` 模式
> 从老版本升级时,建议使用 `compatible` 模式平滑过渡,待老表数据消亡后切换到 `new` 模式
### 错误码配置
```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) // 删除
```
### 批量操作(性能优化)
当需要同时操作多个 Session 字段时,使用批量操作可显著提升性能:
```go
// SessionsSet - 批量设置(N个字段只触发1次数据库写入)
that.SessionsSet(Map{
"user_id": userId,
"username": "张三",
"login_time": time.Now().Unix(),
"role": "admin",
})
// SessionsGet - 批量获取(1次调用获取多个字段)
result := that.SessionsGet("user_id", "username", "role")
// result = Map{"user_id": 123, "username": "张三", "role": "admin"}
userId := ObjToInt64(result["user_id"], nil)
username := ObjToStr(result["username"])
// SessionsDelete - 批量删除(N个字段只触发1次数据库写入)
that.SessionsDelete("token", "temp_code", "verify_expire")
```
**性能对比**
| 操作方式 | 设置10个字段 | 数据库写入次数 |
|----------|-------------|---------------|
| 逐个调用 `Session()` | 10次调用 | **10次** |
| 使用 `SessionsSet()` | 1次调用 | **1次** |
| 操作方式 | 获取10个字段 | 缓存查询次数 |
|----------|-------------|--------------|
| 逐个调用 `Session()` | 10次调用 | **10次** |
| 使用 `SessionsGet()` | 1次调用 | **1次** |
> 💡 **最佳实践**:当一次性操作 3 个以上字段时,建议使用批量操作
三级缓存自动运作:**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 (
"time"
. "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.SessionsSet(Map{
"user_id": user.GetInt64("id"),
"username": user.GetString("name"),
"login_time": time.Now().Unix(),
})
that.Display(0, Map{"user": user})
},
"info": func(that *Context) {
// 使用批量获取,一次读取多个字段
sess := that.SessionsGet("user_id", "username", "login_time")
userId := ObjToInt64(sess["user_id"], nil)
user := that.Db.Get("user", "*", Map{"id": userId})
that.Display(0, Map{
"user": user,
"login_time": sess["login_time"],
})
},
"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.SessionsDelete("user_id", "username", "login_time")
that.Display(0, "退出成功")
},
},
},
})
}
```
---
**下一步**
- [HoTimeDB 使用说明](HoTimeDB_使用说明.md) - 完整数据库教程
- [HoTimeDB API 参考](HoTimeDB_API参考.md) - API 速查手册
+183
View 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 | 初始版本,记录分析结果和待改进项 |
+142
View File
@@ -0,0 +1,142 @@
package aliyun
import (
. "code.hoteas.com/golang/hotime/common"
"fmt"
"io/ioutil"
"net/http"
//"fmt"
)
type company struct {
ApiCode string
Url string
}
var Company = company{}
func (that *company) Init(apiCode string) {
//"06c6a07e89dd45c88de040ee1489eef7"
that.ApiCode = apiCode
that.Url = "http://api.81api.com"
}
// GetCompanyOtherAll 获取企业基础信息
func (that *company) GetCompanyOtherAll(name string) Map {
res := Map{}
data, e := that.GetCompanyPatentsInfo(name) //获取专利信息
if e != nil {
fmt.Println(e)
} else {
res["PatentsInfo"] = data.GetMap("data")
}
data, e = that.GetCompanyOtherCopyrightsInfo(name) //获取其他专利
if e != nil {
fmt.Println(e)
} else {
res["OtherCopyrightsInfo"] = data.GetMap("data")
}
data, e = that.GetCompanyTrademarksInfo(name) //获取商标
if e != nil {
fmt.Println(e)
} else {
res["TrademarksInfo"] = data.GetMap("data")
}
data, e = that.GetCompanySoftwareCopyrightsInfo(name) //获取软著
if e != nil {
fmt.Println(e)
} else {
res["SoftwareCopyrightsInfo"] = data.GetMap("data")
}
data, e = that.GetCompanyProfileTags(name) //获取大数据标签
if e != nil {
fmt.Println(e)
} else {
res["ProfileTags"] = data.GetSlice("data")
}
return res
}
// GetCompanyBaseInfo 获取企业基础信息
func (that *company) GetCompanyList(name string) (Map, error) {
url := "/fuzzyQueryCompanyInfo/"
body, err := that.basePost(url, name)
return ObjToMap(body), err
}
// GetCompanyBaseInfo 获取企业基础信息
func (that *company) GetCompanyBaseInfo(name string) (Map, error) {
url := "/getCompanyBaseInfo/"
body, err := that.basePost(url, name)
return ObjToMap(body), err
}
// GetCompanyPatentsInfo 获取专利信息
func (that *company) GetCompanyPatentsInfo(name string) (Map, error) {
url := "/getCompanyPatentsInfo/"
body, err := that.basePost(url, name)
return ObjToMap(body), err
}
// GetCompanyTrademarksInfo 获取商标信息
func (that *company) GetCompanyTrademarksInfo(name string) (Map, error) {
url := "/getCompanyTrademarksInfo/"
body, err := that.basePost(url, name)
return ObjToMap(body), err
}
// GetCompanySoftwareCopyrightsInfo 获取软著信息
func (that *company) GetCompanySoftwareCopyrightsInfo(name string) (Map, error) {
url := "/getCompanySoftwareCopyrightsInfo/"
body, err := that.basePost(url, name)
return ObjToMap(body), err
}
// GetCompanyOtherCopyrightsInfo 获取其他著作信息
func (that *company) GetCompanyOtherCopyrightsInfo(name string) (Map, error) {
url := "/getCompanyOtherCopyrightsInfo/"
body, err := that.basePost(url, name)
return ObjToMap(body), err
}
// GetCompanyProfileTags 获取大数据标签
func (that *company) GetCompanyProfileTags(name string) (Map, error) {
url := "/getCompanyProfileTags/"
body, err := that.basePost(url, name)
return ObjToMap(body), err
}
func (that *company) basePost(url string, name string) (string, error) {
client := &http.Client{}
reqest, err := http.NewRequest("GET", that.Url+url+name+"/?isRaiseErrorCode=1", nil)
if err != nil {
fmt.Println("Fatal error ", err.Error())
return "", err
}
reqest.Header.Add("Authorization", "APPCODE "+that.ApiCode)
response, err := client.Do(reqest)
defer response.Body.Close()
if err != nil {
fmt.Println("Fatal error ", err.Error())
return "", err
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", err
}
res := string(body)
fmt.Println(res)
return res, err
}
+142
View File
@@ -0,0 +1,142 @@
package baidu
import (
. "code.hoteas.com/golang/hotime/common"
"fmt"
"io/ioutil"
"net/http"
"net/url"
)
type baiduMap struct {
Ak string
Url string
}
var BaiDuMap = baiduMap{}
func (that *baiduMap) Init(Ak string) {
//"ak=ZeT902EZvVgIoGVWEFK3osUm"
that.Ak = Ak
that.Url = "https://api.map.baidu.com/place/v2/suggestion?output=json" + "&ak=" + Ak
//query
}
// from 源坐标类型:
// 1GPS标准坐标;
// 2:搜狗地图坐标;
// 3:火星坐标(gcj02),即高德地图、腾讯地图和MapABC等地图使用的坐标;
// 4:3中列举的地图坐标对应的墨卡托平面坐标;
// 5:百度地图采用的经纬度坐标(bd09ll);
// 6:百度地图采用的墨卡托平面坐标(bd09mc);
// 7:图吧地图坐标;
// 851地图坐标;
// int 1 1 否
// to
// 目标坐标类型:
// 3:火星坐标(gcj02),即高德地图、腾讯地图及MapABC等地图使用的坐标;
// 5:百度地图采用的经纬度坐标(bd09ll);
// 6:百度地图采用的墨卡托平面坐标(bd09mc);
func (that *baiduMap) Geoconv(latlngs []Map, from, to int) (Slice, error) {
client := &http.Client{}
latlngsStr := ""
for _, v := range latlngs {
if latlngsStr != "" {
latlngsStr = latlngsStr + ";" + v.GetString("lng") + "," + v.GetString("lat")
} else {
latlngsStr = v.GetString("lng") + "," + v.GetString("lat")
}
}
url := "https://api.map.baidu.com/geoconv/v1/?from=" + ObjToStr(from) + "&to=" + ObjToStr(to) + "&ak=" + that.Ak + "&coords=" + latlngsStr
reqest, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("Fatal error ", err.Error())
return nil, err
}
response, err := client.Do(reqest)
defer response.Body.Close()
if err != nil {
fmt.Println("Fatal error ", err.Error())
return nil, err
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
//fmt.Println(string(body))
data := ObjToMap(string(body))
if data.GetCeilInt64("status") != 0 {
return nil, err
}
return data.GetSlice("result"), err
}
func (that *baiduMap) GetAddress(lat string, lng string) (string, error) {
client := &http.Client{}
url := "https://api.map.baidu.com/reverse_geocoding/v3/?ak=" + that.Ak + "&output=json&location=" + lat + "," + lng
reqest, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("Fatal error ", err.Error())
return "", err
}
response, err := client.Do(reqest)
defer response.Body.Close()
if err != nil {
fmt.Println("Fatal error ", err.Error())
return "", err
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", err
}
//fmt.Println(string(body))
return string(body), err
}
// GetPosition 获取定位列表
func (that *baiduMap) GetPosition(name string, region string) (string, error) {
client := &http.Client{}
if region == "" {
region = "全国"
}
reqest, err := http.NewRequest("GET", that.Url+"&query="+url.PathEscape(name)+"&region="+url.PathEscape(region), nil)
if err != nil {
fmt.Println("Fatal error ", err.Error())
return "", err
}
response, err := client.Do(reqest)
defer response.Body.Close()
if err != nil {
fmt.Println("Fatal error ", err.Error())
return "", err
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", err
}
//fmt.Println(string(body))
return string(body), err
}
+20 -18
View File
@@ -10,31 +10,33 @@ import (
//"fmt"
)
type DDY struct {
type dingdongyun struct {
ApiKey string
YzmUrl string
TzUrl string
}
func (this *DDY) Init(apikey string) {
this.ApiKey = apikey
this.YzmUrl = "https://api.dingdongcloud.com/v2/sms/single_send"
this.TzUrl = "https://api.dingdongcloud.com/v1/sms/notice/multi_send"
var DDY = dingdongyun{}
func (that *dingdongyun) Init(apikey string) {
that.ApiKey = apikey
that.YzmUrl = "https://api.dingdongcloud.com/v2/sms/captcha/send.json"
that.TzUrl = "https://api.dingdongcloud.com/v2/sms/notice/send.json"
}
//发送短信验证码 code验证码如:123456 返回true表示发送成功flase表示发送失败
func (this *DDY) SendYZM(umoblie string, tpt string, data map[string]string) (bool, error) {
// SendYZM 发送短信验证码 code验证码如:123456 返回true表示发送成功flase表示发送失败
func (that *dingdongyun) SendYZM(umoblie string, tpt string, data map[string]string) (bool, error) {
for k, v := range data {
tpt = strings.Replace(tpt, "{"+k+"}", v, -1)
}
return this.send(this.YzmUrl, umoblie, tpt)
return that.send(that.YzmUrl, umoblie, tpt)
}
//发送通知
func (this *DDY) SendTz(umoblie []string, tpt string, data map[string]string) (bool, error) {
// SendTz 发送通知
func (that *dingdongyun) SendTz(umoblie []string, tpt string, data map[string]string) (bool, error) {
for k, v := range data {
tpt = strings.Replace(tpt, "["+k+"]", v, -1)
tpt = strings.Replace(tpt, "{"+k+"}", v, -1)
}
umobleStr := ""
for i, v := range umoblie {
@@ -44,14 +46,14 @@ func (this *DDY) SendTz(umoblie []string, tpt string, data map[string]string) (b
}
umobleStr += "," + v
}
return this.send(this.TzUrl, umobleStr, tpt)
return that.send(that.TzUrl, umobleStr, tpt)
}
//发送短信
func (this *DDY) send(mUrl string, umoblie string, content string) (bool, error) {
// 发送短信
func (that *dingdongyun) send(mUrl string, umoblie string, content string) (bool, error) {
data_send_sms_yzm := url.Values{"apikey": {this.ApiKey}, "mobile": {umoblie}, "content": {content}}
res, err := this.httpsPostForm(mUrl, data_send_sms_yzm)
data_send_sms_yzm := url.Values{"apikey": {that.ApiKey}, "mobile": {umoblie}, "content": {content}}
res, err := that.httpsPostForm(mUrl, data_send_sms_yzm)
if err != nil && res == "" {
return false, errors.New("连接错误")
}
@@ -72,8 +74,8 @@ func (this *DDY) send(mUrl string, umoblie string, content string) (bool, error)
return true, nil
}
//调用url发送短信的连接
func (this *DDY) httpsPostForm(url string, data url.Values) (string, error) {
// 调用url发送短信的连接
func (that *dingdongyun) httpsPostForm(url string, data url.Values) (string, error) {
resp, err := http.PostForm(url, data)
if err != nil {
+5 -5
View File
@@ -1,15 +1,15 @@
package download
import (
. "../../common"
"bytes"
. "code.hoteas.com/golang/hotime/common"
"io"
"io/ioutil"
"net/http"
"os"
)
//下载文件
// Down 下载文件
func Down(url, path, name string, e ...*Error) bool {
os.MkdirAll(path, os.ModeDir)
@@ -18,13 +18,13 @@ func Down(url, path, name string, e ...*Error) bool {
}
out, err := os.Create(path + name)
if err != nil && e[0] != nil {
if err != nil && len(e) != 0 {
e[0].SetError(err)
return false
}
defer out.Close()
resp, err := http.Get(url)
if err != nil && e[0] != nil {
if err != nil && len(e) != 0 {
e[0].SetError(err)
return false
}
@@ -32,7 +32,7 @@ func Down(url, path, name string, e ...*Error) bool {
pix, err := ioutil.ReadAll(resp.Body)
_, err = io.Copy(out, bytes.NewReader(pix))
if err != nil && e[0] != nil {
if err != nil && len(e) != 0 {
e[0].SetError(err)
return false
}
+149
View File
@@ -0,0 +1,149 @@
package mongodb
import (
. "code.hoteas.com/golang/hotime/common"
"context"
"fmt"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type MongoDb struct {
Client *mongo.Client
Ctx context.Context
DataBase *mongo.Database
Connect *mongo.Collection
LastErr error
}
func GetMongoDb(database, url string) (*MongoDb, error) {
db := MongoDb{}
clientOptions := options.Client().ApplyURI(url)
db.Ctx = context.TODO()
// Connect to MongoDb
var err error
db.Client, err = mongo.Connect(db.Ctx, clientOptions)
if err != nil {
return nil, err
}
// Check the connection
err = db.Client.Ping(db.Ctx, nil)
if err != nil {
return nil, err
}
fmt.Println("Connected to MongoDb!")
//databases, err := db.Client.ListDatabaseNames(db.Ctx, bson.M{})
//if err != nil {
// return nil, err
//}
//fmt.Println(databases)
db.DataBase = db.Client.Database(database)
return &db, nil
}
func (that *MongoDb) Insert(table string, data interface{}) string {
collection := that.DataBase.Collection(table)
re, err := collection.InsertOne(that.Ctx, data)
if err != nil {
that.LastErr = err
return ""
}
return ObjToStr(re.InsertedID)
}
func (that *MongoDb) InsertMany(table string, data ...interface{}) Slice {
collection := that.DataBase.Collection(table)
re, err := collection.InsertMany(that.Ctx, data)
if err != nil {
that.LastErr = err
return Slice{}
}
return ObjToSlice(re.InsertedIDs)
}
func (that *MongoDb) Update(table string, data Map, where Map) int64 {
collection := that.DataBase.Collection(table)
re, err := collection.UpdateMany(that.Ctx, where, data)
if err != nil {
that.LastErr = err
return 0
}
return re.ModifiedCount
}
func (that *MongoDb) Delete(table string, where Map) int64 {
collection := that.DataBase.Collection(table)
re, err := collection.DeleteMany(that.Ctx, where)
if err != nil {
that.LastErr = err
return 0
}
return re.DeletedCount
}
func (that *MongoDb) Get(table string, where Map) Map {
results := []Map{}
var cursor *mongo.Cursor
var err error
collection := that.DataBase.Collection(table)
if cursor, err = collection.Find(that.Ctx, where, options.Find().SetSkip(0), options.Find().SetLimit(2)); err != nil {
that.LastErr = err
return nil
}
//延迟关闭游标
defer func() {
if err := cursor.Close(that.Ctx); err != nil {
that.LastErr = err
}
}()
//这里的结果遍历可以使用另外一种更方便的方式:
if err = cursor.All(that.Ctx, &results); err != nil {
that.LastErr = err
return nil
}
if len(results) > 0 {
return results[0]
}
return nil
}
func (that MongoDb) Select(table string, where Map, page, pageRow int64) []Map {
page = (page - 1) * pageRow
if page < 0 {
page = 0
}
results := []Map{}
var cursor *mongo.Cursor
var err error
collection := that.DataBase.Collection(table)
if cursor, err = collection.Find(that.Ctx, where, options.Find().SetSkip(page), options.Find().SetLimit(pageRow)); err != nil {
that.LastErr = err
return results
}
//延迟关闭游标
defer func() {
if err := cursor.Close(that.Ctx); err != nil {
that.LastErr = err
}
}()
//这里的结果遍历可以使用另外一种更方便的方式:
if err = cursor.All(that.Ctx, &results); err != nil {
that.LastErr = err
return results
}
return results
}
+143
View File
@@ -0,0 +1,143 @@
package rsa
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"fmt"
"os"
)
func FileGet(path string) []byte {
file, err := os.Open(path)
if err != nil {
panic(err)
}
defer file.Close()
//读取文件的内容
info, _ := file.Stat()
buf := make([]byte, info.Size())
file.Read(buf)
return buf
}
// RSA_Encrypt RSA加密
// plainText 要加密的数据
// path 公钥匙文件地址
func RSA_Encrypt(plainText []byte, buf []byte) []byte {
//pem解码
block, _ := pem.Decode(buf)
//x509解码
publicKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
panic(err)
}
//类型断言
publicKey := publicKeyInterface.(*rsa.PublicKey)
//对明文进行加密
cipherText, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, plainText)
if err != nil {
panic(err)
}
//返回密文
return cipherText
}
// RSA_Decrypt RSA解密
// cipherText 需要解密的byte数据
// path 私钥文件路径
func RSA_Decrypt(cipherText []byte, buf []byte) []byte {
//pem解码
block, _ := pem.Decode(buf)
//X509解码
private, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
panic(err)
}
//对密文进行解密
//plainText,_:=rsa.DecryptPKCS1v15(rand.Reader,privateKey,cipherText)
v, err := rsa.DecryptPKCS1v15(rand.Reader, private.(*rsa.PrivateKey), cipherText)
//返回明文
return v
}
func MarshalPKCS8PrivateKey(key *rsa.PrivateKey) []byte {
info := struct {
Version int
PrivateKeyAlgorithm []asn1.ObjectIdentifier
PrivateKey []byte
}{}
info.Version = 0
info.PrivateKeyAlgorithm = make([]asn1.ObjectIdentifier, 1)
info.PrivateKeyAlgorithm[0] = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
info.PrivateKey = x509.MarshalPKCS1PrivateKey(key)
k, err := asn1.Marshal(info)
if err != nil {
panic(err.Error())
}
return k
}
func Demo() {
//生成密钥对,保存到文件
GenerateRSAKey(2048, "./")
//加密
data := []byte("hello world")
encrypt := RSA_Encrypt(data, FileGet("public.pem"))
fmt.Println(string(encrypt))
// 解密
decrypt := RSA_Decrypt(encrypt, FileGet("private.pem"))
fmt.Println(string(decrypt))
}
// GenerateRSAKey 生成RSA私钥和公钥,保存到文件中
// bits 证书大小
func GenerateRSAKey(bits int, path string) {
//GenerateKey函数使用随机数据生成器random生成一对具有指定字位数的RSA密钥
//Reader是一个全局、共享的密码用强随机数生成器
privateKey, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
panic(err)
}
//保存私钥
//通过x509标准将得到的ras私钥序列化为ASN.1 的 DER编码字符串
X509PrivateKey := x509.MarshalPKCS1PrivateKey(privateKey)
//使用pem格式对x509输出的内容进行编码
//创建文件保存私钥
privateFile, err := os.Create(path + "private.pem")
if err != nil {
panic(err)
}
defer privateFile.Close()
//构建一个pem.Block结构体对象
privateBlock := pem.Block{Type: "RSA Private Key", Bytes: X509PrivateKey}
//将数据保存到文件
pem.Encode(privateFile, &privateBlock)
//保存公钥
//获取公钥的数据
publicKey := privateKey.PublicKey
//X509对公钥编码
X509PublicKey, err := x509.MarshalPKIXPublicKey(&publicKey)
if err != nil {
panic(err)
}
//pem格式编码
//创建用于保存公钥的文件
publicFile, err := os.Create(path + "public.pem")
if err != nil {
panic(err)
}
defer publicFile.Close()
//创建一个pem.Block结构体对象
publicBlock := pem.Block{Type: "RSA Public Key", Bytes: X509PublicKey}
//保存到文件
pem.Encode(publicFile, &publicBlock)
}
+119
View File
@@ -0,0 +1,119 @@
package tencent
import (
. "code.hoteas.com/golang/hotime/common"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"net/http"
gourl "net/url"
"strings"
"time"
)
type company struct {
secretId string
secretKey string
}
var Company = company{}
func (that *company) Init(secretId, secretKey string) {
// 云市场分配的密钥Id
//secretId := "xxxx"
//// 云市场分配的密钥Key
//secretKey := "xxxx"
that.secretId = secretId
that.secretKey = secretKey
}
func (that *company) calcAuthorization(source string) (auth string, datetime string, err error) {
timeLocation, _ := time.LoadLocation("Etc/GMT")
datetime = time.Now().In(timeLocation).Format("Mon, 02 Jan 2006 15:04:05 GMT")
signStr := fmt.Sprintf("x-date: %s\nx-source: %s", datetime, source)
// hmac-sha1
mac := hmac.New(sha1.New, []byte(that.secretKey))
mac.Write([]byte(signStr))
sign := base64.StdEncoding.EncodeToString(mac.Sum(nil))
auth = fmt.Sprintf("hmac id=\"%s\", algorithm=\"hmac-sha1\", headers=\"x-date x-source\", signature=\"%s\"",
that.secretId, sign)
return auth, datetime, nil
}
func (that *company) urlencode(params map[string]string) string {
var p = gourl.Values{}
for k, v := range params {
p.Add(k, v)
}
return p.Encode()
}
func (that *company) GetCompany(name string) Map {
// 云市场分配的密钥Id
//secretId := "xxxx"
//// 云市场分配的密钥Key
//secretKey := "xxxx"
source := "market"
// 签名
auth, datetime, _ := that.calcAuthorization(source)
// 请求方法
method := "GET"
// 请求头
headers := map[string]string{"X-Source": source, "X-Date": datetime, "Authorization": auth}
// 查询参数
queryParams := make(map[string]string)
queryParams["keyword"] = name
// body参数
bodyParams := make(map[string]string)
// url参数拼接
url := "https://service-3jnh3ku8-1256140209.gz.apigw.tencentcs.com/release/business4/geet"
if len(queryParams) > 0 {
url = fmt.Sprintf("%s?%s", url, that.urlencode(queryParams))
}
bodyMethods := map[string]bool{"POST": true, "PUT": true, "PATCH": true}
var body io.Reader = nil
if bodyMethods[method] {
body = strings.NewReader(that.urlencode(bodyParams))
headers["Content-Type"] = "application/x-www-form-urlencoded"
}
client := &http.Client{
Timeout: 5 * time.Second,
}
request, err := http.NewRequest(method, url, body)
if err != nil {
fmt.Println(err)
return nil
}
for k, v := range headers {
request.Header.Set(k, v)
}
response, err := client.Do(request)
if err != nil {
fmt.Println(err)
return nil
}
defer response.Body.Close()
bodyBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
fmt.Println(err)
return nil
}
res := string(bodyBytes)
fmt.Println(res)
return ObjToMap(res)
}
+104
View File
@@ -0,0 +1,104 @@
package tencent
import (
"fmt"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
ocr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ocr/v20181119"
)
type tencent struct {
secretId string
secretKey string
credential *common.Credential
}
var Tencent = tencent{}
func (that *tencent) Init(secretId, secretKey string) {
// 云市场分配的密钥Id
//secretId := "xxxx"
//// 云市场分配的密钥Key
//secretKey := "xxxx"
that.secretId = secretId
that.secretKey = secretKey
that.credential = common.NewCredential(
that.secretId,
that.secretKey,
)
}
func (that *tencent) OCRCOMPANY(base64Str string) string {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ocr.tencentcloudapi.com"
client, _ := ocr.NewClient(that.credential, "ap-guangzhou", cpf)
request := ocr.NewBizLicenseOCRRequest()
//request.ImageUrl = common.StringPtr("https://img0.baidu.com/it/u=2041013181,3227632688&fm=26&fmt=auto")
request.ImageBase64 = common.StringPtr(base64Str)
response, err := client.BizLicenseOCR(request)
if _, ok := err.(*errors.TencentCloudSDKError); ok {
fmt.Println("An API error has returned: %s", err)
return ""
}
if err != nil {
fmt.Println("An API error has returned: %s", err)
return ""
}
//fmt.Printf("%s", response.ToJsonString())
return response.ToJsonString()
}
func (that *tencent) OCR(base64Str string) string {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ocr.tencentcloudapi.com"
client, _ := ocr.NewClient(that.credential, "ap-guangzhou", cpf)
request := ocr.NewGeneralAccurateOCRRequest()
//request.ImageUrl = common.StringPtr("https://img0.baidu.com/it/u=2041013181,3227632688&fm=26&fmt=auto")
request.ImageBase64 = common.StringPtr(base64Str)
response, err := client.GeneralAccurateOCR(request)
if _, ok := err.(*errors.TencentCloudSDKError); ok {
fmt.Println("An API error has returned: %s", err)
return ""
}
if err != nil {
fmt.Println("An API error has returned: %s", err)
return ""
}
//fmt.Printf("%s", response.ToJsonString())
return response.ToJsonString()
}
func (that *tencent) Qrcode(base64Str string) string {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ocr.tencentcloudapi.com"
client, _ := ocr.NewClient(that.credential, "ap-guangzhou", cpf)
request := ocr.NewQrcodeOCRRequest()
request.ImageBase64 = common.StringPtr(base64Str)
response, err := client.QrcodeOCR(request)
if _, ok := err.(*errors.TencentCloudSDKError); ok {
fmt.Println("An API error has returned: %s", err)
return ""
}
if err != nil {
fmt.Println("An API error has returned: %s", err)
return ""
}
//fmt.Printf("%s", response.ToJsonString())
return response.ToJsonString()
}
+4 -4
View File
@@ -1,7 +1,7 @@
package upload
import (
. "../../common"
. "code.hoteas.com/golang/hotime/common"
"errors"
"io"
"mime/multipart"
@@ -15,7 +15,7 @@ type Upload struct {
Path string
}
func (this *Upload) UpFile(Request *http.Request, fieldName, savefilepath, savePath string) (string, error) {
func (that *Upload) UpFile(Request *http.Request, fieldName, savefilepath, savePath string) (string, error) {
Request.ParseMultipartForm(32 << 20)
var filePath string
files := Request.MultipartForm.File
@@ -36,7 +36,7 @@ func (this *Upload) UpFile(Request *http.Request, fieldName, savefilepath, saveP
data := time.Unix(int64(t), 0).Format("2006-01")
path := ""
if strings.EqualFold(savefilepath, "") {
path = this.Path + data
path = that.Path + data
} else {
path = savefilepath + data
}
@@ -51,7 +51,7 @@ func (this *Upload) UpFile(Request *http.Request, fieldName, savefilepath, saveP
}
}
filename := time.Unix(int64(t), 0).Format("2006-01-02-15-22-25") + ObjToStr(Rand(6))
filePath = path + "/" + filename + this.Path
filePath = path + "/" + filename + that.Path
} else {
filePath = savePath
}
+79
View File
@@ -0,0 +1,79 @@
package wechat
import (
"github.com/silenceper/wechat/v2"
"github.com/silenceper/wechat/v2/cache"
"github.com/silenceper/wechat/v2/officialaccount"
h5config "github.com/silenceper/wechat/v2/officialaccount/config"
"github.com/silenceper/wechat/v2/officialaccount/js"
"github.com/silenceper/wechat/v2/officialaccount/oauth"
)
// 基于此文档开发
// https://github.com/silenceper/wechat/blob/v2/doc/api/officialaccount.md
type h5Program struct {
Memory *cache.Memory
Config *h5config.Config
*officialaccount.OfficialAccount
weixin *wechat.Wechat //微信登录实例
}
var H5Program = h5Program{}
// Init 初始化
func (that *h5Program) Init(appid string, appsecret string) {
that.weixin = wechat.NewWechat()
that.Memory = cache.NewMemory()
that.Config = &h5config.Config{
AppID: appid,
AppSecret: appsecret,
//Token: "xxx",
//EncodingAESKey: "xxxx",
Cache: that.Memory,
}
that.OfficialAccount = that.weixin.GetOfficialAccount(that.Config)
}
// GetUserInfo 获取用户信息
func (that *h5Program) GetUserInfo(code string) (appid string, resToken oauth.ResAccessToken, userInfo oauth.UserInfo, err error) {
auth := that.GetOauth()
//weixin.GetOpenPlatform()
resToken, err = auth.GetUserAccessToken(code)
if err != nil {
return auth.AppID, resToken, userInfo, err
}
//getUserInfo
userInfo, err = auth.GetUserInfo(resToken.AccessToken, resToken.OpenID, "")
if err != nil {
return auth.AppID, resToken, userInfo, err
}
return auth.AppID, resToken, userInfo, err
}
// GetSignUrl js url签名
func (that *h5Program) GetSignUrl(signUrl string) (*js.Config, error) {
js := that.OfficialAccount.GetJs()
cfg1, e := js.GetConfig(signUrl)
if e != nil {
return nil, e
}
return cfg1, nil
}
// GetSignUrl js url签名
//func (that *h5Program) GetJsPay(signUrl string) (*js.Config, error) {
// //
// //js := that.OfficialAccount().GetJs()
// //
// //cfg1, e := js.GetConfig(signUrl)
// //if e != nil {
// // return nil, e
// //}
//
// return cfg1, nil
//}
+66
View File
@@ -0,0 +1,66 @@
package wechat
import (
"errors"
"github.com/silenceper/wechat/v2"
"github.com/silenceper/wechat/v2/cache"
"github.com/silenceper/wechat/v2/miniprogram"
"github.com/silenceper/wechat/v2/miniprogram/auth"
"github.com/silenceper/wechat/v2/miniprogram/config"
"github.com/silenceper/wechat/v2/miniprogram/encryptor"
)
type miniProgram struct {
Memory *cache.Memory
Config *config.Config
weixin *wechat.Wechat //微信登录实例
*miniprogram.MiniProgram
}
var MiniProgram = miniProgram{}
// Init 初始化
func (that *miniProgram) Init(appid string, appsecret string) {
that.weixin = wechat.NewWechat()
that.Memory = cache.NewMemory()
that.Config = &config.Config{
AppID: appid,
AppSecret: appsecret,
//Token: "xxx",
//EncodingAESKey: "xxxx",
Cache: that.Memory,
}
that.MiniProgram = that.weixin.GetMiniProgram(that.Config)
}
func (that *miniProgram) GetBaseUserInfo(code string) (appid string, re auth.ResCode2Session, err error) {
appid = that.Config.AppID
a := that.GetAuth()
re, err = a.Code2Session(code)
if err != nil {
return appid, re, err
}
return appid, re, err
}
func (that *miniProgram) GetPhoneNumber(sessionkey, encryptedData, iv string) (appid string, re *encryptor.PlainData, err error) {
appid = that.Config.AppID
if sessionkey == "" || encryptedData == "" || iv == "" {
return appid, re, errors.New("参数不足")
}
eny := that.GetEncryptor()
re, err = eny.Decrypt(sessionkey, encryptedData, iv)
if err != nil {
return appid, re, err
}
return appid, re, err
}
+142
View File
@@ -0,0 +1,142 @@
package wechat
import (
"context"
"fmt"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/wechat/v3"
"net/http"
"time"
)
// 基于此文档开发
// https://github.com/silenceper/wechat/blob/v2/doc/api/officialaccount.md
type wxpay struct {
client *wechat.ClientV3
ctx context.Context
apiV3Key string
MchId string
}
var WxPay = wxpay{}
// Init 初始化
func (that *wxpay) Init(MchId, SerialNo, APIv3Key, PrivateKey string) {
client, err := wechat.NewClientV3(MchId, SerialNo, APIv3Key, PrivateKey)
if err != nil {
//xlog.Error(err)
fmt.Println(err)
return
}
that.client = client
that.apiV3Key = APIv3Key
that.MchId = MchId
// 设置微信平台API证书和序列号(如开启自动验签,请忽略此步骤)
//client.SetPlatformCert([]byte(""), "")
that.ctx = context.Background()
// 启用自动同步返回验签,并定时更新微信平台API证书(开启自动验签时,无需单独设置微信平台API证书和序列号)
err = client.AutoVerifySign()
if err != nil {
//xlog.Error(err)
fmt.Println(err)
return
}
// 打开Debug开关,输出日志,默认是关闭的
client.DebugSwitch = gopay.DebugOn
}
// GetUserInfo 获取用户信息
func (that *wxpay) GetJsOrder(money int64, appid, openid, name, tradeNo, notifyUrl string) (jsApiParams *wechat.JSAPIPayParams, err error) {
fmt.Println("dasdas", money, appid, name, tradeNo, notifyUrl)
PrepayId, err := that.getPrepayId(money, appid, that.MchId, openid, name, tradeNo, notifyUrl)
if err != nil {
return nil, err
}
//小程序
jsapi, err := that.client.PaySignOfJSAPI(appid, PrepayId)
return jsapi, err
}
func (that *wxpay) CallbackJsOrder(req *http.Request) (*wechat.V3DecryptResult, error) {
notifyReq, err := wechat.V3ParseNotify(req)
if err != nil {
//xlog.Error(err)
return nil, err
}
// wxPublicKey 通过 client.WxPublicKey() 获取
err = notifyReq.VerifySignByPK(that.client.WxPublicKey())
if err != nil {
//xlog.Error(err)
return nil, err
}
// ========异步通知敏感信息解密========
// 普通支付通知解密
result, err := notifyReq.DecryptCipherText(that.apiV3Key)
//that.client.V3TransactionQueryOrder(that.ctx,result.BankType,result.OR)
return result, err
// 合单支付通知解密
//result, err := notifyReq.DecryptCombineCipherText(apiV3Key)
//// 退款通知解密
//result, err := notifyReq.DecryptRefundCipherText(apiV3Key)
// ========异步通知应答========
// 退款通知http应答码为200且返回状态码为SUCCESS才会当做商户接收成功,否则会重试。
// 注意:重试过多会导致微信支付端积压过多通知而堵塞,影响其他正常通知。
// 此写法是 gin 框架返回微信的写法
//c.JSON(http.StatusOK, &wechat.V3NotifyRsp{Code: gopay.SUCCESS, Message: "成功"})
//
//// 此写法是 echo 框架返回微信的写法
//return c.JSON(http.StatusOK, &wechat.V3NotifyRsp{Code: gopay.SUCCESS, Message: "成功"})
}
// GetUserInfo 获取用户信息
//func (that *wxpay) GetMiniOrder(money int64,appid,name,tradeNo,notifyUrl string) (jsApiParams *wechat.AppletParams,err error){
//
// PrepayId,err:=that.getPrepayId(money,name,tradeNo,notifyUrl)
// if err!=nil{
// return nil,err
// }
//
// //小程序
// applet, err := that.client.PaySignOfApplet(appid,PrepayId)
//
// return applet,err
//}
func (that *wxpay) getPrepayId(money int64, appid, mchid, openid, name, tradeNo, notifyUrl string) (prepayid string, err error) {
expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
// 初始化 BodyMap
bm := make(gopay.BodyMap)
bm.Set("appid", appid).
Set("mchid", mchid).
//Set("sub_mchid", "sub_mchid").
Set("description", name).
Set("out_trade_no", tradeNo).
Set("time_expire", expire).
Set("notify_url", notifyUrl).
SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", money).
Set("currency", "CNY")
}).
SetBodyMap("payer", func(bm gopay.BodyMap) {
bm.Set("openid", openid)
})
//ctx:=context.Context()
wxRsp, err := that.client.V3TransactionJsapi(that.ctx, bm)
fmt.Println("获取PrepayId", wxRsp, err)
if err != nil {
//xlog.Error(err)
return "", err
}
return wxRsp.Response.PrepayId, nil
}
-64
View File
@@ -1,64 +0,0 @@
package admin
import (
. "../../../hotime"
. "../../../hotime/common"
)
var ID = "3c5fc9732b8ddcb0a91b428976f2ce86"
// Project 管理端项目
var Project = Proj{
//"user": UserCtr,
"admin": adminCtr,
"category": categoryCtr,
"ctg_order_date": ctg_order_dateCtr,
"order": orderCtr,
"org": orgCtr,
"role": roleCtr,
"user": userCtr,
"hotime": Ctr{
"login": func(this *Context) {
name := this.Req.FormValue("name")
password := this.Req.FormValue("password")
if name == "" || password == "" {
this.Display(3, "参数不足")
return
}
user := this.Db.Get("admin", "*", Map{"AND": Map{"name": name, "password": Md5(password)}})
if user == nil {
this.Display(5, "登录失败")
return
}
this.Session("admin_id", user.GetCeilInt("id"))
this.Session("admin_name", name)
this.Display(0, this.SessionId)
},
"logout": func(this *Context) {
this.Session("admin_id", nil)
this.Session("admin_name", nil)
this.Display(0, "退出登录成功")
},
"info": func(that *Context) {
re := that.Db.Get("admin", that.MakeCode.Info("admin"), Map{"id": that.Session("admin_id").ToCeilInt()})
if re == nil {
that.Display(4, "找不到对应信息")
return
}
for k, v := range re {
column := that.MakeCode.TableColumns["admin"][k]
if column == nil {
continue
}
if (column["list"] == nil || column.GetBool("list")) && column.GetString("link") != "" {
re[column.GetString("link")] = that.Db.Get(column.GetString("link"), column.GetString("value"), Map{"id": v})
}
}
that.Display(0, re)
},
},
}
-34
View File
@@ -1,34 +0,0 @@
package app
import (
. "../../../hotime"
. "../../../hotime/common"
)
var categoryCtr = Ctr{
"info": func(that *Context) {
parentId := ObjToInt(that.Req.FormValue("id"))
//parentId := ObjToInt(that.RouterString[2])
childData:=[]Map{}
if parentId == 0 {
childData1 := that.Db.Select("category", "*", Map{"parent_id": nil})
for _, v := range childData1 {
data := that.Db.Get("category", "*", Map{"parent_id": v.GetCeilInt("id")})
childData=append(childData,data)
}
}else{
childData = that.Db.Select("category", "*", Map{"parent_id": parentId})
}
for _, v := range childData {
v["child"] = that.Db.Select("category", "*", Map{"parent_id": v.GetCeilInt("id")})
}
that.Display(0, childData)
},
}
-104
View File
@@ -1,104 +0,0 @@
package app
import (
. "../../../hotime"
. "../../../hotime/common"
"fmt"
"time"
)
var ctg_order_dateCtr = Ctr{
"info": func(that *Context) {
//today:=time.Now().Weekday()
id := ObjToInt(that.Req.FormValue("id"))
category := that.Db.Get("category", "*", Map{"id": id})
if category == nil {
that.Display(4, "找不到该类别!")
return
}
todayPMTime, _ := time.Parse("2006-01-02 15:04", time.Now().Format("2006-01-02")+" 14:00")
todayAMTime, _ := time.Parse("2006-01-02 15:04", time.Now().Format("2006-01-02"+" 09:00"))
weekDay := 1
switch time.Now().Weekday().String() {
case "Monday":
weekDay = 1
case "Tuesday":
weekDay = 2
case "Wednesday":
weekDay = 3
case "Thursday":
weekDay = 4
case "Friday":
weekDay = 5
case "Saturday":
weekDay = 6
case "Sunday":
weekDay = 7
}
////future:=that.Db.Select("ctg_order_date","*",Map{"category_id":that.RouterString[2],"date[>]":time})
date := Slice{}
for i := 0; i < 7; i++ {
day := weekDay + i + 1
if day > 7 {
day = day - 7
}
if day == 6 || day == 7 {
continue
}
//fmt.Println(todayAMTime.Unix() + int64(24*60*60*(i+1)))
dayAM := that.Db.Get("ctg_order_date", "*", Map{"AND": Map{"category_id": category.GetCeilInt("id"),
"date": todayAMTime.Unix() + int64(24*60*60*(i+1))}})
if dayAM == nil {
dayAM = Map{"name": "9:00-12:00",
"date": todayAMTime.Unix() + int64(24*60*60*(i+1)),
"create_time": time.Now().Unix(),
"modify_time": time.Now().Unix(),
"start_sn": category.GetCeilInt("start_sn"),
"max_sn": category.GetCeilInt("start_sn") + category.GetCeilInt("am"+ObjToStr(day)),
"now_sn": category.GetCeilInt("start_sn"),
"category_id": category.GetCeilInt("id"),
}
dayAM["id"] = that.Db.Insert("ctg_order_date", dayAM)
if dayAM.GetCeilInt64("id") == 0 {
that.Display(4, "内部错误!")
return
}
}
dayPM := that.Db.Get("ctg_order_date", "*", Map{"AND": Map{"category_id": category.GetCeilInt("id"), "date": todayPMTime.Unix() + int64(24*60*60*(i+1))}})
fmt.Println(that.Db.LastQuery, that.Db.LastData, dayPM, that.Db.LastErr)
if dayPM == nil {
fmt.Println("dasdasdasda")
dayPM = Map{"name": "14:00-16:00",
"date": todayPMTime.Unix() + int64(24*60*60*(i+1)),
"create_time": time.Now().Unix(),
"modify_time": time.Now().Unix(),
"start_sn": category.GetCeilInt("start_sn"),
"max_sn": category.GetCeilInt("start_sn") + category.GetCeilInt("pm"+ObjToStr(day)),
"now_sn": category.GetCeilInt("start_sn"),
"category_id": category.GetCeilInt("id"),
}
dayPM["id"] = that.Db.Insert("ctg_order_date", dayPM)
if dayPM.GetCeilInt64("id") == 0 {
that.Display(4, "内部错误!")
return
}
}
date = append(date, Map{"name": "星期" + ObjToStr(day) + "(" + time.Unix(todayPMTime.Unix()+int64(24*60*60*(i+1)), 0).Format("01-02") + ")",
"am": dayAM,
"pm": dayPM,
})
}
that.Display(0, date)
},
}
-86
View File
@@ -1,86 +0,0 @@
package app
import (
. "../../../hotime"
. "../../../hotime/common"
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
"time"
)
var ID = "aad1472b19e575a71ee8f75629e27867"
// Project 管理端项目
var Project = Proj{
//"user": UserCtr,
"category": categoryCtr,
"ctg_order_date": ctg_order_dateCtr,
"order": orderCtr,
"user": userCtr,
"sms": Sms,
}
//生成随机码的4位随机数
func getCode() string {
//res := ""
//for i := 0; i < 4; i++ {
res := ObjToStr(RandX(1000, 9999))
//}
return res
}
func tencentSendYzm(umobile, code string) error {
random := RandX(999999, 9999999)
url := "https://yun.tim.qq.com/v5/tlssmssvr/sendsms?sdkappid=1400235813&random=" + ObjToStr(random)
fmt.Println("URL:>", url)
h := sha256.New()
h.Write([]byte(`appkey=d511de15e5ccb43fc171772dbb8b599f&random=` + ObjToStr(random) + `&time=` + ObjToStr(time.Now().Unix()) + `&mobile=` + umobile))
bs := h.Sum(nil)
s256 := hex.EncodeToString(bs)
//json序列化
post := `{
"ext": "",
"extend": "",
"params": [
"` + code + `"
],
"sig": "` + s256 + `",
"sign": "乐呵呵旅游网",
"tel": {
"mobile": "` + umobile + `",
"nationcode": "86"
},
"time": ` + ObjToStr(time.Now().Unix()) + `,
"tpl_id": 378916
}`
fmt.Println(url, "post", post)
var jsonStr = []byte(post)
fmt.Println("jsonStr", jsonStr)
fmt.Println("new_str", bytes.NewBuffer(jsonStr))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
// req.Header.Set("X-Custom-Header", "myvalue")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Println("response Status:", resp.Status)
fmt.Println("response Headers:", resp.Header)
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println("response Body:", string(body))
return nil
}
-70
View File
@@ -1,70 +0,0 @@
package app
import (
. "../../../hotime"
. "../../../hotime/common"
"time"
)
var orderCtr = Ctr{
"count": func(this *Context) {
if this.Session("id").ToCeilInt() == 0 {
this.Display(2, "没有登录!")
return
}
re := Map{}
re["total"] = this.Db.Count("order", Map{"user_id": this.Session("id").ToCeilInt()})
re["finish"] = this.Db.Count("order", Map{"AND": Map{"user_id": this.Session("id").ToCeilInt(), "status": 2}})
re["late"] = this.Db.Count("order", Map{"AND": Map{"user_id": this.Session("id").ToCeilInt(), "status": 3}})
this.Display(0, re)
},
"add": func(this *Context) {
if this.Session("id").ToCeilInt() == 0 {
this.Display(2, "没有登录!")
return
}
ctgOrderDateId := ObjToInt(this.Req.FormValue("ctg_order_date_id"))
//ctgId:=ObjToInt(this.Req.FormValue("category_id"))
ctgOrderDate := this.Db.Get("ctg_order_date", "*", Map{"id": ctgOrderDateId})
if ctgOrderDate.GetCeilInt64("now_sn")+1 > ctgOrderDate.GetCeilInt64("max_sn") {
this.Display(5, "当前排号已经用完")
return
}
data := Map{"create_time": time.Now().Unix(),
"modify_time": time.Now().Unix(),
"user_id": this.Session("id").ToCeilInt(),
"date": ctgOrderDate.GetString("date"),
"sn": ctgOrderDate.GetCeilInt64("now_sn") + 1,
"category_id": ctgOrderDate.GetCeilInt("category_id"),
"admin_id": 1,
"status": 1,
"name": time.Unix(ctgOrderDate.GetCeilInt64("date"), 0).Format("2006-01-02 ") + ctgOrderDate.GetString("name"),
}
data["id"] = this.Db.Insert("order", data)
if data.GetCeilInt("id") == 0 {
this.Display(5, "预约失败")
return
}
this.Db.Update("ctg_order_date", Map{"now_sn": ctgOrderDate.GetCeilInt64("now_sn") + 1, "modify_time": time.Now().Unix()}, Map{"id": ctgOrderDate.GetCeilInt("id")})
this.Display(0, data)
},
"search": func(that *Context) {
if that.Session("id").ToCeilInt() == 0 {
that.Display(2, "没有登录!")
return
}
data := that.Db.Select("order", "*", Map{"user_id": that.Session("id").ToCeilInt()})
for _, v := range data {
v["category"] = that.Db.Get("category", "*", Map{"id": v.GetCeilInt("category_id")})
v["parent_category"] = that.Db.Get("category", "id,name", Map{"id": v.GetMap("category").GetCeilInt("parent_id")})
}
that.Display(0, data)
},
}
-32
View File
@@ -1,32 +0,0 @@
package app
import (
. "../../../hotime"
)
var Sms = Ctr{
//只允许微信验证过的或者登录成功的发送短信
"send": func(this *Context) {
//if this.Session("uid").Data == nil && this.Session("wechatInfo").Data == nil {
// this.Display(2, "没有授权")
// return
//}
if len(this.Req.FormValue("token")) != 32 {
this.Display(2, "没有授权")
return
}
phone := this.Req.FormValue("phone")
if len(phone) < 11 {
this.Display(3, "手机号格式错误")
return
}
code := getCode()
this.Session("phone", phone)
this.Session("code", code)
tencentSendYzm(phone, code)
this.Display(0, "发送成功")
},
}
-54
View File
@@ -1,54 +0,0 @@
package app
import (
. "../../../hotime"
. "../../../hotime/common"
"time"
)
var userCtr = Ctr{
"token": func(this *Context) {
this.Display(0, this.SessionId)
},
"test": func(this *Context) {
this.Session("id", 1)
},
"add": func(this *Context) {
if this.Req.FormValue("code") != this.Session("code").ToStr() ||
this.Req.FormValue("phone") != this.Session("phone").ToStr() {
this.Display(3, "短信验证不通过")
return
}
phone := this.Req.FormValue("phone")
idcard := this.Req.FormValue("idcard")
name := this.Req.FormValue("name")
user := this.Db.Get("user", "*", Map{"phone": phone})
if user == nil {
user = Map{"phone": phone, "idcard": idcard, "name": name, "create_time": time.Now().Unix(), "modify_time": time.Now().Unix()}
user["id"] = this.Db.Insert("user", user)
} else {
user["phone"] = phone
user["idcard"] = idcard
user["name"] = name
user["modify_time"] = time.Now().Unix()
re := this.Db.Update("user", user, Map{"id": user.GetCeilInt64("id")})
if re == 0 {
this.Display(4, "系统错误")
return
}
}
if user.GetCeilInt64("id") == 0 {
this.Display(5, "登录失败")
return
}
this.Session("id", user.GetCeilInt("id"))
this.Display(0, "登录成功")
},
}
+487
View File
@@ -0,0 +1,487 @@
package main
import (
"encoding/json"
"fmt"
"os"
"time"
"code.hoteas.com/golang/hotime"
"code.hoteas.com/golang/hotime/cache"
. "code.hoteas.com/golang/hotime/common"
)
const debugLogPath = `d:\work\hotimev1.5\.cursor\debug.log`
// debugLog 写入调试日志
func debugLog(hypothesisId, location, message string, data map[string]interface{}) {
logFile, _ := os.OpenFile(debugLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if logFile != nil {
logEntry, _ := json.Marshal(map[string]interface{}{
"sessionId": "batch-cache-test",
"runId": "test-run",
"hypothesisId": hypothesisId,
"location": location,
"message": message,
"data": data,
"timestamp": time.Now().UnixMilli(),
})
logFile.Write(append(logEntry, '\n'))
logFile.Close()
}
}
// TestBatchCacheOperations 测试所有批量缓存操作
func TestBatchCacheOperations(app *hotime.Application) {
fmt.Println("\n========== 批量缓存操作测试开始 ==========")
// 测试1: 测试 CacheMemory 批量操作
fmt.Println("\n--- 测试1: CacheMemory 批量操作 ---")
testCacheMemoryBatch(app)
// 测试2: 测试 CacheDb 批量操作
fmt.Println("\n--- 测试2: CacheDb 批量操作 ---")
testCacheDbBatch(app)
// 测试3: 测试 HoTimeCache 三级缓存批量操作
fmt.Println("\n--- 测试3: HoTimeCache 三级缓存批量操作 ---")
testHoTimeCacheBatch(app)
// 测试4: 测试 SessionIns 批量操作
fmt.Println("\n--- 测试4: SessionIns 批量操作 ---")
testSessionInsBatch(app)
// 测试5: 测试缓存反哺机制
fmt.Println("\n--- 测试5: 缓存反哺机制测试 ---")
testCacheBackfill(app)
// 测试6: 测试批量操作效率(一次性写入验证)
fmt.Println("\n--- 测试6: 批量操作效率测试 ---")
testBatchEfficiency(app)
fmt.Println("\n========== 批量缓存操作测试完成 ==========")
}
// testCacheMemoryBatch 测试内存缓存批量操作
func testCacheMemoryBatch(app *hotime.Application) {
// #region agent log
debugLog("E", "batch_cache_test.go:testCacheMemoryBatch:start", "开始测试CacheMemory批量操作", nil)
// #endregion
memCache := &cache.CacheMemory{TimeOut: 3600, DbSet: true, SessionSet: true}
memCache.SetError(&Error{})
// 测试 CachesSet
testData := Map{
"mem_key1": "value1",
"mem_key2": "value2",
"mem_key3": "value3",
}
memCache.CachesSet(testData)
// #region agent log
debugLog("E", "batch_cache_test.go:testCacheMemoryBatch:afterSet", "CacheMemory.CachesSet完成", map[string]interface{}{"count": len(testData)})
// #endregion
// 测试 CachesGet
keys := []string{"mem_key1", "mem_key2", "mem_key3", "mem_key_not_exist"}
result := memCache.CachesGet(keys)
// #region agent log
debugLog("E", "batch_cache_test.go:testCacheMemoryBatch:afterGet", "CacheMemory.CachesGet完成", map[string]interface{}{
"requested_keys": keys,
"result_count": len(result),
"result_keys": getMapKeys(result),
})
// #endregion
if len(result) != 3 {
fmt.Printf(" [FAIL] CacheMemory.CachesGet: 期望3个结果,实际%d个\n", len(result))
} else {
fmt.Println(" [PASS] CacheMemory.CachesGet: 批量获取正确")
}
// 测试 CachesDelete
memCache.CachesDelete([]string{"mem_key1", "mem_key2"})
result2 := memCache.CachesGet(keys)
// #region agent log
debugLog("E", "batch_cache_test.go:testCacheMemoryBatch:afterDelete", "CacheMemory.CachesDelete完成", map[string]interface{}{
"deleted_keys": []string{"mem_key1", "mem_key2"},
"remaining_count": len(result2),
"remaining_keys": getMapKeys(result2),
})
// #endregion
if len(result2) != 1 || result2["mem_key3"] == nil {
fmt.Printf(" [FAIL] CacheMemory.CachesDelete: 删除后期望1个结果,实际%d个\n", len(result2))
} else {
fmt.Println(" [PASS] CacheMemory.CachesDelete: 批量删除正确")
}
}
// testCacheDbBatch 测试数据库缓存批量操作
func testCacheDbBatch(app *hotime.Application) {
// #region agent log
debugLog("E", "batch_cache_test.go:testCacheDbBatch:start", "开始测试CacheDb批量操作", nil)
// #endregion
// 使用应用的数据库连接
dbCache := &cache.CacheDb{
TimeOut: 3600,
DbSet: true,
SessionSet: true,
Mode: cache.CacheModeNew,
Db: &app.Db,
}
dbCache.SetError(&Error{})
// 清理测试数据
dbCache.CachesDelete([]string{"db_batch_key1", "db_batch_key2", "db_batch_key3"})
// 测试 CachesSet
testData := Map{
"db_batch_key1": "db_value1",
"db_batch_key2": "db_value2",
"db_batch_key3": "db_value3",
}
dbCache.CachesSet(testData)
// #region agent log
debugLog("E", "batch_cache_test.go:testCacheDbBatch:afterSet", "CacheDb.CachesSet完成", map[string]interface{}{"count": len(testData)})
// #endregion
// 测试 CachesGet
keys := []string{"db_batch_key1", "db_batch_key2", "db_batch_key3", "db_not_exist"}
result := dbCache.CachesGet(keys)
// #region agent log
debugLog("E", "batch_cache_test.go:testCacheDbBatch:afterGet", "CacheDb.CachesGet完成", map[string]interface{}{
"requested_keys": keys,
"result_count": len(result),
"result_keys": getMapKeys(result),
"result_values": result,
})
// #endregion
if len(result) != 3 {
fmt.Printf(" [FAIL] CacheDb.CachesGet: 期望3个结果,实际%d个\n", len(result))
} else {
fmt.Println(" [PASS] CacheDb.CachesGet: 批量获取正确")
}
// 验证值正确性
if result["db_batch_key1"] != "db_value1" {
fmt.Printf(" [FAIL] CacheDb.CachesGet: db_batch_key1 值不正确,期望 db_value1,实际 %v\n", result["db_batch_key1"])
} else {
fmt.Println(" [PASS] CacheDb.CachesGet: 值内容正确")
}
// 测试 CachesDelete
dbCache.CachesDelete([]string{"db_batch_key1", "db_batch_key2"})
result2 := dbCache.CachesGet(keys)
// #region agent log
debugLog("E", "batch_cache_test.go:testCacheDbBatch:afterDelete", "CacheDb.CachesDelete完成", map[string]interface{}{
"deleted_keys": []string{"db_batch_key1", "db_batch_key2"},
"remaining_count": len(result2),
"remaining_keys": getMapKeys(result2),
})
// #endregion
if len(result2) != 1 {
fmt.Printf(" [FAIL] CacheDb.CachesDelete: 删除后期望1个结果,实际%d个\n", len(result2))
} else {
fmt.Println(" [PASS] CacheDb.CachesDelete: 批量删除正确")
}
// 清理
dbCache.CachesDelete([]string{"db_batch_key3"})
}
// testHoTimeCacheBatch 测试 HoTimeCache 三级缓存批量操作
func testHoTimeCacheBatch(app *hotime.Application) {
// #region agent log
debugLog("D", "batch_cache_test.go:testHoTimeCacheBatch:start", "开始测试HoTimeCache三级缓存批量操作", nil)
// #endregion
htCache := app.HoTimeCache
// 清理测试数据
htCache.SessionsDelete([]string{
hotime.HEAD_SESSION_ADD + "ht_batch_key1",
hotime.HEAD_SESSION_ADD + "ht_batch_key2",
hotime.HEAD_SESSION_ADD + "ht_batch_key3",
})
// 测试 SessionsSet
testData := Map{
hotime.HEAD_SESSION_ADD + "ht_batch_key1": Map{"user": "test1", "role": "admin"},
hotime.HEAD_SESSION_ADD + "ht_batch_key2": Map{"user": "test2", "role": "user"},
hotime.HEAD_SESSION_ADD + "ht_batch_key3": Map{"user": "test3", "role": "guest"},
}
htCache.SessionsSet(testData)
// #region agent log
debugLog("D", "batch_cache_test.go:testHoTimeCacheBatch:afterSet", "HoTimeCache.SessionsSet完成", map[string]interface{}{"count": len(testData)})
// #endregion
// 测试 SessionsGet
keys := []string{
hotime.HEAD_SESSION_ADD + "ht_batch_key1",
hotime.HEAD_SESSION_ADD + "ht_batch_key2",
hotime.HEAD_SESSION_ADD + "ht_batch_key3",
hotime.HEAD_SESSION_ADD + "ht_not_exist",
}
result := htCache.SessionsGet(keys)
// #region agent log
debugLog("D", "batch_cache_test.go:testHoTimeCacheBatch:afterGet", "HoTimeCache.SessionsGet完成", map[string]interface{}{
"requested_keys": len(keys),
"result_count": len(result),
"result_keys": getMapKeys(result),
})
// #endregion
if len(result) != 3 {
fmt.Printf(" [FAIL] HoTimeCache.SessionsGet: 期望3个结果,实际%d个\n", len(result))
} else {
fmt.Println(" [PASS] HoTimeCache.SessionsGet: 批量获取正确")
}
// 测试 SessionsDelete
htCache.SessionsDelete([]string{
hotime.HEAD_SESSION_ADD + "ht_batch_key1",
hotime.HEAD_SESSION_ADD + "ht_batch_key2",
})
result2 := htCache.SessionsGet(keys)
// #region agent log
debugLog("D", "batch_cache_test.go:testHoTimeCacheBatch:afterDelete", "HoTimeCache.SessionsDelete完成", map[string]interface{}{
"remaining_count": len(result2),
})
// #endregion
if len(result2) != 1 {
fmt.Printf(" [FAIL] HoTimeCache.SessionsDelete: 删除后期望1个结果,实际%d个\n", len(result2))
} else {
fmt.Println(" [PASS] HoTimeCache.SessionsDelete: 批量删除正确")
}
// 清理
htCache.SessionsDelete([]string{hotime.HEAD_SESSION_ADD + "ht_batch_key3"})
}
// testSessionInsBatch 测试 SessionIns 批量操作
func testSessionInsBatch(app *hotime.Application) {
// #region agent log
debugLog("B", "batch_cache_test.go:testSessionInsBatch:start", "开始测试SessionIns批量操作", nil)
// #endregion
// 创建一个模拟的 SessionIns
session := &hotime.SessionIns{
SessionId: "test_batch_session_" + ObjToStr(time.Now().UnixNano()),
}
session.Init(app.HoTimeCache)
// 测试 SessionsSet
testData := Map{
"field1": "value1",
"field2": 123,
"field3": Map{"nested": "data"},
}
session.SessionsSet(testData)
// #region agent log
debugLog("B", "batch_cache_test.go:testSessionInsBatch:afterSet", "SessionIns.SessionsSet完成", map[string]interface{}{
"session_id": session.SessionId,
"count": len(testData),
})
// #endregion
// 测试 SessionsGet
result := session.SessionsGet("field1", "field2", "field3", "not_exist")
// #region agent log
debugLog("B", "batch_cache_test.go:testSessionInsBatch:afterGet", "SessionIns.SessionsGet完成", map[string]interface{}{
"result_count": len(result),
"result_keys": getMapKeys(result),
"result": result,
})
// #endregion
if len(result) != 3 {
fmt.Printf(" [FAIL] SessionIns.SessionsGet: 期望3个结果,实际%d个\n", len(result))
} else {
fmt.Println(" [PASS] SessionIns.SessionsGet: 批量获取正确")
}
// 验证值类型
if result["field1"] != "value1" {
fmt.Printf(" [FAIL] SessionIns.SessionsGet: field1 值不正确\n")
} else {
fmt.Println(" [PASS] SessionIns.SessionsGet: 字符串值正确")
}
var convErr Error
if ObjToInt(result["field2"], &convErr) != 123 {
fmt.Printf(" [FAIL] SessionIns.SessionsGet: field2 值不正确\n")
} else {
fmt.Println(" [PASS] SessionIns.SessionsGet: 数值类型正确")
}
// 测试 SessionsDelete
session.SessionsDelete("field1", "field2")
result2 := session.SessionsGet("field1", "field2", "field3")
// #region agent log
debugLog("B", "batch_cache_test.go:testSessionInsBatch:afterDelete", "SessionIns.SessionsDelete完成", map[string]interface{}{
"remaining_count": len(result2),
"remaining_keys": getMapKeys(result2),
})
// #endregion
if len(result2) != 1 {
fmt.Printf(" [FAIL] SessionIns.SessionsDelete: 删除后期望1个结果,实际%d个\n", len(result2))
} else {
fmt.Println(" [PASS] SessionIns.SessionsDelete: 批量删除正确")
}
}
// testCacheBackfill 测试缓存反哺机制
func testCacheBackfill(app *hotime.Application) {
// #region agent log
debugLog("D", "batch_cache_test.go:testCacheBackfill:start", "开始测试缓存反哺机制", nil)
// #endregion
htCache := app.HoTimeCache
// 直接写入数据库缓存(绕过 memory)模拟只有 db 有数据的情况
dbCache := &cache.CacheDb{
TimeOut: 3600,
DbSet: true,
SessionSet: true,
Mode: cache.CacheModeNew,
Db: &app.Db,
}
dbCache.SetError(&Error{})
testKey := "backfill_test_key_" + ObjToStr(time.Now().UnixNano())
testValue := Map{"backfill": "test_data"}
// 直接写入 db
dbCache.Cache(testKey, testValue)
// #region agent log
debugLog("D", "batch_cache_test.go:testCacheBackfill:dbWritten", "数据直接写入DB", map[string]interface{}{
"key": testKey,
"value": testValue,
})
// #endregion
// 通过 HoTimeCache 批量获取,应该触发反哺到 memory
keys := []string{testKey}
result := htCache.CachesGet(keys)
// #region agent log
debugLog("D", "batch_cache_test.go:testCacheBackfill:afterGet", "HoTimeCache.CachesGet完成", map[string]interface{}{
"result_count": len(result),
"has_key": result[testKey] != nil,
})
// #endregion
if len(result) != 1 || result[testKey] == nil {
fmt.Println(" [FAIL] 缓存反哺: 从 DB 读取失败")
} else {
fmt.Println(" [PASS] 缓存反哺: 从 DB 读取成功")
}
// 清理
htCache.CachesDelete(keys)
}
// testBatchEfficiency 测试批量操作效率
func testBatchEfficiency(app *hotime.Application) {
// #region agent log
debugLog("A", "batch_cache_test.go:testBatchEfficiency:start", "开始测试批量操作效率", nil)
// #endregion
session := &hotime.SessionIns{
SessionId: "efficiency_test_" + ObjToStr(time.Now().UnixNano()),
}
session.Init(app.HoTimeCache)
// 记录批量设置开始时间
startTime := time.Now()
// 设置10个字段
testData := Map{}
for i := 0; i < 10; i++ {
testData[fmt.Sprintf("eff_field_%d", i)] = fmt.Sprintf("value_%d", i)
}
session.SessionsSet(testData)
batchDuration := time.Since(startTime)
// #region agent log
debugLog("A", "batch_cache_test.go:testBatchEfficiency:batchSet", "批量设置完成", map[string]interface{}{
"count": len(testData),
"duration_ms": batchDuration.Milliseconds(),
})
// #endregion
// 对比单个设置
session2 := &hotime.SessionIns{
SessionId: "efficiency_test_single_" + ObjToStr(time.Now().UnixNano()),
}
session2.Init(app.HoTimeCache)
startTime2 := time.Now()
for i := 0; i < 10; i++ {
session2.Session(fmt.Sprintf("single_field_%d", i), fmt.Sprintf("value_%d", i))
}
singleDuration := time.Since(startTime2)
// #region agent log
debugLog("A", "batch_cache_test.go:testBatchEfficiency:singleSet", "单个设置完成", map[string]interface{}{
"count": 10,
"duration_ms": singleDuration.Milliseconds(),
})
// #endregion
fmt.Printf(" 批量设置10个字段耗时: %v\n", batchDuration)
fmt.Printf(" 单个设置10个字段耗时: %v\n", singleDuration)
if batchDuration < singleDuration {
fmt.Println(" [PASS] 批量操作效率: 批量操作更快")
} else {
fmt.Println(" [WARN] 批量操作效率: 批量操作未体现优势(可能数据量太小)")
}
// 批量获取测试
startTime3 := time.Now()
keys := make([]string, 10)
for i := 0; i < 10; i++ {
keys[i] = fmt.Sprintf("eff_field_%d", i)
}
session.SessionsGet(keys...)
batchGetDuration := time.Since(startTime3)
// #region agent log
debugLog("A", "batch_cache_test.go:testBatchEfficiency:batchGet", "批量获取完成", map[string]interface{}{
"count": 10,
"duration_ms": batchGetDuration.Milliseconds(),
})
// #endregion
fmt.Printf(" 批量获取10个字段耗时: %v\n", batchGetDuration)
}
// getMapKeys 获取 Map 的所有键
func getMapKeys(m Map) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
+414
View File
@@ -0,0 +1,414 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"runtime"
"sync"
"sync/atomic"
"time"
)
// 压测配置
type BenchConfig struct {
URL string
Concurrency int
Duration time.Duration
Timeout time.Duration
}
// 压测结果
type BenchResult struct {
TotalRequests int64
SuccessRequests int64
FailedRequests int64
TotalDuration time.Duration
MinLatency int64 // 纳秒
MaxLatency int64
AvgLatency int64
P50Latency int64 // 50分位
P90Latency int64 // 90分位
P99Latency int64 // 99分位
QPS float64
}
// 延迟收集器
type LatencyCollector struct {
latencies []int64
mu sync.Mutex
}
func (lc *LatencyCollector) Add(latency int64) {
lc.mu.Lock()
lc.latencies = append(lc.latencies, latency)
lc.mu.Unlock()
}
func (lc *LatencyCollector) GetPercentile(p float64) int64 {
lc.mu.Lock()
defer lc.mu.Unlock()
if len(lc.latencies) == 0 {
return 0
}
// 简单排序取百分位
n := len(lc.latencies)
idx := int(float64(n) * p)
if idx >= n {
idx = n - 1
}
// 部分排序找第idx个元素
return quickSelect(lc.latencies, idx)
}
func quickSelect(arr []int64, k int) int64 {
if len(arr) == 1 {
return arr[0]
}
pivot := arr[len(arr)/2]
var left, right, equal []int64
for _, v := range arr {
if v < pivot {
left = append(left, v)
} else if v > pivot {
right = append(right, v)
} else {
equal = append(equal, v)
}
}
if k < len(left) {
return quickSelect(left, k)
} else if k < len(left)+len(equal) {
return pivot
}
return quickSelect(right, k-len(left)-len(equal))
}
func main() {
// 最大化利用CPU
runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Println("==========================================")
fmt.Println(" 🔥 HoTime 极限压力测试 🔥")
fmt.Println("==========================================")
fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
fmt.Println()
// 极限测试配置
configs := []BenchConfig{
{URL: "http://127.0.0.1:8081/app/test/hello", Concurrency: 500, Duration: 15 * time.Second, Timeout: 10 * time.Second},
{URL: "http://127.0.0.1:8081/app/test/hello", Concurrency: 1000, Duration: 15 * time.Second, Timeout: 10 * time.Second},
{URL: "http://127.0.0.1:8081/app/test/hello", Concurrency: 2000, Duration: 15 * time.Second, Timeout: 10 * time.Second},
{URL: "http://127.0.0.1:8081/app/test/hello", Concurrency: 5000, Duration: 15 * time.Second, Timeout: 10 * time.Second},
{URL: "http://127.0.0.1:8081/app/test/hello", Concurrency: 10000, Duration: 15 * time.Second, Timeout: 10 * time.Second},
}
// 检查服务
fmt.Println("正在检查服务是否可用...")
if !checkService(configs[0].URL) {
fmt.Println("❌ 服务不可用,请先启动示例应用")
return
}
fmt.Println("✅ 服务已就绪")
fmt.Println()
// 预热
fmt.Println("🔄 预热中 (5秒)...")
warmup(configs[0].URL, 100, 5*time.Second)
fmt.Println("✅ 预热完成")
fmt.Println()
var maxQPS float64
var maxConcurrency int
// 执行极限测试
for i, config := range configs {
fmt.Printf("══════════════════════════════════════════\n")
fmt.Printf("【极限测试 %d】并发数: %d, 持续时间: %v\n", i+1, config.Concurrency, config.Duration)
fmt.Printf("══════════════════════════════════════════\n")
result := runBenchmark(config)
printResult(result)
if result.QPS > maxQPS {
maxQPS = result.QPS
maxConcurrency = config.Concurrency
}
// 检查是否达到瓶颈
successRate := float64(result.SuccessRequests) / float64(result.TotalRequests) * 100
if successRate < 95 {
fmt.Println("\n⚠️ 成功率低于95%,已达到服务极限!")
break
}
if result.AvgLatency > int64(100*time.Millisecond) {
fmt.Println("\n⚠️ 平均延迟超过100ms,已达到服务极限!")
break
}
fmt.Println()
// 测试间隔
if i < len(configs)-1 {
fmt.Println("冷却 5 秒...")
time.Sleep(5 * time.Second)
fmt.Println()
}
}
// 最终报告
fmt.Println()
fmt.Println("══════════════════════════════════════════")
fmt.Println(" 📊 极限测试总结")
fmt.Println("══════════════════════════════════════════")
fmt.Printf("最高 QPS: %.2f 请求/秒\n", maxQPS)
fmt.Printf("最佳并发数: %d\n", maxConcurrency)
fmt.Println()
// 并发用户估算
estimateUsers(maxQPS)
}
func checkService(url string) bool {
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(url)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == 200
}
func warmup(url string, concurrency int, duration time.Duration) {
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConns: concurrency * 2,
MaxIdleConnsPerHost: concurrency * 2,
},
}
done := make(chan struct{})
var wg sync.WaitGroup
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-done:
return
default:
resp, err := client.Get(url)
if err == nil {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
}
}
}()
}
time.Sleep(duration)
close(done)
wg.Wait()
}
func runBenchmark(config BenchConfig) BenchResult {
var (
totalRequests int64
successRequests int64
failedRequests int64
totalLatency int64
minLatency int64 = int64(time.Hour)
maxLatency int64
mu sync.Mutex
)
collector := &LatencyCollector{
latencies: make([]int64, 0, 100000),
}
// 高性能HTTP客户端
client := &http.Client{
Timeout: config.Timeout,
Transport: &http.Transport{
MaxIdleConns: config.Concurrency * 2,
MaxIdleConnsPerHost: config.Concurrency * 2,
MaxConnsPerHost: config.Concurrency * 2,
IdleConnTimeout: 90 * time.Second,
DisableKeepAlives: false,
DisableCompression: true,
},
}
done := make(chan struct{})
var wg sync.WaitGroup
startTime := time.Now()
// 启动并发
for i := 0; i < config.Concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-done:
return
default:
reqStart := time.Now()
success := makeRequest(client, config.URL)
latency := time.Since(reqStart).Nanoseconds()
atomic.AddInt64(&totalRequests, 1)
atomic.AddInt64(&totalLatency, latency)
if success {
atomic.AddInt64(&successRequests, 1)
} else {
atomic.AddInt64(&failedRequests, 1)
}
// 采样收集延迟(每100个请求采样1个,减少内存开销)
if atomic.LoadInt64(&totalRequests)%100 == 0 {
collector.Add(latency)
}
mu.Lock()
if latency < minLatency {
minLatency = latency
}
if latency > maxLatency {
maxLatency = latency
}
mu.Unlock()
}
}
}()
}
time.Sleep(config.Duration)
close(done)
wg.Wait()
totalDuration := time.Since(startTime)
result := BenchResult{
TotalRequests: totalRequests,
SuccessRequests: successRequests,
FailedRequests: failedRequests,
TotalDuration: totalDuration,
MinLatency: minLatency,
MaxLatency: maxLatency,
P50Latency: collector.GetPercentile(0.50),
P90Latency: collector.GetPercentile(0.90),
P99Latency: collector.GetPercentile(0.99),
}
if totalRequests > 0 {
result.AvgLatency = totalLatency / totalRequests
result.QPS = float64(totalRequests) / totalDuration.Seconds()
}
return result
}
func makeRequest(client *http.Client, url string) bool {
resp, err := client.Get(url)
if err != nil {
return false
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false
}
if resp.StatusCode != 200 {
return false
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return false
}
if status, ok := result["status"].(float64); !ok || status != 0 {
return false
}
return true
}
func printResult(result BenchResult) {
successRate := float64(result.SuccessRequests) / float64(result.TotalRequests) * 100
fmt.Printf("总请求数: %d\n", result.TotalRequests)
fmt.Printf("成功请求: %d\n", result.SuccessRequests)
fmt.Printf("失败请求: %d\n", result.FailedRequests)
fmt.Printf("成功率: %.2f%%\n", successRate)
fmt.Printf("总耗时: %v\n", result.TotalDuration.Round(time.Millisecond))
fmt.Printf("QPS: %.2f 请求/秒\n", result.QPS)
fmt.Println("------------------------------------------")
fmt.Printf("最小延迟: %v\n", time.Duration(result.MinLatency).Round(time.Microsecond))
fmt.Printf("平均延迟: %v\n", time.Duration(result.AvgLatency).Round(time.Microsecond))
fmt.Printf("P50延迟: %v\n", time.Duration(result.P50Latency).Round(time.Microsecond))
fmt.Printf("P90延迟: %v\n", time.Duration(result.P90Latency).Round(time.Microsecond))
fmt.Printf("P99延迟: %v\n", time.Duration(result.P99Latency).Round(time.Microsecond))
fmt.Printf("最大延迟: %v\n", time.Duration(result.MaxLatency).Round(time.Microsecond))
// 性能评级
fmt.Print("\n性能评级: ")
switch {
case result.QPS >= 200000:
fmt.Println("🏆 卓越 (QPS >= 200K)")
case result.QPS >= 100000:
fmt.Println("🚀 优秀 (QPS >= 100K)")
case result.QPS >= 50000:
fmt.Println("⭐ 良好 (QPS >= 50K)")
case result.QPS >= 20000:
fmt.Println("👍 中上 (QPS >= 20K)")
case result.QPS >= 10000:
fmt.Println("📊 中等 (QPS >= 10K)")
default:
fmt.Println("⚠️ 一般 (QPS < 10K)")
}
}
func estimateUsers(maxQPS float64) {
fmt.Println("📈 并发用户数估算(基于不同使用场景):")
fmt.Println("------------------------------------------")
scenarios := []struct {
name string
requestInterval float64 // 用户平均请求间隔(秒)
}{
{"高频交互(每秒1次请求)", 1},
{"活跃用户(每5秒1次请求)", 5},
{"普通浏览(每10秒1次请求)", 10},
{"低频访问(每30秒1次请求)", 30},
{"偶尔访问(每60秒1次请求)", 60},
}
for _, s := range scenarios {
users := maxQPS * s.requestInterval
fmt.Printf("%-30s ~%d 用户\n", s.name, int(users))
}
fmt.Println()
fmt.Println("💡 实际生产环境建议保留 30-50% 性能余量")
fmt.Printf(" 安全并发用户数: %d - %d (普通浏览场景)\n",
int(maxQPS*10*0.5), int(maxQPS*10*0.7))
}
+327
View File
@@ -0,0 +1,327 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"runtime"
"sync"
"sync/atomic"
"time"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Println("==========================================")
fmt.Println(" 🔥 HoTime 极限压力测试 🔥")
fmt.Println("==========================================")
fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
fmt.Println()
baseURL := "http://127.0.0.1:8081/app/test/hello"
// 检查服务
fmt.Println("正在检查服务是否可用...")
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(baseURL)
if err != nil || resp.StatusCode != 200 {
fmt.Println("❌ 服务不可用,请先启动示例应用")
return
}
resp.Body.Close()
fmt.Println("✅ 服务已就绪")
fmt.Println()
// 预热
fmt.Println("🔄 预热中 (5秒)...")
doWarmup(baseURL, 100, 5*time.Second)
fmt.Println("✅ 预热完成")
fmt.Println()
// 极限测试配置
concurrencyLevels := []int{500, 1000, 2000, 5000, 10000}
testDuration := 15 * time.Second
var maxQPS float64
var maxConcurrency int
for i, concurrency := range concurrencyLevels {
fmt.Printf("══════════════════════════════════════════\n")
fmt.Printf("【极限测试 %d】并发数: %d, 持续时间: %v\n", i+1, concurrency, testDuration)
fmt.Printf("══════════════════════════════════════════\n")
total, success, failed, qps, minLat, avgLat, maxLat, p50, p90, p99 := doBenchmark(baseURL, concurrency, testDuration)
successRate := float64(success) / float64(total) * 100
fmt.Printf("总请求数: %d\n", total)
fmt.Printf("成功请求: %d\n", success)
fmt.Printf("失败请求: %d\n", failed)
fmt.Printf("成功率: %.2f%%\n", successRate)
fmt.Printf("QPS: %.2f 请求/秒\n", qps)
fmt.Println("------------------------------------------")
fmt.Printf("最小延迟: %v\n", time.Duration(minLat))
fmt.Printf("平均延迟: %v\n", time.Duration(avgLat))
fmt.Printf("P50延迟: %v\n", time.Duration(p50))
fmt.Printf("P90延迟: %v\n", time.Duration(p90))
fmt.Printf("P99延迟: %v\n", time.Duration(p99))
fmt.Printf("最大延迟: %v\n", time.Duration(maxLat))
// 性能评级
fmt.Print("\n性能评级: ")
switch {
case qps >= 200000:
fmt.Println("🏆 卓越 (QPS >= 200K)")
case qps >= 100000:
fmt.Println("🚀 优秀 (QPS >= 100K)")
case qps >= 50000:
fmt.Println("⭐ 良好 (QPS >= 50K)")
case qps >= 20000:
fmt.Println("👍 中上 (QPS >= 20K)")
case qps >= 10000:
fmt.Println("📊 中等 (QPS >= 10K)")
default:
fmt.Println("⚠️ 一般 (QPS < 10K)")
}
if qps > maxQPS {
maxQPS = qps
maxConcurrency = concurrency
}
// 检查是否达到瓶颈
if successRate < 95 {
fmt.Println("\n⚠️ 成功率低于95%,已达到服务极限!")
break
}
if avgLat > int64(100*time.Millisecond) {
fmt.Println("\n⚠️ 平均延迟超过100ms,已达到服务极限!")
break
}
fmt.Println()
if i < len(concurrencyLevels)-1 {
fmt.Println("冷却 5 秒...")
time.Sleep(5 * time.Second)
fmt.Println()
}
}
// 最终报告
fmt.Println()
fmt.Println("══════════════════════════════════════════")
fmt.Println(" 📊 极限测试总结")
fmt.Println("══════════════════════════════════════════")
fmt.Printf("最高 QPS: %.2f 请求/秒\n", maxQPS)
fmt.Printf("最佳并发数: %d\n", maxConcurrency)
fmt.Println()
// 并发用户估算
fmt.Println("📈 并发用户数估算:")
fmt.Println("------------------------------------------")
fmt.Printf("高频交互(1秒/次): ~%d 用户\n", int(maxQPS*1))
fmt.Printf("活跃用户(5秒/次): ~%d 用户\n", int(maxQPS*5))
fmt.Printf("普通浏览(10秒/次): ~%d 用户\n", int(maxQPS*10))
fmt.Printf("低频访问(30秒/次): ~%d 用户\n", int(maxQPS*30))
fmt.Println()
fmt.Println("💡 生产环境建议保留 30-50% 性能余量")
fmt.Printf(" 安全并发用户数: %d - %d (普通浏览场景)\n",
int(maxQPS*10*0.5), int(maxQPS*10*0.7))
}
func doWarmup(url string, concurrency int, duration time.Duration) {
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConns: concurrency * 2,
MaxIdleConnsPerHost: concurrency * 2,
},
}
done := make(chan struct{})
var wg sync.WaitGroup
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-done:
return
default:
resp, err := client.Get(url)
if err == nil {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
}
}
}()
}
time.Sleep(duration)
close(done)
wg.Wait()
}
func doBenchmark(url string, concurrency int, duration time.Duration) (total, success, failed int64, qps float64, minLat, avgLat, maxLat, p50, p90, p99 int64) {
var totalLatency int64
minLat = int64(time.Hour)
var mu sync.Mutex
// 采样收集器
latencies := make([]int64, 0, 50000)
var latMu sync.Mutex
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: concurrency * 2,
MaxIdleConnsPerHost: concurrency * 2,
MaxConnsPerHost: concurrency * 2,
IdleConnTimeout: 90 * time.Second,
DisableCompression: true,
},
}
done := make(chan struct{})
var wg sync.WaitGroup
startTime := time.Now()
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-done:
return
default:
reqStart := time.Now()
ok := doRequest(client, url)
lat := time.Since(reqStart).Nanoseconds()
atomic.AddInt64(&total, 1)
atomic.AddInt64(&totalLatency, lat)
if ok {
atomic.AddInt64(&success, 1)
} else {
atomic.AddInt64(&failed, 1)
}
// 采样 (每50个采1个)
if atomic.LoadInt64(&total)%50 == 0 {
latMu.Lock()
latencies = append(latencies, lat)
latMu.Unlock()
}
mu.Lock()
if lat < minLat {
minLat = lat
}
if lat > maxLat {
maxLat = lat
}
mu.Unlock()
}
}
}()
}
time.Sleep(duration)
close(done)
wg.Wait()
totalDuration := time.Since(startTime)
if total > 0 {
avgLat = totalLatency / total
qps = float64(total) / totalDuration.Seconds()
}
// 计算百分位
if len(latencies) > 0 {
p50 = getPercentile(latencies, 0.50)
p90 = getPercentile(latencies, 0.90)
p99 = getPercentile(latencies, 0.99)
}
return
}
func doRequest(client *http.Client, url string) bool {
resp, err := client.Get(url)
if err != nil {
return false
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false
}
if resp.StatusCode != 200 {
return false
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return false
}
if status, ok := result["status"].(float64); !ok || status != 0 {
return false
}
return true
}
func getPercentile(arr []int64, p float64) int64 {
n := len(arr)
if n == 0 {
return 0
}
// 复制一份避免修改原数组
tmp := make([]int64, n)
copy(tmp, arr)
idx := int(float64(n) * p)
if idx >= n {
idx = n - 1
}
return quickSelect(tmp, idx)
}
func quickSelect(arr []int64, k int) int64 {
if len(arr) == 1 {
return arr[0]
}
pivot := arr[len(arr)/2]
var left, right, equal []int64
for _, v := range arr {
if v < pivot {
left = append(left, v)
} else if v > pivot {
right = append(right, v)
} else {
equal = append(equal, v)
}
}
if k < len(left) {
return quickSelect(left, k)
} else if k < len(left)+len(equal) {
return pivot
}
return quickSelect(right, k-len(left)-len(equal))
}
-49
View File
@@ -1,49 +0,0 @@
{
"cache": {
"db": {
"db": false,
"session": true
},
"memory": {
"db": true,
"session": true,
"timeout": 7200
}
},
"codeConfig": {
"admin": "config/app.json"
},
"codeConfig1": {
"admin": {
"config": "config/app.json",
"package": "admin",
"rule": "config/rule.json"
}
},
"crossDomain": "auto",
"db": {
"mysql": {
"host": "192.168.6.253",
"name": "bzyy",
"password": "dasda8454456",
"port": "3306",
"prefix": "",
"user": "root"
}
},
"defFile": [
"index.html",
"index.htm"
],
"error": {
"1": "内部系统异常",
"2": "访问权限异常",
"3": "请求参数异常",
"4": "数据处理异常",
"5": "数据结果异常"
},
"mode": 0,
"port": "80",
"sessionName": "HOTIME",
"tpt": "tpt"
}
-72
View File
@@ -1,72 +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": "默认服务ip127.0.0.1,必须,如果需要使用redis服务时配置,",
"password": "默认密码空,必须,如果需要使用redis服务时配置,默认密码空",
"port": "默认服务端口:6379,必须,如果需要使用redis服务时配置,",
"session": "默认true,非必须,缓存web session,同时缓存session保持的用户缓存",
"timeout": "默认60 * 60 * 24 * 15,非必须,过期时间,超时自动删除"
},
"注释": "可配置memorydbredis,默认启用memory,默认优先级为memory\u003eredis\u003edb,memory与数据库缓存设置项一致,缓存数据填充会自动反方向反哺,加入memory缓存过期将自动从redis更新,但memory永远不会更新redis,如果是集群建议不要开启memory,配置即启用"
},
"codeConfig": {
"packageName": "默认无,必须,包名称以及应用名,生成代码的配置文件地址,比如config/app.json,数据库有更新时自动更新配置文件以及对应的生成文件",
"注释": "配置即启用,非必须,默认无"
},
"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,开发模式,在开发模式下会显示更多的数据用于开发测试,并能够辅助研发,自动生成配置文件、代码等功能,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开启此功能"
}
Binary file not shown.
+1278 -163
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.el-upload img[data-v-3b5105e6]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-3b5105e6]{font-size:40px;margin:30% 31%;display:block}.custom-tree-node[data-v-3b5105e6]{font-size:14px;padding-right:8px}.info-descriptions[data-v-4d4566fc] .el-descriptions__body .el-descriptions__label{min-width:90px;text-align:right;background:transparent;color:#606266;font-weight:400}.el-descriptions .is-bordered th[data-v-4d4566fc],.info-descriptions[data-v-4d4566fc] .el-descriptions .is-bordered td{border:transparent;max-width:25vw}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}.dialog-box .el-descriptions__header{margin:20px 0 5px}.dialog-box .el-descriptions__body{background:#fff}.textarea-box *{word-break:break-all;white-space:pre-wrap}.textarea-box table{width:100%!important}.textarea-box img{max-width:80%!important}.textarea-box::-webkit-scrollbar-thumb{height:5px;background-color:rgba(0,0,0,.2)!important}
@@ -0,0 +1 @@
body[data-v-c08f3364],dd[data-v-c08f3364],dl[data-v-c08f3364],form[data-v-c08f3364],h1[data-v-c08f3364],h2[data-v-c08f3364],h3[data-v-c08f3364],h4[data-v-c08f3364],h5[data-v-c08f3364],h6[data-v-c08f3364],html[data-v-c08f3364],ol[data-v-c08f3364],p[data-v-c08f3364],pre[data-v-c08f3364],tbody[data-v-c08f3364],textarea[data-v-c08f3364],tfoot[data-v-c08f3364],thead[data-v-c08f3364],ul[data-v-c08f3364]{margin:0;font-size:14px;font-family:Microsoft YaHei}dl[data-v-c08f3364],ol[data-v-c08f3364],ul[data-v-c08f3364]{padding:0}li[data-v-c08f3364]{list-style:none}input[data-v-c08f3364]{border:none;outline:none;font-family:Microsoft YaHei;background-color:#fff}a[data-v-c08f3364]{font-family:Microsoft YaHei;text-decoration:none}[data-v-c08f3364]{margin:0;padding:0}.login[data-v-c08f3364]{position:relative;width:100%;height:100%;background-color:#353d56;background-repeat:no-repeat;background-attachment:fixed;background-size:cover}.login-item[data-v-c08f3364]{position:absolute;top:calc(50% - 30vh);left:60%;min-width:388px;width:18vw;max-width:588px;padding:8vh 30px;box-sizing:border-box;background-size:468px 468px;background:hsla(0,0%,100%,.85);border-radius:10px}.login-item .right-content[data-v-c08f3364]{box-sizing:border-box;width:100%}.login-item .right-content .login-title[data-v-c08f3364]{font-size:26px;font-weight:700;color:#4f619b;text-align:center}.errorMsg[data-v-c08f3364]{width:100%;height:34px;line-height:34px;color:red;font-size:14px;overflow:hidden}.login-item .right-content .inputWrap[data-v-c08f3364]{width:90%;height:32px;line-height:32px;color:#646464;font-size:16px;border:1px solid #b4b4b4;margin:0 auto 5%;padding:2% 10px;border-radius:10px}.login-item .right-content .inputWrap.inputFocus[data-v-c08f3364]{border:1px solid #4f619b;box-shadow:0 0 0 3px rgba(91,113,185,.4)}.login-item .right-content .inputWrap input[data-v-c08f3364]{background-color:transparent;color:#646464;display:inline-block;height:100%;width:80%}.login-btn[data-v-c08f3364]{width:97%;height:52px;text-align:center;line-height:52px;font-size:17px;color:#fff;background-color:#4f619b;border-radius:10px;margin:0 auto;margin-top:50px;cursor:pointer;font-weight:800}
@@ -0,0 +1 @@
.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.el-upload img[data-v-51dbed3c]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-51dbed3c]{font-size:40px;margin:30% 31%;display:block}.custom-tree-node[data-v-51dbed3c]{font-size:14px;padding-right:8px}.info-descriptions[data-v-4d4566fc] .el-descriptions__body .el-descriptions__label{min-width:90px;text-align:right;background:transparent;color:#606266;font-weight:400}.el-descriptions .is-bordered th[data-v-4d4566fc],.info-descriptions[data-v-4d4566fc] .el-descriptions .is-bordered td{border:transparent;max-width:25vw}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}.dialog-box .el-descriptions__header{margin:20px 0 5px}.dialog-box .el-descriptions__body{background:#fff}.textarea-box *{word-break:break-all;white-space:pre-wrap}.textarea-box table{width:100%!important}.textarea-box img{max-width:80%!important}.textarea-box::-webkit-scrollbar-thumb{height:5px;background-color:rgba(0,0,0,.2)!important}
@@ -1 +0,0 @@
h3[data-v-b9167eee]{margin:40px 0 0}ul[data-v-b9167eee]{list-style-type:none;padding:0}li[data-v-b9167eee]{display:inline-block;margin:0 10px}a[data-v-b9167eee]{color:#42b983}
@@ -0,0 +1 @@
.full-screen-container[data-v-12e6b782]{z-index:10000}.file-upload .el-upload{background:transparent;width:100%;height:auto;text-align:left;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-upload .el-upload .el-button{margin-right:10px}.el-upload img[data-v-69e31561]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-69e31561]{font-size:40px;margin:30% 31%;display:block}.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}
@@ -0,0 +1 @@
.left-nav-home-bar{background:#2c3759!important;overflow:hidden;text-overflow:ellipsis}.left-nav-home-bar,.left-nav-home-bar i{color:#fff!important}.el-submenu .el-menu-item{height:40px;line-height:40px;width:auto;min-width:60px;padding:0 10px 0 25px!important;text-overflow:ellipsis;overflow:hidden}.el-menu .el-submenu__title{height:46px;line-height:46px;padding-left:10px!important;text-overflow:ellipsis;overflow:hidden}.left-nav-home-bar i{margin-bottom:6px!important}.el-menu-item-group__title{padding:0 0 0 10px}.el-menu--collapse .el-menu-item-group__title,.el-menu--collapse .el-submenu__title{padding-left:20px!important}.el-menu-item i,.el-submenu__title i{margin-top:-4px;vertical-align:middle;margin:-3px 5px 0 0;right:1px}.head-left[data-v-b2941c10],.head-right[data-v-b2941c10]{display:flex;justify-content:center;flex-direction:column}.head-right[data-v-b2941c10]{align-items:flex-end}.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.el-upload img[data-v-51dbed3c]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-51dbed3c]{font-size:40px;margin:30% 31%;display:block}.custom-tree-node[data-v-51dbed3c]{font-size:14px;padding-right:8px}.info-descriptions[data-v-4d4566fc] .el-descriptions__body .el-descriptions__label{min-width:90px;text-align:right;background:transparent;color:#606266;font-weight:400}.el-descriptions .is-bordered th[data-v-4d4566fc],.info-descriptions[data-v-4d4566fc] .el-descriptions .is-bordered td{border:transparent;max-width:25vw}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}.dialog-box .el-descriptions__header{margin:20px 0 5px}.dialog-box .el-descriptions__body{background:#fff}.textarea-box *{word-break:break-all;white-space:pre-wrap}.textarea-box table{width:100%!important}.textarea-box img{max-width:80%!important}.textarea-box::-webkit-scrollbar-thumb{height:5px;background-color:rgba(0,0,0,.2)!important}.el-dialog{margin:auto!important;top:50%;transform:translateY(-50%)}.el-dialog-div{height:75vh;overflow:auto}.el-dialog__body{padding-top:15px;padding-bottom:45px}.el-dialog-div .el-tabs__header{position:absolute;left:1px;top:69px;width:calc(90vw - 42px);margin:0 20px;z-index:1}.el-dialog-div .el-tabs__content{padding-top:50px}.el-dialog-div .el-affix--fixed{bottom:20vh}.el-dialog-div .el-affix--fixed .el-form-item{padding:0!important;background:transparent!important}
@@ -0,0 +1 @@
.full-screen-container[data-v-12e6b782]{z-index:10000}.file-upload .el-upload{background:transparent;width:100%;height:auto;text-align:left;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-upload .el-upload .el-button{margin-right:10px}.el-upload img[data-v-96cdfb38]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-96cdfb38]{font-size:40px;margin:30% 31%;display:block}.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}
@@ -0,0 +1 @@
.not-show-tab-label .el-tabs__header{display:none}.el-descriptions__body{background:#f0f0f0}.not-show-tab-search{display:none}.el-table__body-wrapper{margin-bottom:4px;padding-bottom:2px}.el-table__body-wrapper::-webkit-scrollbar{width:8px;height:8px}.el-table__body-wrapper::-webkit-scrollbar-track{border-radius:10px;-webkit-box-shadow:inset 0 0 6px hsla(0,0%,93.3%,.3);background-color:#eee}.el-table__body-wrapper::-webkit-scrollbar-thumb{border-radius:10px;-webkit-box-shadow:inset 0 0 6px rgba(145,143,143,.3);background-color:#918f8f}.input-with-select .el-input-group__prepend{background-color:#fff}.daterange-box .select .el-input__inner{border-top-right-radius:0;border-bottom-right-radius:0}.daterange-box .daterange.el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0;vertical-align:bottom}[data-v-e9f58da4] .el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content{background-color:#409eff!important;color:#fff}
@@ -0,0 +1 @@
.left-nav-home-bar{background:#2c3759!important;overflow:hidden;text-overflow:ellipsis}.left-nav-home-bar,.left-nav-home-bar i{color:#fff!important}.el-submenu .el-menu-item{height:40px;line-height:40px;width:auto;min-width:60px;padding:0 10px 0 25px!important;text-overflow:ellipsis;overflow:hidden}.el-menu .el-submenu__title{height:46px;line-height:46px;padding-left:10px!important;text-overflow:ellipsis;overflow:hidden}.left-nav-home-bar i{margin-bottom:6px!important}.el-menu-item-group__title{padding:0 0 0 10px}.el-menu--collapse .el-menu-item-group__title,.el-menu--collapse .el-submenu__title{padding-left:20px!important}.el-menu-item i,.el-submenu__title i{margin-top:-4px;vertical-align:middle;margin:-3px 5px 0 0;right:1px}.head-left[data-v-b2941c10],.head-right[data-v-b2941c10]{display:flex;justify-content:center;flex-direction:column}.head-right[data-v-b2941c10]{align-items:flex-end}.el-upload{height:100px;width:100px;background:#eee;overflow:hidden}.el-upload img[data-v-3b5105e6]{height:100%;width:100%;-o-object-fit:cover;object-fit:cover;display:block}.el-upload i[data-v-3b5105e6]{font-size:40px;margin:30% 31%;display:block}.custom-tree-node[data-v-3b5105e6]{font-size:14px;padding-right:8px}.info-descriptions[data-v-4d4566fc] .el-descriptions__body .el-descriptions__label{min-width:90px;text-align:right;background:transparent;color:#606266;font-weight:400}.el-descriptions .is-bordered th[data-v-4d4566fc],.info-descriptions[data-v-4d4566fc] .el-descriptions .is-bordered td{border:transparent;max-width:25vw}.tree-line .el-tree-node{position:relative;padding-left:16px}.tree-line .el-tree-node__content{line-height:18px;font-size:14px}.tree-line .el-tree-node__children{padding-left:16px}.tree-line .el-tree-node:before{content:"";height:100%;width:1px;position:absolute;left:-3px;top:-26px;border-width:1px;border-left:1px dashed #52627c}.tree-line .el-tree-node:last-child:before{height:38px}.tree-line .el-tree-node:after{content:"";width:24px;height:20px;position:absolute;left:-3px;top:12px;border-width:1px;border-top:1px dashed #52627c}.tree-line>.el-tree-node:after{border-top:none}.tree-line>.el-tree-node:before{border-left:none}.tree-line .el-tree-node__expand-icon{font-size:18px;color:#000}.tree-line .el-tree-node__expand-icon.is-leaf{color:transparent}.dialog-box .el-descriptions__header{margin:20px 0 5px}.dialog-box .el-descriptions__body{background:#fff}.textarea-box *{word-break:break-all;white-space:pre-wrap}.textarea-box table{width:100%!important}.textarea-box img{max-width:80%!important}.textarea-box::-webkit-scrollbar-thumb{height:5px;background-color:rgba(0,0,0,.2)!important}.el-dialog{margin:auto!important;top:50%;transform:translateY(-50%)}.el-dialog-div{height:75vh;overflow:auto}.el-dialog__body{padding-top:15px;padding-bottom:45px}.el-dialog-div .el-tabs__header{position:absolute;left:1px;top:69px;width:calc(90vw - 42px);margin:0 20px;z-index:1}.el-dialog-div .el-tabs__content{padding-top:50px}.el-dialog-div .el-affix--fixed{bottom:20vh}.el-dialog-div .el-affix--fixed .el-form-item{padding:0!important;background:transparent!important}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.full-screen-container[data-v-12e6b782]{z-index:10000}.not-show-tab-label .el-tabs__header{display:none}.el-descriptions__body{background:#f0f0f0}.not-show-tab-search{display:none}.el-table__body-wrapper{margin-bottom:4px;padding-bottom:2px}.el-table__body-wrapper::-webkit-scrollbar{width:8px;height:8px}.el-table__body-wrapper::-webkit-scrollbar-track{border-radius:10px;-webkit-box-shadow:inset 0 0 6px hsla(0,0%,93.3%,.3);background-color:#eee}.el-table__body-wrapper::-webkit-scrollbar-thumb{border-radius:10px;-webkit-box-shadow:inset 0 0 6px rgba(145,143,143,.3);background-color:#918f8f}
+3 -3
View File
@@ -1,3 +1,3 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>hotime</title><style>body{
margin: 0px;
}</style><link href="css/chunk-038340db.e4ca99de.css" rel="prefetch"><link href="css/chunk-0524801b.1e910850.css" rel="prefetch"><link href="css/chunk-1c438cad.2fd7d31d.css" rel="prefetch"><link href="css/chunk-1d1a12b6.8c684de3.css" rel="prefetch"><link href="css/chunk-2c1d9b2f.78c69bda.css" rel="prefetch"><link href="css/chunk-5ccac4d0.5f1c91ca.css" rel="prefetch"><link href="css/chunk-c569a38e.01bc1b83.css" rel="prefetch"><link href="css/chunk-c7a51d14.4a0871eb.css" rel="prefetch"><link href="js/chunk-038340db.3c8b0cb6.js" rel="prefetch"><link href="js/chunk-0524801b.de9b433f.js" rel="prefetch"><link href="js/chunk-14f17cfa.c5104e8c.js" rel="prefetch"><link href="js/chunk-1c438cad.72a22c19.js" rel="prefetch"><link href="js/chunk-1d1a12b6.6900cc01.js" rel="prefetch"><link href="js/chunk-2c1d9b2f.39065c9e.js" rel="prefetch"><link href="js/chunk-5b2ead63.af868ff8.js" rel="prefetch"><link href="js/chunk-5ccac4d0.98ff45de.js" rel="prefetch"><link href="js/chunk-68815fbc.04152d5f.js" rel="prefetch"><link href="js/chunk-c569a38e.16d55c64.js" rel="prefetch"><link href="js/chunk-c7a51d14.3cd28dc4.js" rel="prefetch"><link href="css/app.5e2eb449.css" rel="preload" as="style"><link href="js/app.f5f26baa.js" rel="preload" as="script"><link href="css/app.5e2eb449.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but hotime doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/app.f5f26baa.js"></script></body></html>
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><script src="js/manage.js"></script><script src="https://api.map.baidu.com/api?v=2.0&ak=bF4Y6tQg94hV2vesn2ZIaUIXO4aRxxRk"></script><title></title><style>body{
margin: 0px;
}</style><link href="css/chunk-0de923e9.c4e8272a.css" rel="prefetch"><link href="css/chunk-1a458102.466135f0.css" rel="prefetch"><link href="css/chunk-291edee4.508cb3c8.css" rel="prefetch"><link href="css/chunk-4aefa5ec.fcc75990.css" rel="prefetch"><link href="css/chunk-4f81d902.91f1ef17.css" rel="prefetch"><link href="css/chunk-856f3c38.9b9508b8.css" rel="prefetch"><link href="css/chunk-e3f8e5a6.7876554f.css" rel="prefetch"><link href="js/chunk-0de923e9.1f0fca7e.js" rel="prefetch"><link href="js/chunk-1a458102.9a54e9ae.js" rel="prefetch"><link href="js/chunk-25ddb50b.55ec750b.js" rel="prefetch"><link href="js/chunk-291edee4.e8dbe8c8.js" rel="prefetch"><link href="js/chunk-4aefa5ec.c237610d.js" rel="prefetch"><link href="js/chunk-4f81d902.c5fdb7dd.js" rel="prefetch"><link href="js/chunk-7ea17297.38d572ef.js" rel="prefetch"><link href="js/chunk-856f3c38.29c2fcc0.js" rel="prefetch"><link href="js/chunk-e3f8e5a6.ff58e6f8.js" rel="prefetch"><link href="js/chunk-e624c1ca.62513cbc.js" rel="prefetch"><link href="css/app.abfb5de2.css" rel="preload" as="style"><link href="js/app.25e49e88.js" rel="preload" as="script"><link href="css/app.abfb5de2.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but hotime doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/app.25e49e88.js"></script></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-0de923e9"],{"578a":function(o,n,t){"use strict";t.r(n);t("b0c0");var i=t("f2bf"),s={class:"login-item"},r={class:"right-content"},u={class:"login-title"},l={style:{height:"60px"}},d=Object(i.r)(" 账号:"),b=Object(i.r)(" 密码:");var e=t("2934"),c={name:"Login",data:function(){return{showLog:!1,showLogInfo:"",label:"HoTime DashBoard",form:{name:"",password:""},backgroundImage:window.Hotime.data.name+"/hotime/wallpaper?random=1&type=1",focusName:!1,focusPassword:!1}},methods:{login:function(){var n=this;if(""==this.name||""==this.password)return this.showLogInfo="参数不足!",void(this.showLog=!0);Object(e.a)(window.Hotime.data.name+"/hotime/login",n.form).then(function(o){if(0!=o.status)return n.showLogInfo=o.error.msg,void(n.showLog=!0);location.hash="#/",location.reload()})},getBgImg:function(){var n=this;Object(e.e)(window.Hotime.data.name+"/hotime/wallpaper?random=1").then(function(o){0==o.status&&(n.backgroundImage=o.result.url)})},focusPrice:function(o){this["focus"+o]=!0},blurPrice:function(o){this["focus"+o]=!1}},mounted:function(){var n=this;this.label=window.Hotime.data.label,document.onkeydown=function(o){o=window.event||o;13==(o.keyCode||o.which||o.charCode)&&n.login()}}},a=(t("fad9"),t("6b0d")),t=t.n(a);n.default=t()(c,[["render",function(o,n,t,e,c,a){return Object(i.L)(),Object(i.n)("div",{class:"login",style:Object(i.C)({width:"100%",height:"100vh","background-image":"url("+c.backgroundImage+")"})},[Object(i.o)("div",s,[Object(i.o)("div",r,[Object(i.o)("p",u,Object(i.Y)(c.label),1),Object(i.o)("div",l,[Object(i.lb)(Object(i.o)("p",{class:"errorMsg"},Object(i.Y)(c.showLogInfo),513),[[i.hb,c.showLog]])]),Object(i.o)("p",{class:Object(i.B)(["inputWrap",{inputFocus:c.focusName}])},[d,Object(i.lb)(Object(i.o)("input",{type:"text","onUpdate:modelValue":n[0]||(n[0]=function(o){return c.form.name=o}),class:"accountVal",onKeyup:n[1]||(n[1]=Object(i.mb)(function(){return a.login&&a.login.apply(a,arguments)},["enter"])),onFocus:n[2]||(n[2]=function(o){return a.focusPrice("Name")}),onBlur:n[3]||(n[3]=function(o){return a.blurPrice("Name")})},null,544),[[i.gb,c.form.name]])],2),Object(i.o)("p",{class:Object(i.B)(["inputWrap",{inputFocus:c.focusPassword}])},[b,Object(i.lb)(Object(i.o)("input",{type:"password","onUpdate:modelValue":n[4]||(n[4]=function(o){return c.form.password=o}),class:"passwordVal",onKeyup:n[5]||(n[5]=Object(i.mb)(function(){return a.login&&a.login.apply(a,arguments)},["enter"])),onFocus:n[6]||(n[6]=function(o){return a.focusPrice("Password")}),onBlur:n[7]||(n[7]=function(o){return a.blurPrice("Password")})},null,544),[[i.gb,c.form.password]])],2),Object(i.o)("p",{class:"login-btn",onClick:n[8]||(n[8]=function(){return a.login&&a.login.apply(a,arguments)})},"登录")])])],4)}],["__scopeId","data-v-c08f3364"]])},dbeb:function(o,n,t){},fad9:function(o,n,t){"use strict";t("dbeb")}}]);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-25ddb50b"],{"107c":function(t,e,n){var r=n("d039"),c=n("da84").RegExp;t.exports=r(function(){var t=c("(?<a>b)","g");return"b"!==t.exec("b").groups.a||"bc"!=="b".replace(t,"$<a>c")})},"129f":function(t,e){t.exports=Object.is||function(t,e){return t===e?0!==t||1/t==1/e:t!=t&&e!=e}},"14c3":function(t,e,n){var r=n("c6b6"),c=n("9263");t.exports=function(t,e){var n=t.exec;if("function"==typeof n){n=n.call(t,e);if("object"!=typeof n)throw TypeError("RegExp exec method returned something other than an Object or null");return n}if("RegExp"!==r(t))throw TypeError("RegExp#exec called on incompatible receiver");return c.call(t,e)}},"159b":function(t,e,n){var r,c=n("da84"),i=n("fdbc"),o=n("17c2"),a=n("9112");for(r in i){var l=c[r],l=l&&l.prototype;if(l&&l.forEach!==o)try{a(l,"forEach",o)}catch(t){l.forEach=o}}},"17c2":function(t,e,n){"use strict";var r=n("b727").forEach,n=n("a640")("forEach");t.exports=n?[].forEach:function(t){return r(this,t,1<arguments.length?arguments[1]:void 0)}},"83c5":function(t,e,n){"use strict";n("159b");e.a={list:{},constructor:function(){this.list={}},$on:function(t,e){this.list[t]=this.list[t]||[],this.list[t].push(e)},$emit:function(t,e){this.list[t]&&this.list[t].forEach(function(t){t(e)})},$off:function(t){this.list[t]&&delete this.list[t]}}},"841c":function(t,e,n){"use strict";var r=n("d784"),o=n("825a"),a=n("1d80"),l=n("129f"),s=n("577e"),u=n("14c3");r("search",function(r,c,i){return[function(t){var e=a(this),n=null==t?void 0:t[r];return void 0!==n?n.call(t,e):new RegExp(t)[r](s(e))},function(t){var e=o(this),t=s(t),n=i(c,e,t);if(n.done)return n.value;n=e.lastIndex,l(n,0)||(e.lastIndex=0),t=u(e,t);return l(e.lastIndex,n)||(e.lastIndex=n),null===t?-1:t.index}]})},9263:function(t,e,n){"use strict";var r,p=n("577e"),h=n("ad6d"),c=n("9f7f"),i=n("5692"),g=n("7c73"),v=n("69f3").get,o=n("fce3"),n=n("107c"),E=RegExp.prototype.exec,b=i("native-string-replace",String.prototype.replace),I=E,R=(i=/a/,r=/b*/g,E.call(i,"a"),E.call(r,"a"),0!==i.lastIndex||0!==r.lastIndex),y=c.UNSUPPORTED_Y||c.BROKEN_CARET,w=void 0!==/()??/.exec("")[1];(R||w||y||o||n)&&(I=function(t){var e,n,r,c,i,o,a=this,l=v(a),t=p(t),s=l.raw;if(s)return s.lastIndex=a.lastIndex,f=I.call(s,t),a.lastIndex=s.lastIndex,f;var u=l.groups,s=y&&a.sticky,f=h.call(a),l=a.source,d=0,x=t;if(s&&(-1===(f=f.replace("y","")).indexOf("g")&&(f+="g"),x=t.slice(a.lastIndex),0<a.lastIndex&&(!a.multiline||a.multiline&&"\n"!==t.charAt(a.lastIndex-1))&&(l="(?: "+l+")",x=" "+x,d++),e=new RegExp("^(?:"+l+")",f)),w&&(e=new RegExp("^"+l+"$(?!\\s)",f)),R&&(n=a.lastIndex),r=E.call(s?e:a,x),s?r?(r.input=r.input.slice(d),r[0]=r[0].slice(d),r.index=a.lastIndex,a.lastIndex+=r[0].length):a.lastIndex=0:R&&r&&(a.lastIndex=a.global?r.index+r[0].length:n),w&&r&&1<r.length&&b.call(r[0],e,function(){for(c=1;c<arguments.length-2;c++)void 0===arguments[c]&&(r[c]=void 0)}),r&&u)for(r.groups=i=g(null),c=0;c<u.length;c++)i[(o=u[c])[0]]=r[o[1]];return r}),t.exports=I},"9f7f":function(t,e,n){var r=n("d039"),c=n("da84").RegExp;e.UNSUPPORTED_Y=r(function(){var t=c("a","y");return t.lastIndex=2,null!=t.exec("abcd")}),e.BROKEN_CARET=r(function(){var t=c("^r","gy");return t.lastIndex=2,null!=t.exec("str")})},ac1f:function(t,e,n){"use strict";var r=n("23e7"),n=n("9263");r({target:"RegExp",proto:!0,forced:/./.exec!==n},{exec:n})},ad6d:function(t,e,n){"use strict";var r=n("825a");t.exports=function(){var t=r(this),e="";return t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.dotAll&&(e+="s"),t.unicode&&(e+="u"),t.sticky&&(e+="y"),e}},d784:function(t,e,n){"use strict";n("ac1f");var l=n("6eeb"),s=n("9263"),u=n("d039"),f=n("b622"),d=n("9112"),x=f("species"),p=RegExp.prototype;t.exports=function(n,t,e,r){var o,c=f(n),a=!u(function(){var t={};return t[c]=function(){return 7},7!=""[n](t)}),i=a&&!u(function(){var t=!1,e=/a/;return"split"===n&&((e={constructor:{}}).constructor[x]=function(){return e},e.flags="",e[c]=/./[c]),e.exec=function(){return t=!0,null},e[c](""),!t});a&&i&&!e||(o=/./[c],i=t(c,""[n],function(t,e,n,r,c){var i=e.exec;return i===s||i===p.exec?a&&!c?{done:!0,value:o.call(e,n,r)}:{done:!0,value:t.call(n,e,r)}:{done:!1}}),l(String.prototype,n,i[0]),l(p,c,i[1])),r&&d(p[c],"sham",!0)}},fce3:function(t,e,n){var r=n("d039"),c=n("da84").RegExp;t.exports=r(function(){var t=c(".","s");return!(t.dotAll&&t.exec("\n")&&"s"===t.flags)})}}]);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More