From fcaf6de2e30efe4c44a37562e2fdf77e26c4491c Mon Sep 17 00:00:00 2001 From: xiangheng <11675084@qq.com> Date: Mon, 12 Aug 2024 02:25:15 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E8=A6=81=EF=BC=9A=E6=9B=B4=E6=96=B0Ts?= =?UTF-8?q?Time=EF=BC=8Cexcel2=E5=AF=BC=E5=87=BA=E4=B8=8D=E4=BD=BF?= =?UTF-8?q?=E7=94=A8tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/views/system/log/sms/edit.vue | 6 +- server/admin/common/album/service.go | 7 +- .../tpl_utils/templates/gocode/model.go.tpl | 8 +- .../tpl_utils/templates/gocode/route.go.tpl | 2 +- server/admin/monitor_project_route.go | 2 +- .../admin/monitor_web/monitor_web_schema.go | 18 +- server/admin/system/admin/service.go | 2 +- .../system_log_sms/system_log_sms_ctl.go | 221 ++++++++++-------- .../system_log_sms/system_log_sms_schema.go | 2 +- server/admin/system_log_sms_route.go | 14 +- server/core/{time.go => time.go1} | 0 server/core/time2.go | 97 ++++++++ server/middleware/log.go | 8 +- server/model/monitor_project.go | 14 +- server/model/monitor_web.go | 18 +- server/model/system_log_sms.go | 18 +- server/util/array.go | 8 +- server/util/convert.go | 13 +- server/util/excel2/excel.go | 97 ++++++++ server/util/excel2/excel_export.go | 219 +++++++++++++++++ server/util/excel2/excel_export_2.go | 173 ++++++++++++++ server/util/excel2/excel_import.go | 176 ++++++++++++++ server/util/excel2/excel_tag.go | 74 ++++++ server/util/excel2/excel_test.go | 76 ++++++ server/util/excel2/测试.xlsx | Bin 0 -> 6892 bytes 25 files changed, 1102 insertions(+), 171 deletions(-) rename server/core/{time.go => time.go1} (100%) create mode 100644 server/core/time2.go create mode 100644 server/util/excel2/excel.go create mode 100644 server/util/excel2/excel_export.go create mode 100644 server/util/excel2/excel_export_2.go create mode 100644 server/util/excel2/excel_import.go create mode 100644 server/util/excel2/excel_tag.go create mode 100644 server/util/excel2/excel_test.go create mode 100644 server/util/excel2/测试.xlsx diff --git a/admin/src/views/system/log/sms/edit.vue b/admin/src/views/system/log/sms/edit.vue index abffed5..0201eba 100644 --- a/admin/src/views/system/log/sms/edit.vue +++ b/admin/src/views/system/log/sms/edit.vue @@ -11,7 +11,7 @@ > - + @@ -84,12 +84,12 @@ const popupTitle = computed(() => { const formData = reactive({ id: '', - scene: 0, + scene: '', mobile: '', content: '', status: '', results: '', - send_time: 0 + send_time: '' }) const formRules = { diff --git a/server/admin/common/album/service.go b/server/admin/common/album/service.go index 9c26325..a02da0a 100644 --- a/server/admin/common/album/service.go +++ b/server/admin/common/album/service.go @@ -18,7 +18,7 @@ type IAlbumService interface { AlbumMove(ids []uint, cid int) (e error) AlbumAdd(addReq CommonAlbumAddReq) (res uint, e error) AlbumDel(ids []uint) (e error) - CateList(listReq CommonCateListReq) (mapList []interface{}, e error) + CateList(listReq CommonCateListReq) (mapList []CommonCateListResp, e error) CateAdd(addReq CommonCateAddReq) (e error) CateRename(id uint, name string) (e error) CateDel(id uint) (e error) @@ -159,7 +159,7 @@ func (albSrv albumService) AlbumDel(ids []uint) (e error) { } // CateList 相册分类列表 -func (albSrv albumService) CateList(listReq CommonCateListReq) (mapList []interface{}, e error) { +func (albSrv albumService) CateList(listReq CommonCateListReq) (mapList []CommonCateListResp, e error) { var cates []common_model.AlbumCate cateModel := albSrv.db.Where("is_delete = ?", 0).Order("id desc") if listReq.Type > 0 { @@ -174,8 +174,7 @@ func (albSrv albumService) CateList(listReq CommonCateListReq) (mapList []interf } cateResps := []CommonCateListResp{} response.Copy(&cateResps, cates) - return util.ArrayUtil.ListToTree( - util.ConvertUtil.StructsToMaps(cateResps), "id", "pid", "children"), nil + return cateResps, nil } // CateAdd 分类新增 diff --git a/server/admin/generator/tpl_utils/templates/gocode/model.go.tpl b/server/admin/generator/tpl_utils/templates/gocode/model.go.tpl index 9c28f9c..6f92093 100644 --- a/server/admin/generator/tpl_utils/templates/gocode/model.go.tpl +++ b/server/admin/generator/tpl_utils/templates/gocode/model.go.tpl @@ -9,14 +9,14 @@ type {{{ toUpperCamelCase .EntityName }}} struct { {{{- range .Columns }}} {{{- if not (contains $.SubTableFields .ColumnName) }}} {{{- if eq .GoField "is_delete" }}} - IsDelete soft_delete.DeletedAt `mapstructure:"{{{ .GoField }}}" gorm:"not null;default:0;softDelete:flag,DeletedAtField:DeleteTime;comment:'是否删除: 0=否, 1=是'"` + IsDelete soft_delete.DeletedAt `mapstructure:"{{{ .GoField }}}" json:"{{{ .GoField }}}" gorm:"not null;default:0;softDelete:flag,DeletedAtField:DeleteTime;comment:'是否删除: 0=否, 1=是'"` {{{- else }}} {{{- if eq .GoType "core.TsTime" }}} - {{{ toUpperCamelCase .GoField }}} core.TsTime `mapstructure:"{{{ .GoField }}}" gorm:"{{{ if eq .GoField "create_time" }}}autoCreateTime;{{{ else }}}{{{if eq .GoField "update_time"}}}autoUpdateTime;{{{ end }}}{{{ end }}}comment:'{{{ .ColumnComment }}}'" excel:"name:{{{ .ColumnComment }}};"` // {{{ .ColumnComment }}} + {{{ toUpperCamelCase .GoField }}} core.TsTime `mapstructure:"{{{ .GoField }}}" json:"{{{ .GoField }}}" gorm:"{{{ if eq .GoField "create_time" }}}autoCreateTime;{{{ else }}}{{{if eq .GoField "update_time"}}}autoUpdateTime;{{{ end }}}{{{ end }}}comment:'{{{ .ColumnComment }}}'"` // {{{ .ColumnComment }}} {{{- else if .IsPk }}} - {{{ toUpperCamelCase .GoField }}} {{{ .GoType }}} `mapstructure:"{{{ .GoField }}}" gorm:"primarykey;" excel:"name:{{{ .ColumnComment }}};"` // {{{ .ColumnComment }}} + {{{ toUpperCamelCase .GoField }}} {{{ .GoType }}} `mapstructure:"{{{ .GoField }}}" json:"{{{ .GoField }}}" gorm:"primarykey;"` // {{{ .ColumnComment }}} {{{- else }}} - {{{ toUpperCamelCase .GoField }}} {{{ .GoType }}} `mapstructure:"{{{ .GoField }}}" excel:"name:{{{ .ColumnComment }}};"` // {{{ .ColumnComment }}} + {{{ toUpperCamelCase .GoField }}} {{{ .GoType }}} `mapstructure:"{{{ .GoField }}}" json:"{{{ .GoField }}}"` // {{{ .ColumnComment }}} {{{- end }}} {{{- end }}} diff --git a/server/admin/generator/tpl_utils/templates/gocode/route.go.tpl b/server/admin/generator/tpl_utils/templates/gocode/route.go.tpl index 88439eb..7ba406a 100644 --- a/server/admin/generator/tpl_utils/templates/gocode/route.go.tpl +++ b/server/admin/generator/tpl_utils/templates/gocode/route.go.tpl @@ -52,5 +52,5 @@ func {{{ toUpperCamelCase .ModuleName }}}Route(rg *gin.RouterGroup) { r.POST("/{{{ .ModuleName }}}/edit",middleware.RecordLog("{{{ .FunctionName }}}编辑"), handle.Edit) r.POST("/{{{ .ModuleName }}}/del", middleware.RecordLog("{{{ .FunctionName }}}删除"), handle.Del) r.GET("/{{{ .ModuleName }}}/ExportFile", middleware.RecordLog("{{{ .FunctionName }}}导出"), handle.ExportFile) - r.POST("/{{{ .ModuleName }}}/ImportFile",middleware.RecordLog("{{{ .FunctionName }}}导入"), handle.ImportFile) + r.POST("/{{{ .ModuleName }}}/ImportFile", handle.ImportFile) } \ No newline at end of file diff --git a/server/admin/monitor_project_route.go b/server/admin/monitor_project_route.go index f390e42..177e029 100644 --- a/server/admin/monitor_project_route.go +++ b/server/admin/monitor_project_route.go @@ -38,5 +38,5 @@ func MonitorProjectRoute(rg *gin.RouterGroup) { r.POST("/monitor_project/edit", middleware.RecordLog("错误项目编辑"), handle.Edit) r.POST("/monitor_project/del", middleware.RecordLog("错误项目删除"), handle.Del) r.GET("/monitor_project/ExportFile", middleware.RecordLog("错误项目导出"), handle.ExportFile) - r.POST("/monitor_project/ImportFile", middleware.RecordLog("错误项目导入"), handle.ImportFile) + r.POST("/monitor_project/ImportFile", handle.ImportFile) } diff --git a/server/admin/monitor_web/monitor_web_schema.go b/server/admin/monitor_web/monitor_web_schema.go index 257af73..a74e5bd 100644 --- a/server/admin/monitor_web/monitor_web_schema.go +++ b/server/admin/monitor_web/monitor_web_schema.go @@ -51,13 +51,13 @@ type MonitorWebDelReq struct { //MonitorWebResp 错误收集error返回信息 type MonitorWebResp struct { - Id int `json:"id" structs:"id"` // uuid - ProjectKey string `json:"projectKey" structs:"projectKey" excel:"name:项目key;"` // 项目key - ClientId string `json:"clientId" structs:"clientId" excel:"name:sdk生成的客户端id;"` // sdk生成的客户端id - EventType string `json:"eventType" structs:"eventType" excel:"name:事件类型;"` // 事件类型 - Page string `json:"page" structs:"page" excel:"name:URL地址;"` // URL地址 - Message string `json:"message" structs:"message" excel:"name:错误消息;"` // 错误消息 - Stack string `json:"stack" structs:"stack" excel:"name:错误堆栈;"` // 错误堆栈 - ClientTime core.TsTime `json:"clientTime" structs:"clientTime" excel:"name:客户端时间;"` // 客户端时间 - CreateTime core.TsTime `json:"createTime" structs:"createTime" excel:"name:创建时间;"` // 创建时间 + Id int `json:"id"` // uuid + ProjectKey string `json:"projectKey" excel:"name:项目key;"` // 项目key + ClientId string `json:"clientId" excel:"name:sdk生成的客户端id;"` // sdk生成的客户端id + EventType string `json:"eventType" excel:"name:事件类型;"` // 事件类型 + Page string `json:"page" excel:"name:URL地址;"` // URL地址 + Message string `json:"message" excel:"name:错误消息;"` // 错误消息 + Stack string `json:"stack" excel:"name:错误堆栈;"` // 错误堆栈 + ClientTime core.TsTime `json:"clientTime" excel:"name:客户端时间;"` // 客户端时间 + CreateTime core.TsTime `json:"createTime" excel:"name:创建时间;"` // 创建时间 } diff --git a/server/admin/system/admin/service.go b/server/admin/system/admin/service.go index b916cc6..cc46ccb 100644 --- a/server/admin/system/admin/service.go +++ b/server/admin/system/admin/service.go @@ -466,7 +466,7 @@ func (adminSrv systemAuthAdminService) Del(c *gin.Context, id uint) (e error) { if id == config.AdminConfig.GetAdminId(c) { return response.AssertArgumentError.SetMessage("不能删除自己!") } - err = adminSrv.db.Model(&admin).Updates(system_model.SystemAuthAdmin{IsDelete: 1, DeleteTime: core.TsTime(time.Now())}).Error + err = adminSrv.db.Model(&admin).Updates(system_model.SystemAuthAdmin{IsDelete: 1, DeleteTime: core.NowTime()}).Error e = response.CheckErr(err, "Del Updates err") return } diff --git a/server/admin/system_log_sms/system_log_sms_ctl.go b/server/admin/system_log_sms/system_log_sms_ctl.go index e3588e8..0e2ccd6 100644 --- a/server/admin/system_log_sms/system_log_sms_ctl.go +++ b/server/admin/system_log_sms/system_log_sms_ctl.go @@ -4,37 +4,39 @@ import ( "net/http" "strconv" "time" - "github.com/gin-gonic/gin" "x_admin/core/request" "x_admin/core/response" "x_admin/util" "x_admin/util/excel" + "x_admin/util/excel2" + + "github.com/gin-gonic/gin" "golang.org/x/sync/singleflight" ) - type SystemLogSmsHandler struct { requestGroup singleflight.Group } -// @Summary 系统短信日志列表 -// @Tags system_log_sms-系统短信日志 -// @Produce json -// @Param Token header string true "token" -// @Param PageNo query int true "页码" -// @Param PageSize query int true "每页数量" -// @Param scene query int false "场景编号" -// @Param mobile query string false "手机号码" -// @Param content query string false "发送内容" -// @Param status query int false "发送状态:[0=发送中, 1=发送成功, 2=发送失败]" -// @Param results query string false "短信结果" -// @Param send_time query int false "发送时间" -// @Param create_timeStart query core.TsTime false "创建时间" -// @Param create_timeEnd query core.TsTime false "创建时间" -// @Param update_timeStart query core.TsTime false "更新时间" -// @Param update_timeEnd query core.TsTime false "更新时间" -//@Success 200 {object} response.Response{ data=response.PageResp{ lists= []SystemLogSmsResp}} "成功" -//@Router /api/admin/system_log_sms/list [get] +// @Summary 系统短信日志列表 +// @Tags system_log_sms-系统短信日志 +// @Produce json +// @Param Token header string true "token" +// @Param PageNo query int true "页码" +// @Param PageSize query int true "每页数量" +// @Param scene query int false "场景编号" +// @Param mobile query string false "手机号码" +// @Param content query string false "发送内容" +// @Param status query int false "发送状态:[0=发送中, 1=发送成功, 2=发送失败]" +// @Param results query string false "短信结果" +// @Param send_time query int false "发送时间" +// @Param create_timeStart query core.TsTime false "创建时间" +// @Param create_timeEnd query core.TsTime false "创建时间" +// @Param update_timeStart query core.TsTime false "更新时间" +// @Param update_timeEnd query core.TsTime false "更新时间" +// +// @Success 200 {object} response.Response{ data=response.PageResp{ lists= []SystemLogSmsResp}} "成功" +// @Router /api/admin/system_log_sms/list [get] func (hd *SystemLogSmsHandler) List(c *gin.Context) { var page request.PageReq var listReq SystemLogSmsListReq @@ -48,21 +50,21 @@ func (hd *SystemLogSmsHandler) List(c *gin.Context) { response.CheckAndRespWithData(c, res, err) } -// @Summary 系统短信日志列表-所有 -// @Tags system_log_sms-系统短信日志 -// @Produce json -// @Param scene query int false "场景编号" -// @Param mobile query string false "手机号码" -// @Param content query string false "发送内容" -// @Param status query int false "发送状态:[0=发送中, 1=发送成功, 2=发送失败]" -// @Param results query string false "短信结果" -// @Param send_time query int false "发送时间" -// @Param create_timeStart query core.TsTime false "创建时间" -// @Param create_timeEnd query core.TsTime false "创建时间" -// @Param update_timeStart query core.TsTime false "更新时间" -// @Param update_timeEnd query core.TsTime false "更新时间" -// @Success 200 {object} response.Response{ data=[]SystemLogSmsResp} "成功" -// @Router /api/admin/system_log_sms/listAll [get] +// @Summary 系统短信日志列表-所有 +// @Tags system_log_sms-系统短信日志 +// @Produce json +// @Param scene query int false "场景编号" +// @Param mobile query string false "手机号码" +// @Param content query string false "发送内容" +// @Param status query int false "发送状态:[0=发送中, 1=发送成功, 2=发送失败]" +// @Param results query string false "短信结果" +// @Param send_time query int false "发送时间" +// @Param create_timeStart query core.TsTime false "创建时间" +// @Param create_timeEnd query core.TsTime false "创建时间" +// @Param update_timeStart query core.TsTime false "更新时间" +// @Param update_timeEnd query core.TsTime false "更新时间" +// @Success 200 {object} response.Response{ data=[]SystemLogSmsResp} "成功" +// @Router /api/admin/system_log_sms/listAll [get] func (hd *SystemLogSmsHandler) ListAll(c *gin.Context) { var listReq SystemLogSmsListReq if response.IsFailWithResp(c, util.VerifyUtil.VerifyQuery(c, &listReq)) { @@ -72,13 +74,13 @@ func (hd *SystemLogSmsHandler) ListAll(c *gin.Context) { response.CheckAndRespWithData(c, res, err) } -// @Summary 系统短信日志详情 -// @Tags system_log_sms-系统短信日志 -// @Produce json -// @Param Token header string true "token" -// @Param id query int false "id" -// @Success 200 {object} response.Response{ data=SystemLogSmsResp} "成功" -// @Router /api/admin/system_log_sms/detail [get] +// @Summary 系统短信日志详情 +// @Tags system_log_sms-系统短信日志 +// @Produce json +// @Param Token header string true "token" +// @Param id query int false "id" +// @Success 200 {object} response.Response{ data=SystemLogSmsResp} "成功" +// @Router /api/admin/system_log_sms/detail [get] func (hd *SystemLogSmsHandler) Detail(c *gin.Context) { var detailReq SystemLogSmsDetailReq if response.IsFailWithResp(c, util.VerifyUtil.VerifyQuery(c, &detailReq)) { @@ -92,54 +94,55 @@ func (hd *SystemLogSmsHandler) Detail(c *gin.Context) { response.CheckAndRespWithData(c, res, err) } - -// @Summary 系统短信日志新增 -// @Tags system_log_sms-系统短信日志 -// @Produce json -// @Param Token header string true "token" -// @Param scene body int false "场景编号" -// @Param mobile body string false "手机号码" -// @Param content body string false "发送内容" -// @Param status body int false "发送状态:[0=发送中, 1=发送成功, 2=发送失败]" -// @Param results body string false "短信结果" -// @Param send_time body int false "发送时间" -// @Success 200 {object} response.Response "成功" -// @Router /api/admin/system_log_sms/add [post] +// @Summary 系统短信日志新增 +// @Tags system_log_sms-系统短信日志 +// @Produce json +// @Param Token header string true "token" +// @Param scene body int false "场景编号" +// @Param mobile body string false "手机号码" +// @Param content body string false "发送内容" +// @Param status body int false "发送状态:[0=发送中, 1=发送成功, 2=发送失败]" +// @Param results body string false "短信结果" +// @Param send_time body int false "发送时间" +// @Success 200 {object} response.Response "成功" +// @Router /api/admin/system_log_sms/add [post] func (hd *SystemLogSmsHandler) Add(c *gin.Context) { var addReq SystemLogSmsAddReq if response.IsFailWithResp(c, util.VerifyUtil.VerifyJSON(c, &addReq)) { return } createId, e := SystemLogSmsService.Add(addReq) - response.CheckAndRespWithData(c,createId, e) + response.CheckAndRespWithData(c, createId, e) } -// @Summary 系统短信日志编辑 -// @Tags system_log_sms-系统短信日志 -// @Produce json -// @Param Token header string true "token" -// @Param id body int false "id" -// @Param scene body int false "场景编号" -// @Param mobile body string false "手机号码" -// @Param content body string false "发送内容" -// @Param status body int false "发送状态:[0=发送中, 1=发送成功, 2=发送失败]" -// @Param results body string false "短信结果" -// @Param send_time body int false "发送时间" -// @Success 200 {object} response.Response "成功" -// @Router /api/admin/system_log_sms/edit [post] + +// @Summary 系统短信日志编辑 +// @Tags system_log_sms-系统短信日志 +// @Produce json +// @Param Token header string true "token" +// @Param id body int false "id" +// @Param scene body int false "场景编号" +// @Param mobile body string false "手机号码" +// @Param content body string false "发送内容" +// @Param status body int false "发送状态:[0=发送中, 1=发送成功, 2=发送失败]" +// @Param results body string false "短信结果" +// @Param send_time body int false "发送时间" +// @Success 200 {object} response.Response "成功" +// @Router /api/admin/system_log_sms/edit [post] func (hd *SystemLogSmsHandler) Edit(c *gin.Context) { var editReq SystemLogSmsEditReq if response.IsFailWithResp(c, util.VerifyUtil.VerifyJSON(c, &editReq)) { return } - response.CheckAndRespWithData(c,editReq.Id, SystemLogSmsService.Edit(editReq)) + response.CheckAndRespWithData(c, editReq.Id, SystemLogSmsService.Edit(editReq)) } -// @Summary 系统短信日志删除 -// @Tags system_log_sms-系统短信日志 -// @Produce json -// @Param Token header string true "token" -// @Param id body int false "id" -// @Success 200 {object} response.Response "成功" -// @Router /api/admin/system_log_sms/del [post] + +// @Summary 系统短信日志删除 +// @Tags system_log_sms-系统短信日志 +// @Produce json +// @Param Token header string true "token" +// @Param id body int false "id" +// @Success 200 {object} response.Response "成功" +// @Router /api/admin/system_log_sms/del [post] func (hd *SystemLogSmsHandler) Del(c *gin.Context) { var delReq SystemLogSmsDelReq if response.IsFailWithResp(c, util.VerifyUtil.VerifyJSON(c, &delReq)) { @@ -148,23 +151,21 @@ func (hd *SystemLogSmsHandler) Del(c *gin.Context) { response.CheckAndResp(c, SystemLogSmsService.Del(delReq.Id)) } - - -// @Summary 系统短信日志导出 -// @Tags system_log_sms-系统短信日志 -// @Produce json -// @Param Token header string true "token" -// @Param scene query int false "场景编号" -// @Param mobile query string false "手机号码" -// @Param content query string false "发送内容" -// @Param status query int false "发送状态:[0=发送中, 1=发送成功, 2=发送失败]" -// @Param results query string false "短信结果" -// @Param send_time query int false "发送时间" -// @Param create_timeStart query core.TsTime false "创建时间" -// @Param create_timeEnd query core.TsTime false "创建时间" -// @Param update_timeStart query core.TsTime false "更新时间" -// @Param update_timeEnd query core.TsTime false "更新时间" -// @Router /api/admin/system_log_sms/ExportFile [get] +// @Summary 系统短信日志导出 +// @Tags system_log_sms-系统短信日志 +// @Produce json +// @Param Token header string true "token" +// @Param scene query int false "场景编号" +// @Param mobile query string false "手机号码" +// @Param content query string false "发送内容" +// @Param status query int false "发送状态:[0=发送中, 1=发送成功, 2=发送失败]" +// @Param results query string false "短信结果" +// @Param send_time query int false "发送时间" +// @Param create_timeStart query core.TsTime false "创建时间" +// @Param create_timeEnd query core.TsTime false "创建时间" +// @Param update_timeStart query core.TsTime false "更新时间" +// @Param update_timeEnd query core.TsTime false "更新时间" +// @Router /api/admin/system_log_sms/ExportFile [get] func (hd *SystemLogSmsHandler) ExportFile(c *gin.Context) { var listReq SystemLogSmsListReq if response.IsFailWithResp(c, util.VerifyUtil.VerifyQuery(c, &listReq)) { @@ -175,18 +176,36 @@ func (hd *SystemLogSmsHandler) ExportFile(c *gin.Context) { response.FailWithMsg(c, response.SystemError, "查询信息失败") return } - f, err := excel.NormalDynamicExport(res, "Sheet1", "系统短信日志", nil) + // f, err := excel.NormalDynamicExport(res, "Sheet1", "系统短信日志", nil) + // if err != nil { + // response.FailWithMsg(c, response.SystemError, "导出失败") + // return + // } + + var cols = []excel2.Col{ + {Name: "场景编号", Key: "scene", Width: 15}, + {Name: "手机号码", Key: "mobile", Width: 15}, + {Name: "发送内容", Key: "content", Width: 15}, + {Name: "发送状态", Key: "status", Width: 20}, + {Name: "短信结果", Key: "results", Width: 21}, + {Name: "发送时间", Key: "send_time", Width: 20}, + {Name: "创建时间", Key: "create_time", Width: 25}, + {Name: "更新时间", Key: "update_time", Width: 30}, + } + list := util.ConvertUtil.StructsToMaps(res) + f, err := excel2.NormalDynamicExport2(list, cols, "Sheet1", "系统短信日志") if err != nil { response.FailWithMsg(c, response.SystemError, "导出失败") return } - excel.DownLoadExcel("系统短信日志" + time.Now().Format("20060102-150405"), c.Writer, f) + + excel.DownLoadExcel("系统短信日志"+time.Now().Format("20060102-150405"), c.Writer, f) } -// @Summary 系统短信日志导入 -// @Tags system_log_sms-系统短信日志 -// @Produce json -// @Router /api/admin/system_log_sms/ImportFile [post] +// @Summary 系统短信日志导入 +// @Tags system_log_sms-系统短信日志 +// @Produce json +// @Router /api/admin/system_log_sms/ImportFile [post] func (hd *SystemLogSmsHandler) ImportFile(c *gin.Context) { file, _, err := c.Request.FormFile("file") if err != nil { @@ -203,4 +222,4 @@ func (hd *SystemLogSmsHandler) ImportFile(c *gin.Context) { err = SystemLogSmsService.ImportFile(importList) response.CheckAndResp(c, err) -} \ No newline at end of file +} diff --git a/server/admin/system_log_sms/system_log_sms_schema.go b/server/admin/system_log_sms/system_log_sms_schema.go index 24c1d85..f2c0ee2 100644 --- a/server/admin/system_log_sms/system_log_sms_schema.go +++ b/server/admin/system_log_sms/system_log_sms_schema.go @@ -55,7 +55,7 @@ type SystemLogSmsResp struct { Scene int `mapstructure:"scene" json:"scene" excel:"name:场景编号;"` // 场景编号 Mobile string `mapstructure:"mobile" json:"mobile" excel:"name:手机号码;"` // 手机号码 Content string `mapstructure:"content" json:"content" excel:"name:发送内容;"` // 发送内容 - Status int `mapstructure:"status" json:"status" excel:"name:发送状态"` // 发送状态:[0=发送中, 1=发送成功, 2=发送失败] + Status int `mapstructure:"status" json:"status" excel:"name:发送状态;"` // 发送状态:[0=发送中, 1=发送成功, 2=发送失败] Results string `mapstructure:"results" json:"results" excel:"name:短信结果;"` // 短信结果 SendTime int `mapstructure:"send_time" json:"send_time" excel:"name:发送时间;"` // 发送时间 CreateTime core.TsTime `mapstructure:"create_time" json:"create_time" excel:"name:创建时间;"` // 创建时间 diff --git a/server/admin/system_log_sms_route.go b/server/admin/system_log_sms_route.go index 7cd0b63..ac33e33 100644 --- a/server/admin/system_log_sms_route.go +++ b/server/admin/system_log_sms_route.go @@ -1,9 +1,10 @@ package admin import ( - "github.com/gin-gonic/gin" - "x_admin/middleware" "x_admin/admin/system_log_sms" + "x_admin/middleware" + + "github.com/gin-gonic/gin" ) /** @@ -39,7 +40,6 @@ INSERT INTO x_system_auth_menu (pid, menu_type, menu_name, perms,is_cache, is_sh INSERT INTO x_system_auth_menu (pid, menu_type, menu_name, perms,is_cache, is_show, is_disable, create_time, update_time) VALUES (0, 'A', '系统短信日志导入excel','admin:system_log_sms:ImportFile', 0, 1, 0, now(), now()); */ - // SystemLogSmsRoute(rg) func SystemLogSmsRoute(rg *gin.RouterGroup) { handle := system_log_sms.SystemLogSmsHandler{} @@ -48,9 +48,9 @@ func SystemLogSmsRoute(rg *gin.RouterGroup) { r.GET("/system_log_sms/list", handle.List) r.GET("/system_log_sms/listAll", handle.ListAll) r.GET("/system_log_sms/detail", handle.Detail) - r.POST("/system_log_sms/add",middleware.RecordLog("系统短信日志新增"), handle.Add) - r.POST("/system_log_sms/edit",middleware.RecordLog("系统短信日志编辑"), handle.Edit) + r.POST("/system_log_sms/add", middleware.RecordLog("系统短信日志新增"), handle.Add) + r.POST("/system_log_sms/edit", middleware.RecordLog("系统短信日志编辑"), handle.Edit) r.POST("/system_log_sms/del", middleware.RecordLog("系统短信日志删除"), handle.Del) r.GET("/system_log_sms/ExportFile", middleware.RecordLog("系统短信日志导出"), handle.ExportFile) - r.POST("/system_log_sms/ImportFile",middleware.RecordLog("系统短信日志导入"), handle.ImportFile) -} \ No newline at end of file + r.POST("/system_log_sms/ImportFile", handle.ImportFile) +} diff --git a/server/core/time.go b/server/core/time.go1 similarity index 100% rename from server/core/time.go rename to server/core/time.go1 diff --git a/server/core/time2.go b/server/core/time2.go new file mode 100644 index 0000000..4d3d828 --- /dev/null +++ b/server/core/time2.go @@ -0,0 +1,97 @@ +package core + +import ( + "database/sql/driver" + "encoding/json" + "time" + + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +const DateFormat = "2006-01-02" +const TimeFormat = "2006-01-02 15:04:05" + +// 注解:时间类型从time.Time改为string的原因是struct转interface{}时,丢弃了部分信息,导致导出excel时,时间格式不对 +// TsTime 自定义时间格式 +type TsTime string + +// 通过时间字符串生成时间戳 +// +// func ToUnix(date string) int64 { +// if date == "" { +// return 0 +// } +// tt, _ := time.ParseInLocation(TimeFormat, date, time.Local) +// return tt.Unix() +// } +func ParseTimeToTsTime(date time.Time) TsTime { + return TsTime(date.Format(TimeFormat)) +} + +func NowTime() TsTime { + return TsTime(time.Now().Format(TimeFormat)) +} + +func (tst *TsTime) UnmarshalJSON(bs []byte) error { + var date string + err := json.Unmarshal(bs, &date) + if err != nil { + return err + } + tt, _ := time.ParseInLocation(TimeFormat, date, time.Local) + *tst = TsTime( + tt.Format(TimeFormat), + ) + return nil +} + +// MarshalJSON 将TsTime类型的时间转化为JSON字符串格式 +// 返回转化后的JSON字符串和错误信息 +func (tst TsTime) MarshalJSON() ([]byte, error) { + // tt := time.Time(tst.Str).Format(TimeFormat) + tt, _ := time.Parse(TimeFormat, tst.String()) + str := tt.Format(TimeFormat) + + return json.Marshal(str) +} + +// 写入数据库gorm调用 +func (t TsTime) Value() (driver.Value, error) { + // timeStr := t.String() + // if timeStr == "0001-01-01 00:00:00" { + // return nil, nil + // } + return t.String(), nil +} + +// 读取数据gorm调用 +func (t *TsTime) Scan(v any) error { + // pt, err := time.ParseInLocation("2006-01-02 15:04:05", v.(time.Time).String(), time.Local) + if _, ok := v.(time.Time); ok { + *t = TsTime(v.(time.Time).Format(TimeFormat)) + return nil + } else { + *t = "0001-01-01 00:00:00" + return nil + } +} + +func (t TsTime) String() string { + return string(t) +} + +func (TsTime) GormDBDataType(db *gorm.DB, field *schema.Field) string { + // 使用 field.Tag、field.TagSettings 获取字段的 tag + // 查看 https://github.com/go-gorm/gorm/blob/master/schema/field.go 获取全部的选项 + + // 根据不同的数据库驱动返回不同的数据类型 + // switch db.Dialector.Name() { + // case "mysql", "sqlite": + // return "JSON" + // case "postgres": + // return "JSONB" + // } + // return "" + return "DATETIME" +} diff --git a/server/middleware/log.go b/server/middleware/log.go index c7e2460..a3f4cd7 100644 --- a/server/middleware/log.go +++ b/server/middleware/log.go @@ -96,8 +96,8 @@ func RecordLog(title string, reqTypes ...requestType) gin.HandlerFunc { err := core.GetDB().Create(&system_model.SystemLogOperate{ AdminId: adminId, Type: reqMethod, Title: title, Ip: ip, Url: urlPath, Method: method, Args: args, Error: errStr, Status: status, - StartTime: core.TsTime(startTime), - EndTime: core.TsTime(endTime), + StartTime: core.ParseTimeToTsTime(startTime), + EndTime: core.ParseTimeToTsTime(endTime), TaskTime: taskTime, }).Error response.CheckErr(err, "RecordLog recover Create err") @@ -124,8 +124,8 @@ func RecordLog(title string, reqTypes ...requestType) gin.HandlerFunc { err := core.GetDB().Create(&system_model.SystemLogOperate{ AdminId: adminId, Type: reqMethod, Title: title, Ip: ip, Url: urlPath, Method: method, Args: args, Error: errStr, Status: status, - StartTime: core.TsTime(startTime), - EndTime: core.TsTime(endTime), + StartTime: core.ParseTimeToTsTime(startTime), + EndTime: core.ParseTimeToTsTime(endTime), TaskTime: taskTime, }).Error response.CheckErr(err, "RecordLog Create err") diff --git a/server/model/monitor_project.go b/server/model/monitor_project.go index 32d2309..331fbd6 100644 --- a/server/model/monitor_project.go +++ b/server/model/monitor_project.go @@ -8,20 +8,20 @@ import ( // MonitorProject 错误项目实体 type MonitorProject struct { - Id int `gorm:"primarykey;comment:'项目id'" excel:"name:项目id;"` // 项目id + Id int `gorm:"primarykey;comment:'项目id'"` // 项目id - ProjectKey string `gorm:"comment:'项目uuid'" excel:"name:项目uuid;"` // 项目uuid + ProjectKey string `gorm:"comment:'项目uuid'"` // 项目uuid - ProjectName string `gorm:"comment:'项目名称'" excel:"name:项目名称;"` // 项目名称 + ProjectName string `gorm:"comment:'项目名称'"` // 项目名称 - ProjectType string `gorm:"comment:'项目类型go java web node php 等'" excel:"name:项目类型"` // 项目类型go java web node php 等 + ProjectType string `gorm:"comment:'项目类型go java web node php 等'"` // 项目类型go java web node php 等 IsDelete soft_delete.DeletedAt `gorm:"not null;default:0;softDelete:flag,DeletedAtField:DeleteTime;comment:'是否删除: 0=否, 1=是'"` - UpdateTime core.TsTime `gorm:"autoUpdateTime;comment:'更新时间'" excel:"name:更新时间;"` // 更新时间 + UpdateTime core.TsTime `gorm:"autoUpdateTime;comment:'更新时间'"` // 更新时间 - CreateTime core.TsTime `gorm:"autoCreateTime;comment:'创建时间'" excel:"name:创建时间;"` // 创建时间 + CreateTime core.TsTime `gorm:"autoCreateTime;comment:'创建时间'"` // 创建时间 - DeleteTime core.TsTime `gorm:"default:null;comment:'删除时间'" excel:"name:删除时间;"` // 删除时间 + DeleteTime core.TsTime `gorm:"default:null;comment:'删除时间'"` // 删除时间 } diff --git a/server/model/monitor_web.go b/server/model/monitor_web.go index de438fe..8fa1323 100644 --- a/server/model/monitor_web.go +++ b/server/model/monitor_web.go @@ -4,22 +4,22 @@ import "x_admin/core" //MonitorWeb 错误收集error实体 type MonitorWeb struct { - Id int `gorm:"primarykey;comment:'uuid'" excel:"name:uuid;"` // uuid + Id int `gorm:"primarykey;comment:'uuid'"` // uuid - ProjectKey string `gorm:"comment:'项目key'" excel:"name:项目key;"` // 项目key + ProjectKey string `gorm:"comment:'项目key'"` // 项目key - ClientId string `gorm:"comment:'sdk生成的客户端id'" excel:"name:sdk生成的客户端id;"` // sdk生成的客户端id + ClientId string `gorm:"comment:'sdk生成的客户端id'"` // sdk生成的客户端id - EventType string `gorm:"comment:'事件类型'" excel:"name:事件类型;"` // 事件类型 + EventType string `gorm:"comment:'事件类型'"` // 事件类型 - Page string `gorm:"comment:'URL地址'" excel:"name:URL地址;"` // URL地址 + Page string `gorm:"comment:'URL地址'"` // URL地址 - Message string `gorm:"comment:'错误消息'" excel:"name:错误消息;"` // 错误消息 + Message string `gorm:"comment:'错误消息'"` // 错误消息 - Stack string `gorm:"comment:'错误堆栈'" excel:"name:错误堆栈;"` // 错误堆栈 + Stack string `gorm:"comment:'错误堆栈'"` // 错误堆栈 - ClientTime core.TsTime `gorm:"comment:'客户端时间'" excel:"name:客户端时间;"` // 客户端时间 + ClientTime core.TsTime `gorm:"comment:'客户端时间'"` // 客户端时间 - CreateTime core.TsTime `gorm:"autoCreateTime;comment:'创建时间'" excel:"name:创建时间;"` // 创建时间 + CreateTime core.TsTime `gorm:"autoCreateTime;comment:'创建时间'"` // 创建时间 } diff --git a/server/model/system_log_sms.go b/server/model/system_log_sms.go index 99c15fd..9a4084a 100644 --- a/server/model/system_log_sms.go +++ b/server/model/system_log_sms.go @@ -6,13 +6,13 @@ import ( // SystemLogSms 系统短信日志实体 type SystemLogSms struct { - Id int `json:"id" gorm:"primarykey;" excel:"name:id;"` // id - Scene int `json:"scene" excel:"name:场景编号;"` // 场景编号 - Mobile string `json:"mobile" excel:"name:手机号码;"` // 手机号码 - Content string `json:"content" excel:"name:发送内容;"` // 发送内容 - Status int `json:"status" excel:"name:发送状态:[0=发送中, 1=发送成功, 2=发送失败];"` // 发送状态:[0=发送中, 1=发送成功, 2=发送失败] - Results string `json:"results" excel:"name:短信结果;"` // 短信结果 - SendTime int `mapstructure:"send_time" excel:"name:发送时间;"` // 发送时间 - CreateTime core.TsTime `json:"CreateTime" gorm:"autoCreateTime;comment:'创建时间'" excel:"name:创建时间;"` // 创建时间 - UpdateTime core.TsTime `json:"UpdateTime" gorm:"autoUpdateTime;comment:'更新时间'" excel:"name:更新时间;"` // 更新时间 + Id int `json:"id" gorm:"primarykey;"` // id + Scene int `json:"scene"` // 场景编号 + Mobile string `json:"mobile"` // 手机号码 + Content string `json:"content"` // 发送内容 + Status int `json:"status"` // 发送状态:[0=发送中, 1=发送成功, 2=发送失败] + Results string `json:"results"` // 短信结果 + SendTime int `json:"send_time" mapstructure:"send_time"` // 发送时间 + CreateTime core.TsTime `json:"CreateTime" gorm:"autoCreateTime;comment:'创建时间'"` // 创建时间 + UpdateTime core.TsTime `json:"UpdateTime" gorm:"autoUpdateTime;comment:'更新时间'"` // 更新时间 } diff --git a/server/util/array.go b/server/util/array.go index 28da4a5..788e86f 100644 --- a/server/util/array.go +++ b/server/util/array.go @@ -2,10 +2,10 @@ package util var ArrayUtil = arrayUtil{} -//arrayUtil 数组工具类 +// arrayUtil 数组工具类 type arrayUtil struct{} -//ListToTree 字典列表转树形结构 +// ListToTree 字典列表转树形结构 func (au arrayUtil) ListToTree(arr []map[string]interface{}, id string, pid string, child string) (mapList []interface{}) { mapList = []interface{}{} // 遍历以id_为key生成map @@ -29,6 +29,10 @@ func (au arrayUtil) ListToTree(arr []map[string]interface{}, id string, pid stri } pNode.(map[string]interface{})[child] = cVal continue + } else { + cVal := []interface{}{m} + pNode.(map[string]interface{})[child] = cVal + continue } } } diff --git a/server/util/convert.go b/server/util/convert.go index cbceb64..a137222 100644 --- a/server/util/convert.go +++ b/server/util/convert.go @@ -14,14 +14,15 @@ var ConvertUtil = convertUtil{} type convertUtil struct{} // StructsToMaps 将结构体转换成Map列表 -func (c convertUtil) StructsToMaps(obj interface{}) (data []map[string]interface{}) { +func (c convertUtil) StructsToMaps(from interface{}) (data []map[string]interface{}) { var objList []interface{} - err := copier.Copy(&objList, obj) + err := copier.Copy(&objList, from) if err != nil { core.Logger.Errorf("convertUtil.StructsToMaps err: err=[%+v]", err) return nil } for _, v := range objList { + // data = append(data, structs.Map(v)) data = append(data, c.StructToMap(v)) } return data @@ -29,15 +30,11 @@ func (c convertUtil) StructsToMaps(obj interface{}) (data []map[string]interface // StructToMap 结构体转换成map func (c convertUtil) StructToMap(from interface{}) map[string]interface{} { - // var y = map[string]interface{}{} - // mapstructure.Decode(from, &y) //mapstructure + // var m = map[string]interface{}{} + // mapstructure.Decode(from, &m) //mapstructure - // copier.Copy(&m, from) m, _ := convertor.StructToMap(from) // 需要tag:json - // if e != nil { - // return nil, err - // } return m } diff --git a/server/util/excel2/excel.go b/server/util/excel2/excel.go new file mode 100644 index 0000000..01acb37 --- /dev/null +++ b/server/util/excel2/excel.go @@ -0,0 +1,97 @@ +package excel2 + +import ( + "github.com/xuri/excelize/v2" +) + +type Excel struct { + F *excelize.File // excel 对象 + TitleStyle int // 表头样式 + HeadStyle int // 表头样式 + ContentStyle1 int // 主体样式1,无背景色 + ContentStyle2 int // 主体样式2,有背景色 +} + +// 初始化 +func ExcelInit() (e *Excel) { + e = &Excel{} + // excel构建 + e.F = excelize.NewFile() + // 初始化样式 + e.getTitleRowStyle() + e.getHeadRowStyle() + e.getDataRowStyle() + return e +} + +// 获取边框样式 +func getBorder() []excelize.Border { + return []excelize.Border{ // 边框 + {Type: "top", Color: "000000", Style: 1}, + {Type: "bottom", Color: "000000", Style: 1}, + {Type: "left", Color: "000000", Style: 1}, + {Type: "right", Color: "000000", Style: 1}, + } +} + +// 标题样式 +func (e *Excel) getTitleRowStyle() { + e.TitleStyle, _ = e.F.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ // 对齐方式 + Horizontal: "center", // 水平对齐居中 + Vertical: "center", // 垂直对齐居中 + }, + Fill: excelize.Fill{ // 背景颜色 + Type: "pattern", + Color: []string{"#fff2cc"}, + Pattern: 1, + }, + Font: &excelize.Font{ // 字体 + Bold: true, + Size: 16, + }, + Border: getBorder(), + }) +} + +// 列头行样式 +func (e *Excel) getHeadRowStyle() { + e.HeadStyle, _ = e.F.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ // 对齐方式 + Horizontal: "center", // 水平对齐居中 + Vertical: "center", // 垂直对齐居中 + WrapText: true, // 自动换行 + }, + Fill: excelize.Fill{ // 背景颜色 + Type: "pattern", + Color: []string{"#FDE9D8"}, + Pattern: 1, + }, + Font: &excelize.Font{ // 字体 + Bold: true, + Size: 14, + }, + Border: getBorder(), + }) +} + +// 数据行样式 +func (e *Excel) getDataRowStyle() { + style := excelize.Style{} + style.Border = getBorder() + style.Alignment = &excelize.Alignment{ + Horizontal: "center", // 水平对齐居中 + Vertical: "center", // 垂直对齐居中 + WrapText: true, // 自动换行 + } + style.Font = &excelize.Font{ + Size: 12, + } + e.ContentStyle1, _ = e.F.NewStyle(&style) + style.Fill = excelize.Fill{ // 背景颜色 + Type: "pattern", + Color: []string{"#cce7f5"}, + Pattern: 1, + } + e.ContentStyle2, _ = e.F.NewStyle(&style) +} diff --git a/server/util/excel2/excel_export.go b/server/util/excel2/excel_export.go new file mode 100644 index 0000000..bc29478 --- /dev/null +++ b/server/util/excel2/excel_export.go @@ -0,0 +1,219 @@ +package excel2 + +import ( + "errors" + "fmt" + "net/http" + "reflect" + "sort" + "strconv" + "strings" + + "github.com/xuri/excelize/v2" +) + +// GetExcelColumnName 根据列数生成 Excel 列名 +func GetExcelColumnName(columnNumber int) string { + columnName := "" + for columnNumber > 0 { + remainder := (columnNumber - 1) % 26 + columnName = string(rune('A'+remainder)) + columnName + columnNumber = (columnNumber - 1) / 26 + } + return columnName +} + +// NormalDynamicExport 导出excel +// +// 需要在传入的结构体中的字段加上tag:excel:"title:列头名称;index:列下标(从0开始);" +// +// list 要导出的数据 +// sheet 文档 +// title 标题 +// changeHead map[string]string{"Id": "账号", "Name": "真实姓名"} +func NormalDynamicExport(list interface{}, sheet string, title string, changeHead map[string]string) (file *excelize.File, err error) { + e := ExcelInit() + err = ExportExcel(sheet, title, list, changeHead, e) + if err != nil { + return + } + return e.F, err +} + +// ExportExcel excel导出 +func ExportExcel(sheet, title string, list interface{}, changeHead map[string]string, e *Excel) (err error) { + index, _ := e.F.GetSheetIndex(sheet) + if index < 0 { // 如果sheet名称不存在 + e.F.NewSheet(sheet) + } + // 构造excel表格 + // 取目标对象的元素类型、字段类型和 tag + dataValue := reflect.ValueOf(list) + // 判断数据的类型 + if dataValue.Kind() != reflect.Slice { + err = errors.New("invalid data type") + return + } + // 构造表头 + endColName, dataRow, err := normalBuildTitle(e, sheet, title, changeHead, dataValue) + if err != nil { + return + } + // 构造数据行 + err = normalBuildDataRow(e, sheet, endColName, dataRow, dataValue) + return +} + +// 构造表头(endColName 最后一列的列名 dataRow 数据行开始的行号) +func normalBuildTitle(e *Excel, sheet, title string, changeHead map[string]string, dataValue reflect.Value) (endColName string, dataRow int, err error) { + dataType := dataValue.Type().Elem() // 获取导入目标对象的类型信息 + var exportTitle []ExcelTag // 遍历目标对象的字段 + for i := 0; i < dataType.NumField(); i++ { + var excelTag ExcelTag + field := dataType.Field(i) // 获取字段信息和tag + tag := field.Tag.Get(ExcelTagKey) + if tag == "" { // 如果非导出则跳过 + continue + } + + err = excelTag.GetTag(tag) + if err != nil { + return + } + // 更改指定字段的表头标题 + if changeHead != nil && changeHead[field.Name] != "" { + excelTag.Name = changeHead[field.Name] + } + exportTitle = append(exportTitle, excelTag) + } + // 排序 + sort.Slice(exportTitle, func(i, j int) bool { + return exportTitle[i].Index < exportTitle[j].Index + }) + var titleRowData []interface{} // 列头行 + for i, colTitle := range exportTitle { + endColName := GetExcelColumnName(i + 1) + if colTitle.Width > 0 { // 根据给定的宽度设置列宽 + _ = e.F.SetColWidth(sheet, endColName, endColName, float64(colTitle.Width)) + } else { + _ = e.F.SetColWidth(sheet, endColName, endColName, float64(20)) // 默认宽度为20 + } + titleRowData = append(titleRowData, colTitle.Name) + } + endColName = GetExcelColumnName(len(titleRowData)) // 根据列数生成 Excel 列名 + if title != "" { + dataRow = 3 // 如果有title,那么从第3行开始就是数据行,第1行是title,第2行是表头 + e.F.SetCellValue(sheet, "A1", title) + e.F.MergeCell(sheet, "A1", endColName+"1") // 合并标题单元格 + e.F.SetCellStyle(sheet, "A1", endColName+"1", e.TitleStyle) + e.F.SetRowHeight(sheet, 1, float64(30)) // 第一行行高 + e.F.SetRowHeight(sheet, 2, float64(30)) // 第二行行高 + e.F.SetCellStyle(sheet, "A2", endColName+"2", e.HeadStyle) + if err = e.F.SetSheetRow(sheet, "A2", &titleRowData); err != nil { + return + } + } else { + dataRow = 2 // 如果没有title,那么从第2行开始就是数据行,第1行是表头 + e.F.SetRowHeight(sheet, 1, float64(30)) + e.F.SetCellStyle(sheet, "A1", endColName+"1", e.HeadStyle) + if err = e.F.SetSheetRow(sheet, "A1", &titleRowData); err != nil { + return + } + } + return +} + +// 构造数据行 +func normalBuildDataRow(e *Excel, sheet, endColName string, row int, dataValue reflect.Value) (err error) { + //实时写入数据 + for i := 0; i < dataValue.Len(); i++ { + startCol := fmt.Sprintf("A%d", row) + endCol := fmt.Sprintf("%s%d", endColName, row) + item := dataValue.Index(i) + typ := item.Type() + num := item.NumField() + var exportRow []ExcelTag + maxLen := 0 // 记录这一行中,数据最多的单元格的值的长度 + //遍历结构体的所有字段 + for j := 0; j < num; j++ { + dataField := typ.Field(j) //获取到struct标签,需要通过reflect.Type来获取tag标签的值 + tagVal := dataField.Tag.Get(ExcelTagKey) + if tagVal == "" { // 如果非导出则跳过 + continue + } + + var dataCol ExcelTag + err = dataCol.GetTag(tagVal) + fieldData := item.FieldByName(dataField.Name) // 取字段值 + if fieldData.Type().String() == "string" { // string类型的才计算长度 + rwsTemp := fieldData.Len() // 当前单元格内容的长度 + if rwsTemp > maxLen { //这里取每一行中的每一列字符长度最大的那一列的字符 + maxLen = rwsTemp + } + } + // 替换 + if dataCol.Replace != "" { + split := strings.Split(dataCol.Replace, ",") + for j := range split { + s := strings.Split(split[j], "_") // 根据下划线进行分割,格式:需要替换的内容_替换后的内容 + value := fieldData.String() + if strings.Contains(fieldData.Type().String(), "int") { + value = strconv.Itoa(int(fieldData.Int())) + } else if fieldData.Type().String() == "bool" { + value = strconv.FormatBool(fieldData.Bool()) + } else if strings.Contains(fieldData.Type().String(), "float") { + value = strconv.FormatFloat(fieldData.Float(), 'f', -1, 64) + } + if s[0] == value { + dataCol.Value = s[1] + } + } + } else { + dataCol.Value = fieldData + } + if err != nil { + return + } + exportRow = append(exportRow, dataCol) + } + // 排序 + sort.Slice(exportRow, func(i, j int) bool { + return exportRow[i].Index < exportRow[j].Index + }) + var rowData []interface{} // 数据列 + for _, colTitle := range exportRow { + rowData = append(rowData, colTitle.Value) + } + if row%2 == 0 { + _ = e.F.SetCellStyle(sheet, startCol, endCol, e.ContentStyle2) + } else { + _ = e.F.SetCellStyle(sheet, startCol, endCol, e.ContentStyle1) + } + if maxLen > 25 { // 自适应行高 + d := maxLen / 25 + f := 25 * d + _ = e.F.SetRowHeight(sheet, row, float64(f)) + } else { + _ = e.F.SetRowHeight(sheet, row, float64(25)) // 默认行高25 + } + if err = e.F.SetSheetRow(sheet, startCol, &rowData); err != nil { + return + } + row++ + } + return +} + +// 下载 +func DownLoadExcel(fileName string, res http.ResponseWriter, file *excelize.File) { + // 设置响应头 + res.Header().Set("Content-Type", "text/html; charset=UTF-8") + res.Header().Set("Content-Type", "application/octet-stream") + res.Header().Set("Content-Disposition", "attachment; filename="+fileName+".xlsx") + res.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") + err := file.Write(res) // 写入Excel文件内容到响应体 + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/server/util/excel2/excel_export_2.go b/server/util/excel2/excel_export_2.go new file mode 100644 index 0000000..3f5b3b2 --- /dev/null +++ b/server/util/excel2/excel_export_2.go @@ -0,0 +1,173 @@ +package excel2 + +import ( + "fmt" + "net/http" + + "github.com/xuri/excelize/v2" +) + +type Col struct { + Name string + Key string + Width int +} + +// GetExcelColumnName2 根据列数生成 Excel 列名 +func GetExcelColumnName2(columnNumber int) string { + columnName := "" + for columnNumber > 0 { + remainder := (columnNumber - 1) % 26 + columnName = string(rune('A'+remainder)) + columnName + columnNumber = (columnNumber - 1) / 26 + } + return columnName +} + +// NormalDynamicExport 导出excel +// +// 需要在传入的结构体中的字段加上tag:excel:"title:列头名称;index:列下标(从0开始);" +// +// list 要导出的数据 +// +// cols 列 +// +// sheet 文档 +// title 标题 +func NormalDynamicExport2(lists []map[string]interface{}, cols []Col, sheet string, title string) (file *excelize.File, err error) { + e := ExcelInit() + err = ExportExcel2(sheet, title, lists, cols, e) + if err != nil { + return + } + return e.F, err +} + +// ExportExcel2 excel导出 +func ExportExcel2(sheet, title string, lists []map[string]interface{}, cols []Col, e *Excel) (err error) { + index, _ := e.F.GetSheetIndex(sheet) + if index < 0 { // 如果sheet名称不存在 + e.F.NewSheet(sheet) + } + // 构造表头 + endColName, startDataRow, err := normalBuildTitle2(e, sheet, title, cols) + if err != nil { + return + } + // 构造数据行 + err = normalBuildDataRow2(e, sheet, endColName, startDataRow, lists, cols) + return +} + +// 构造表头(endColName 最后一列的列名 startDataRow 数据行开始的行号) +func normalBuildTitle2(e *Excel, sheet, title string, cols []Col) (endColName string, startDataRow int, err error) { + var titleRowData []interface{} // 列头行 + for i, colTitle := range cols { + endColName := GetExcelColumnName2(i + 1) + if colTitle.Width > 0 { // 根据给定的宽度设置列宽 + _ = e.F.SetColWidth(sheet, endColName, endColName, float64(colTitle.Width)) + } else { + _ = e.F.SetColWidth(sheet, endColName, endColName, float64(20)) // 默认宽度为20 + } + titleRowData = append(titleRowData, colTitle.Name) + } + endColName = GetExcelColumnName2(len(titleRowData)) // 根据列数生成 Excel 列名 + if title != "" { + startDataRow = 3 // 如果有title,那么从第3行开始就是数据行,第1行是title,第2行是表头 + e.F.SetCellValue(sheet, "A1", title) + e.F.MergeCell(sheet, "A1", endColName+"1") // 合并标题单元格 + e.F.SetCellStyle(sheet, "A1", endColName+"1", e.TitleStyle) + e.F.SetRowHeight(sheet, 1, float64(30)) // 第一行行高 + e.F.SetRowHeight(sheet, 2, float64(30)) // 第二行行高 + e.F.SetCellStyle(sheet, "A2", endColName+"2", e.HeadStyle) + if err = e.F.SetSheetRow(sheet, "A2", &titleRowData); err != nil { + return + } + } else { + startDataRow = 2 // 如果没有title,那么从第2行开始就是数据行,第1行是表头 + e.F.SetRowHeight(sheet, 1, float64(30)) + e.F.SetCellStyle(sheet, "A1", endColName+"1", e.HeadStyle) + if err = e.F.SetSheetRow(sheet, "A1", &titleRowData); err != nil { + return + } + } + return +} + +// 构造数据行 +func normalBuildDataRow2(e *Excel, sheet, endColName string, startDataRow int, lists []map[string]interface{}, cols []Col) (err error) { + //实时写入数据 + for i := 0; i < len(lists); i++ { + startCol := fmt.Sprintf("A%d", startDataRow) + endCol := fmt.Sprintf("%s%d", endColName, startDataRow) + + var rowData []interface{} // 数据列 + + list := lists[i] + for j := 0; j < len(cols); j++ { + col := cols[j] + val := list[col.Key] + // switch val.(type) { + + // default: + // v, _ := json.Marshal(list[col.Key]) + // rowData = append(rowData, v) + // } + rowData = append(rowData, val) + } + + // // 替换 + // if dataCol.Replace != "" { + // split := strings.Split(dataCol.Replace, ",") + // for j := range split { + // s := strings.Split(split[j], "_") // 根据下划线进行分割,格式:需要替换的内容_替换后的内容 + // value := fieldData.String() + // if strings.Contains(fieldData.Type().String(), "int") { + // value = strconv.Itoa(int(fieldData.Int())) + // } else if fieldData.Type().String() == "bool" { + // value = strconv.FormatBool(fieldData.Bool()) + // } else if strings.Contains(fieldData.Type().String(), "float") { + // value = strconv.FormatFloat(fieldData.Float(), 'f', -1, 64) + // } + // if s[0] == value { + // dataCol.Value = s[1] + // } + // } + // } else { + // dataCol.Value = fieldData + // } + // if err != nil { + // return + // } + // exportRow = append(exportRow, dataCol) + // } + + if startDataRow%2 == 0 { + _ = e.F.SetCellStyle(sheet, startCol, endCol, e.ContentStyle2) + } else { + _ = e.F.SetCellStyle(sheet, startCol, endCol, e.ContentStyle1) + } + + _ = e.F.SetRowHeight(sheet, startDataRow, float64(25)) // 默认行高25 + + if err = e.F.SetSheetRow(sheet, startCol, &rowData); err != nil { + return + } + startDataRow++ + } + return +} + +// 下载 +func DownLoadExcel2(fileName string, res http.ResponseWriter, file *excelize.File) { + // 设置响应头 + res.Header().Set("Content-Type", "text/html; charset=UTF-8") + res.Header().Set("Content-Type", "application/octet-stream") + res.Header().Set("Content-Disposition", "attachment; filename="+fileName+".xlsx") + res.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") + err := file.Write(res) // 写入Excel文件内容到响应体 + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/server/util/excel2/excel_import.go b/server/util/excel2/excel_import.go new file mode 100644 index 0000000..0c4e6c4 --- /dev/null +++ b/server/util/excel2/excel_import.go @@ -0,0 +1,176 @@ +package excel2 + +import ( + "bytes" + "errors" + "fmt" + "io" + "mime/multipart" + "reflect" + "strconv" + + "github.com/xuri/excelize/v2" +) + +func GetExcelData(file multipart.File, dst interface{}) (err error) { + // 创建缓冲区 + buf := new(bytes.Buffer) + + // 将文件内容复制到缓冲区 + _, err = io.Copy(buf, file) + if err != nil { + fmt.Println(err) + // c.String(http.StatusInternalServerError, "读取失败") + err = errors.New("读取失败") + return + } + // 创建Excel文件对象 + f, err := excelize.OpenReader(bytes.NewReader(buf.Bytes())) + if err != nil { + fmt.Println(err) + // c.String(http.StatusInternalServerError, "Excel读取失败") + err = errors.New("Excel读取失败") + return + } + err = ImportExcel(f, dst, 1, 2) + // if err != nil { + // fmt.Println(err) + // } + return err +} + +// ImportExcel 导入数据(单个sheet) +// 需要在传入的结构体中的字段加上tag:excel:"title:列头名称;" +// f 获取到的excel对象、dst 导入目标对象【传指针】 +// headIndex 表头的索引,从0开始(用于获取表头名字) +// startRow 头行行数(从第startRow+1行开始扫) +func ImportExcel(f *excelize.File, dst interface{}, headIndex, startRow int) (err error) { + sheetName := f.GetSheetName(0) // 单个sheet时,默认读取第一个sheet + err = importData(f, dst, sheetName, headIndex, startRow) + return +} + +// ImportBySheet 导入数据(读取指定sheet)sheetName Sheet名称 +func ImportBySheet(f *excelize.File, dst interface{}, sheetName string, headIndex, startRow int) (err error) { + // 当需要读取多个sheet时,可以通过下面的方式,来调用 ImportBySheet 这个函数 + //sheetList := f.GetSheetList() + //for _, sheetName := range sheetList { + // ImportBySheet(f,dst,sheetName,headIndex,startRow) + //} + err = importData(f, dst, sheetName, headIndex, startRow) + return +} + +// 获取在数组中得下标 +func GetIndex(items []string, item string) int { + for i, v := range items { + if v == item { + return i + } + } + return -1 +} + +// 判断数组中是否包含指定元素 +// func IsContain(items interface{}, item interface{}) bool { +// switch items.(type) { +// case []int: +// intArr := items.([]int) +// for _, value := range intArr { +// if value == item.(int) { +// return true +// } +// } +// case []string: +// strArr := items.([]string) +// for _, value := range strArr { +// if value == item.(string) { +// return true +// } +// } +// default: +// return false +// } +// return false +// } + +// 解析数据 +func importData(f *excelize.File, dst interface{}, sheetName string, headIndex, startRow int) (err error) { + rows, err := f.GetRows(sheetName) // 获取所有行 + if err != nil { + err = errors.New(sheetName + "工作表不存在") + return + } + dataValue := reflect.ValueOf(dst) // 取目标对象的元素类型、字段类型和 tag + // 判断数据的类型 + if dataValue.Kind() != reflect.Ptr || dataValue.Elem().Kind() != reflect.Slice { + err = errors.New("invalid data type") + } + heads := []string{} // 表头 + dataType := dataValue.Elem().Type().Elem() // 获取导入目标对象的类型信息 + // 遍历行,解析数据并填充到目标对象中 + for rowIndex, row := range rows { + if rowIndex == headIndex { + heads = row + } + if rowIndex < startRow { // 跳过头行 + continue + } + newData := reflect.New(dataType).Elem() // 创建新的目标对象 + // 遍历目标对象的字段 + for i := 0; i < dataType.NumField(); i++ { + // 这里要用构造函数,构造函数里指定了Index默认值为-1,当目标结构体的tag没有指定index的话,那么 excelTag.Index 就一直为0 + // 那么 row[excelizeIndex] 就始终是 row[0],始终拿的是第一列的数据 + var excelTag = NewExcelTag() + field := dataType.Field(i) // 获取字段信息和tag + tag := field.Tag.Get(ExcelTagKey) + if tag == "" { // 如果tag不存在,则跳过 + continue + } + err = excelTag.GetTag(tag) + if err != nil { + return + } + cellValue := "" + if excelTag.Index >= 0 { // 当tag里指定了index时,根据这个index来拿数据 + excelizeIndex := excelTag.Index // 解析tag的值 + if excelizeIndex >= len(row) { // 防止下标越界 + continue + } + cellValue = row[excelizeIndex] // 获取单元格的值 + } else { // 否则根据表头名称来拿数据 + var index = GetIndex(heads, excelTag.Name) + if index != -1 { + if index >= len(row) { // 防止下标越界 + continue + } + cellValue = row[index] // 获取单元格的值 + } + } + fmt.Println("Type.Name:", field.Type.Name(), field.Type.Kind()) + // 根据字段类型设置值 + switch field.Type.Kind() { + case reflect.Int: + v, _ := strconv.ParseInt(cellValue, 10, 64) + newData.Field(i).SetInt(v) + case reflect.Int64: + v, _ := strconv.ParseInt(cellValue, 10, 64) + newData.Field(i).SetInt(v) + case reflect.Uint: + v, _ := strconv.ParseUint(cellValue, 10, 64) + newData.Field(i).SetUint(v) + case reflect.Uint8: + v, _ := strconv.ParseUint(cellValue, 10, 64) + newData.Field(i).SetUint(v) + case reflect.Uint16: + v, _ := strconv.ParseUint(cellValue, 10, 64) + newData.Field(i).SetUint(v) + case reflect.String: + newData.Field(i).SetString(cellValue) + } + } + // 将新的目标对象添加到导入目标对象的slice中 + dataValue.Elem().Set(reflect.Append(dataValue.Elem(), newData)) + } + return +} diff --git a/server/util/excel2/excel_tag.go b/server/util/excel2/excel_tag.go new file mode 100644 index 0000000..ce2b0df --- /dev/null +++ b/server/util/excel2/excel_tag.go @@ -0,0 +1,74 @@ +package excel2 + +import ( + "errors" + "regexp" + "strconv" + "strings" +) + +// 定义正则表达式模式 +const ( + ExcelTagKey = "excel" + Pattern = "name:(.*?);|index:(.*?);|width:(.*?);|needMerge:(.*?);|replace:(.*?);" +) + +type ExcelTag struct { + Value interface{} + Name string // 表头标题 + Index int // 列下标(从0开始) + Width int // 列宽 + NeedMerge bool // 是否需要合并 + Replace string // 替换(需要替换的内容_替换后的内容。比如:1_未开始 ==> 表示1替换为未开始) +} + +// 构造函数,返回一个带有默认值的 ExcelTag 实例 +func NewExcelTag() ExcelTag { + return ExcelTag{ + // 导入时会根据这个下标来拿单元格的值,当目标结构体字段没有设置index时, + // 解析字段tag值时Index没读到就一直默认为0,拿单元格的值时,就始终拿的是第一列的值 + Index: -1, // 设置 Index 的默认值为 -1 + } +} + +// 读取字段tag值 +func (e *ExcelTag) GetTag(tag string) (err error) { + // 编译正则表达式 + re := regexp.MustCompile(Pattern) + matches := re.FindAllStringSubmatch(tag, -1) + if len(matches) > 0 { + for _, match := range matches { + for i, val := range match { + if i != 0 && val != "" { + e.setValue(match, val) + } + } + } + } else { + err = errors.New("未匹配到值") + return + } + return +} + +// 设置ExcelTag 对应字段的值 +func (e *ExcelTag) setValue(tag []string, value string) { + if strings.Contains(tag[0], "name") { + e.Name = value + } + if strings.Contains(tag[0], "index") { + v, _ := strconv.ParseInt(value, 10, 8) + e.Index = int(v) + } + if strings.Contains(tag[0], "width") { + v, _ := strconv.ParseInt(value, 10, 8) + e.Width = int(v) + } + if strings.Contains(tag[0], "needMerge") { + v, _ := strconv.ParseBool(value) + e.NeedMerge = v + } + if strings.Contains(tag[0], "replace") { + e.Replace = value + } +} diff --git a/server/util/excel2/excel_test.go b/server/util/excel2/excel_test.go new file mode 100644 index 0000000..93f2bed --- /dev/null +++ b/server/util/excel2/excel_test.go @@ -0,0 +1,76 @@ +// 测试源码的文件名以 _test.go 结尾。 +// 测试函数的函数名以 Test 开头。 +// 函数签名为 func (t *testing.T)。 + +// https://blog.csdn.net/weixin_43165220/article/details/132939884 + +package excel2 + +import ( + "fmt" + "testing" + + "github.com/xuri/excelize/v2" +) + +type Test struct { + Id string `excel:"name:用户账号;"` + Name string `excel:"name:用户姓名;index:1;"` + Email string `excel:"name:用户邮箱;width:25;"` + Com string `excel:"name:所属公司;"` + Dept string `excel:"name:所在部门;"` + RoleKey string `excel:"name:角色代码;"` + RoleName string `excel:"name:角色名称;replace:1_超级管理员,2_普通用户;"` + Remark string `excel:"name:备注;width:40;"` +} + +// 导出 +func TestExport(t *testing.T) { + var testList = []Test{ + {"fuhua", "符华", "fuhua@123.com", "太虚剑派", "开发部", "CJGLY", "1", "备注备注"}, + {"baiye", "白夜", "baiye@123.com", "天命科技有限公司", "执行部", "PTYG", "2", ""}, + {"chiling", "炽翎", "chiling@123.com", "太虚剑派", "行政部", "PTYG", "2", "备注备注备注备注"}, + {"yunmo", "云墨", "yunmo@123.com", "太虚剑派", "财务部", "CJGLY", "1", ""}, + {"yuelun", "月轮", "yuelun@123.com", "天命科技有限公司", "执行部", "CJGLY", "1", ""}, + {"xunyu", "迅羽", + "xunyu@123.com哈哈哈哈哈哈哈哈这里是最大行高测试哈哈哈哈哈哈哈哈这11111111111里是最大行高测试哈哈哈哈哈哈哈哈这里是最大行高测试", + "天命科技有限公司", "开发部", "PTYG", "2", + "备注备注备注备注com哈哈哈哈哈哈哈哈这里是最大行高测试哈哈哈哈哈哈哈哈这里是最大行高测试哈哈哈哈哈哈哈哈这里是最大行高测里是最大行高测试哈哈哈哈哈哈哈哈这里是最大行高测试"}, + } + changeHead := map[string]string{"Id": "账号", "Name": "真实姓名"} + //f, err := excel.NormalExport(testList, "Sheet1", "用户信息", "Id,Email,", true, true, changeHead) + f, err := NormalDynamicExport(testList, "Sheet1", "用户信息", changeHead) + if err != nil { + fmt.Println(err) + return + } + f.Path = "./测试.xlsx" + if err := f.Save(); err != nil { + fmt.Println(err) + return + } +} + +// 导入 +func TestImports(t *testing.T) { + f, err := excelize.OpenFile("./测试.xlsx") + if err != nil { + fmt.Println("文件打开失败") + } + importList := []Test{} + err = ImportExcel(f, &importList, 1, 2) + if err != nil { + fmt.Println(err) + } + for _, t := range importList { + fmt.Println(t) + } +} + +func TestGetExcelColumnName(t *testing.T) { + for i := 0; i < 100; i++ { + var col = GetExcelColumnName(i) + fmt.Println("col:", col) + } + +} diff --git a/server/util/excel2/测试.xlsx b/server/util/excel2/测试.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..65ff8d75e494e3a7e7fb4427da4ebd19af4ad905 GIT binary patch literal 6892 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 a<^Nmu5Or+S;EsldkGg_Un{4=Vzy1eM1gp;g literal 0 HcmV?d00001