|
@@ -1,14 +1,14 @@
|
|
package upload
|
|
package upload
|
|
|
|
|
|
import (
|
|
import (
|
|
- "bytes"
|
|
|
|
"fmt"
|
|
"fmt"
|
|
- "io"
|
|
|
|
"kpt-pasture/config"
|
|
"kpt-pasture/config"
|
|
"kpt-pasture/http/middleware"
|
|
"kpt-pasture/http/middleware"
|
|
|
|
+ "mime/multipart"
|
|
"net/http"
|
|
"net/http"
|
|
"os"
|
|
"os"
|
|
"path/filepath"
|
|
"path/filepath"
|
|
|
|
+ "strings"
|
|
"time"
|
|
"time"
|
|
|
|
|
|
"gitee.com/xuyiping_admin/pkg/apierr"
|
|
"gitee.com/xuyiping_admin/pkg/apierr"
|
|
@@ -40,96 +40,143 @@ func Photos(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "Msg": "ok", "data": res})
|
|
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "Msg": "ok", "data": res})
|
|
}
|
|
}
|
|
|
|
|
|
-func Files(c *gin.Context) {
|
|
|
|
- // 获取上传的文件
|
|
|
|
- file, err := c.FormFile("file")
|
|
|
|
- if err != nil {
|
|
|
|
- apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Customf("获取文件失败: %s", err.Error()))
|
|
|
|
- return
|
|
|
|
|
|
+func Files2(c *gin.Context) {
|
|
|
|
+ // 定义配置
|
|
|
|
+ cf := ExcelImportConfig{
|
|
|
|
+ AllowedExtensions: []string{".xlsx", ".xls"},
|
|
|
|
+ RequiredHeaders: []string{"耳号", "牛舍名称", "性别", "出生日期", "胎次", "进场日期", "父号", "母号", "最近配种日期", "预产期",
|
|
|
|
+ "配后天数", "累计配次", "最近妊检日期", "最近产犊日期", "产后天数", "品种", "出生重量", "断奶日期", "是否禁配", "体重", "体高", "流产日期", "流产原因",
|
|
|
|
+ },
|
|
|
|
+ MaxFileSize: 10 << 20,
|
|
}
|
|
}
|
|
|
|
|
|
- // 检查文件类型
|
|
|
|
- ext := filepath.Ext(file.Filename)
|
|
|
|
- if ext != ".xlsx" && ext != ".xls" {
|
|
|
|
- apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Custom("只支持Excel文件(.xlsx, .xls)"))
|
|
|
|
- return
|
|
|
|
- }
|
|
|
|
|
|
+ // 实现处理逻辑
|
|
|
|
+ ImportExcel(c, cf)
|
|
|
|
+}
|
|
|
|
|
|
- // 检查文件大小
|
|
|
|
- if file.Size == 0 {
|
|
|
|
- apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Custom("文件为空"))
|
|
|
|
- return
|
|
|
|
|
|
+// ExcelImportConfig 导入配置
|
|
|
|
+type ExcelImportConfig struct {
|
|
|
|
+ AllowedExtensions []string
|
|
|
|
+ MaxFileSize int64
|
|
|
|
+ RequiredHeaders []string // 必须包含的表头
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// DefaultExcelImportConfig 默认配置
|
|
|
|
+var DefaultExcelImportConfig = ExcelImportConfig{
|
|
|
|
+ AllowedExtensions: []string{".xlsx", ".xls"},
|
|
|
|
+ MaxFileSize: 10 << 20, // 10MB
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// ExcelHandler Excel处理接口
|
|
|
|
+type ExcelHandler interface {
|
|
|
|
+ ProcessExcelData([][]string) error
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// ImportExcel 导入Excel文件
|
|
|
|
+func ImportExcel(c *gin.Context, config ...ExcelImportConfig) {
|
|
|
|
+ // 获取配置
|
|
|
|
+ cfg := DefaultExcelImportConfig
|
|
|
|
+ if len(config) > 0 {
|
|
|
|
+ cfg = config[0]
|
|
}
|
|
}
|
|
|
|
|
|
- // 创建保存目录
|
|
|
|
- uploadDir := fmt.Sprintf("%s/files/excel", config.WorkDir)
|
|
|
|
- if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
|
|
|
- apierr.AbortBadRequest(c, http.StatusInternalServerError, xerr.Customf("创建目录失败: %s", err.Error()))
|
|
|
|
|
|
+ // 1. 获取上传的文件
|
|
|
|
+ file, err := c.FormFile("file")
|
|
|
|
+ if err != nil {
|
|
|
|
+ apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Customf("获取文件失败: %s", err.Error()))
|
|
return
|
|
return
|
|
}
|
|
}
|
|
|
|
|
|
- // 生成唯一文件名
|
|
|
|
- filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
|
|
|
|
- filePath := filepath.Join(uploadDir, filename)
|
|
|
|
-
|
|
|
|
- // 保存文件
|
|
|
|
- if err := c.SaveUploadedFile(file, filePath); err != nil {
|
|
|
|
- apierr.AbortBadRequest(c, http.StatusInternalServerError, xerr.Customf("保存文件失败: %s", err.Error()))
|
|
|
|
|
|
+ // 2. 验证文件基本属性
|
|
|
|
+ if err = validateFile(file, cfg); err != nil {
|
|
|
|
+ apierr.AbortBadRequest(c, http.StatusBadRequest, err)
|
|
return
|
|
return
|
|
}
|
|
}
|
|
|
|
|
|
- // 打开上传的文件
|
|
|
|
- src, err := file.Open()
|
|
|
|
|
|
+ // 3. 保存文件到临时目录
|
|
|
|
+ filePath, err := saveUploadedFile(c, file)
|
|
if err != nil {
|
|
if err != nil {
|
|
- apierr.AbortBadRequest(c, http.StatusInternalServerError, xerr.Customf("打开文件失败: %s", err.Error()))
|
|
|
|
|
|
+ apierr.AbortBadRequest(c, http.StatusInternalServerError, err)
|
|
return
|
|
return
|
|
}
|
|
}
|
|
- defer src.Close()
|
|
|
|
-
|
|
|
|
- // 读取文件内容
|
|
|
|
- fileBytes, err := io.ReadAll(src)
|
|
|
|
|
|
+ defer os.Remove(filePath) // 处理完成后删除临时文件
|
|
|
|
+ // 4. 读取并验证Excel文件
|
|
|
|
+ excelData, err := readExcelFile(filePath, filepath.Ext(file.Filename))
|
|
if err != nil {
|
|
if err != nil {
|
|
- apierr.AbortBadRequest(c, http.StatusInternalServerError, xerr.Customf("读取文件内容失败: %s", err.Error()))
|
|
|
|
|
|
+ apierr.AbortBadRequest(c, http.StatusBadRequest, err)
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ // 5. 验证表头
|
|
|
|
+ if err = validateHeaders(excelData[0], cfg.RequiredHeaders); err != nil {
|
|
|
|
+ apierr.AbortBadRequest(c, http.StatusBadRequest, err)
|
|
return
|
|
return
|
|
}
|
|
}
|
|
|
|
|
|
- // 检查文件头部签名
|
|
|
|
- if len(fileBytes) < 8 {
|
|
|
|
- apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Custom("文件格式不正确"))
|
|
|
|
|
|
+ if err = middleware.BackendOperation(c).OpsService.ImportExcel(c, excelData); err != nil {
|
|
|
|
+ apierr.AbortBadRequest(c, http.StatusBadRequest, err)
|
|
return
|
|
return
|
|
}
|
|
}
|
|
|
|
|
|
- // 检查Excel文件签名
|
|
|
|
- isExcel := false
|
|
|
|
- if ext == ".xlsx" {
|
|
|
|
- // XLSX文件签名
|
|
|
|
- if bytes.Equal(fileBytes[:4], []byte{0x50, 0x4B, 0x03, 0x04}) {
|
|
|
|
- isExcel = true
|
|
|
|
- }
|
|
|
|
- } else if ext == ".xls" {
|
|
|
|
- // XLS文件签名
|
|
|
|
- if bytes.Equal(fileBytes[:8], []byte{0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1}) {
|
|
|
|
- isExcel = true
|
|
|
|
|
|
+ c.JSON(http.StatusOK, gin.H{
|
|
|
|
+ "code": http.StatusOK,
|
|
|
|
+ "msg": "导入成功",
|
|
|
|
+ "data": nil,
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// validateFile 验证文件基本属性
|
|
|
|
+func validateFile(file *multipart.FileHeader, cfg ExcelImportConfig) error {
|
|
|
|
+ // 检查文件扩展名
|
|
|
|
+ ext := strings.ToLower(filepath.Ext(file.Filename))
|
|
|
|
+ validExt := false
|
|
|
|
+ for _, allowedExt := range cfg.AllowedExtensions {
|
|
|
|
+ if ext == allowedExt {
|
|
|
|
+ validExt = true
|
|
|
|
+ break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+ if !validExt {
|
|
|
|
+ return xerr.Customf("不支持的文件类型,仅支持: %v", cfg.AllowedExtensions)
|
|
|
|
+ }
|
|
|
|
|
|
- if !isExcel {
|
|
|
|
- apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Custom("文件格式不正确,请确保上传的是有效的Excel文件"))
|
|
|
|
- return
|
|
|
|
|
|
+ // 检查文件大小
|
|
|
|
+ if file.Size > cfg.MaxFileSize {
|
|
|
|
+ return xerr.Customf("文件大小超过限制(最大%dMB)", cfg.MaxFileSize>>20)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// saveUploadedFile 保存上传的文件
|
|
|
|
+func saveUploadedFile(c *gin.Context, file *multipart.FileHeader) (string, error) {
|
|
|
|
+ // 创建临时目录
|
|
|
|
+ uploadDir := filepath.Join(config.WorkDir, "temp", "excel")
|
|
|
|
+ if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
|
|
|
+ return "", xerr.Customf("创建临时目录失败: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
|
|
- // 读取Excel文件
|
|
|
|
- options := excelize.Options{
|
|
|
|
- RawCellValue: true,
|
|
|
|
|
|
+ // 生成唯一文件名
|
|
|
|
+ filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), filepath.Ext(file.Filename))
|
|
|
|
+ filePath := filepath.Join(uploadDir, filename)
|
|
|
|
+
|
|
|
|
+ // 保存文件
|
|
|
|
+ if err := c.SaveUploadedFile(file, filePath); err != nil {
|
|
|
|
+ return "", xerr.Customf("保存文件失败: %s", err.Error())
|
|
}
|
|
}
|
|
- f, err := excelize.OpenReader(bytes.NewReader(fileBytes), options)
|
|
|
|
|
|
+
|
|
|
|
+ return filePath, nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// readExcelFile 读取Excel文件
|
|
|
|
+func readExcelFile(filePath, ext string) ([][]string, error) {
|
|
|
|
+ // 尝试使用excelize读取
|
|
|
|
+ f, err := excelize.OpenFile(filePath)
|
|
if err != nil {
|
|
if err != nil {
|
|
- // 如果直接读取失败,尝试从保存的文件读取
|
|
|
|
- f, err = excelize.OpenFile(filePath)
|
|
|
|
- if err != nil {
|
|
|
|
- apierr.AbortBadRequest(c, http.StatusInternalServerError, xerr.Customf("读取Excel文件失败,请确保文件格式正确: %s", err.Error()))
|
|
|
|
- return
|
|
|
|
|
|
+ // 如果是xls格式且读取失败,尝试使用xls库
|
|
|
|
+ if ext == ".xls" {
|
|
|
|
+ return readXlsFile(filePath)
|
|
}
|
|
}
|
|
|
|
+ return nil, xerr.Customf("读取Excel文件失败: %s", err.Error())
|
|
}
|
|
}
|
|
defer f.Close()
|
|
defer f.Close()
|
|
|
|
|
|
@@ -137,49 +184,44 @@ func Files(c *gin.Context) {
|
|
sheetName := f.GetSheetName(0)
|
|
sheetName := f.GetSheetName(0)
|
|
rows, err := f.GetRows(sheetName)
|
|
rows, err := f.GetRows(sheetName)
|
|
if err != nil {
|
|
if err != nil {
|
|
- apierr.AbortBadRequest(c, http.StatusInternalServerError, xerr.Customf("读取工作表失败: %s", err.Error()))
|
|
|
|
- return
|
|
|
|
|
|
+ return nil, xerr.Customf("读取工作表失败: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
|
|
- // 处理Excel数据
|
|
|
|
- if len(rows) < 2 {
|
|
|
|
- apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Custom("Excel文件数据为空"))
|
|
|
|
- return
|
|
|
|
|
|
+ if len(rows) == 0 {
|
|
|
|
+ return nil, xerr.Custom("Excel文件为空")
|
|
}
|
|
}
|
|
|
|
|
|
- // 获取表头并转换为中文
|
|
|
|
- headers := rows[0]
|
|
|
|
- headerMap := make(map[string]string)
|
|
|
|
- for _, header := range headers {
|
|
|
|
- headerMap[header] = header
|
|
|
|
- }
|
|
|
|
|
|
+ return rows, nil
|
|
|
|
+}
|
|
|
|
|
|
- // 处理数据行
|
|
|
|
- var data []map[string]string
|
|
|
|
- for i, row := range rows[1:] {
|
|
|
|
- if len(row) != len(headers) {
|
|
|
|
- apierr.AbortBadRequest(c, http.StatusBadRequest, xerr.Customf("第%d行数据列数与表头不匹配", i+2))
|
|
|
|
- return
|
|
|
|
- }
|
|
|
|
|
|
+// readXlsFile 使用xls库读取旧版Excel文件
|
|
|
|
+func readXlsFile(filePath string) ([][]string, error) {
|
|
|
|
+ // 这里需要实现xls文件的读取逻辑
|
|
|
|
+ // 可以使用第三方库如github.com/extrame/xls
|
|
|
|
+ return nil, xerr.Custom("暂不支持.xls格式文件")
|
|
|
|
+}
|
|
|
|
|
|
- rowData := make(map[string]string)
|
|
|
|
- for j, cell := range row {
|
|
|
|
- rowData[headers[j]] = cell
|
|
|
|
- }
|
|
|
|
- data = append(data, rowData)
|
|
|
|
|
|
+// validateHeaders 验证表头
|
|
|
|
+func validateHeaders(headers []string, requiredHeaders []string) error {
|
|
|
|
+ if len(requiredHeaders) == 0 {
|
|
|
|
+ return nil
|
|
}
|
|
}
|
|
|
|
|
|
- // 调用后端服务处理数据
|
|
|
|
- if err := middleware.BackendOperation(c).OpsService.ImportExcel(c, data); err != nil {
|
|
|
|
- apierr.AbortBadRequest(c, http.StatusBadRequest, err)
|
|
|
|
- return
|
|
|
|
|
|
+ headerMap := make(map[string]bool)
|
|
|
|
+ for _, h := range headers {
|
|
|
|
+ headerMap[strings.TrimSpace(h)] = true
|
|
}
|
|
}
|
|
|
|
|
|
- c.JSON(http.StatusOK, gin.H{
|
|
|
|
- "code": http.StatusOK,
|
|
|
|
- "msg": "导入成功",
|
|
|
|
- "data": nil,
|
|
|
|
- })
|
|
|
|
|
|
+ var missingHeaders []string
|
|
|
|
+ for _, req := range requiredHeaders {
|
|
|
|
+ if !headerMap[req] {
|
|
|
|
+ missingHeaders = append(missingHeaders, req)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if len(missingHeaders) > 0 {
|
|
|
|
+ return xerr.Customf("缺少必要表头: %v", missingHeaders)
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
}
|
|
}
|
|
|
|
|
|
func OssVideo(c *gin.Context) {
|
|
func OssVideo(c *gin.Context) {
|