107 lines
4.0 KiB
Markdown
107 lines
4.0 KiB
Markdown
# 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 页面)
|
||
```
|