336 lines
9.4 KiB
Go
336 lines
9.4 KiB
Go
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()
|
||
}
|
||
}
|