package exportcenter import ( "context" "errors" "github.com/gogf/gf/v2/encoding/gjson" "github.com/xuri/excelize/v2" "reflect" "strings" "testing" "time" "github.com/gogf/gf/v2/os/gtime" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" ) func TestSubmitQueueBoundedBeforePersistence(t *testing.T) { for len(slots) < cap(slots) { slots <- struct{}{} } defer func() { for len(slots) > 0 { <-slots } }() _, err := Submit(context.Background(), &Request{ BizKey: "test", BizName: "test", Tags: func() ([]string, error) { return nil, nil }, Fetch: func(context.Context, int, int) ([]interface{}, int, error) { return []interface{}{1}, 1, nil }, }) if err == nil || !strings.Contains(err.Error(), "排队已满") { t.Fatalf("队列满时应拒绝提交: %v", err) } } func TestColumnDictBatchAndExcel(t *testing.T) { columns := []model.ExportColumn{{Field: "status", DictType: "enabled"}, {Field: "", DictType: "ignored"}, {Field: "other", DictType: "enabled"}, {Field: "inverse", DictType: "yes", DictValueMap: map[string]string{"0": "1", "1": "0"}}} calls := 0 dicts, err := loadColumnDicts(context.Background(), columns, func(_ context.Context, types []string) ([]model.SysDictDataListModel, error) { calls++ if !reflect.DeepEqual(types, []string{"enabled", "yes"}) { t.Fatalf("字典未去重: %v", types) } return []model.SysDictDataListModel{{DictType: "enabled", DictValue: "0", DictLabel: "启用"}, {DictType: "enabled", DictValue: "1", DictLabel: "停用"}, {DictType: "yes", DictValue: "1", DictLabel: "否"}}, nil }) if err != nil || calls != 1 { t.Fatalf("批量加载失败: %v %d", err, calls) } for _, tc := range []struct{ input, want interface{} }{{0, "启用"}, {"0", "启用"}, {1, "停用"}, {"", ""}, {nil, nil}, {"未知", "未知"}, {"启用", "启用"}} { values := []interface{}{tc.input, "保留", 0} translateRow(values, columns, dicts) if !reflect.DeepEqual(values, []interface{}{tc.want, "保留", "否"}) { t.Fatalf("映射 %v 得到 %v", tc.input, values) } } f := excelize.NewFile() defer f.Close() stream, err := f.NewStreamWriter("Sheet1") if err != nil { t.Fatal(err) } values := []interface{}{0, "未知", 0} translateRow(values, columns, dicts) if err = stream.SetRow("A1", values); err != nil { t.Fatal(err) } if err = stream.Flush(); err != nil { t.Fatal(err) } buffer, err := f.WriteToBuffer() if err != nil { t.Fatal(err) } read, err := excelize.OpenReader(buffer) if err != nil { t.Fatal(err) } defer read.Close() rows, err := read.GetRows("Sheet1") if err != nil || !reflect.DeepEqual(rows, [][]string{{"启用", "未知", "否"}}) { t.Fatalf("xlsx 内容: %v %v", rows, err) } } func TestColumnDictNoMetadataAndLoadFailure(t *testing.T) { called := false _, err := loadColumnDicts(context.Background(), nil, func(context.Context, []string) ([]model.SysDictDataListModel, error) { called = true; return nil, nil }) if called || err != nil { t.Fatal("旧页面不应加载字典") } failure := errors.New("字典加载失败") _, err = loadColumnDicts(context.Background(), []model.ExportColumn{{Field: "status", DictType: "x"}}, func(context.Context, []string) ([]model.SysDictDataListModel, error) { return nil, failure }) if !errors.Is(err, failure) { t.Fatal("应传播字典加载错误") } } func TestNestedDictFieldsAndFormattedRow(t *testing.T) { row := struct { BatchInfo *gjson.Json `json:"batchInfo"` }{gjson.New(map[string]interface{}{"inboundOptions": map[string]interface{}{"status": 0}, "legacy": 1})} columns := []model.ExportColumn{{Field: "batchInfo.inboundOptions.status"}, {Field: "batchInfo.inboundOptions.missing", FallbackField: "batchInfo.legacy"}} req := &Request{Columns: columns} _, fields, _ := resolveColumns(req) values := rowValues(row, fields, req) if !reflect.DeepEqual(values, []interface{}{0, 1}) { t.Fatalf("JSON 嵌套与备用字段: %v", values) } req = &Request{Columns: []model.ExportColumn{{Field: "status", DictType: "x"}}, RowFields: []string{"status"}, ToRow: func(interface{}) []interface{} { return []interface{}{"已销户"} }} values = rowValues(struct { Status int `json:"status"` }{9}, fieldList{"status"}, req) translateRow(values, req.Columns, map[string]map[string]string{"x": {"9": "已销户"}}) if values[0] != "已销户" { t.Fatalf("已中文行被改变: %v", values) } } type columnSampleRow struct { Iccid string `json:"iccid"` DeptName string `json:"deptName"` UsedFlowMb float64 `json:"usedFlowMb"` ActiveAt *gtime.Time `json:"activeAt"` Extra *string `json:"extra"` } func TestResolveColumnsUsesRequestedOrder(t *testing.T) { req := &Request{ Columns: []model.ExportColumn{ {Field: "deptName", Label: "所属企业"}, {Field: "iccid"}, {Field: " ", Label: "空字段忽略"}, {Field: "usedFlowMb", Label: "已用流量(MB)"}, }, Tags: func() ([]string, error) { t.Fatal("有列清单时不应回退到 Tags") return nil, nil }, } header, fields, err := resolveColumns(req) if err != nil { t.Fatalf("resolveColumns error: %v", err) } wantHeader := []interface{}{"所属企业", "iccid", "已用流量(MB)"} if len(header) != len(wantHeader) { t.Fatalf("header len = %d, want %d", len(header), len(wantHeader)) } for i := range wantHeader { if header[i] != wantHeader[i] { t.Fatalf("header[%d] = %v, want %v", i, header[i], wantHeader[i]) } } wantFields := fieldList{"deptName", "iccid", "usedFlowMb"} if len(fields) != len(wantFields) { t.Fatalf("fields len = %d, want %d", len(fields), len(wantFields)) } for i := range wantFields { if fields[i] != wantFields[i] { t.Fatalf("fields[%d] = %q, want %q", i, fields[i], wantFields[i]) } } } func TestResolveColumnsFallsBackToTags(t *testing.T) { req := &Request{ Tags: func() ([]string, error) { return []string{"ICCID", "所属企业"}, nil }, } header, fields, err := resolveColumns(req) if err != nil { t.Fatalf("resolveColumns error: %v", err) } if fields != nil { t.Fatalf("fields should be nil when no columns, got %v", fields) } if len(header) != 2 || header[0] != "ICCID" || header[1] != "所属企业" { t.Fatalf("unexpected header: %v", header) } } func TestValuesByFields(t *testing.T) { activeAt := gtime.NewFromTime(time.Date(2026, 10, 19, 15, 4, 5, 0, time.Local)) row := &columnSampleRow{ Iccid: "8986001", DeptName: "测试企业", UsedFlowMb: 12.5, ActiveAt: activeAt, } values := valuesByFields(row, fieldList{"deptName", "missingField", "usedFlowMb", "activeAt", "extra"}) if len(values) != 5 { t.Fatalf("values len = %d, want 5", len(values)) } if values[0] != "测试企业" { t.Fatalf("deptName = %v", values[0]) } if values[1] != "" { t.Fatalf("missing field should be empty, got %v", values[1]) } if values[2] != 12.5 { t.Fatalf("usedFlowMb = %v", values[2]) } if values[3] != "2026-10-19 15:04:05" { t.Fatalf("activeAt = %v", values[3]) } if values[4] != "" { t.Fatalf("nil pointer should be empty, got %v", values[4]) } } func TestValuesByFieldsKeepsRowOrder(t *testing.T) { row := &columnSampleRow{Iccid: "A", DeptName: "B"} values := valuesByFields(row, fieldList{"iccid", "deptName"}) if values[0] != "A" || values[1] != "B" { t.Fatalf("unexpected values: %v", values) } } // TestValueMapColumn 覆盖非字典的固定值映射(页面用三元表达式渲染的列)。 func TestValueMapColumn(t *testing.T) { columns := []model.ExportColumn{ { Field: "isFlowPool", ValueMap: map[string]string{"1": "池卡", "0": "单卡"}, ValueDefault: "单卡", }, {Field: "status", DictType: "common_status"}, {Field: "keep", ValueMap: map[string]string{"1": "是"}}, } dicts := map[string]map[string]string{"common_status": {"1": "正常", "0": "停用"}} // 1/0 命中映射;9 未命中走兜底;状态列仍走字典 values := []interface{}{1, 1, 1} translateRow(values, columns, dicts) if !reflect.DeepEqual(values, []interface{}{"池卡", "正常", "是"}) { t.Fatalf("命中映射失败: %v", values) } // 0 必须映射,不能被当成空值跳过 values = []interface{}{0, "0", 1} translateRow(values, columns, dicts) if values[0] != "单卡" || values[1] != "停用" { t.Fatalf("0 值映射失败: %v", values) } // 未命中且无兜底时保留原值 values = []interface{}{9, "", 7} translateRow(values, columns, dicts) if values[0] != "单卡" { t.Fatalf("兜底失败: %v", values) } if values[2] != 7 { t.Fatalf("无兜底应保留原值: %v", values) } } // TestMissingRowFields 自定义 ToRow 导出:列 field 必须能在 RowFields 里找到。 func TestMissingRowFields(t *testing.T) { rowFields := []string{"iccid", "createdBy"} if missing := missingRowFields(fieldList{"iccid"}, rowFields); len(missing) != 0 { t.Fatalf("不应有缺失: %v", missing) } if missing := missingRowFields(fieldList{"iccid", "createdBySumma"}, rowFields); !reflect.DeepEqual(missing, []string{"createdBySumma"}) { t.Fatalf("缺失识别错误: %v", missing) } }