Files
data-engine/.codebuddy/memory/MEMORY.md
2026-06-11 13:06:54 +08:00

27 KiB
Raw Blame History

数据引擎 - 项目记忆 (MEMORY.md)

项目概述

Golang + GoFrame v2 + PostgreSQL 的配置化数据同步引擎。通过 api_datasource_platform + api_interface 两表驱动,支持多平台多接口的数据同步,无需重新编译代码。

核心表结构

sql/init_core_tables.sql

  • api_datasource_platform — 数据源平台配置
  • api_interface — API 接口配置request_config / response_config / table_definition 三大 JSONB
  • sync_tracker — 同步跟踪last_sync_time, sync_status
  • sync_task_log — 同步任务日志(用于补偿重试)

认证类型auth_type模板

1. OAuth2腾讯广告

auth_type: OAUTH2
token: access_token 值
client_id / client_secret: OAuth2 凭证
auth_config: {
  "token_in_query": true,         // token 放 URL 查询参数
  "query_key": "access_token",    // 参数名
  "refresh_token": "xxx",         // 刷新 token
  "extra_query_params": {
    "timestamp": "{timestamp}",
    "nonce": "{nonce}"
  }
}

安全校验:applyAuthHeader 中 OAUTH2/TOKEN 模式会设 Authorization: Bearer {token},但 token_in_query=true 时跳过 Header转加 URL 参数。

2. API_KEY + 签名(快手电商)

auth_type: API_KEY
token/access_token: 调用凭证
auth_config: {
  "sign_algorithm": "md5_upper",       // 签名算法: md5 / md5_upper
  "app_key": "xxx",
  "app_secret": "xxx",
  "token_in_query": true,
  "query_key": "access_token",
  "extra_query_params": {
    "timestamp": "{timestamp_ms}",
    "signMethod": "MD5"
  }
}

签名过程:所有查询参数按 key 排序 → k1=v1&k2=v2&...&key=app_secret → MD5 摘要 → 追加 sign 参数。 {timestamp_ms} 替换为毫秒时间戳,{timestamp} 替换为秒时间戳,{nonce} 替换为随机数。

3. TOKEN简单 Bearer Token

auth_type: TOKEN
token: "xxx"

applyAuthHeaderAuthorization: Bearer {token}

4. SIGN仅签名无额外 token

auth_type: SIGN
auth_config: { "app_key": "...", "app_secret": "..." }

applyAuthHeader 不设特殊 Header仅通过 applySignature 追加签名参数。PlatformManagerauth_config 读取 app_key/app_secret。

request_config 字段完整参考

字段 类型 含义 适用平台
parameters_location string "query"=URL查询参数不设则POST走JSON body 通用
page_param string 分页参数名,默认"page" 通用
page_size_param string 每页条数参数名,默认"page_size" 通用
page_size int 每页条数,覆盖 config.yml 全局默认值需按各平台最大限制设置如快手最大50腾讯100 通用
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 预取配置

"prefetch": {
  "url": "/advertiser/get",        // 预取接口的相对路径
  "method": "GET",                 // HTTP 方法
  "response_path": "data.list",    // 从响应中取列表的 JSON 路径
  "target_param": "account_id",    // 预取值注入到数据接口的参数名
  "value_field": "account_id"      // 从预取列表里取哪个字段的值
}

流程:先分页拉取预取接口 → 提取所有 ID → 逐个 ID 并发拉取数据接口 → 将 ID 注入每行数据。

