package exportcenter import ( "context" "reflect" "strings" "sync" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/util/gconv" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" ) // fieldList 导出列字段名清单(json tag)。 type fieldList []string // columnsFromRequest 从当前请求里读取导出列清单。 // 前端统一传 exportColumnsJson(JSON 字符串):导出接口多为 x-www-form-urlencoded, // 数组套对象会在表单编码时被拆散,字符串形式不会。 // 这样各导出无需改参数结构即可支持「按页面显示列导出」。 func columnsFromRequest(ctx context.Context) []model.ExportColumn { r := ghttp.RequestFromCtx(ctx) if r == nil { return nil } if raw := strings.TrimSpace(r.Get("exportColumnsJson").String()); raw != "" { cols := make([]model.ExportColumn, 0) if err := gjson.New(raw).Scan(&cols); err == nil && len(cols) > 0 { return cols } } // 兼容直接传数组的调用(如 JSON 请求体) if v := r.Get("exportColumns"); v != nil && !v.IsNil() { cols := make([]model.ExportColumn, 0) if err := gconv.Structs(v.Interface(), &cols); err == nil && len(cols) > 0 { return cols } } return nil } // resolveColumns 生成表头。 // 有列清单时按清单顺序取 label 作表头,并按 field 逐行取值; // 没有列清单时沿用调用方给的 Tags(兼容未改造的导出)。 func resolveColumns(req *Request) (header []interface{}, fields fieldList, err error) { for _, c := range req.Columns { field := strings.TrimSpace(c.Field) if field == "" { continue } label := strings.TrimSpace(c.Label) if label == "" { label = field } fields = append(fields, field) header = append(header, label) } if len(fields) > 0 { // 自定义取值的导出(ToRow)要按 RowFields 取列,字段名对不上会静默出空列,这里留一条日志。 if req.ToRow != nil && len(req.RowFields) > 0 { if missing := missingRowFields(fields, req.RowFields); len(missing) > 0 { g.Log().Warningf(context.Background(), "[exportcenter] 导出列在 RowFields 中不存在,将输出空列 biz=%s fields=%v", req.BizKey, missing) } } return header, fields, nil } tags, err := req.Tags() if err != nil { return nil, nil, err } header = make([]interface{}, len(tags)) for i, v := range tags { header[i] = v } return header, nil, nil } // rowValues 按列清单取值;无列清单时保持原有转换方式。 func rowValues(v interface{}, fields fieldList, req *Request) []interface{} { if len(fields) > 0 { // 自定义取值的导出(ToRow)按 RowFields 声明的位置取列,避免绕过原有格式化。 if req.ToRow != nil && len(req.RowFields) > 0 { values := pickRowFields(req.ToRow(v), req.RowFields, fields) i := 0 for _, column := range req.Columns { if strings.TrimSpace(column.Field) == "" { continue } // 字典列/固定值映射列始终取列表模型的原值,避免对 ToRow 已翻译的中文再次查 key。 if (column.DictType != "" || len(column.ValueMap) > 0) && v != nil { values[i] = valuesByFields(v, fieldList{column.Field})[0] } i++ } return values } values := valuesByFields(v, fields) i := 0 for _, column := range req.Columns { if strings.TrimSpace(column.Field) == "" { continue } if column.FallbackField != "" && (values[i] == nil || gconv.String(values[i]) == "") { values[i] = valuesByFields(v, fieldList{column.FallbackField})[0] } i++ } return values } if req.ToRow != nil { return req.ToRow(v) } return structValues(v) } // loadColumnDicts 每个任务一次性加载列表列使用的字典。 func loadColumnDicts(ctx context.Context, columns []model.ExportColumn, load func(context.Context, []string) ([]model.SysDictDataListModel, error)) (map[string]map[string]string, error) { types := make([]string, 0) seen := make(map[string]bool) for _, column := range columns { typ := strings.TrimSpace(column.DictType) if strings.TrimSpace(column.Field) != "" && typ != "" && !seen[typ] { seen[typ] = true types = append(types, typ) } } result := make(map[string]map[string]string) if len(types) == 0 { return result, nil } items, err := load(ctx, types) if err != nil { return nil, err } for _, item := range items { if result[item.DictType] == nil { result[item.DictType] = make(map[string]string) } result[item.DictType][item.DictValue] = item.DictLabel } return result, nil } func translateRow(values []interface{}, columns []model.ExportColumn, dicts map[string]map[string]string) { i := 0 for _, column := range columns { if strings.TrimSpace(column.Field) == "" { continue } if i >= len(values) { return } value := values[i] str := "" if value != nil { str = gconv.String(value) } // 固定值映射优先:页面用三元表达式渲染的列(非字典)直接出显示文案。 if len(column.ValueMap) > 0 { if mapped, ok := column.ValueMap[str]; ok { values[i] = mapped i++ continue } if column.ValueDefault != "" { values[i] = column.ValueDefault i++ continue } } if str != "" { key := str if normalized, ok := column.DictValueMap[key]; ok { key = normalized } if label, ok := dicts[strings.TrimSpace(column.DictType)][key]; ok { values[i] = label } } i++ } } // missingRowFields 找出列清单里 RowFields 无法提供的字段。 func missingRowFields(fields fieldList, rowFields []string) []string { known := make(map[string]bool, len(rowFields)) for _, name := range rowFields { known[name] = true } missing := make([]string, 0) for _, name := range fields { if !known[name] { missing = append(missing, name) } } return missing } // pickRowFields 从自定义行数据里按字段名取列:row 与 rowFields 顺序一一对应。 func pickRowFields(row []interface{}, rowFields []string, fields fieldList) []interface{} { index := make(map[string]int, len(rowFields)) for i, name := range rowFields { index[name] = i } out := make([]interface{}, len(fields)) for i, name := range fields { pos, ok := index[name] if !ok || pos < 0 || pos >= len(row) { out[i] = "" continue } out[i] = row[pos] } return out } // jsonFieldIndexCache 缓存「结构体类型 -> json tag 到字段下标的映射」, // 导出大结果集时避免每行重复解析 tag。 var jsonFieldIndexCache sync.Map func jsonFieldIndexes(t reflect.Type) map[string]int { if cached, ok := jsonFieldIndexCache.Load(t); ok { return cached.(map[string]int) } indexes := make(map[string]int, t.NumField()) for i := 0; i < t.NumField(); i++ { field := t.Field(i) if field.PkgPath != "" { // 未导出字段 continue } name := strings.Split(field.Tag.Get("json"), ",")[0] if name == "" || name == "-" { continue } if _, exists := indexes[name]; !exists { indexes[name] = i } } jsonFieldIndexCache.Store(t, indexes) return indexes } // valuesByFields 按字段名(json tag)从结构体取值,缺失字段留空。 // 支持 "createdBySumma.name" 这类嵌套路径(列表页常按嵌套字段显示)。 func valuesByFields(v interface{}, fields fieldList) []interface{} { out := make([]interface{}, len(fields)) rv := reflect.ValueOf(v) for rv.IsValid() && rv.Kind() == reflect.Ptr { if rv.IsNil() { return out } rv = rv.Elem() } if !rv.IsValid() || rv.Kind() != reflect.Struct { return out } for i, name := range fields { if value, ok := structFieldValue(rv, name); ok { out[i] = exportCellValue(value) } else { out[i] = "" } } return out } // structFieldValue 按 "a.b.c" 的 json tag 路径逐层取值。 func structFieldValue(rv reflect.Value, path string) (reflect.Value, bool) { cur := rv for _, name := range strings.Split(path, ".") { for cur.IsValid() { if (cur.Kind() == reflect.Ptr || cur.Kind() == reflect.Interface) && cur.IsNil() { return reflect.Value{}, false } if cur.CanInterface() { if object, ok := cur.Interface().(interface{ Map() map[string]interface{} }); ok { cur = reflect.ValueOf(object.Map()) break } } if cur.Kind() != reflect.Ptr && cur.Kind() != reflect.Interface { break } cur = cur.Elem() } if !cur.IsValid() { return reflect.Value{}, false } if cur.Kind() == reflect.Map && cur.Type().Key().Kind() == reflect.String { cur = cur.MapIndex(reflect.ValueOf(name)) if !cur.IsValid() { return reflect.Value{}, false } continue } if cur.Kind() != reflect.Struct { return reflect.Value{}, false } pos, ok := jsonFieldIndexes(cur.Type())[name] if !ok { return reflect.Value{}, false } cur = cur.Field(pos) } return cur, true } // exportCellValue 把字段值转成可写入单元格的值。 // 指针为空、时间、嵌套结构都要处理,避免把内存地址写进 Excel。 func exportCellValue(v reflect.Value) interface{} { for v.IsValid() && v.Kind() == reflect.Ptr { if v.IsNil() { return "" } v = v.Elem() } if !v.IsValid() || !v.CanInterface() { return "" } switch v.Kind() { case reflect.Struct, reflect.Map, reflect.Slice, reflect.Array: // 时间等有 String() 的类型交给 gconv,其余复合类型同样转字符串。 if s, ok := v.Interface().(interface{ String() string }); ok { return s.String() } return gconv.String(v.Interface()) default: return v.Interface() } }