@@ -17,6 +17,9 @@ import (
"github.com/sirupsen/logrus"
)
// syncRunningMap 防止同一个接口被并发执行同步
var syncRunningMap sync . Map
// SyncResult 同步结果
type SyncResult struct {
TableName string
@@ -37,6 +40,14 @@ type PrefetchConfig struct {
// SyncByConfig 执行同步
func SyncByConfig ( ctx context . Context , platformCode , interfaceCode string , isFullSync bool ) ( * SyncResult , error ) {
// 内存锁:防止同一个接口被并发执行(两个调度周期重叠时跳过)
lockKey := platformCode + "/" + interfaceCode
if _ , loaded := syncRunningMap . LoadOrStore ( lockKey , true ) ; loaded {
logrus . Warnf ( "接口 [%s] 正在同步中,跳过重复请求" , lockKey )
return nil , fmt . Errorf ( "接口 [%s] 正在同步中,跳过" , lockKey )
}
defer syncRunningMap . Delete ( lockKey )
start := time . Now ( )
pm := & PlatformManager { }
@@ -87,7 +98,7 @@ func SyncByConfig(ctx context.Context, platformCode, interfaceCode string, isFul
if prefetch != nil {
return syncWithPrefetch ( ctx , api , platform , iface , ifaces , td , prefetch , isFullSync , lastSyncTime , start )
}
return syncSingleAPI ( ctx , api , platform , iface , td , lastSyncTime , start )
return syncSingleAPI ( ctx , api , platform , iface , td , isFullSync , lastSyncTime , start )
}
// paramsInQuery 判断参数是否应放在 URL 查询字符串中
@@ -104,24 +115,39 @@ func paramsInQuery(iface *entity.ApiInterface) bool {
}
// syncSingleAPI 单接口分页同步
func syncSingleAPI ( ctx context . Context , api * ApiClient , platform * PlatformConfig , iface * entity . ApiInterface , td * TableDefinition , lastSyncTime int64 , start time . Time ) ( * SyncResult , error ) {
func syncSingleAPI ( ctx context . Context , api * ApiClient , platform * PlatformConfig , iface * entity . ApiInterface , td * TableDefinition , isFullSync bool , lastSyncTime int64 , start time . Time ) ( * SyncResult , error ) {
pageSize := GetSyncPageSize ( ctx )
if ps , ok := iface . RequestConfig [ "page_size" ] . ( float64 ) ; ok {
pageSize = int ( ps )
}
taskType := "incremental"
if isFullSync || lastSyncTime <= 0 {
taskType = "full"
}
inQuery := paramsInQuery ( iface )
method := string ( iface . Method )
body := buildReqBody ( iface , 1 , pageSize , lastSyncTime , nil )
// 游标分页首次请求需要 cursor=""(通过 extraParams 覆盖 buildReqBody 的 page=1 赋值)
firstExtra := map [ string ] interface { } { }
if isCursorPagination ( iface ) {
cp := "cursor"
if p , ok := iface . RequestConfig [ "page_param" ] . ( string ) ; ok && p != "" {
cp = p
}
firstExtra [ cp ] = ""
}
body := buildReqBody ( iface , 1 , pageSize , lastSyncTime , firstExtra )
resp , err := api . Request ( ctx , method , iface . Url , body , inQuery )
if err != nil {
recordFailure ( ctx , platform . PlatformCode , iface . Code , err . Error ( ) )
recordFailure ( ctx , platform . PlatformCode , iface . Code , taskType , err . Error ( ) )
return nil , fmt . Errorf ( "获取第一页失败: %w" , err )
}
rows , totalPages , maxTime , err := parseResp ( resp . Body , iface . ResponseConfig )
rows , totalPages , maxTime , nextCursor , err := parseRespExt ( resp . Body , iface . ResponseConfig )
if err != nil {
recordFailure ( ctx , platform . PlatformCode , iface . Code , taskType , fmt . Sprintf ( "解析第一页响应失败: %v" , err ) )
return nil , err
}
@@ -130,24 +156,68 @@ func syncSingleAPI(ctx context.Context, api *ApiClient, platform *PlatformConfig
result . InsertedRows += inserted
result . TotalRows += len ( rows )
for page := 2 ; page <= totalPages ; page ++ {
body := buildReqBody ( iface , page , pageSize , lastSyncTime , nil )
resp , er r : = api . Request ( ctx , method , iface . Url , body , inQuery )
if err ! = nil {
logrus . Errorf ( "第 %d 页失败: %v" , page , err )
continue
// 游标分页
if isCursorPagination ( iface ) {
for nextCurso r ! = "" && nextCursor != "nomore" {
cp : = "cursor"
if p , ok := iface . RequestConfig [ "page_param" ] . ( string ) ; ok && p != "" {
cp = p
}
body := buildReqBody ( iface , 1 , pageSize , lastSyncTime , map [ string ] interface { } {
cp : nextCursor ,
} )
resp , err := api . Request ( ctx , method , iface . Url , body , inQuery )
if err != nil {
logrus . Errorf ( "游标 %s 请求失败: %v" , nextCursor , err )
recordFailure ( ctx , platform . PlatformCode , iface . Code , taskType , fmt . Sprintf ( "游标 %s 请求失败: %v" , nextCursor , err ) )
break
}
rows , _ , mt , nc , pe := parseRespExt ( resp . Body , iface . ResponseConfig )
if pe != nil {
logrus . Errorf ( "游标 %s 解析失败: %v" , nextCursor , pe )
recordFailure ( ctx , platform . PlatformCode , iface . Code , taskType , fmt . Sprintf ( "游标 %s 解析失败: %v" , nextCursor , pe ) )
break
}
if len ( rows ) == 0 {
break
}
nextCursor = nc
inserted , _ = savePage ( ctx , td , rows )
result . InsertedRows += inserted
result . TotalRows += len ( rows )
if mt > maxTime {
maxTime = mt
}
result . TotalPages ++
time . Sleep ( 100 * time . Millisecond )
}
rows , _ , mt , err := parseResp ( resp . Body , iface . ResponseConfig )
if err != nil {
continue
} else {
// 普通分页
for page := 2 ; page <= totalPages ; page ++ {
body := buildReqBody ( iface , page , pageSize , lastSyncTime , nil )
resp , err := api . Request ( ctx , method , iface . Url , body , inQuery )
if err != nil {
logrus . Errorf ( "第 %d 页请求失败: %v" , page , err )
recordFailure ( ctx , platform . PlatformCode , iface . Code , taskType , fmt . Sprintf ( "第 %d 页请求失败: %v" , page , err ) )
continue
}
rows , _ , mt , _ , pe := parseRespExt ( resp . Body , iface . ResponseConfig )
if pe != nil {
logrus . Errorf ( "第 %d 页解析失败: %v" , page , pe )
recordFailure ( ctx , platform . PlatformCode , iface . Code , taskType , fmt . Sprintf ( "第 %d 页解析失败: %v" , page , pe ) )
continue
}
inserted , _ = savePage ( ctx , td , rows )
result . InsertedRows += inserted
result . TotalRows += len ( rows )
if mt > maxTime {
maxTime = mt
}
time . Sleep ( 100 * time . Millisecond )
}
inserted , _ = savePage ( ctx , td , rows )
result . InsertedRows += inserted
result . TotalRows += len ( rows )
if mt > maxTime {
maxTime = mt
}
time . Sleep ( 100 * time . Millisecond )
}
if maxTime <= 0 {
@@ -160,84 +230,150 @@ func syncSingleAPI(ctx context.Context, api *ApiClient, platform *PlatformConfig
return result , nil
}
func isCursorPagination ( iface * entity . ApiInterface ) bool {
if iface . RequestConfig == nil {
return false
}
cp , _ := iface . RequestConfig [ "cursor_pagination" ] . ( bool )
return cp
}
// collectPrefetchEntities 从 rows 中收集实体和行数据
func collectPrefetchEntities ( rows [ ] map [ string ] interface { } , prefetch * PrefetchConfig , allEntities * [ ] interface { } , allRows * [ ] map [ string ] interface { } ) {
for _ , item := range rows {
* allRows = append ( * allRows , item )
if prefetch . ValueField == "" {
* allEntities = append ( * allEntities , item )
} else if v , ok := item [ prefetch . ValueField ] ; ok {
if f , ok := v . ( float64 ) ; ok {
* allEntities = append ( * allEntities , int64 ( f ) )
} else {
* allEntities = append ( * allEntities , v )
}
}
}
}
// syncWithPrefetch 预取模式同步(先分页拉取全部实体列表,再并发处理每个实体)
func syncWithPrefetch ( ctx context . Context , api * ApiClient , platform * PlatformConfig , iface * entity . ApiInterface , allIfaces [ ] entity . ApiInterface , td * TableDefinition , prefetch * PrefetchConfig , isFullSync bool , lastSyncTime int64 , start time . Time ) ( * SyncResult , error ) {
logrus . Infof ( "预取模式: %s -> %s" , prefetch . URL , iface . Url )
// 1. 查找匹配 prefetch URL 的接口配置(用于获取正确的请求参数)
taskType := "incremental"
if isFullSync || lastSyncTime <= 0 {
taskType = "full"
}
// ====== 1. 预取阶段:分页拉取全部实体列表 ======
prefetchIface := findInterfaceByURL ( allIfaces , prefetch . URL )
prefetchParams := buildPrefetchParams ( iface )
// 判断预取来源是否游标分页,以及分页参数名
prefetchIsCursor := false
prefetchPageParam := "page"
if prefetchIface != nil && prefetchIface . RequestConfig != nil {
// 使用 prefetch 目标接口的 r equest_c onfig 重建参数(覆盖默认值)
for k , v := range prefetchIface . RequestConfig {
if k == "headers" || k == "prefetch" || k == "page_param" ||
k = = "page_size_ param" || k == "time_field" || k = = "parameters_location " ||
k == "filtering" || k == "group_by" || k == "date_range" {
continue
}
prefetchParams [ k ] = v
if cp , ok := prefetchIface . R equestC onfig[ "cursor_pagination" ] . ( bool ) ; ok {
prefetchIsCursor = cp
}
if p , o k : = prefetchIface . RequestConfig [ "page_param" ] . ( string ) ; ok && p ! = "" {
prefetchPageParam = p
}
}
method := strings . ToUpper ( prefetch . Method )
inQuery := paramsInQu ery ( iface )
prefetchMethod := strings . ToUpp er( prefetch . Method )
prefetchPageSize := 100
if prefetchIface != nil && prefetchIface . RequestConfig != nil {
if ps , ok := prefetchIface . RequestConfig [ "pageSize" ] . ( float64 ) ; ok {
prefetchPageSize = int ( ps )
}
}
// 使用 prefetch 来源接口自己的配置判断参数位置
var prefetchInQuery bool
if prefetchIface != nil {
prefetchInQuery = paramsInQuery ( prefetchIface )
} else {
prefetchInQuery = paramsInQuery ( iface )
}
// prefetch 来源接口的 response_config( 用于正确解析列表路径)
var prefetchRespCfg map [ string ] interface { }
if prefetchIface != nil {
prefetchRespCfg = prefetchIface . ResponseConfig
}
allEntities := make ( [ ] interface { } , 0 )
allRows := make ( [ ] map [ string ] interface { } , 0 )
prefetchPage := 1
prefetchTotalPages := 1
for prefetchPage <= prefetchTotalPages {
params := make ( map [ string ] interface { } )
for k , v := range prefetchParams {
params [ k ] = v
}
pageParam := "page"
if p, ok := iface . RequestConfig [ "page_param" ] . ( string ) ; ok {
pageParam = p
}
params [ pageParam ] = prefetchPage
// 第一页(游标分页首次 cursor="")
firstExtra := make ( map [ string ] interface { } )
i f prefetchIsCursor {
firstExtra [ prefetchPageParam ] = ""
}
prefetchReqIface := prefetchIface
if prefetchReqIface == nil {
prefetchReqIface = iface
}
body := buildReqBody ( prefetchReqIface , 1 , prefetchPageSize , lastSyncTime , firstExtra )
resp , err := api . Request ( ctx , prefetchMethod , prefetch . URL , body , prefetchInQuery )
if err != nil {
recordFailure ( ctx , platform . PlatformCode , iface . Code , taskType , fmt . Sprintf ( "预取第一页请求失败: %v" , err ) )
return nil , fmt . Errorf ( "预取第一页失败: %w" , err )
}
resp , err := api . Requ est ( ctx , method , prefetch . URL , params , true )
if err != nil {
return nil , fmt . Error f( "预取第 %d 页 失败: %w " , prefetchPage , err)
}
rows , prefetchTotalPag es, _ , nextCursor , err := parseRespExt ( resp . Body , prefetchRespCfg )
if err != nil {
recordFailure ( ctx , platform . PlatformCode , iface . Code , taskType , fmt . Sprint f( "解析预取响应 失败: %v " , err) )
return nil , fmt . Errorf ( "解析预取响应失败: %w" , err )
}
collectPrefetchEntities ( rows , prefetch , & allEntities , & allRows )
entities , _ , _ , err := parseResp ( resp . Body , nil )
if err != nil {
return nil , fmt . Errorf ( "解析预取第 %d 页响应失败: %w" , prefetchPage , err )
}
// 收集完整数据行(用于存库)和提取的 ID 值(用于遍历)
for _ , item := range entities {
allRows = append ( allRows , item )
if prefetch . ValueField = = "" {
allEntities = append ( allEntities , item )
} else if v , ok := item [ prefetch . ValueField ] ; ok {
// 将 float64 转 int64, 避免后续 URL 参数中出现科学计数法
if f , ok := v . ( float64 ) ; ok {
allEntities = append ( allEntities , int64 ( f ) )
} else {
allEntities = append ( allEntities , v )
}
}
}
if prefetchPage == 1 {
if tp := getTotalPages ( resp . Body ) ; tp > 0 {
prefetchTotalPages = tp
} else {
// 分页循环
if prefetchIsCursor {
// 游标分页
for nextCursor != "" && nextCursor != "nomore" {
body := buildReqBody ( prefetchReqIface , 1 , prefetchPageSize , lastSyncTime , map [ string ] interface { } {
prefetchPageParam : nextCursor ,
} )
resp , err : = api . Request ( ctx , prefetchMethod , prefetch . URL , body , prefetchInQuery )
if err ! = nil {
logrus . Errorf ( "预取游标 %s 请求失败: %v" , nextCursor , err )
break
}
rows , _ , _ , nc , pe := parseRespExt ( resp . Body , prefetchRespCfg )
if pe != nil {
logrus . Errorf ( "预取游标 %s 解析失败: %v" , nextCursor , pe )
break
}
if len ( rows ) == 0 {
break
}
nextCursor = nc
collectPrefetchEntities ( rows , prefetch , & allEntities , & allRows )
time . Sleep ( 100 * time . Millisecond )
}
} else {
// 普通分页
for page := 2 ; page <= prefetchTotalPages ; page ++ {
body := buildReqBody ( prefetchReqIface , page , prefetchPageSize , lastSyncTime , nil )
resp , err := api . Request ( ctx , prefetchMethod , prefetch . URL , body , prefetchInQuery )
if err != nil {
logrus . Errorf ( "预取第 %d 页请求失败: %v" , page , err )
continue
}
rows , _ , _ , _ , pe := parseRespExt ( resp . Body , prefetchRespCfg )
if pe != nil {
logrus . Errorf ( "预取第 %d 页解析失败: %v" , page , pe )
continue
}
collectPrefetchEntities ( rows , prefetch , & allEntities , & allRows )
time . Sleep ( 100 * time . Millisecond )
}
prefetchPage ++
time . Sleep ( 50 * time . Millisecond )
}
if len ( allEntities ) == 0 {
logrus . Warn ( "预取结果为空列表,跳过同步" )
return & SyncResult { TableName : td . TableName , Duration : fmt . Sprintf ( "%.1fs" , time . Since ( start ) . Seconds ( ) ) } , nil
}
logrus . Infof ( "预取到 %d 个实体(共 %d 页) " , len ( allEntities ) , prefetchPage - 1 )
logrus . Infof ( "预取到 %d 个实体" , len ( allEntities ) )
// 2. 将预取的数据也存入库(如账户列表存入 tencent_account_relation)
if prefetchIface != nil && prefetchIface . TableDefinition != nil {
@@ -258,6 +394,7 @@ func syncWithPrefetch(ctx context.Context, api *ApiClient, platform *PlatformCon
}
dataMethod := string ( iface . Method )
inQuery := paramsInQuery ( iface )
concurrency := GetSyncConcurrency ( ctx )
var mu sync . Mutex
@@ -346,21 +483,24 @@ func syncWithPrefetch(ctx context.Context, api *ApiClient, platform *PlatformCon
// getTotalPages 从响应中提取总页数
func getTotalPages ( raw [ ] byte ) int {
var r struct {
Data map [ string ] interface { } ` json:"data" `
}
if err := json . Unmarshal ( raw , & r ) ; err != nil {
rows , tp , _ , _ , err := parseRespExt ( raw , nil )
if err != nil || len ( rows ) == 0 {
return 0
}
if r . Data == nil {
return 0
return tp
}
func toFloat64 ( v interface { } ) ( float64 , bool ) {
switch val := v . ( type ) {
case float64 :
return val , true
case int :
return float64 ( val ) , true
case int64 :
return float64 ( val ) , true
default :
return 0 , false
}
if pi , ok := r . Data [ "page_info" ] . ( map [ string ] interface { } ) ; ok {
if tp , ok := pi [ "total_page" ] . ( float64 ) ; ok {
return int ( tp )
}
}
return 0
}
// buildPrefetchParams 构建预取接口的请求参数
@@ -382,7 +522,9 @@ func buildPrefetchParams(iface *entity.ApiInterface) map[string]interface{} {
for k , v := range iface . RequestConfig {
if k == "headers" || k == "prefetch" || k == "page_param" ||
k == "page_size_param" || k == "time_field" || k == "parameters_location" ||
k == "filtering" || k == "group_by" || k == "date_range" {
k == "filtering" || k == "group_by" || k == "date_range" ||
k == "body_wrapper_field" || k == "exclude_from_wrapper" ||
k == "cursor_pagination" || k == "time_field_mode" {
continue
}
if k == pageParam || k == psParam {
@@ -467,7 +609,10 @@ func buildReqBody(iface *entity.ApiInterface, page, pageSize int, lastSyncTime i
if iface . RequestConfig != nil {
for k , v := range iface . RequestConfig {
if k == "time_field" || k == "headers" || k == "prefetch" ||
k == "page_param" || k == "page_size_param" || k == "parameters_location" {
k == "page_param" || k == "page_size_param" || k == "parameters_location" ||
k == "cursor_pagination" || k == "time_field_mode" ||
k == "body_wrapper_field" || k == "exclude_from_wrapper" ||
k == "top_level_params" {
continue
}
body [ k ] = v
@@ -485,16 +630,33 @@ func buildReqBody(iface *entity.ApiInterface, page, pageSize int, lastSyncTime i
}
body [ pageParam ] = page
body [ psParam ] = pageSize
// 增量同步:将 time_field 转为 API 期望的 filtering 格式
// 如 filtering=[{"field":"last_modified_time","operator":"GREATER_EQUALS","values":["1780037982"]}]
if lastSyncTime > 0 {
if tf , ok := iface . RequestConfig [ "time_field" ] . ( string ) ; ok && tf != "" {
// 时间过滤处理:支持两种模式
// 1. "filtering" 模式(默认):生成 filtering=[{"field":"...","operator":"GREATER_EQUALS","values":["..."]}](腾讯)
// 2. "range" 模式:生成 beginTime/endTime + queryType( 快手)
if tf , ok := iface . RequestConfig [ "time_field" ] . ( string ) ; ok && tf != "" {
timeMode := "filtering"
if tm , ok := iface . RequestConfig [ "time_field_mode" ] . ( string ) ; ok && tm != "" {
timeMode = tm
}
if timeMode == "range" {
// 快手模式: beginTime/endTime( 毫秒时间戳)
timeMs := lastSyncTime
if timeMs <= 0 {
// 全量: 默认90天前
timeMs = time . Now ( ) . Add ( - 90 * 24 * time . Hour ) . UnixMilli ( )
}
body [ "queryType" ] = 2
body [ "beginTime" ] = timeMs
body [ "endTime" ] = time . Now ( ) . UnixMilli ( )
} else if lastSyncTime > 0 {
// 腾讯 filtering 模式(仅增量时)
timeFilter := map [ string ] interface { } {
"field" : tf ,
"operator" : "GREATER_EQUALS" ,
"values" : [ ] interface { } { fmt . Sprintf ( "%d" , lastSyncTime ) } ,
}
// 合并已有的 filtering( 如果 request_config 中已定义其他过滤条件)
if existing , ok := body [ "filtering" ] . ( [ ] interface { } ) ; ok {
body [ "filtering" ] = append ( existing , timeFilter )
} else {
@@ -502,64 +664,197 @@ func buildReqBody(iface *entity.ApiInterface, page, pageSize int, lastSyncTime i
}
}
}
for k , v := range extraParams {
body [ k ] = v
}
// body_wrapper_field 支持:将业务参数包装到指定字段(如快手 API 的 param JSON)
if iface . RequestConfig != nil {
if wf , ok := iface . RequestConfig [ "body_wrapper_field" ] . ( string ) ; ok && wf != "" {
excludeSet := map [ string ] bool { "method" : true , "version" : true , "signMethod" : true }
if excl , ok := iface . RequestConfig [ "exclude_from_wrapper" ] . ( [ ] interface { } ) ; ok {
for _ , v := range excl {
if s , ok := v . ( string ) ; ok {
excludeSet [ s ] = true
}
}
}
wrapperObj := make ( map [ string ] interface { } )
for k , v := range body {
if ! excludeSet [ k ] && k != wf {
wrapperObj [ k ] = v
delete ( body , k )
}
}
b , _ := json . Marshal ( wrapperObj )
body [ wf ] = string ( b )
}
}
return body
}
// parseResp 解析同步接口返回值
func parseResp ( raw [ ] byte , responseConfig map [ string ] interface { } ) ( [ ] map [ string ] interface { } , int , int64 , error ) {
var r struct {
Code int ` json:"code" `
Message string ` json:"message" `
Data map [ string ] interface { } ` json:"data" `
// parseRespExt 解析响应,支持自定义成功判断和数据路径
func parseRespExt ( raw [ ] byte , rc map [ string ] interface { } ) ( [ ] map [ string ] interface { } , int , int64 , string , error ) {
var respMap map [ string ] interface { }
if err := json . Unmarshal ( raw , & respMap ) ; err != nil {
return nil , 0 , 0 , "" , fmt . Errorf ( "JSON解析失败: %w" , err )
}
if err := json . Unmarshal ( raw , & r ) ; err != nil {
return nil , 0 , 0 , fmt . Errorf ( "解析响应失败: %w " , err )
}
if r. Code != 0 {
return nil , 0 , 0 , fmt . Errorf ( "API错误: code=%d, message=%s" , r . Code , r . Message )
}
var rows [ ] map [ string ] interface { }
totalPages := 1
maxTime : = int64 ( 0 )
var listData [ ] interface { }
if lp , ok := r . Data [ "list" ] ; ok {
listData , _ = lp . ( [ ] interface { } )
} else if lp , ok := r . Data [ "data" ] ; ok {
if m , ok := lp . ( map [ string ] interface { } ) ; ok {
if l , ok := m [ "list" ] . ( [ ] interface { } ) ; ok {
listData = l
successField , successVal := "code" , float64 ( 0 )
msgField , listPath , cursorPath := "message" , "data " , ""
singleRecord := false
if rc != nil {
if sf , _ := rc [ "success_field" ] . ( string ) ; sf != "" {
successField = sf
}
if sv , ok := rc [ "success_value" ] ; ok {
if f , ok := toFloat64 ( sv ) ; ok {
successVal = f
}
}
if mf , _ := rc [ "message_field" ] . ( string ) ; mf != "" {
msgField = mf
}
if lp , _ := rc [ "list_path" ] . ( string ) ; lp != "" {
listPath = lp
}
if cf , _ := rc [ "cursor_field" ] . ( string ) ; cf != "" {
cursorPath = cf
}
if sr , _ := rc [ "single_record" ] . ( bool ) ; sr {
singleRecord = true
}
}
if v , ok := respMap [ successField ] ; ok {
actual , _ := toFloat64 ( v )
if actual != successVal {
msg , _ := respMap [ msgField ] . ( string )
return nil , 0 , 0 , "" , fmt . Errorf ( "API错误: %s=%v, %s=%s" , successField , v , msgField , msg )
}
}
// 解析 list_path, 支持最后一段是数组的情况( 如 data.orderList)
var listData [ ] interface { }
var dataContainer map [ string ] interface { }
if listPath != "" {
parts := strings . Split ( listPath , "." )
cur := respMap
for i , p := range parts {
if i == len ( parts ) - 1 {
// 最后一段:可能直接是数组,也可能是包含 list/orderList 的 map
listData , _ = cur [ p ] . ( [ ] interface { } )
if listData == nil {
if m , ok := cur [ p ] . ( map [ string ] interface { } ) ; ok {
dataContainer = m
if l , ok := m [ "list" ] ; ok {
listData , _ = l . ( [ ] interface { } )
}
if listData == nil {
if ol , ok := m [ "orderList" ] ; ok {
listData , _ = ol . ( [ ] interface { } )
}
}
}
} else {
dataContainer = cur
}
} else {
next , ok := cur [ p ] . ( map [ string ] interface { } )
if ! ok {
return nil , 0 , 0 , "" , fmt . Errorf ( "路径 %s 在 %s 处中断" , listPath , p )
}
cur = next
}
}
}
if listData == nil {
if singleRecord && listPath != "" {
// 详情接口: list_path 指向单个对象,包装为单元素数组
parts := strings . Split ( listPath , "." )
cur := respMap
ok := true
for _ , p := range parts {
if m , exists := cur [ p ] . ( map [ string ] interface { } ) ; exists {
cur = m
} else {
ok = false
break
}
}
if ok {
listData = [ ] interface { } { cur }
dataContainer = cur
}
}
}
if listData == nil {
// 回退到根级字段
listData , _ = respMap [ "list" ] . ( [ ] interface { } )
if listData == nil {
listData , _ = respMap [ "orderList" ] . ( [ ] interface { } )
}
dataContainer = respMap
}
var rows [ ] map [ string ] interface { }
totalPages , maxTime := 1 , int64 ( 0 )
for _ , item := range listData {
if m , ok := item . ( map [ string ] interface { } ) ; ok {
// 展平嵌套 map: 将子 map 的字段合并到顶层(如 orderBaseInfo.oid → oid)
flat := flattenRow ( m )
j , _ := json . Marshal ( m )
m [ "raw_data" ] = string ( j )
i f t , ok := m [ "last_modified_time" ] . ( float64 ) ; ok && int64 ( t ) > maxTime {
maxTime = int64 ( t )
flat [ "raw_data" ] = string ( j )
for _ , tf := range [ ] string { "last_modified_time" , "created_time" , "update_time" , "createTime" , "updateTime" , "lastModifiedTime" } {
if t , ok := flat [ tf ] . ( float64 ) ; ok && int64 ( t ) > maxTime {
maxTime = int64 ( t )
}
}
if t , ok : = m [ "created_time" ] . ( float64 ) ; ok && int64 ( t ) > maxTime {
maxTime = int64 ( t )
}
rows = append ( rows , m )
rows = append ( rows , flat )
}
}
if pi , ok : = r . Data [ "page_info" ] . ( map [ string ] interface { } ) ; ok {
nextCursor := ""
if cursorPath ! = "" {
cp := strings . Split ( cursorPath , "." )
cc := respMap
for i , p := range cp {
if i == len ( cp ) - 1 {
if s , ok := cc [ p ] . ( string ) ; ok {
nextCursor = s
}
} else if m , ok := cc [ p ] . ( map [ string ] interface { } ) ; ok {
cc = m
}
}
}
if pi , ok := dataContainer [ "page_info" ] . ( map [ string ] interface { } ) ; ok {
if tp , ok := pi [ "total_page" ] . ( float64 ) ; ok {
totalPages = int ( tp )
} else if tp , ok := pi [ "total_page" ] . ( int ) ; ok {
totalPages = tp
}
}
return rows , totalPages , maxTime , nextCursor , nil
}
return rows , totalPages , maxTime , nil
// flattenRow 展平嵌套 map: 将子 map 的字段递归合并到顶层
// 数组类型的字段保持原样不展平
func flattenRow ( m map [ string ] interface { } ) map [ string ] interface { } {
result := make ( map [ string ] interface { } , len ( m ) )
for k , v := range m {
if sub , ok := v . ( map [ string ] interface { } ) ; ok {
// 子 map 递归展平后合并到顶层
for sk , sv := range flattenRow ( sub ) {
result [ sk ] = sv
}
} else {
result [ k ] = v
}
}
return result
}
// parseResp 兼容旧版, 保持4个返回值
func parseResp ( raw [ ] byte , responseConfig map [ string ] interface { } ) ( [ ] map [ string ] interface { } , int , int64 , error ) {
rows , tp , mt , _ , err := parseRespExt ( raw , responseConfig )
return rows , tp , mt , err
}
func savePage ( ctx context . Context , td * TableDefinition , rows [ ] map [ string ] interface { } ) ( int , error ) {
@@ -631,18 +926,17 @@ func updateSyncTime(ctx context.Context, platformCode, interfaceCode string, t i
Save ( )
}
func recordFailure ( ctx context . Context , platformCode , interfaceCode , errMsg string ) {
func recordFailure ( ctx context . Context , platformCode , interfaceCode , taskType , errMsg string ) {
dao . SyncTaskLog . Create ( ctx , & taskDto . CreateSyncTaskLogReq {
TaskID : fmt . Sprintf ( "%s_%s_%d" , platformCode , interfaceCode , time . Now ( ) . UnixNano ( ) ) ,
TaskType : fmt . Sprintf ( "%s_%s" , platformCode , interfaceCode ) ,
TaskType : taskType ,
PlatformCode : platformCode ,
InterfaceCode : interfaceCode ,
Status : "failed" ,
MaxRetry : 3 ,
StartTime : time . Now ( ) ,
RequestParams : map [ string ] interface { } {
"platform_code " : platformCode ,
"interface_code" : interfaceCode ,
"error" : errMsg ,
"error " : errMsg ,
} ,
} )
}