Files
iotlcm/admin-svr/server/internal/library/exportcenter/columns.go
T

336 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}
}