重构数据引擎和报表引擎
This commit is contained in:
@@ -27,3 +27,27 @@
|
||||
- prefetch 流程补充游标分页、参数构建、响应解析的详细说明
|
||||
- 新增「并发保护」章节,记录内存锁和调度器时序
|
||||
- 调度器章节补充 for+sleep 模式说明
|
||||
|
||||
## 钉钉平台新增
|
||||
- 创建 `seed_data_dingtalk.sql`:钉钉开放平台 + 部门列表接口
|
||||
- auth_type: OAUTH2,access_token 在 URL 查询参数中
|
||||
- 部门列表:POST + parameters_location=query + recursive 递归遍历,响应 `result` 为数组
|
||||
- 表名:`dingtalk_department`,冲突键:dept_id
|
||||
|
||||
## 递归遍历支持
|
||||
- 新增 `RecursiveConfig` + `parseRecursiveConfig` + `syncRecursive` 函数
|
||||
- `syncRecursive`:BFS 队列遍历,先查根级 → 对每个子部门递归查下级,防重复 + 最大深度限制
|
||||
- `buildReqBody` / `buildPrefetchParams` 增加过滤 `recursive` / `max_recursive_depth`
|
||||
|
||||
## 钉钉智能人事平台
|
||||
- 新建 `seed_data_dingtalk_hrm.sql`:钉钉智能人事专用平台
|
||||
- auth_type: OAUTH2 + Header 认证(`x-acs-dingtalk-access-token`),独立于 oapi 平台
|
||||
- 企业职位列表 `position_list`:POST + cursor 分页 + hasMore,API base `api.dingtalk.com`
|
||||
|
||||
## 钉钉角色列表 + 引擎增强
|
||||
- `parseRespExt` 增加 `has_more_field` 支持 + 数字游标(float64→string)
|
||||
- `syncWithPrefetch` 支持递归预取来源(BFS)
|
||||
- `buildReqBody` 增加 `pagination_mode: "offset"` 支持(计算 offset = (page-1)*pageSize)
|
||||
- `syncSingleAPI` 增加 hasMore 驱动的分页循环(钉钉 offset/size + hasMore)
|
||||
- 新增 `hasMoreCheck` 辅助函数
|
||||
- 新增 `seed_data_dingtalk.sql` 角色列表接口:偏移分页 + hasMore
|
||||
|
||||
36
.codebuddy/memory/2026-06-03.md
Normal file
36
.codebuddy/memory/2026-06-03.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 2026-06-03 工作记录
|
||||
|
||||
## 钉钉智能薪酬平台对接
|
||||
- 新增 `dingtalk_salary` 平台 + 2个接口到 `sql/seed_data_dingtalk_salary.sql`
|
||||
- 新认证类型 `APP_SIGNATURE`:app-id + signature 头部认证
|
||||
- 签名算法:MD5(body_string + app_secret).toUpperCase()
|
||||
|
||||
### 接口明细
|
||||
1. **salary_dept_list** — `POST /oapi/salary/dept/list`,无入参,单次拉取全量部门
|
||||
2. **salary_statistics** — `POST /oapi/salary/statistics/dept`,prefetch 自 salary_dept_list,row_inject statisticsMonth
|
||||
|
||||
### 代码改动
|
||||
- `api_client.go`: `applyAuthHeader` 扩展支持 `APP_SIGNATURE` 认证,新增 `applyAppSignatureAuth` + `computeBodySignature`
|
||||
- `platform_manager.go`: 新增 `APP_SIGNATURE` case,从 auth_config 提取 app_id/app_secret
|
||||
- `dynamic_sync.go`:
|
||||
- `toFloat64` 扩展支持 string→float64(适配 salary API 返回 code: "200" 字符串)
|
||||
- `buildReqBody`/`buildPrefetchParams` skip list 新增 `row_inject`
|
||||
- 新增 `injectRowFields` 函数:将请求参数注入响应行
|
||||
- `injectRowFields` 调用于 syncWithPrefetch 中3处 row 注入点
|
||||
|
||||
## 新增钉钉智能薪酬人力成本报表接口 (2026-06-03 第二阶段)
|
||||
|
||||
### 新增接口
|
||||
3. **salary_statistic_report** — `POST /oapi/salary/statistic/report/data`,无分页,single_record,获取指定月份的人力成本报表
|
||||
- 入参:`calBizId`(格式 yyyyMM"M",如 "202606M",为空默认当月)
|
||||
- 出参:`data.calBizId` + `data.sumStatisticsData`(成本项数组 JSONB)
|
||||
- 表:`dingtalk_salary_statistic_report`,conflict_key: `calBizId`
|
||||
|
||||
4. **salary_statistic_report_group** — `POST /oapi/salary/statistic/report/groupData`,无分页,single_record
|
||||
- 入参:`calBizId` + `salaryGroupName`(必填),使用 `row_inject` 注入 salaryGroupName
|
||||
- 表:`dingtalk_salary_statistic_report_group`,conflict_keys: `calBizId` + `salaryGroupName`
|
||||
|
||||
### 代码改动
|
||||
- `dynamic_sync.go`:修复主同步路径(非 prefetch)中 `injectRowFields` 未调用的问题
|
||||
- 在 5 处 savePage 调用前添加 injectRowFields:初始页、游标分页循环、hasMore 分页、普通分页(有/无 response_config)
|
||||
- 使 `row_inject` 在单记录(single_record)非 prefetch 接口中也能生效
|
||||
15
.codebuddy/memory/2026-06-08.md
Normal file
15
.codebuddy/memory/2026-06-08.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 2026-06-08 工作日志
|
||||
|
||||
## goview-report 项目初始化
|
||||
- 基于 GoView (gitee.com/bufanyun/go-view-server) 初始化报表引擎项目
|
||||
- 位置: `/Users/xujiaqian/GolandProjects/HDWL/goview-report/`
|
||||
- 改造内容:
|
||||
- 模块名 hotgo → goview-report
|
||||
- MySQL 驱动替换为 PostgreSQL 驱动 (github.com/gogf/gf/contrib/drivers/pgsql/v2)
|
||||
- Go 版本升级到 1.26.0,GoFrame 升级到 v2.10.0
|
||||
- config.yaml 中数据库连接改为 pgsql://postgres:root@127.0.0.1:5432/engine
|
||||
- 端口保持 8090,与 data-engine(:3002) 不冲突
|
||||
- 元数据表前缀 hg_,data-engine 同步表无前缀,同库不同前缀
|
||||
- 创建 Dockerfile + k8s/deployment+service+configmap
|
||||
- 编译验证通过
|
||||
- 与 data-engine 关系: 读写分离,goview-report 只读 PG 中的同步表做报表展示
|
||||
18
.codebuddy/memory/2026-06-09.md
Normal file
18
.codebuddy/memory/2026-06-09.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 2026-06-09 工作日志
|
||||
|
||||
## 公共查询接口开发
|
||||
- 新增 `/public/query` POST 接口,支持:
|
||||
- 字段白名单验证(只允许表定义中声明的字段)
|
||||
- 表名白名单验证(只允许 api_interface 中有 table_definition 的表)
|
||||
- WHERE 条件(支持 `_eq/_ne/_gt/_lt/_ge/_le/_like/_in/_between` 操作符)
|
||||
- GROUP BY / ORDER BY / 分页
|
||||
- 强制 tenant_id = 1 租户隔离
|
||||
- 新增辅助接口:
|
||||
- `GET /public/tables` - 获取可查询表列表
|
||||
- `GET /public/tables/{table}/columns` - 获取表字段列表
|
||||
- `DELETE /public/cache/clear` - 清除表缓存
|
||||
- 文件位置:
|
||||
- `model/dto/public/public_query_dto.go`
|
||||
- `service/public/public_query_service.go`
|
||||
- `controller/public/public_query_controller.go`
|
||||
- 同步修复 `dynamic_sync.go` 中残留的无效代码 "了呢"
|
||||
179
.codebuddy/memory/2026-06-10.md
Normal file
179
.codebuddy/memory/2026-06-10.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# 2026-06-10
|
||||
|
||||
## 通用报表引擎 - 补充前端 CRUD 能力
|
||||
|
||||
为 `common/report` 包补充了配置的 CRUD 接口,使前端可以直接维护报表配置,无需操作数据库。
|
||||
|
||||
### 新增文件变更
|
||||
- `model/model.go`: 新增 SaveBusinessReq/SaveReportReq/SaveFieldReq/SaveExtractConfigReq 等请求结构体,SaveResult/DeleteResult 响应结构体
|
||||
- `config/loader.go`: 新增 Create/Update/Delete + GetByID 方法(Business/Report/Field/ExtractConfig 四种配置)
|
||||
- `api.go`: 新增 SaveBusiness/SaveReport/SaveField/SaveExtractConfig + Delete/Get 接口(新增/修改合一,id 为空则新增)
|
||||
- `example_usage.go`: 新增第五、六节,CRUD 调用示例 + 任意新平台零代码接入流程
|
||||
|
||||
### 关键设计决策
|
||||
- **Save 合一**:通过 `ID *int64` 区分新增(nil)和修改(有值),简化前端调用
|
||||
- **软删除**:status → INACTIVE + deleted_at=NOW(),数据不丢失
|
||||
- **缓存失效**:写操作后自动失效对应业务/报表缓存,保证下次读取一致性
|
||||
- 所有 CRUD 方法先调用 initTables() 确保表存在
|
||||
|
||||
### example_usage.go 完整重写
|
||||
将示例文件从分节的注释风格重写为6个完整场景的 Go 函数示例:
|
||||
1. **快手电商完整接入**(最详细,从业务注册到前端查询6步全流程)
|
||||
2. **已有配置的 CRUD 二次开发参考**(业务/报表/字段/抽取 增删改查)
|
||||
3. **任意平台接入通用模式**(淘宝示例,零硬编码)
|
||||
4. **外部业务服务调用**(goview-report 等直接 import)
|
||||
5. **Direct 模式示例**(不聚合的逐行抽取)
|
||||
6. **前端 HTTP API 交互流程**(伪代码,展示 API 调用顺序)
|
||||
|
||||
每个场景都有实际可运行的代码片段、生成的 SQL 预览、返回值输出示例。
|
||||
|
||||
## 报表引擎管理前端页面
|
||||
|
||||
为报表引擎创建了可直接使用的前端管理页面,访问 `/admin/report`。
|
||||
|
||||
### 新增文件
|
||||
- `controller/report/report_controller.go`: HTTP API Controller,16个端点暴露公共报表服务的全部能力(业务/报表/字段/抽取配置的 CRUD、数据抽取触发、自动建表、用户选择查询)
|
||||
- `controller/report/report_admin_controller.go`: 嵌入式HTML SPA,5个Tab页:
|
||||
- **业务管理**:列表 CRUD
|
||||
- **报表配置**:按业务筛选列表 CRUD
|
||||
- **字段配置**:按业务+报表筛选,维度/指标/筛选角色管理
|
||||
- **抽取配置**:按业务+报表筛选,支持 DIRECT/AGGREGATE 模式配置 + 一键执行抽取
|
||||
- **数据查询**:可视化维度选择器、指标聚合选择器、筛选条件构建器、查询结果展示
|
||||
- `main.go`: 注册 reportCtrl.ReportController 到 RouteRegister + `/admin/report` 页面路由
|
||||
|
||||
### 技术要点
|
||||
- 纯原生 HTML/CSS/JS,零框架依赖,与现有管理后台 `/admin` 风格统一
|
||||
- 所有 CRUD 通过 `ID *int64` 区分新增(nil)/修改(有值)
|
||||
- 查询Tab支持动态字段选择:维度多选、指标聚合方式选择、筛选条件动态构建(支持 BETWEEN/IN 等操作符)
|
||||
- 管理后台页面增加了返回链接 `← 返回管理后台`
|
||||
|
||||
## 修复 report_common_ddl.sql 语法错误
|
||||
|
||||
PostgreSQL 不支持 MySQL 风格的内联 `COMMENT '...'` 语法。修复了全部5张表的列定义:
|
||||
- 移除所有列定义中的内联 `COMMENT 'xxx'`(MySQL 语法)
|
||||
- 已有的 `COMMENT ON COLUMN` 独立语句保留不变
|
||||
- `report_extract_log` 表补充了缺失的 `COMMENT ON COLUMN` 语句(business_code/report_code/extract_code/stat_date/extract_type/status/total_count/success_count/fail_count)
|
||||
|
||||
关键区别:MySQL 支持 `column_name VARCHAR(64) COMMENT 'xxx'`,PostgreSQL 必须用 `COMMENT ON COLUMN table.column IS 'xxx'`。
|
||||
|
||||
## 补充 tenant_id 到报表系统表
|
||||
|
||||
5张报表系统表原先缺失 `tenant_id`,与项目核心表(`init_core_tables.sql`)不一致。修改内容:
|
||||
- **DDL SQL** (`sql/report_common_ddl.sql`):5张表全部新增 `tenant_id BIGINT NOT NULL DEFAULT 0` 列,所有 UNIQUE 约束和索引都加入 tenant_id
|
||||
- **Go initTables** (`common/report/report.go`):同步更新 5 张表的 DDL 定义,加上 tenant_id
|
||||
- 约束变更:`uk_business_report_code UNIQUE (business_code, report_code)` → `(tenant_id, business_code, report_code)`,其余类推
|
||||
|
||||
当前 report 模块租户支持为占位符级别(硬编码 tenant_id=1),后续需从 context 动态获取。
|
||||
- **单独迁移脚本**: `sql/report_add_tenant_id.sql`,对已存在的表执行 ALTER TABLE ADD COLUMN + 重建约束/索引
|
||||
|
||||
## 快手电商报表初始化种子数据
|
||||
|
||||
**初版(已废弃)**:字段名随意捏造,与真实 DDL 不对齐。
|
||||
|
||||
**重写版** `sql/seed_data_report_kuaishou.sql`(基于 seed_data_kuaishou.sql 实际 DDL):
|
||||
- 1 条业务配置 (kuaishou_ecommerce)
|
||||
- 5 条报表配置:order_analysis / item_analysis / refund_analysis / cps_order_analysis / dropshipping_analysis
|
||||
- 76 条字段配置(订单23+商品15+售后12+分销13+代发13),含维度/指标/衍生字段
|
||||
- 5 条抽取配置(AGGREGATE 聚合模式,每个报表一条)
|
||||
|
||||
**严格设计原则**:
|
||||
- sourceField 对齐源表实际列名(camelCase:oid, status, createTime, totalFee, buyerNick 等)
|
||||
- targetField / field_code 使用 snake_case(stat_date, total_fee, buyer_nick 等)
|
||||
- 金额字段源表存分(BIGINT),报表提供衍生元字段(CALCULATED expression / 100.0)
|
||||
- 增量抽取依据统一用 updateTime(毫秒时间戳 BIGINT)
|
||||
- transform_rules:ts_to_date FORMAT createTime → stat_date(yyyy-MM-dd)
|
||||
- 所有 INSERT 使用 ON CONFLICT DO NOTHING 保证幂等
|
||||
|
||||
## 修复报表管理前端编辑不显示数据问题
|
||||
|
||||
在 `report_admin_controller.go` 中修复了编辑弹窗不显示数据的问题:
|
||||
|
||||
**根因**:
|
||||
- GoFrame `MiddlewareHandlerResponse` 包裹响应为 `{code:0, data: <struct>}`,而单条查询返回结构体为 `getBusinessRes{Data: ...}` 序列化为 `{"data": {...}}`,最终响应为 `{code:0, data: {"data": {...}}}` 双层嵌套
|
||||
- 某些 GoFrame 版本可能序列化 Go 字段名(首字母大写 `Data`)而非 json tag(`data`),导致 `res.data` 为 undefined
|
||||
|
||||
**修复内容**:
|
||||
1. `api()` 函数增加兼容逻辑:当 `json.data.Data` 存在而 `json.data.data` 不存在时,自动取 `json.data.Data`
|
||||
2. `api()` 函数增加 `console.log` 调试日志,打印原始响应和解包后的数据
|
||||
3. 四个编辑函数(`openBizModal/openReportModal/openFieldModal/openExtractModal`):
|
||||
- 增加 fallback:`res.data !== undefined ? res.data : res.Data`
|
||||
- 增加数据有效性校验:`if (!d || !d.id)` 时弹出错误提示并阻止打开空表单
|
||||
- 增加 `console.log` 调试日志
|
||||
|
||||
## 修复 report_report_config 查询 SQL 永假条件问题
|
||||
|
||||
**现象**:查询 `report_report_config` 时生成的 SQL 包含 `WHERE (("id" = 1) AND ("id" = 0 AND "business_code" = '' AND ...))`,id 不可能同时为 1 和 0,导致查询始终返回 0 行。
|
||||
|
||||
**根因**:`common/db/gfdb/gfdb.go` 中 `Model()` 方法使用了 `OmitNil()`(只过滤 nil 指针,不过滤零值)。当 GoFrame v2 内部的软删除机制(`deleted_at IS NULL`)自动构建 struct WHERE 条件时,由于 `OmitNil()` 不忽略零值字段,结构体中所有字段的零值(`id=0`, `business_code=""`, `report_code=""` 等)都被加入 WHERE 条件,与显式的 `Where("id", 1)` 叠加形成 `id=1 AND id=0` 的永假条件。
|
||||
|
||||
**修复**:`OmitNil()` **保留**,额外追加 `.OmitEmptyWhere()`,两者同时生效:
|
||||
- `OmitNil()`:保持 INSERT/UPDATE 的 Data() 行为不变(nil 指针/map 字段不写入 SQL,走 DB 默认值),避免破坏数据引擎写入逻辑
|
||||
- `OmitEmptyWhere()`:过滤 struct WHERE 条件中的零值字段,解决软删除 hook 注入零值条件的问题
|
||||
|
||||
最终代码:`OmitNil().OmitEmptyWhere()`
|
||||
|
||||
**影响范围**:`common/db/gfdb/gfdb.go` - 全局 DB Model 初始化。写入行为完全兼容,查询条件不再包含零值 struct 字段。
|
||||
|
||||
**安全性审计(2026-06-10)**:逐文件审查了数据引擎核心同步路径全部 40+ 处 `gfdb.Model()` 调用:
|
||||
- 100% 传字符串表名(非 struct),不会触发 struct WHERE 推断
|
||||
- 100% 使用 `Where("col", val)` 字符串键值对(非 struct/map),`OmitEmptyWhere()` 对此无效
|
||||
- `OmitEmptyWhere()` 不影响 `Data()`/`Insert()`/`Update()`/`Save()`
|
||||
- 结论:`OmitEmptyWhere()` 对数据引擎核心同步逻辑完全无影响,唯一生效场景是 `common/report` 软删除 hook
|
||||
|
||||
## 修复 GetXxxByID 方法零值结构体返回问题
|
||||
|
||||
**现象**:前端编辑报表/业务/字段/抽取配置时一直提示"未获取到XX数据(ID=N)"。与上述 OmitNil 问题同根:SQL 返回 0 行时,GoFrame 的 `One()` 可能不返回 `sql.ErrNoRows`(驱动差异),此时 `GetXxxByID` 判断 `err == nil` 就返回了零值结构体 `{ID: 0, ...}`。Controller 包装为 `{code:0, data:{"data":{id:0}}}`,前端 `!d.id` 判定失败弹出错误提示。
|
||||
|
||||
**修复**:`common/report/config/loader.go` 中全部 7 个 `One()` 调用方法,在 `err` 检查之外增加 `result.IsEmpty()` 检查:
|
||||
- `GetBusinessByID` / `GetReportByID` / `GetFieldByID` / `GetExtractConfigByID`:四者模式一致,`_` 改为 `result` 变量并追加 `if result.IsEmpty()`
|
||||
- `GetBusiness` / `GetReport`(by code)、`GetExtractLog`:同上,追加 `IsEmpty()` 守卫
|
||||
|
||||
**双重防御**:gfdb.go 的 `OmitEmptyWhere()` 是根本修复(消除错误 SQL);loader.go 的 `IsEmpty()` 是兜底守卫(即使 SQL 返回 0 行也不返回零值结构体)。
|
||||
|
||||
## 修复编辑弹窗数据提取的三层兼容
|
||||
|
||||
**根因**:`api()` 返回 `json.data` 给调用者。当 GoFrame `MiddlewareHandlerResponse` 双层包裹时(`{code:0, data: {"data": {...}}}`),`api()` 返回 `{"data": {...}}`,调用者通过 `res.data` 正确提取。但当 GoFrame 单层包裹时(`{code:0, data: {...}}`),`api()` 直接返回 `{...config...}`,调用者 `res.data` 为 undefined → `d` 为 undefined → 触发 "未获取到XX数据" 错误。
|
||||
|
||||
**修复**:4 个编辑函数(`openBizModal`/`openReportModal`/`openFieldModal`/`openExtractModal`)的数据提取改为三层 fallback:
|
||||
```javascript
|
||||
const d = res && res.data !== undefined ? res.data : (res && res.Data !== undefined ? res.Data : res);
|
||||
```
|
||||
- 第一层:`res.data`(双层包裹,json tag 小写)
|
||||
- 第二层:`res.Data`(Go 字段名大写 D)
|
||||
- 第三层:`res` 本身(单层包裹,数据直接在 res 里)
|
||||
|
||||
同时 console.log 增加打印 `raw:` 和 `data:` 便于排查。
|
||||
|
||||
## 修复报表引擎实体缺失 TenantId 字段 → 查询 tenant_id 不匹配返回零值
|
||||
|
||||
**现象**:`GET /report/business?id=1` 返回 `code: 0` 但 data 为全零值 `{id: 0, businessCode: "", ...}`,尽管 DB 中实际有数据。
|
||||
|
||||
**根因**:四个报表引擎实体结构体均缺失 `TenantId` 字段,但 DDL 中表定义有 `tenant_id BIGINT NOT NULL DEFAULT 0`:
|
||||
1. `CreateBusiness` 序列化 `BusinessConfig` 时,JSON 中无 `tenant_id` → insert map 中无 `tenant_id` key
|
||||
2. `insertHook` 检查 `in.Data[i]["tenant_id"]` 是否存在 → 不存在 → 不设置 → DB 用 DEFAULT 0
|
||||
3. `selectHook` 从 `ctxWithUser` 获取 `TenantId=1`,注入 `WHERE tenant_id = 1`
|
||||
4. DB 行 `tenant_id=0` 与查询 `tenant_id=1` 不匹配 → 查不到行 → 返回零值结构体
|
||||
|
||||
**修复**(`common/report/model/model.go`):给 `BusinessConfig`、`ReportConfig`、`FieldConfig`、`ExtractConfig` 四个结构体新增 `TenantId uint64 \`orm:"tenant_id" json:"tenant_id"\``。
|
||||
|
||||
**原理**:添加后序列化 JSON 包含 `"tenant_id": 0`,`insertHook` 检测到 key 存在且为零值,自动赋值为 `userInfo.TenantId`。
|
||||
|
||||
**调试日志**(`common/report/config/loader.go`):`GetBusinessByID` 增加 `g.Log().Infof` 打印 `id`、`err`、`IsEmpty`、`biz.ID`、`biz.BusinessCode`,便于排查后续类似问题。同时补充 import `"github.com/gogf/gf/v2/frame/g"`。
|
||||
|
||||
## 修复 GoFrame One(&struct) 直接映射失效 → 改用两步模式
|
||||
|
||||
**现象**:修复 TenantId 后,SQL 能正确执行并返回1行(`[rows:1]`, `IsEmpty=false`),但 struct 字段全为零值(`biz.ID=0, biz.BusinessCode=""`)。
|
||||
|
||||
**根因**:`loader.go` 中 7 处 `One(&struct)` 直接传结构体指针,在 GoFrame v2.10.0 + `selectHook` 修改 SQL 的组合下,struct 自动映射失效 —— 查询返回 `gdb.Record` 但未 populate 结构体。
|
||||
|
||||
**修复**:全部 7 处改为项目已验证的两步模式(参照 `dao/dict/api_interface_dao.go`):
|
||||
```go
|
||||
// 旧写法(struct 映射失效)
|
||||
result, err := gfdb.DB(ctx).Model(ctx, "table").Where(...).One(&biz)
|
||||
|
||||
// 新写法(先取 Record,再手动 Struct)
|
||||
r, err := gfdb.DB(ctx).Model(ctx, "table").Where(...).One()
|
||||
if err = r.Struct(&biz); err != nil { return nil, err }
|
||||
```
|
||||
|
||||
**影响的方法**:`GetBusiness`、`GetBusinessByID`、`GetReport`、`GetReportByID`、`GetFieldByID`、`GetExtractConfigByID`、`GetExtractLog`。
|
||||
@@ -73,11 +73,15 @@ auth_config: { "app_key": "...", "app_secret": "..." }
|
||||
| `cursor_pagination` | bool | 是否游标分页(true=游标,false/无=普通分页) | 通用 |
|
||||
| `time_field` | string | 增量时间字段名 | 通用 |
|
||||
| `time_field_mode` | string | `"range"`=beginTime/endTime模式(快手),`"filtering"`=filtering数组模式(腾讯,默认) | 通用 |
|
||||
| `full_sync_start_time` | int | 全量同步时的时间起点,毫秒时间戳。`lastSyncTime=0` 时优先读取此值,不存在则回退90天前(range)或不传时间过滤(filtering) | 通用 |
|
||||
| `prefetch` | object | 预取配置(见下方) | 需遍历实体的接口 |
|
||||
| `recursive` | object | 递归遍历配置(见下方) | 树形结构接口(如钉钉部门) |
|
||||
| `max_recursive_depth` | int | 递归最大深度,默认20 | 递归遍历使用 |
|
||||
| `body_wrapper_field` | string | 业务参数包装字段名,如`"param"` | 快手 |
|
||||
| `exclude_from_wrapper` | string[] | body_wrapper 时不包装的字段 | 快手 |
|
||||
| `top_level_params` | string[] | 保留在顶层的字段(备用) | 通用 |
|
||||
| `fields` | string[] | API 的 fields 参数 | 腾讯 |
|
||||
| `row_inject` | string[] | 将请求参数值注入到每行数据中(如 salaryGroupName),解决响应不含某请求参数但需存储的场景 | 通用(prefetch 和非 prefetch 均支持) |
|
||||
|
||||
### prefetch 预取配置
|
||||
```json
|
||||
@@ -198,6 +202,15 @@ WARN 接口 [kuaishou/order_list] 正在同步中,跳过重复请求
|
||||
|
||||
**调度器时序**:使用 `for { runAutoSync(); time.Sleep(interval) }` 模式,而非 `time.NewTicker`。每次同步**完全结束后**才开始计时 interval,避免前一次未跑完就启动下一次导致重叠。无论全量跑 30 分钟还是 70 分钟,下一次都在「本次完成时间 + interval」后执行。
|
||||
|
||||
## 三种遍历模式对比
|
||||
|
||||
| 模式 | 配置 | 适用场景 | 流程 |
|
||||
|------|------|---------|------|
|
||||
| **普通分页** | `page_param` + `page_size_param` | 列表接口分页 | 请求第1页 → 读 `page_info.total_page` → 遍历后续页 |
|
||||
| **游标分页** | `cursor_pagination: true` | 实时滚动列表(快手订单) | 首次 `cursor=""` → 响应返回 `cursor` → 直到 `""` 或 `"nomore"` |
|
||||
| **prefetch 预取** | `prefetch: {url, ...}` | 先取实体列表,再查详情 | 分页拉取实体来源列表 → 提取全部 ID → 并发查详情 |
|
||||
| **recursive 递归** | `recursive: {key_field, target_param}` | 树形结构(钉钉部门) | BFS 队列:根级调用 → 提取子ID → 逐级递归下级 → 防重复 |
|
||||
|
||||
## 新增平台 + 接口的操作步骤
|
||||
|
||||
### 操作步骤
|
||||
@@ -461,6 +474,69 @@ INSERT INTO api_interface (
|
||||
```
|
||||
注意:POST 方法且无 `parameters_location: "query"` 时,参数以 JSON body 形式发送。
|
||||
|
||||
#### 模板 E:OAuth2 + POST + 无分页(钉钉风格)
|
||||
```sql
|
||||
INSERT INTO api_datasource_platform (
|
||||
tenant_id, creator, created_at, updater, updated_at,
|
||||
platform_code, platform_name, description, status,
|
||||
api_base_url, auth_type,
|
||||
auth_config,
|
||||
rate_limit_per_minute, rate_limit_per_hour,
|
||||
concurrency_limit, request_timeout_ms, max_retries, retry_delay_ms
|
||||
) VALUES (
|
||||
1, 'admin', NOW(), 'admin', NOW(),
|
||||
'dingtalk', '钉钉', '钉钉开放平台数据同步', 'ACTIVE',
|
||||
'https://oapi.dingtalk.com', 'OAUTH2',
|
||||
'{
|
||||
"token_in_query": true,
|
||||
"query_key": "access_token"
|
||||
}'::jsonb,
|
||||
60, 3600, 5, 30000, 3, 1000
|
||||
);
|
||||
|
||||
INSERT INTO api_interface (
|
||||
tenant_id, creator, created_at, updater, updated_at,
|
||||
platform_id, name, code, url, method, status, auth_type,
|
||||
request_config, response_config, table_definition
|
||||
) VALUES (
|
||||
1, 'admin', NOW(), 'admin', NOW(),
|
||||
(SELECT id FROM api_datasource_platform WHERE platform_code = 'dingtalk'),
|
||||
'{接口名称}', '{接口编码}',
|
||||
'{相对路径}', 'POST', 'active', 'inherit',
|
||||
'{
|
||||
"parameters_location": "query",
|
||||
"page_param": "cursor",
|
||||
"page_size_param": "pageSize",
|
||||
"{业务参数}": {值}
|
||||
}'::jsonb,
|
||||
'{
|
||||
"success_field": "errcode",
|
||||
"success_value": 0,
|
||||
"message_field": "errmsg",
|
||||
"list_path": "result"
|
||||
}'::jsonb,
|
||||
'{
|
||||
"table_name": "{表名}",
|
||||
"columns": [
|
||||
{"name": "id", "type": "BIGINT", "comment": "ID"},
|
||||
{"name": "name", "type": "VARCHAR(300)", "comment": "名称"}
|
||||
],
|
||||
"conflict_keys": ["id"]
|
||||
}'::jsonb
|
||||
);
|
||||
```
|
||||
注意:钉钉使用 `access_token` 作为 URL 查询参数,通过 `token_in_query: true` + `query_key: "access_token"` 配置。POST 请求通过 `parameters_location: "query"` 将所有参数放在 URL 查询字符串中(DingTalk 同时支持 query 和 body 传参)。
|
||||
|
||||
如需递归遍历树形结构(如部门全量),在 `request_config` 中添加:
|
||||
```json
|
||||
"recursive": {
|
||||
"key_field": "dept_id", // 从响应行中提取哪个字段的值用于递归
|
||||
"target_param": "dept_id" // 递归参数注入到请求中的参数名
|
||||
},
|
||||
"max_recursive_depth": 20 // 可选,最大递归深度,默认20
|
||||
```
|
||||
递归流程:BFS 遍历,先查根级空参 → 提取每行的 `key_field` → 逐级注入 `target_param` 查询下级 → 防重复循环保护 → 所有结果合并入库。
|
||||
|
||||
## 新增接口步骤总结
|
||||
|
||||
1. **确定平台**:已有平台直接跳到第3步,否则先新增平台(参考对应认证类型的模板)
|
||||
@@ -474,6 +550,67 @@ INSERT INTO api_interface (
|
||||
- 是否需要遍历实体(prefetch)
|
||||
- 增量时间字段(字段名 + 值类型)
|
||||
4. **选择模板**并填入对应值
|
||||
|
||||
> ⚠️ **全量同步起始时间**:如果接口数据量很大,首次全量不想从 90 天前拉取,可以在 `request_config` 中加 `"full_sync_start_time": <毫秒时间戳>` 指定起点。例如只同步最近 30 天:
|
||||
> ```json
|
||||
> "full_sync_start_time": 1745942400000
|
||||
> ```
|
||||
> - **range 模式**(快手 `time_field_mode: "range"`):全量时取此时间作为 beginTime
|
||||
> - **filtering 模式**(腾讯 `time_field_mode: "filtering"`):全量时生成 `GREATER_EQUALS` 过滤条件
|
||||
> - **不配此字段**:行为不变(range 回退 90 天,filtering 不传时间过滤)
|
||||
|
||||
5. **执行 seed SQL**
|
||||
6. **在管理后台验证**:`http://{host}:3002/admin`
|
||||
|
||||
## 代码审计发现的项目规范
|
||||
|
||||
### 包命名规范(2026-06-03 审计)
|
||||
- `model/dto/dict/` 下的 `.go` 文件必须用 `package dict`(曾错误写为 `package api_feature`,已修复)
|
||||
- `model/entity/dict/` 下的 `.go` 文件用 `package dict`
|
||||
- `consts/api-feature/` 目录下的文件用 `package api_feature`
|
||||
- `consts/public/` 用 `package public`
|
||||
- **不要**在 `consts/dict/` 中重复定义 `PlatformStatus`/`ApiMethod` 等类型,应使用 `consts/api-feature` 中的定义
|
||||
|
||||
### 数据库状态值规范
|
||||
- `api_datasource_platform.status`:使用大写 `"ACTIVE"` / `"INACTIVE"`
|
||||
- `api_interface.status`:使用小写 `"active"` / `"inactive"`
|
||||
- 代码中查平台状态时不要使用 `api_feature.PlatformStatusActive`(其值为小写),应直接用 `"ACTIVE"` 字符串
|
||||
|
||||
### Known Bad Practices(已知待改进项)
|
||||
- 敏感信息(access_token / app_secret)仍分散在 SQL seed文件和 config.yml 中,建议迁移到环境变量或 Vault
|
||||
- `consts/dict/consts.go` 和 `scheduler/run_sync_task_log_task.go` 可能随业务扩展需要更新
|
||||
|
||||
## 通用报表引擎 (common/report)
|
||||
|
||||
2026-06-10: 通用报表公共包,配置驱动的报表子系统,支持前端自由选择维度/指标/筛选/时间查询。
|
||||
|
||||
### 架构分层 (9个Go文件)
|
||||
```
|
||||
common/report/
|
||||
├── api.go # ReportService 统一门面(单例)
|
||||
├── report.go # 5张系统表 DDL
|
||||
├── example_usage.go # 完整使用示例文档
|
||||
├── model/model.go # 实体/请求/响应/常量
|
||||
├── config/loader.go # 配置加载器(读缓存+CRUD)
|
||||
├── builder/sql_builder.go # 动态 SQL 构建
|
||||
├── executor/executor.go # 查询执行器
|
||||
├── extract/extract.go # 天级数据抽取(DIRECT/AGGREGATE)
|
||||
└── ddlsync/creator.go # 统计宽表自动创建
|
||||
```
|
||||
|
||||
### 核心能力
|
||||
1. **自动建表**:根据 FieldConfig 自动 CREATE TABLE stat_xxx
|
||||
2. **数据抽取**:DIRECT(逐行)/ AGGREGATE(聚合 SUM/COUNT)模式,幂等保证
|
||||
3. **动态查询**:前端选维度+指标+筛选+时间 → 实时构建 SQL → 分页返回
|
||||
4. **配置 CRUD**(2026-06-10 新增):BusinessConfig/ReportConfig/FieldConfig/ExtractConfig 全部可前端维护
|
||||
|
||||
### 对外接口 (ReportService)
|
||||
- 查询:`QueryReportByUserSelect` / `GetReportFields` / `GetAllBusinesses` / `GetAllReports`
|
||||
- 抽取:`ExtractDailyData` / `AutoCreateStatTable`
|
||||
- CRUD:`SaveBusiness/DeleteBusiness/GetBusiness`、`SaveReport/DeleteReport/GetReport`、`SaveField/DeleteField/GetField`、`SaveExtractConfig/DeleteExtractConfig/GetExtractConfig/GetExtractConfigs`
|
||||
|
||||
### 设计原则
|
||||
- 零硬编码:任意平台/接口通过配置接入,不改代码
|
||||
- 配置驱动:5张 PG 表存储全部配置,CRUD API 支持前端维护
|
||||
- 单例模式:`report.GetService()` 可在任意业务服务中直接调用
|
||||
|
||||
|
||||
Reference in New Issue
Block a user