|
| 1 | +# Golang Gin实践 连载十四 实现导出、导入 Excel |
| 2 | + |
| 3 | +项目地址:https://github.com/EDDYCJY/go-gin-example |
| 4 | + |
| 5 | +如果对你有所帮助,欢迎点个 Star 👍 |
| 6 | + |
| 7 | +## 前言 |
| 8 | + |
| 9 | +在本节,我们将实现对标签信息的导出、导入功能,这是很标配功能了,希望你掌握基础的使用方式 |
| 10 | + |
| 11 | +另外在本文我们使用了 2 个 Excel 的包,excelize 最初的 XML 格式文件的一些结构,是通过 tealeg/xlsx 格式文件结构演化而来的,因此特意在此都展示了,你可以根据自己的场景和喜爱去使用 |
| 12 | + |
| 13 | +## 配置 |
| 14 | + |
| 15 | +首先要指定导出的 Excel 文件的存储路径,在 app.ini 中增加配置: |
| 16 | + |
| 17 | +``` |
| 18 | +[app] |
| 19 | +... |
| 20 | +
|
| 21 | +ExportSavePath = export/ |
| 22 | +``` |
| 23 | + |
| 24 | +修改 setting.go 的 App struct: |
| 25 | + |
| 26 | +``` go |
| 27 | +type App struct { |
| 28 | + JwtSecret string |
| 29 | + PageSize int |
| 30 | + PrefixUrl string |
| 31 | + |
| 32 | + RuntimeRootPath string |
| 33 | + |
| 34 | + ImageSavePath string |
| 35 | + ImageMaxSize int |
| 36 | + ImageAllowExts []string |
| 37 | + |
| 38 | + ExportSavePath string |
| 39 | + |
| 40 | + LogSavePath string |
| 41 | + LogSaveName string |
| 42 | + LogFileExt string |
| 43 | + TimeFormat string |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +在这里需增加 ExportSavePath 配置项,另外将先前 ImagePrefixUrl 改为 PrefixUrl 用于支撑两者的 HOST 获取 |
| 48 | + |
| 49 | +(注意修改 image.go 的 GetImageFullUrl 方法) |
| 50 | + |
| 51 | +## pkg |
| 52 | + |
| 53 | +新建 pkg/export/excel.go 文件,如下: |
| 54 | + |
| 55 | +``` |
| 56 | +package export |
| 57 | +
|
| 58 | +import "github.com/EDDYCJY/go-gin-example/pkg/setting" |
| 59 | +
|
| 60 | +func GetExcelFullUrl(name string) string { |
| 61 | + return setting.AppSetting.PrefixUrl + "/" + GetExcelPath() + name |
| 62 | +} |
| 63 | +
|
| 64 | +func GetExcelPath() string { |
| 65 | + return setting.AppSetting.ExportSavePath |
| 66 | +} |
| 67 | +
|
| 68 | +func GetExcelFullPath() string { |
| 69 | + return setting.AppSetting.RuntimeRootPath + GetExcelPath() |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +这里编写了一些常用的方法,以后取值方式如果有变动,直接改内部代码即可,对外不可见 |
| 74 | + |
| 75 | +## 尝试一下标准库 |
| 76 | + |
| 77 | +``` |
| 78 | +f, err := os.Create(export.GetExcelFullPath() + "test.csv") |
| 79 | +if err != nil { |
| 80 | + panic(err) |
| 81 | +} |
| 82 | +defer f.Close() |
| 83 | +
|
| 84 | +f.WriteString("\xEF\xBB\xBF") |
| 85 | +
|
| 86 | +w := csv.NewWriter(f) |
| 87 | +data := [][]string{ |
| 88 | + {"1", "test1", "test1-1"}, |
| 89 | + {"2", "test2", "test2-1"}, |
| 90 | + {"3", "test3", "test3-1"}, |
| 91 | +} |
| 92 | +
|
| 93 | +w.WriteAll(data) |
| 94 | +``` |
| 95 | + |
| 96 | +在 Go 提供的标准库 encoding/csv 中,天然的支持 csv 文件的读取和处理,在本段代码中,做了如下工作: |
| 97 | + |
| 98 | +1、os.Create: |
| 99 | + |
| 100 | +创建了一个 test.csv 文件 |
| 101 | + |
| 102 | +2、f.WriteString("\xEF\xBB\xBF"): |
| 103 | + |
| 104 | +`\xEF\xBB\xBF` 是 UTF-8 BOM 的 16 进制格式,在这里的用处是标识文件的编码格式,通常会出现在文件的开头,因此第一步就要将其写入。如果不标识 UTF-8 的编码格式的话,写入的汉字会显示为乱码 |
| 105 | + |
| 106 | +3、csv.NewWriter: |
| 107 | + |
| 108 | +``` |
| 109 | +func NewWriter(w io.Writer) *Writer { |
| 110 | + return &Writer{ |
| 111 | + Comma: ',', |
| 112 | + w: bufio.NewWriter(w), |
| 113 | + } |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +4、w.WriteAll: |
| 118 | + |
| 119 | +``` |
| 120 | +func (w *Writer) WriteAll(records [][]string) error { |
| 121 | + for _, record := range records { |
| 122 | + err := w.Write(record) |
| 123 | + if err != nil { |
| 124 | + return err |
| 125 | + } |
| 126 | + } |
| 127 | + return w.w.Flush() |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +WriteAll 实际是对 Write 的封装,需要注意在最后调用了 `w.w.Flush()`,这充分了说明了 WriteAll 的使用场景,你可以想想作者的设计用意 |
| 132 | + |
| 133 | +## 导出 |
| 134 | + |
| 135 | +### Service 方法 |
| 136 | + |
| 137 | +打开 service/tag.go,增加 Export 方法,如下: |
| 138 | + |
| 139 | +``` |
| 140 | +func (t *Tag) Export() (string, error) { |
| 141 | + tags, err := t.GetAll() |
| 142 | + if err != nil { |
| 143 | + return "", err |
| 144 | + } |
| 145 | +
|
| 146 | + file := xlsx.NewFile() |
| 147 | + sheet, err := file.AddSheet("标签信息") |
| 148 | + if err != nil { |
| 149 | + return "", err |
| 150 | + } |
| 151 | +
|
| 152 | + titles := []string{"ID", "名称", "创建人", "创建时间", "修改人", "修改时间"} |
| 153 | + row := sheet.AddRow() |
| 154 | +
|
| 155 | + var cell *xlsx.Cell |
| 156 | + for _, title := range titles { |
| 157 | + cell = row.AddCell() |
| 158 | + cell.Value = title |
| 159 | + } |
| 160 | +
|
| 161 | + for _, v := range tags { |
| 162 | + values := []string{ |
| 163 | + strconv.Itoa(v.ID), |
| 164 | + v.Name, |
| 165 | + v.CreatedBy, |
| 166 | + strconv.Itoa(v.CreatedOn), |
| 167 | + v.ModifiedBy, |
| 168 | + strconv.Itoa(v.ModifiedOn), |
| 169 | + } |
| 170 | +
|
| 171 | + row = sheet.AddRow() |
| 172 | + for _, value := range values { |
| 173 | + cell = row.AddCell() |
| 174 | + cell.Value = value |
| 175 | + } |
| 176 | + } |
| 177 | +
|
| 178 | + time := strconv.Itoa(int(time.Now().Unix())) |
| 179 | + filename := "tags-" + time + ".xlsx" |
| 180 | +
|
| 181 | + fullPath := export.GetExcelFullPath() + filename |
| 182 | + err = file.Save(fullPath) |
| 183 | + if err != nil { |
| 184 | + return "", err |
| 185 | + } |
| 186 | +
|
| 187 | + return filename, nil |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | +## routers 入口 |
| 192 | + |
| 193 | +打开 routers/api/v1/tag.go,增加如下方法: |
| 194 | + |
| 195 | +``` |
| 196 | +func ExportTag(c *gin.Context) { |
| 197 | + appG := app.Gin{C: c} |
| 198 | + name := c.PostForm("name") |
| 199 | + state := -1 |
| 200 | + if arg := c.PostForm("state"); arg != "" { |
| 201 | + state = com.StrTo(arg).MustInt() |
| 202 | + } |
| 203 | +
|
| 204 | + tagService := tag_service.Tag{ |
| 205 | + Name: name, |
| 206 | + State: state, |
| 207 | + } |
| 208 | +
|
| 209 | + filename, err := tagService.Export() |
| 210 | + if err != nil { |
| 211 | + appG.Response(http.StatusOK, e.ERROR_EXPORT_TAG_FAIL, nil) |
| 212 | + return |
| 213 | + } |
| 214 | +
|
| 215 | + appG.Response(http.StatusOK, e.SUCCESS, map[string]string{ |
| 216 | + "export_url": export.GetExcelFullUrl(filename), |
| 217 | + "export_save_url": export.GetExcelPath() + filename, |
| 218 | + }) |
| 219 | +} |
| 220 | +``` |
| 221 | + |
| 222 | +### 路由 |
| 223 | + |
| 224 | +在 routers/router.go 文件中增加路由方法,如下 |
| 225 | + |
| 226 | +``` |
| 227 | +apiv1 := r.Group("/api/v1") |
| 228 | +apiv1.Use(jwt.JWT()) |
| 229 | +{ |
| 230 | + ... |
| 231 | + //导出标签 |
| 232 | + r.POST("/tags/export", v1.ExportTag) |
| 233 | +} |
| 234 | +``` |
| 235 | + |
| 236 | +### 验证接口 |
| 237 | + |
| 238 | +访问 `http://127.0.0.1:8000/tags/export`,结果如下: |
| 239 | + |
| 240 | +``` |
| 241 | +{ |
| 242 | + "code": 200, |
| 243 | + "data": { |
| 244 | + "export_save_url": "export/tags-1528903393.xlsx", |
| 245 | + "export_url": "http://127.0.0.1:8000/export/tags-1528903393.xlsx" |
| 246 | + }, |
| 247 | + "msg": "ok" |
| 248 | +} |
| 249 | +``` |
| 250 | + |
| 251 | +最终通过接口返回了导出文件的地址和保存地址 |
| 252 | + |
| 253 | +### StaticFS |
| 254 | + |
| 255 | +那你想想,现在直接访问地址肯定是无法下载文件的,那么该如何做呢? |
| 256 | + |
| 257 | +打开 router.go 文件,增加代码如下: |
| 258 | + |
| 259 | +``` |
| 260 | +r.StaticFS("/export", http.Dir(export.GetExcelFullPath())) |
| 261 | +``` |
| 262 | + |
| 263 | +若你不理解,强烈建议温习下前面的章节,举一反三 |
| 264 | + |
| 265 | +## 验证下载 |
| 266 | + |
| 267 | +再次访问上面的 export_url ,如:`http://127.0.0.1:8000/export/tags-1528903393.xlsx`,是不是成功了呢? |
| 268 | + |
| 269 | +## 导入 |
| 270 | + |
| 271 | +### Service 方法 |
| 272 | + |
| 273 | +打开 service/tag.go,增加 Import 方法,如下: |
| 274 | + |
| 275 | +``` |
| 276 | +func (t *Tag) Import(r io.Reader) error { |
| 277 | + xlsx, err := excelize.OpenReader(r) |
| 278 | + if err != nil { |
| 279 | + return err |
| 280 | + } |
| 281 | +
|
| 282 | + rows := xlsx.GetRows("标签信息") |
| 283 | + for irow, row := range rows { |
| 284 | + if irow > 0 { |
| 285 | + var data []string |
| 286 | + for _, cell := range row { |
| 287 | + data = append(data, cell) |
| 288 | + } |
| 289 | +
|
| 290 | + models.AddTag(data[1], 1, data[2]) |
| 291 | + } |
| 292 | + } |
| 293 | +
|
| 294 | + return nil |
| 295 | +} |
| 296 | +``` |
| 297 | + |
| 298 | +## routers 入口 |
| 299 | + |
| 300 | +打开 routers/api/v1/tag.go,增加如下方法: |
| 301 | + |
| 302 | +``` |
| 303 | +func ImportTag(c *gin.Context) { |
| 304 | + appG := app.Gin{C: c} |
| 305 | +
|
| 306 | + file, _, err := c.Request.FormFile("file") |
| 307 | + if err != nil { |
| 308 | + logging.Warn(err) |
| 309 | + appG.Response(http.StatusOK, e.ERROR, nil) |
| 310 | + return |
| 311 | + } |
| 312 | +
|
| 313 | + tagService := tag_service.Tag{} |
| 314 | + err = tagService.Import(file) |
| 315 | + if err != nil { |
| 316 | + logging.Warn(err) |
| 317 | + appG.Response(http.StatusOK, e.ERROR_IMPORT_TAG_FAIL, nil) |
| 318 | + return |
| 319 | + } |
| 320 | +
|
| 321 | + appG.Response(http.StatusOK, e.SUCCESS, nil) |
| 322 | +} |
| 323 | +``` |
| 324 | + |
| 325 | +### 路由 |
| 326 | + |
| 327 | +在 routers/router.go 文件中增加路由方法,如下 |
| 328 | + |
| 329 | +``` |
| 330 | +apiv1 := r.Group("/api/v1") |
| 331 | +apiv1.Use(jwt.JWT()) |
| 332 | +{ |
| 333 | + ... |
| 334 | + //导入标签 |
| 335 | + r.POST("/tags/import", v1.ImportTag) |
| 336 | +} |
| 337 | +``` |
| 338 | + |
| 339 | +### 验证 |
| 340 | + |
| 341 | + |
| 342 | + |
| 343 | +在这里我们将先前导出的 Excel 文件作为入参,访问 `http://127.0.0.01:8000/tags/import`,检查返回和数据是否正确入库 |
| 344 | + |
| 345 | + |
| 346 | +## 总结 |
| 347 | + |
| 348 | +在本文中,简单介绍了 Excel 的导入、导出的使用方式,使用了以下 2 个包: |
| 349 | + |
| 350 | +- [tealeg/xlsx](https://github.com/tealeg/xlsx) |
| 351 | +- [360EntSecGroup-Skylar/excelize](https://github.com/360EntSecGroup-Skylar/excelize) |
| 352 | + |
| 353 | +你可以细细阅读一下它的实现和使用方式,对你的把控更有帮助 🤔 |
| 354 | + |
| 355 | + |
| 356 | +## 课外 |
| 357 | + |
| 358 | +- tag:导出使用 excelize 的方式去实现(可能你会发现更简单哦) |
| 359 | +- tag:导入去重功能实现 |
| 360 | +- artice :导入、导出功能实现 |
| 361 | + |
| 362 | + |
| 363 | +也不失为你很好的练手机会,如果有兴趣,可以试试 |
| 364 | + |
| 365 | +## 参考 |
| 366 | +### 本系列示例代码 |
| 367 | +- [go-gin-example](https://github.com/EDDYCJY/go-gin-example) |
| 368 | + |
0 commit comments