diff --git a/build/database/std/install/sys_config.sql b/build/database/std/install/sys_config.sql index 6281ea96..c3613c57 100644 --- a/build/database/std/install/sys_config.sql +++ b/build/database/std/install/sys_config.sql @@ -46,7 +46,8 @@ INSERT INTO `sys_config` VALUES (28, 'config.sys.i18nOpen', 'sys.i18n.open', 'tr INSERT INTO `sys_config` VALUES (29, 'config.sys.i18nDefault', 'sys.i18n.default', 'en_US', 'Y', '0', 'system', 1704960008300, 'system', 1704960008300, 'config.sys.i18nDefaultRemark'); INSERT INTO `sys_config` VALUES (30, 'config.sys.lockTime', 'sys.lockTime', '0', 'Y', '0', 'system', 1704960008300, 'system', 1704960008300, 'config.sys.lockTimeRemark'); INSERT INTO `sys_config` VALUES (31, 'config.sys.homePage', 'sys.homePage', 'configManage/neOverview/index', 'Y', '0', 'system', 1704960008300, 'system', 1704960008300, 'config.sys.homePageRemark'); -INSERT INTO `sys_config` VALUES (32, 'config.sys.exportTable', 'sys.exportTable', 'B1n9hW6Z2S2wZw4MVPAX6Q4wCuyWKdMk+qH1ZKqpLJxwvq2FBRgAT6WWw+j6O+ExHIJhpJ3XCpMBoiNN/RkW6EPurmqM82gnXWUIf/s6gk7OWrhdvQDD2jjNVBkLCmPLEH3ZLdgnQOZOePA7WyUdXA==', 'Y', '0', 'system', 1704960008300, 'system', 1704960008300, 'config.sys.exportTableRemark'); +INSERT INTO `sys_config` VALUES (32, 'config.neData.exportTableFTP', 'neData.exportTableFTP', 'VXEECeDMoYhX29pqsb753ecJOnPfxB6XrEa9QdUrRqwKI7EmJei5HlvehvL+wL0Osjo3Y2Qs7ADA6eL3SrisiVXAVVXv38KMhvcSU9eaAzl/jrY4ahsq6a/eSbzxFDgE21US7/YnsyDRG7eGAc7W5Q==', 'Y', '0', 'system', 1704960008300, 'system', 1704960008300, 'config.neData.exportTableFTPRemark'); +INSERT INTO `sys_config` VALUES (33, 'config.ne.neConfigBackupFTP', 'ne.neConfigBackupFTP', 'DMb1TFtE/4yBjh/5HX41sMjkaBjIA/Wfqj+w/T0QC6zmYIA/sEHs4THOluwNyR0EEupRQWJI0xY9kUomhynu/lDd8QDtCWsKpFhzXyBofL1qAvruSnJMGP3uYrgFJGqaQ4pBGQpndyUvjiJE80Mo+w==', 'Y', '0', 'system', 1744265760822, 'system', 1744265760822, 'config.ne.neConfigBackupFTPRemark'); UNLOCK TABLES; diff --git a/build/database/std/install/sys_i18n.sql b/build/database/std/install/sys_i18n.sql index 7d9ea644..8c2a5896 100644 --- a/build/database/std/install/sys_i18n.sql +++ b/build/database/std/install/sys_i18n.sql @@ -87,8 +87,8 @@ INSERT INTO `sys_i18n` VALUES (69, 'menu.trace.task', '网元跟踪任务', 'NE INSERT INTO `sys_i18n` VALUES (70, 'menu.trace.analysis', '网元跟踪数据', 'NE Trace Task Data'); INSERT INTO `sys_i18n` VALUES (71, 'menu.trace.pcap', '信令抓包', 'Signaling Capture'); INSERT INTO `sys_i18n` VALUES (72, 'menu.fault', '监控', 'Monitor'); -INSERT INTO `sys_i18n` VALUES (73, 'config.sys.exportTable', '备份文件FTP服务', 'Backup file FTP service'); -INSERT INTO `sys_i18n` VALUES (74, 'config.sys.exportTableRemark', '请通过导出列表页面进行设置FTP信息', 'Please set the FTP information through the export list page.'); +INSERT INTO `sys_i18n` VALUES (73, 'config.neData.exportTableFTP', '网元数据导出备份文件FTP服务', 'NeData Backup file FTP service'); +INSERT INTO `sys_i18n` VALUES (74, 'config.neData.exportTableFTPRemark', '请通过网元数据导出列表页面进行设置FTP信息', 'Please set the FTP information through the export list page.'); INSERT INTO `sys_i18n` VALUES (75, 'menu.ueUser.onlineIMSRemark', 'IMS在线用户菜单', 'IMS Online User Menu'); INSERT INTO `sys_i18n` VALUES (76, 'menu.ueUser.onlineUERemark', 'UE在线信息菜单', 'UE Online Information Menu'); INSERT INTO `sys_i18n` VALUES (77, 'menu.ueUser.base5GRemark', '5G基站信息菜单', '5G Base Station Information Menu'); @@ -691,5 +691,7 @@ INSERT INTO `sys_i18n` VALUES (673, 'config.sys.user.passwdExpireRemark', '数 INSERT INTO `sys_i18n` VALUES (674, 'config.sys.user.passwdNotAllowedHistory', '用户管理-不允许使用最近密码次数', 'User Management-Not Allowed Recent Passwords'); INSERT INTO `sys_i18n` VALUES (675, 'config.sys.user.passwdNotAllowedHistoryRemark', '创建新密码不等于之前使用的x次中的密码', 'Creating a new password that is not equal to the previously used password in x times'); INSERT INTO `sys_i18n` VALUES (676, 'login.errPasswdHistory', '不允许使用最近密码', 'Recent passwords not allowed'); +INSERT INTO `sys_i18n` VALUES (677, 'config.ne.neConfigBackupFTP', '配置文件备份FTP服务', 'NE Config Backup file FTP service'); +INSERT INTO `sys_i18n` VALUES (678, 'config.ne.neConfigBackupFTPRemark', '请通过配置文件备份页面进行设置FTP信息', 'Please set the FTP information through the configuration file backup page.'); -- Dump completed on 2025-02-14 15:26:56 diff --git a/features/lm/file_export/controller.go b/features/lm/file_export/controller.go index ab2f506f..0fc8ee3c 100644 --- a/features/lm/file_export/controller.go +++ b/features/lm/file_export/controller.go @@ -4,19 +4,16 @@ import ( "encoding/json" "net/http" "os" - "path" "path/filepath" "be.ems/lib/file" "be.ems/lib/log" "be.ems/lib/services" - "be.ems/src/framework/config" "be.ems/src/framework/database/db" "be.ems/src/framework/i18n" "be.ems/src/framework/reqctx" "be.ems/src/framework/resp" "be.ems/src/framework/ssh" - "be.ems/src/framework/utils/crypto" systemService "be.ems/src/modules/system/service" "github.com/gin-gonic/gin" ) @@ -161,23 +158,16 @@ func (m *SysJob) SetFTPConfig(c *gin.Context) { } // 获取配置 - cfg := systemService.NewSysConfig.FindByKey("sys.exportTable") + cfg := systemService.NewSysConfig.FindByKey("neData.exportTableFTP") if cfg.ConfigId > 0 { - // 加密body - appKey := config.Get("aes.appKey").(string) byteData, err := json.Marshal(body) if err != nil { c.JSON(200, resp.ErrMsg(err.Error())) return } - bodyEn, err := crypto.AESEncryptBase64(string(byteData), appKey) - if err != nil { - c.JSON(200, resp.ErrMsg(err.Error())) - return - } - // 更新 - cfg.ConfigValue = bodyEn - systemService.NewSysConfig.Update(cfg) + cfg.ConfigValue = string(byteData) + cfg.UpdateBy = reqctx.LoginUserToUserName(c) + systemService.NewSysConfig.UpdateEncryptValue(cfg) } c.JSON(200, resp.Ok(nil)) @@ -187,15 +177,8 @@ func (m *SysJob) SetFTPConfig(c *gin.Context) { // GET /table/ftp func (m *SysJob) GetFTPConfig(c *gin.Context) { // 获取配置 - cfg := systemService.NewSysConfig.FindByKey("sys.exportTable") + cfg := systemService.NewSysConfig.FindByKeyDecryptValue("neData.exportTableFTP") if cfg.ConfigId > 0 { - // 解密body - appKey := config.Get("aes.appKey").(string) - bodyDe, err := crypto.AESDecryptBase64(cfg.ConfigValue, appKey) - if err != nil { - c.JSON(200, resp.ErrMsg(err.Error())) - return - } var body struct { Password string `json:"password" ` Username string `json:"username" binding:"required"` @@ -204,8 +187,7 @@ func (m *SysJob) GetFTPConfig(c *gin.Context) { Enable bool `json:"enable"` Dir string `json:"dir" binding:"required"` } - err = json.Unmarshal([]byte(bodyDe), &body) - if err != nil { + if err := json.Unmarshal([]byte(cfg.ConfigValue), &body); err != nil { c.JSON(200, resp.ErrMsg(err.Error())) return } @@ -246,17 +228,9 @@ func (m *SysJob) PutFTP(c *gin.Context) { Enable bool `json:"enable"` Dir string `json:"dir" binding:"required"` } - cfg := systemService.NewSysConfig.FindByKey("sys.exportTable") + cfg := systemService.NewSysConfig.FindByKeyDecryptValue("neData.exportTableFTP") if cfg.ConfigId > 0 { - // 解密body - appKey := config.Get("aes.appKey").(string) - bodyDe, err := crypto.AESDecryptBase64(cfg.ConfigValue, appKey) - if err != nil { - c.JSON(200, resp.ErrMsg(err.Error())) - return - } - err = json.Unmarshal([]byte(bodyDe), &cfgData) - if err != nil { + if err := json.Unmarshal([]byte(cfg.ConfigValue), &cfgData); err != nil { c.JSON(200, resp.ErrMsg(err.Error())) return } @@ -287,7 +261,7 @@ func (m *SysJob) PutFTP(c *gin.Context) { } defer sftpClient.Close() // 远程文件 - remotePath := filepath.Join(cfgData.Dir, path.Base(body.FilePath), body.FileName) + remotePath := filepath.Join(cfgData.Dir, "/backup", filepath.Base(localFilePath)) // 复制到远程 if err = sftpClient.CopyFileLocalToRemote(localFilePath, remotePath); err != nil { c.JSON(200, resp.ErrMsg("error uploading file")) diff --git a/src/modules/crontask/processor/exportTable/exportTable.go b/src/modules/crontask/processor/exportTable/exportTable.go index 51a3a7d8..e445bfd6 100644 --- a/src/modules/crontask/processor/exportTable/exportTable.go +++ b/src/modules/crontask/processor/exportTable/exportTable.go @@ -3,18 +3,15 @@ package exportTable import ( "encoding/json" "fmt" - "path" "path/filepath" "strings" "time" - "be.ems/src/framework/config" "be.ems/src/framework/cron" "be.ems/src/framework/database/db" "be.ems/src/framework/i18n" "be.ems/src/framework/logger" "be.ems/src/framework/ssh" - "be.ems/src/framework/utils/crypto" "be.ems/src/framework/utils/date" "be.ems/src/framework/utils/file" "be.ems/src/framework/utils/parse" @@ -927,17 +924,9 @@ func (s BarProcessor) putFTP(localFilePath string) { Enable bool `json:"enable"` Dir string `json:"dir" binding:"required"` } - cfg := systemService.NewSysConfig.FindByKey("sys.exportTable") + cfg := systemService.NewSysConfig.FindByKeyDecryptValue("neData.exportTableFTP") if cfg.ConfigId > 0 { - // 解密body - appKey := config.Get("aes.appKey").(string) - bodyDe, err := crypto.AESDecryptBase64(cfg.ConfigValue, appKey) - if err != nil { - logger.Errorf("putFTP decrypt error: %v", err) - return - } - err = json.Unmarshal([]byte(bodyDe), &cfgData) - if err != nil { + if err := json.Unmarshal([]byte(cfg.ConfigValue), &cfgData); err != nil { logger.Errorf("putFTP unmarshal error: %v", err) return } @@ -967,7 +956,7 @@ func (s BarProcessor) putFTP(localFilePath string) { } defer sftpClient.Close() // 远程文件 - remotePath := filepath.Join(cfgData.Dir, path.Base(localFilePath), filepath.Base(localFilePath)) + remotePath := filepath.Join(cfgData.Dir, "/backup", filepath.Base(localFilePath)) // 复制到远程 if err = sftpClient.CopyFileLocalToRemote(localFilePath, remotePath); err != nil { logger.Errorf("putFTP uploading error: %v", err) diff --git a/src/modules/crontask/processor/ne_config_backup/ne_config_backup.go b/src/modules/crontask/processor/ne_config_backup/ne_config_backup.go index e08549b3..84cb9b1c 100644 --- a/src/modules/crontask/processor/ne_config_backup/ne_config_backup.go +++ b/src/modules/crontask/processor/ne_config_backup/ne_config_backup.go @@ -1,18 +1,22 @@ package ne_config_backup import ( + "encoding/json" "fmt" "path/filepath" "be.ems/src/framework/cron" "be.ems/src/framework/logger" + "be.ems/src/framework/ssh" neModel "be.ems/src/modules/network_element/model" neService "be.ems/src/modules/network_element/service" + systemService "be.ems/src/modules/system/service" ) var NewProcessor = &NeConfigBackupProcessor{ neConfigBackupService: neService.NewNeConfigBackup, neInfoService: neService.NewNeInfo, + sysConfigService: systemService.NewSysConfig, count: 0, } @@ -20,6 +24,7 @@ var NewProcessor = &NeConfigBackupProcessor{ type NeConfigBackupProcessor struct { neConfigBackupService *neService.NeConfigBackup // 网元配置文件备份记录服务 neInfoService *neService.NeInfo // 网元信息服务 + sysConfigService *systemService.SysConfig // 参数配置服务 count int // 执行次数 } @@ -50,9 +55,65 @@ func (s *NeConfigBackupProcessor) Execute(data any) (any, error) { Path: zipFilePath, CreateBy: "system", } - s.neConfigBackupService.Insert(item) - result[neTypeAndId] = "ok" + rows := s.neConfigBackupService.Insert(item) + if rows <= 0 { + result[neTypeAndId] = "failed" + continue + } + result[neTypeAndId] = "success" + s.putFTP(zipFilePath) // 上传到FTP服务器 } return result, nil } + +// putFTP 提交到服务器ssh +func (s NeConfigBackupProcessor) putFTP(localFilePath string) { + // 获取配置 + var cfgData struct { + Password string `json:"password" ` + Username string `json:"username" binding:"required"` + ToIp string `json:"toIp" binding:"required"` + ToPort int64 `json:"toPort" binding:"required"` + Enable bool `json:"enable"` + Dir string `json:"dir" binding:"required"` + } + cfg := systemService.NewSysConfig.FindByKeyDecryptValue("neData.exportTableFTP") + if cfg.ConfigId > 0 { + if err := json.Unmarshal([]byte(cfg.ConfigValue), &cfgData); err != nil { + logger.Errorf("putFTP unmarshal error: %v", err) + return + } + } + if !cfgData.Enable { + return + } + + connSSH := ssh.ConnSSH{ + User: cfgData.Username, + Password: cfgData.Password, + Addr: cfgData.ToIp, + Port: cfgData.ToPort, + AuthMode: "0", + } + sshClient, err := connSSH.NewClient() + if err != nil { + logger.Errorf("putFTP ssh error: %v", err) + return + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + logger.Errorf("putFTP sftp error: %v", err) + return + } + defer sftpClient.Close() + // 远程文件 + remotePath := filepath.Join(cfgData.Dir, "/ne_config", filepath.Base(localFilePath)) + // 复制到远程 + if err = sftpClient.CopyFileLocalToRemote(localFilePath, remotePath); err != nil { + logger.Errorf("putFTP uploading error: %v", err) + return + } +} diff --git a/src/modules/network_element/controller/ne_config_backup.go b/src/modules/network_element/controller/ne_config_backup.go index be1577cc..fa42f7be 100644 --- a/src/modules/network_element/controller/ne_config_backup.go +++ b/src/modules/network_element/controller/ne_config_backup.go @@ -1,6 +1,7 @@ package controller import ( + "encoding/json" "fmt" "os" "path/filepath" @@ -9,10 +10,12 @@ import ( "be.ems/src/framework/i18n" "be.ems/src/framework/reqctx" "be.ems/src/framework/resp" + "be.ems/src/framework/ssh" "be.ems/src/framework/utils/file" "be.ems/src/framework/utils/parse" "be.ems/src/modules/network_element/model" neService "be.ems/src/modules/network_element/service" + systemService "be.ems/src/modules/system/service" "github.com/gin-gonic/gin" ) @@ -21,6 +24,7 @@ import ( var NewNeConfigBackup = &NeConfigBackupController{ neConfigBackupService: neService.NewNeConfigBackup, neInfoService: neService.NewNeInfo, + sysConfigService: systemService.NewSysConfig, } // 网元配置文件备份记录 @@ -29,6 +33,7 @@ var NewNeConfigBackup = &NeConfigBackupController{ type NeConfigBackupController struct { neConfigBackupService *neService.NeConfigBackup // 网元配置文件备份记录服务 neInfoService *neService.NeInfo // 网元信息服务 + sysConfigService *systemService.SysConfig // 参数配置服务 } // 网元配置文件备份记录列表 @@ -204,3 +209,133 @@ func (s NeConfigBackupController) Export(c *gin.Context) { s.neConfigBackupService.Insert(item) c.FileAttachment(item.Path, item.Name) } + +// 网元配置文件备份-设置FTP配置 +// +// POST /ftp +func (s NeConfigBackupController) SetFTP(c *gin.Context) { + var body struct { + Password string `json:"password" ` + Username string `json:"username" binding:"required"` + ToIp string `json:"toIp" binding:"required"` + ToPort int64 `json:"toPort" binding:"required"` + Enable bool `json:"enable"` + Dir string `json:"dir" binding:"required"` + } + if err := c.ShouldBindBodyWithJSON(&body); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(40422, errMsgs)) + return + } + + // 获取配置 + cfg := s.sysConfigService.FindByKey("ne.neConfigBackupFTP") + if cfg.ConfigId > 0 { + byteData, err := json.Marshal(body) + if err != nil { + c.JSON(200, resp.ErrMsg(err.Error())) + return + } + cfg.ConfigValue = string(byteData) + cfg.UpdateBy = reqctx.LoginUserToUserName(c) + systemService.NewSysConfig.UpdateEncryptValue(cfg) + } + + c.JSON(200, resp.Ok(nil)) +} + +// 网元配置文件备份-获取FTP配置 +// +// GET /ftp +func (s NeConfigBackupController) GetFTP(c *gin.Context) { + // 获取配置 + cfg := s.sysConfigService.FindByKeyDecryptValue("ne.neConfigBackupFTP") + if cfg.ConfigId > 0 { + var body struct { + Password string `json:"password" ` + Username string `json:"username" binding:"required"` + ToIp string `json:"toIp" binding:"required"` + ToPort int64 `json:"toPort" binding:"required"` + Enable bool `json:"enable"` + Dir string `json:"dir" binding:"required"` + } + if err := json.Unmarshal([]byte(cfg.ConfigValue), &body); err != nil { + c.JSON(200, resp.ErrMsg(err.Error())) + return + } + c.JSON(200, resp.OkData(body)) + return + } + + c.JSON(200, resp.Ok(nil)) +} + +// 网元配置文件备份-文件FTP发送 +// +// PUT /ftp +func (s NeConfigBackupController) PutFTP(c *gin.Context) { + var body struct { + FilePath string `json:"path" binding:"required"` + } + if err := c.ShouldBindBodyWithJSON(&body); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(40422, errMsgs)) + return + } + + // 判断文件是否存在 + if _, err := os.Stat(body.FilePath); os.IsNotExist(err) { + c.JSON(200, resp.ErrMsg(err.Error())) + return + } + + // 获取配置 + var cfgData struct { + Password string `json:"password" ` + Username string `json:"username" binding:"required"` + ToIp string `json:"toIp" binding:"required"` + ToPort int64 `json:"toPort" binding:"required"` + Enable bool `json:"enable"` + Dir string `json:"dir" binding:"required"` + } + cfg := s.sysConfigService.FindByKeyDecryptValue("ne.neConfigBackupFTP") + if cfg.ConfigId > 0 { + if err := json.Unmarshal([]byte(cfg.ConfigValue), &cfgData); err != nil { + c.JSON(200, resp.ErrMsg(err.Error())) + return + } + } + if !cfgData.Enable { + c.JSON(200, resp.ErrMsg("Setting Remote Backup is disabled")) + return + } + + connSSH := ssh.ConnSSH{ + User: cfgData.Username, + Password: cfgData.Password, + Addr: cfgData.ToIp, + Port: cfgData.ToPort, + AuthMode: "0", + } + sshClient, err := connSSH.NewClient() + if err != nil { + c.JSON(200, resp.ErrMsg(err.Error())) + return + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + c.JSON(200, resp.ErrMsg(err.Error())) + return + } + defer sftpClient.Close() + // 远程文件 + remotePath := filepath.Join(cfgData.Dir, "/ne_config", filepath.Base(body.FilePath)) + // 复制到远程 + if err = sftpClient.CopyFileLocalToRemote(body.FilePath, remotePath); err != nil { + c.JSON(200, resp.ErrMsg("error uploading file")) + return + } + c.JSON(200, resp.Ok(nil)) +} diff --git a/src/modules/network_element/network_element.go b/src/modules/network_element/network_element.go index 78a9787a..b2b5249e 100644 --- a/src/modules/network_element/network_element.go +++ b/src/modules/network_element/network_element.go @@ -351,6 +351,20 @@ func Setup(router *gin.Engine) { collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.neConfigBackup", collectlogs.BUSINESS_TYPE_EXPORT)), controller.NewNeConfigBackup.Export, ) + neConfigBackupGroup.GET("/ftp", + middleware.PreAuthorize(nil), + controller.NewNeConfigBackup.GetFTP, + ) + neConfigBackupGroup.POST("/ftp", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.neConfigBackup", collectlogs.BUSINESS_TYPE_OTHER)), + controller.NewNeConfigBackup.SetFTP, + ) + neConfigBackupGroup.PUT("/ftp", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.neConfigBackup", collectlogs.BUSINESS_TYPE_OTHER)), + controller.NewNeConfigBackup.PutFTP, + ) } } diff --git a/src/modules/system/service/sys_config.go b/src/modules/system/service/sys_config.go index bc3664e2..3d06f8a5 100644 --- a/src/modules/system/service/sys_config.go +++ b/src/modules/system/service/sys_config.go @@ -4,8 +4,10 @@ import ( "fmt" "strconv" + "be.ems/src/framework/config" "be.ems/src/framework/constants" "be.ems/src/framework/database/redis" + "be.ems/src/framework/utils/crypto" "be.ems/src/framework/utils/file" "be.ems/src/modules/system/model" "be.ems/src/modules/system/repository" @@ -145,6 +147,32 @@ func (s SysConfig) FindByKey(configKey string) model.SysConfig { return model.SysConfig{} } +// UpdateEncryptValue 更新并加密配置值信息 +func (s SysConfig) UpdateEncryptValue(sysConfig model.SysConfig) int64 { + appKey := config.Get("aes.appKey").(string) + bodyEn, err := crypto.AESEncryptBase64(sysConfig.ConfigValue, appKey) + if err != nil { + return 0 + } + sysConfig.ConfigValue = bodyEn + return s.Update(sysConfig) +} + +// FindByKeyDecryptValue 获取并解密配置值信息 +func (s SysConfig) FindByKeyDecryptValue(configKey string) model.SysConfig { + item := s.FindByKey(configKey) + if item.ConfigKey != configKey { + return item + } + appKey := config.Get("aes.appKey").(string) + bodyDe, err := crypto.AESDecryptBase64(item.ConfigValue, appKey) + if err != nil { + return item + } + item.ConfigValue = bodyDe + return item +} + // ExportData 导出数据表格 func (s SysConfig) ExportData(rows []model.SysConfig, fileName string) (string, error) { // 第一行表头标题