package batchresult import ( "context" "os" "path/filepath" "reflect" "strconv" "strings" "sync" "testing" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcfg" "github.com/xuri/excelize/v2" ) func TestWriteDataServiceWorkbookPreservesReviewStatusAndReason(t *testing.T) { for _, batchType := range []int{consts.BatchTypeDataCut, consts.BatchTypeDataStart, consts.BatchTypeDataStop} { t.Run(TaskName(batchType), func(t *testing.T) { path := filepath.Join(t.TempDir(), "result.xlsx") rows := []Row{ {RowNo: 1, ICCID: "test-success", DetailStatus: consts.BatchDetailStatusSuccess, Success: true}, {RowNo: 2, ICCID: "test-failed", DetailStatus: consts.BatchDetailStatusFailed, Reason: "上游明确拒绝"}, {RowNo: 3, ICCID: "test-unknown", DetailStatus: consts.BatchDetailStatusUnknown, Reason: "查询超时,尚未确认上游执行结果", UpstreamCode: 202, OrderNum: "test-job-id"}, {RowNo: 4, ICCID: "test-review-priority", DetailStatus: consts.BatchDetailStatusUnknown, Success: true, Reason: "明细仍待核查"}, {RowNo: 5, ICCID: "test-legacy-success", Success: true}, {RowNo: 6, ICCID: "test-legacy-failed", Reason: "未受理"}, } if err := writeWorkbook(path, Request{BatchType: batchType, Rows: rows}); err != nil { t.Fatalf("写入数据服务结果失败:%v", err) } file, err := excelize.OpenFile(path) if err != nil { t.Fatalf("打开数据服务结果失败:%v", err) } defer func() { _ = file.Close() }() for cell, want := range map[string]string{ "G1": "执行结果", "H1": "失败/待核查原因", "G2": "成功", "G3": "失败", "H3": "上游明确拒绝", "G4": "待核查", "H4": rows[2].Reason, "I4": "202", "J4": "test-job-id", "G5": "待核查", "H5": rows[3].Reason, "G6": "成功", "G7": "失败", "H7": "未受理", } { got, err := file.GetCellValue("处理结果", cell) if err != nil || got != want { t.Fatalf("单元格 %s = %q,预期 %q,错误:%v", cell, got, want, err) } } }) } } func TestCreateSameBatchUsesIndependentResultFiles(t *testing.T) { // Keep the generated workbook path below Excel's 207-character limit on Windows. root, rootErr := os.MkdirTemp("", "batch-result-") if rootErr != nil { t.Fatal(rootErr) } t.Cleanup(func() { if err := os.RemoveAll(root); err != nil { t.Errorf("remove test result directory: %v", err) } }) t.Setenv("IOTLCM_WORKER_UPLOAD_ROOT", root) oldAdapter := g.Cfg().GetAdapter() adapter, err := gcfg.NewAdapterContent(` system: upload: local: path: "resource/upload" `) if err != nil { t.Fatal(err) } g.Cfg().SetAdapter(adapter) t.Cleanup(func() { g.Cfg().SetAdapter(oldAdapter) }) request := Request{BatchNo: "DATA-REVIEW", BatchType: consts.BatchTypeDataStart, TenantID: "tenant", Rows: []Row{{RowNo: 1, ICCID: "first-card", DetailStatus: consts.BatchDetailStatusUnknown, Reason: "待核查"}}} requests := []Request{request, request} requests[1].Rows = []Row{{RowNo: 1, ICCID: "second-card", DetailStatus: consts.BatchDetailStatusUnknown, Reason: "待核查"}} artifacts := make([]Artifact, 2) errors := make([]error, 2) var workers sync.WaitGroup for index := range requests { workers.Add(1) go func(index int) { defer workers.Done() artifacts[index], errors[index] = New().Create(context.Background(), requests[index]) }(index) } workers.Wait() for _, err := range errors { if err != nil { t.Fatal(err) } } first, second := artifacts[0], artifacts[1] defer RemoveLocalFile(first) defer RemoveLocalFile(second) if first.Path == second.Path || first.LocalPath == second.LocalPath || first.FileName == second.FileName { t.Fatalf("同批结果物理文件冲突: first=%+v second=%+v", first, second) } if first.OriginalName != "DATA-REVIEW-result.xlsx" || second.OriginalName != first.OriginalName { t.Fatalf("用户下载原始文件名被更改: %q / %q", first.OriginalName, second.OriginalName) } RemoveLocalFile(first) if _, err := os.Stat(first.LocalPath); !os.IsNotExist(err) { t.Fatalf("首次生成文件未删除: %v", err) } file, err := excelize.OpenFile(second.LocalPath) if err != nil { t.Fatalf("删除第一份后第二份不可读: %v", err) } defer file.Close() got, err := file.GetCellValue("处理结果", "B2") if err != nil || got != "second-card" { t.Fatalf("第二份结果内容损坏: B2=%q err=%v", got, err) } } func TestCreatePreservesWorkbookAndPermissionsForAllBatchTypes(t *testing.T) { root, err := os.MkdirTemp("", "batchresult-") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.RemoveAll(root) }) t.Setenv("IOTLCM_WORKER_UPLOAD_ROOT", root) oldAdapter := g.Cfg().GetAdapter() adapter, err := gcfg.NewAdapterContent(` system: upload: local: path: "resource/upload" `) if err != nil { t.Fatal(err) } g.Cfg().SetAdapter(adapter) t.Cleanup(func() { g.Cfg().SetAdapter(oldAdapter) }) for _, batchType := range []int{1, 3, 4, 11, 12, 40, 41, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110} { t.Run(strconv.Itoa(batchType), func(t *testing.T) { request := Request{BatchNo: "COMPAT-" + strconv.Itoa(batchType), BatchType: batchType, TenantID: "tenant", Rows: []Row{ {RowNo: 1, ICCID: "test-success", ObjectCode: "test-success", Success: true}, {RowNo: 2, ICCID: "test-failed", ObjectCode: "test-failed", Reason: "原有失败原因"}, }} baseline := filepath.Join(t.TempDir(), "baseline.xlsx") if err := writeWorkbook(baseline, request); err != nil { t.Fatal(err) } artifact, err := New().Create(context.Background(), request) if err != nil { t.Fatal(err) } defer RemoveLocalFile(artifact) if artifact.OriginalName != request.BatchNo+"-result.xlsx" || filepath.Ext(artifact.LocalPath) != ".xlsx" { t.Fatalf("结果文件名或后缀发生变化: %+v", artifact) } baselineInfo, err := os.Stat(baseline) if err != nil { t.Fatal(err) } actualInfo, err := os.Stat(artifact.LocalPath) if err != nil || actualInfo.Mode().Perm() != baselineInfo.Mode().Perm() { t.Fatalf("结果文件权限未保留原SaveAs行为: %v", err) } before, err := excelize.OpenFile(baseline) if err != nil { t.Fatal(err) } defer before.Close() after, err := excelize.OpenFile(artifact.LocalPath) if err != nil { t.Fatal(err) } defer after.Close() want, err := before.GetRows("处理结果") if err != nil { t.Fatal(err) } got, err := after.GetRows("处理结果") if err != nil || !reflect.DeepEqual(got, want) { t.Fatalf("随机物理名改变了批次%d工作簿内容: err=%v", batchType, err) } }) } } func TestCreateWritesToMountedUploadAndKeepsLogicalPath(t *testing.T) { wd := t.TempDir() oldWd, err := os.Getwd() if err != nil { t.Fatalf("get working directory: %v", err) } if err = os.Chdir(wd); err != nil { t.Fatalf("change working directory: %v", err) } defer func() { _ = os.Chdir(oldWd) }() if err = os.MkdirAll(filepath.Join(wd, "upload"), 0o755); err != nil { t.Fatalf("create mounted upload root: %v", err) } oldAdapter := g.Cfg().GetAdapter() adapter, err := gcfg.NewAdapterContent(` system: upload: local: path: "resource/upload" `) if err != nil { t.Fatalf("create config adapter: %v", err) } g.Cfg().SetAdapter(adapter) defer g.Cfg().SetAdapter(oldAdapter) artifact, err := New().Create(context.Background(), Request{ BatchNo: "INB202608140001001", BatchType: 1, TenantID: "000000", Rows: []Row{{RowNo: 1, ICCID: "89860401786527567308", Success: true}}, }) if err != nil { t.Fatalf("Create() error = %v", err) } defer RemoveLocalFile(artifact) logicalPath := filepath.ToSlash(artifact.Path) if !strings.HasPrefix(logicalPath, "resource/upload/000000/user/file/") { t.Fatalf("logical path = %q, want resource/upload path", artifact.Path) } localPath := filepath.ToSlash(artifact.LocalPath) if !strings.HasPrefix(localPath, "upload/000000/user/file/") { t.Fatalf("local path = %q, want mounted upload path", artifact.LocalPath) } if _, err = os.Stat(artifact.LocalPath); err != nil { t.Fatalf("result file was not written at local path %q: %v", artifact.LocalPath, err) } if _, err = os.Stat(artifact.Path); !os.IsNotExist(err) { t.Fatalf("result file should not be written to logical-only path %q, stat error=%v", artifact.Path, err) } if !strings.HasPrefix(artifact.URL, "/upload/000000/user/file/") { t.Fatalf("url = %q, want /upload URL", artifact.URL) } }