# 7.4 数据导入导出 :::tip 概述 Excel 导入导出基于 xuri/excelize/v2(utility/excel 封装):导入解析逐行校验写入并返回统计,导出走 ChunkQuery 分批查询防 OOM。启用模块:user / level / example。 ::: ## utility/excel API ```text ReadSheet(file FileOpener):首行表头,全单元格转字符串(TrimSpace),空行跳过; FileOpener 接口兼容 *ghttp.UploadFile 与 *multipart.FileHeader WriteSheet(dir, baseName, headers, rows [][]any):写文件返回 (fileName, filePath) ``` ## 导入流程(Import,真实模式摘自 example) ```go // Import 导入案例演示 // 表头为表单字段名,按列序写入 func (s *sExample) Import(ctx context.Context, req *v1.ExampleImportReq) (res *v1.ExampleImportRes, err error) { _, rows, err := excel.ReadSheet(req.File) if err != nil { return nil, err } res = &v1.ExampleImportRes{Total: len(rows)} for i, row := range rows { if cellAt(row, 0) == "" { res.Failed++ res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:案例名称为空", i+2)) continue } if _, e := dao.Example.Ctx(ctx).Data(map[string]any{ "name": cellAt(row, 0), "avatar": cellAt(row, 1), "type": cellInt(row, 2, 0), "status": cellInt(row, 3, 0), "sort": cellInt(row, 4, 0), }).Insert(); e != nil { res.Failed++ res.Reasons = append(res.Reasons, fmt.Sprintf("第%d行:写入失败 %v", i+2, e)) continue } res.Success++ } return res, nil } ``` 要点:逐行独立成败(一行失败不影响后续);返回 Total/Success/Failed/Reasons(逐行原因);user 导入还含唯一校验/租户上限/默认密码 bcrypt 预计算/角色与城市解析(ResolveCityCodes)。 ## 导出流程(Export,真实模式摘自 example) ```go // Export 导出案例演示 // 与列表同套筛选条件,枚举列输出字典文本 func (s *sExample) Export(ctx context.Context, req *v1.ExampleExportReq) (res *v1.ExampleExportRes, err error) { rows, err := exampleSpec.applyFilters(ctx, exampleSpec.Model(ctx), req).Order("sort, id").All() if err != nil { return nil, err } headers := []string{"案例名称", "案例图片", "案例类型", "案例状态", "案例排序"} data := make([][]any, 0, len(rows)) for _, r := range rows { data = append(data, []any{r["name"].String(), r["avatar"].String(), DictGetText(ctx, "example_type", r["type"]), DictGetText(ctx, "example_status", r["status"]), r["sort"].String()}) } fileName, _, err := excel.WriteSheet(upload.BaseDir(ctx), "案例演示数据", headers, data) if err != nil { return nil, err } url := upload.GetFileUrl(ctx, fileName) return &url, nil } ``` 要点:**导出统一走 `xxxSpec.applyFilters` 与列表同套筛选(导出=所见)**;枚举列 DictGetText 转文本;大结果集用 ChunkQuery 分批(user 导出 500/页)。 ## 导入导出规范 | 项 | 规范 | |----|------| | 接口 | Import POST multipart(`p:"file"`);Export GET + 筛选参数 | | 杅限 | sys:{module}:import / export | | 日志 | logType import / export("模块名-导入数据/导出数据") | | 模板 | 表头=表单字段名(导出文件即导入模板) | | 文件位置 | 导出写临时文件返回 URL(/upload/temp/...,受 temp 清理回收) | | 防 OOM | 大结果集 ChunkQuery 分批(默认 500/页) | ## 前端组件 ```text Excel 组件(components/Excel + importFile): 导入:选文件 → POST /api/{module}/import → 展示统计与失败原因 导出:调 /api/{module}/export → 拿 URL → downloadFile 下载 详见 9.11 Excel 导入导出组件 ``` ## 开发新模块的导入导出 ```text 1. 请求结构:ExampleImportReq(gmeta + File 字段)/ ExampleExportReq(gmeta + 筛选字段) 2. 接口声明:service 接口加 Import/Export 方法 3. logic 实现:按上方真实模式(ReadSheet 逐行 / applyFilters + WriteSheet) 4. 权限节点:import/export(生成器 HasImport/HasExport 默认 true 自动建) 5. 前端:导入按钮 + 导出按钮(模板参照 example 页面) ```