预取阶段分页:自动适配预取来源接口的分页方式:

  • 游标分页cursor_pagination: true):首次 cursor="",后续从响应 cursor_field 取值,直到 """nomore" 停止
  • 普通分页:读取 page_info.total_page 遍历全部页码

预取阶段参数构建:使用 buildReqBody(prefetchIface) 构建,自动处理 body_wrapper_field 包装param JSON、时间过滤增量时传 beginTime/endTime、分页参数名。响应解析使用 prefetchIface.ResponseConfig,正确识别各种 list_pathdata.orderListdata.itemsdata.cpsOrderList 等)。

预取阶段时间过滤:增量同步时自动复用预取来源接口的 time_field + time_field_mode如果来源支持增量prefetch 会自动带上时间范围只拉增量数据)。

response_config 字段完整参考

字段 类型 含义
success_field string 成功标识字段名,默认"code"
success_value number 成功标识值,默认0
message_field string 错误信息字段名,默认"message"
list_path string 数据列表在 JSON 中的路径,如"data.list""data.orderList"
cursor_field string 游标字段路径,如"data.cursor"
cursor_end_marker string 游标结束标记,如"nomore"(代码中硬判断此字符串)
single_record bool 单记录模式:list_path 指向的路径是单个对象而非数组,会自动包装为单元素数组

响应解析逻辑parseRespExt 函数

  1. 检查 success_field 的值是否等于 success_value | 注意success_value 仅支持数字类型(内部通过 toFloat64 转换),不支持字符串如 "SUCCESS" 或布尔值 true
  2. list_path 遍历到数据列表数组 | 支持路径如 data.orderList,最后一段是数组 OK代码有兼容处理
  3. 每条数据调用 flattenRow 展平嵌套 fieldorderBaseInfo.oidoid
  4. 提取 cursor_field 用于下一页,为空或等于 cursor_end_marker 时停止循环
  5. page_info.total_page 获取总页数(普通分页用)

游标分页首次请求:代码自动传 cursor=""(首页不传游标),由服务端返回第一页数据 + 下一页游标值。

单记录模式"single_record": true 时,list_path 指向单个对象(如 "data"),自动包装成单元素数组用于统一处理。

table_definition 字段完整参考

字段 类型 含义
table_name string 目标表名,如"kuaishou_order_list"
columns array 列定义数组,每个元素 {"name":"字段名","type":"PG类型","comment":"说明"}
conflict_keys string[] upsert 冲突键,如["oid"]["image_id","account_id"]

注意:

  • 列名必须和 API 响应中的字段名完全一致camelCase/snake_case 取决于 API
  • 嵌套字段会被 flattenRow 自动展平到顶层
  • raw_data 列自动包含,存储完整原始 JSON
  • 自动建表时会额外添加 id/tenant_id/creator/created_at/updater/updated_at/deleted_at 审计字段
  • 必须写全columns 中需列出 API 响应中所有平铺的标量字段(字符串/数字/布尔),数组和嵌套对象字段保留在 raw_data 中即可

同步策略原则

  • API 支持时间过滤(有 beginTime/endTime 或 filtering 参数)→ 配 time_field + time_field_mode,实现增量同步
  • API 不支持时间过滤 → 不配 time_field,走全量同步
  • 详情/子接口(通过 prefetch 遍历prefetch 阶段自动复用预取来源接口的 time_field_mode如果来源接口支持增量prefetch 也会带上时间过滤只拉增量数据)

全量同步流程

SyncByConfig(platformCode, interfaceCode, isFullSync=false)
  │
  ├─ getLastSyncTime() → 无记录返回 0
  │   lastSyncTime=0 → 全量
  │
  ├─ markSyncRunning() 标记 running
  │
  ├─ buildReqBody(page=1, cursor="", lastSyncTime=0)
  │   ├─ 游标分页: cursor="" 首次空游标
  │   ├─ range 模式: beginTime=90天前, endTime=now
  │   └─ filtering 模式: 不添加时间过滤
  │
  ├─ API → parseRespExt → flattenRow → savePage(upsert)
  │
  └─ 游标循环 → 直到 cursor="" 或 "nomore"
       updateSyncTime() 记录最大时间戳

增量同步流程

SyncByConfig(platformCode, interfaceCode, isFullSync=false)
  │
  ├─ getLastSyncTime() → 返回上次 maxTime
  │   lastSyncTime>0 → 增量
  │
  ├─ buildReqBody(cursor="", lastSyncTime=xxx)
  │   ├─ range 模式: beginTime=lastSyncTime, endTime=now
  │   └─ filtering 模式: filtering=[{field=time_field, operator=GREATER_EQUALS, values=[lastSyncTime]}]
  │
  └─ 后续同全量(只拉取时间范围内的数据)

并发保护

内存锁(同一进程内)

SyncByConfig 入口使用 sync.MapsyncRunningMap)记录正在执行的接口。若同一接口的同步请求再次到达(如调度器重叠),直接跳过并打印警告:

WARN 接口 [kuaishou/order_list] 正在同步中,跳过重复请求

无论接口类型prefetch/非prefetch、腾讯/快手、现有/新增)均自动生效。

异常中断检测DB 状态)

如果 sync_tracker.sync_status = "running",说明上次同步异常中断(进程崩溃),自动回退全量。

补偿机制

compensation.go 定时扫描 sync_task_log 中 status="failed" 的任务,自动调用 SyncByConfig 重试增量支持指数退避5, 15, 30, 60, 120 分钟)。

自动同步调度

sync_scheduler.gorunAutoSync 遍历所有 ACTIVE 平台下的所有配置了 table_definition 的活跃接口,自动调用同步。

调度器时序:使用 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 → 逐级递归下级 → 防重复

新增平台 + 接口的操作步骤

操作步骤

  1. sql/ 下创建 seed_data_{平台编码}.sql
  2. 先执行 sql/init_core_tables.sql(仅首次需要)
  3. 执行新建的 seed SQL 文件INSERT 平台 → INSERT 接口)
  4. 配置认证凭据token / app_key / app_secret 等)
  5. 重启服务,管理后台验证:http://localhost:3002/admin

种子 SQL 标准模板

模板 AOAuth2 + 普通分页(腾讯广告风格)

-- =============================================
-- 1. 创建 {平台编码} 平台
-- =============================================
INSERT INTO api_datasource_platform (
    tenant_id, creator, created_at, updater, updated_at,
    platform_code, platform_name, description, status,
    api_base_url, auth_type,
    token, client_id, client_secret,
    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(),
    '{平台编码}', '{平台名称}', '{描述}', 'ACTIVE',
    '{API_BASE_URL}', 'OAUTH2',
    '{access_token}', '{client_id}', '{client_secret}',
    '{
        "token_in_query": true,
        "query_key": "access_token",
        "refresh_token": "{refresh_token}",
        "extra_query_params": {
            "timestamp": "{timestamp}",
            "nonce": "{nonce}"
        }
    }'::jsonb,
    60, 3600, 10, 30000, 3, 1000
);

-- 2. 创建 {接口名} 接口(普通分页 + 增量 filtering
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 = '{平台编码}'),
    '{接口名称}', '{接口编码}',
    '{相对路径}', 'GET', 'active', 'inherit',
    '{
        "parameters_location": "query",
        "page": 1,
        "page_size": 100,
        "page_param": "page",
        "page_size_param": "page_size",
        "pagination_mode": "PAGINATION_MODE_NORMAL",
        "time_field": "last_modified_time",
        "fields": ["field1", "field2"]
    }'::jsonb,
    '{
        "success_field": "code",
        "success_value": 0,
        "message_field": "message",
        "list_path": "data.list"
    }'::jsonb,
    '{
        "table_name": "{表名}",
        "columns": [
            {"name": "id", "type": "BIGINT", "comment": "主键"},
            {"name": "name", "type": "VARCHAR(200)", "comment": "名称"},
            {"name": "created_time", "type": "BIGINT", "comment": "创建时间"}
        ],
        "conflict_keys": ["id"]
    }'::jsonb
);

模板 BOAuth2 + prefetch 预取(遍历账户拉取子数据,腾讯广告风格)

-- 预取接口(先拉取实体列表)
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 = '{平台编码}'),
    '{实体列表名称}', '{实体编码}',
    '{实体列表相对路径}', 'GET', 'active', 'inherit',
    '{
        "parameters_location": "query",
        "page": 1,
        "page_size": 100,
        "page_param": "page",
        "page_size_param": "page_size",
        "fields": ["id", "name"]
    }'::jsonb,
    '{
        "success_field": "code",
        "success_value": 0,
        "list_path": "data.list"
    }'::jsonb,
    '{
        "table_name": "{实体表名}",
        "columns": [
            {"name": "id", "type": "BIGINT", "comment": "实体ID"},
            {"name": "name", "type": "VARCHAR(200)", "comment": "实体名称"}
        ],
        "conflict_keys": ["id"]
    }'::jsonb
);

-- 数据接口(遍历每个实体拉取数据)
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 = '{平台编码}'),
    '{数据接口名称}', '{数据接口编码}',
    '{数据接口相对路径}', 'GET', 'active', 'inherit',
    '{
        "parameters_location": "query",
        "page": 1,
        "page_size": 100,
        "page_param": "page",
        "page_size_param": "page_size",
        "time_field": "last_modified_time",
        "prefetch": {
            "url": "{实体列表相对路径}",
            "method": "GET",
            "response_path": "data.list",
            "target_param": "{参数名}",
            "value_field": "{取值字段}"
        }
    }'::jsonb,
    '{
        "success_field": "code",
        "success_value": 0,
        "list_path": "data.list"
    }'::jsonb,
    '{
        "table_name": "{数据表名}",
        "columns": [
            {"name": "id", "type": "BIGINT", "comment": "ID"},
            {"name": "{参数名}", "type": "BIGINT", "comment": "实体ID预取注入"},
            {"name": "created_time", "type": "BIGINT", "comment": "创建时间"}
        ],
        "conflict_keys": ["id", "{参数名}"]
    }'::jsonb
);

模板 CAPI_KEY + MD5 签名 + 游标分页 + param 包装(快手风格)

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(),
    '{平台编码}', '{平台名称}', '{描述}', 'ACTIVE',
    '{API_BASE_URL}', 'API_KEY',
    '{
        "sign_algorithm": "md5_upper",
        "app_key": "{YOUR_APP_KEY}",
        "app_secret": "{YOUR_APP_SECRET}",
        "token_in_query": true,
        "query_key": "access_token",
        "extra_query_params": {
            "timestamp": "{timestamp_ms}",
            "signMethod": "MD5"
        }
    }'::jsonb,
    100, 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 = '{平台编码}'),
    '{接口名称}', '{接口编码}',
    '{相对路径}', 'GET', 'active', 'inherit',
    '{
        "parameters_location": "query",
        "page_param": "cursor",
        "page_size_param": "pageSize",
        "cursor_pagination": true,
        "method": "{API方法名}",
        "version": 1,
        "signMethod": "MD5",
        "pageSize": 50,
        "time_field": "updateTime",
        "time_field_mode": "range",
        "body_wrapper_field": "param",
        "exclude_from_wrapper": ["method", "version", "signMethod"],
        "{其他业务参数}": {值}
    }'::jsonb,
    '{
        "success_field": "result",
        "success_value": 1,
        "list_path": "data.{数组字段名}",
        "cursor_field": "data.cursor",
        "cursor_end_marker": "nomore"
    }'::jsonb,
    '{
        "table_name": "{表名}",
        "columns": [
            {"name": "oid", "type": "BIGINT", "comment": "订单ID"},
            {"name": "createTime", "type": "BIGINT", "comment": "创建时间"},
            {"name": "updateTime", "type": "BIGINT", "comment": "更新时间"},
            {"name": "{字段名}", "type": "{类型}", "comment": "{说明}"}
        ],
        "conflict_keys": ["oid"]
    }'::jsonb
);

模板 DPOST + JSON Body腾讯音频风格无 prefetch无增量

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 = '{平台编码}'),
    '{接口名称}', '{接口编码}',
    '{相对路径}', 'POST', 'active', 'inherit',
    '{
        "page": 1,
        "page_size": 100,
        "page_param": "page",
        "page_size_param": "page_size",
        "fields": ["field1", "field2"]
    }'::jsonb,
    '{
        "success_field": "code",
        "success_value": 0,
        "message_field": "message",
        "list_path": "data.list"
    }'::jsonb,
    '{
        "table_name": "{表名}",
        "columns": [
            {"name": "id", "type": "BIGINT", "comment": "ID"},
            {"name": "name", "type": "VARCHAR(200)", "comment": "名称"}
        ],
        "conflict_keys": ["id"]
    }'::jsonb
);

注意POST 方法且无 parameters_location: "query" 时,参数以 JSON body 形式发送。

模板 EOAuth2 + POST + 无分页(钉钉风格)

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 中添加:

"recursive": {
    "key_field": "dept_id",    // 从响应行中提取哪个字段的值用于递归
    "target_param": "dept_id"  // 递归参数注入到请求中的参数名
},
"max_recursive_depth": 20      // 可选最大递归深度默认20

递归流程BFS 遍历,先查根级空参 → 提取每行的 key_field → 逐级注入 target_param 查询下级 → 防重复循环保护 → 所有结果合并入库。

新增接口步骤总结

  1. 确定平台已有平台直接跳到第3步否则先新增平台参考对应认证类型的模板

  2. 确定平台 auth_typeOAUTH2 / TOKEN / API_KEY / SIGN → 复制对应模板的 auth_config

  3. 分析 API

    • 请求方式GET/POST
    • 分页方式(普通分页 → page + total_page / 游标分页 → cursor + cursor_end
    • 成功标识code=0 / result=1 / errcode=0 等)
    • 数据列表路径data.list / data.orderList / data.records 等)
    • 数据字段名(注意大小写,嵌套结构会被展开)
    • 是否需要遍历实体prefetch
    • 增量时间字段(字段名 + 值类型)
  4. 选择模板并填入对应值

    ⚠️ 全量同步起始时间:如果接口数据量很大,首次全量不想从 90 天前拉取,可以在 request_config 中加 "full_sync_start_time": <毫秒时间戳> 指定起点。例如只同步最近 30 天:

    "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.goscheduler/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. 配置 CRUD2026-06-10 新增BusinessConfig/ReportConfig/FieldConfig/ExtractConfig 全部可前端维护

对外接口 (ReportService)

  • 查询:QueryReportByUserSelect / GetReportFields / GetAllBusinesses / GetAllReports
  • 抽取:ExtractDailyData / AutoCreateStatTable
  • CRUDSaveBusiness/DeleteBusiness/GetBusinessSaveReport/DeleteReport/GetReportSaveField/DeleteField/GetFieldSaveExtractConfig/DeleteExtractConfig/GetExtractConfig/GetExtractConfigs

设计原则

  • 零硬编码:任意平台/接口通过配置接入,不改代码
  • 配置驱动5张 PG 表存储全部配置CRUD API 支持前端维护
  • 单例模式:report.GetService() 可在任意业务服务中直接调用