浏览代码

file: upload update

Yi 1 月之前
父节点
当前提交
b5344baa52
共有 6 个文件被更改,包括 267 次插入132 次删除
  1. 138 96
      http/handler/upload/upload.go
  2. 1 1
      http/route/files_api.go
  3. 25 0
      model/event_enter.go
  4. 1 0
      model/system_role.go
  5. 1 1
      module/backend/interface.go
  6. 101 34
      module/backend/upload_file.go

+ 138 - 96
http/handler/upload/upload.go

@@ -1,14 +1,14 @@
 package upload
 
 import (
-	"bytes"
 	"fmt"
-	"io"
 	"kpt-pasture/config"
 	"kpt-pasture/http/middleware"
+	"mime/multipart"
 	"net/http"
 	"os"
 	"path/filepath"
+	"strings"
 	"time"
 
 	"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})
 }
 
-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
 	}
 
-	// 生成唯一文件名
-	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
 	}
 
-	// 打开上传的文件
-	src, err := file.Open()
+	// 3. 保存文件到临时目录
+	filePath, err := saveUploadedFile(c, file)
 	if err != nil {
-		apierr.AbortBadRequest(c, http.StatusInternalServerError, xerr.Customf("打开文件失败: %s", err.Error()))
+		apierr.AbortBadRequest(c, http.StatusInternalServerError, err)
 		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 {
-		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
 	}
 
-	// 检查文件头部签名
-	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
 	}
 
-	// 检查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 {
-		// 如果直接读取失败,尝试从保存的文件读取
-		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()
 
@@ -137,49 +184,44 @@ func Files(c *gin.Context) {
 	sheetName := f.GetSheetName(0)
 	rows, err := f.GetRows(sheetName)
 	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) {

+ 1 - 1
http/route/files_api.go

@@ -14,7 +14,7 @@ func FilesManageAPI(opts ...func(engine *gin.Engine)) func(s *gin.Engine) {
 		// upload API 组  上传文件
 		uploadRoute := authRouteGroup(s, "/api/v1/upload/")
 		uploadRoute.POST("/photos", upload.Photos)
-		uploadRoute.POST("/files", upload.Files)
+		uploadRoute.POST("/files", upload.Files2)
 
 		// 获取文件列表
 		//ossRoute := authRouteGroup(s, "/api/v1/oss/")

+ 25 - 0
model/event_enter.go

@@ -130,3 +130,28 @@ func (e EventEnterSlice) ToPB(
 	}
 	return res
 }
+
+var PenMap = map[string]int32{
+	"101":  15,
+	"10号":  16,
+	"1101": 17,
+	"11号":  18,
+	"12号":  19,
+	"13号":  20,
+	"14号":  21,
+	"15号舍": 22,
+	"1号":   23,
+	"2号":   24,
+	"301":  25,
+	"3号":   26,
+	"4号":   27,
+	"501":  28,
+	"5号":   29,
+	"601":  30,
+	"6号":   31,
+	"701":  32,
+	"7号":   33,
+	"801":  34,
+	"8号":   35,
+	"9号":   36,
+}

+ 1 - 0
model/system_role.go

@@ -28,6 +28,7 @@ const (
 	LayoutHour   = "2006-01-02 15"
 	LayoutMinute = "2006-01-02 15:04"
 	LayoutYear   = "2006"
+	LayoutTime2  = "2006/1/2 15:04:05"
 )
 
 type SystemRoleSlice []*SystemRole

+ 1 - 1
module/backend/interface.go

@@ -344,7 +344,7 @@ type MilkHallService interface {
 
 type UploadService interface {
 	Photos(ctx context.Context, files []*multipart.FileHeader) ([]string, error)
-	ImportExcel(ctx context.Context, data []map[string]string) error
+	ImportExcel(ctx context.Context, data [][]string) error
 }
 
 type TestService interface {

+ 101 - 34
module/backend/upload_file.go

@@ -9,6 +9,7 @@ import (
 	"mime/multipart"
 	"os"
 	"path/filepath"
+	"strconv"
 	"time"
 
 	"gitee.com/xuyiping_admin/pkg/logger/zaplog"
@@ -72,55 +73,121 @@ func (s *StoreEntry) Photos(ctx context.Context, files []*multipart.FileHeader)
 	return filePaths, nil
 }
 
-func (s *StoreEntry) ImportExcel(ctx context.Context, data []map[string]string) error {
+func (s *StoreEntry) ImportExcel(ctx context.Context, data [][]string) error {
 	// 获取当前用户信息
 	userModel, err := s.GetUserModel(ctx)
 	if err != nil {
 		return err
 	}
 
+	penMap := model.PenMap
+
 	// 验证牧场ID
 	if userModel.AppPasture.Id <= 0 {
 		return xerr.Custom("无效的牧场ID")
 	}
 
-	cowList := make([]*model.Cow, 0)
-
-	// 批量处理数据
-	for _, row := range data {
-		// 验证必要字段
-		if _, ok := row["耳号"]; !ok {
-			return xerr.Custom("缺少必要字段: 耳号")
+	// 处理Excel数据
+	//headers := data[0]
+	eventEnterList := make([]*pasturePb.EventEnterRequest, 0)
+	for _, row := range data[1:] {
+		if len(row) <= 0 {
+			continue
 		}
-
-		sex := pasturePb.Genders_Female
-		if _, ok := row["性别"]; !ok {
-			return xerr.Custom("缺少必要字段: 性别")
-		} else {
-			if row["性别"] == "公" {
-				sex = pasturePb.Genders_Male
+		ts := &pasturePb.EventEnterRequest{}
+		zaplog.Error("row", zap.Any("row", row))
+		for j, d := range row {
+			switch j {
+			case 0:
+				continue
+			case 1:
+				continue
+			case 2:
+				ts.EarNumber = d
+			case 3:
+				if pn, ok := penMap[d]; ok {
+					ts.PenId = pn
+					ts.PenName = d
+				}
+			case 4:
+				ts.Sex = pasturePb.Genders_Female
+				if d == "公" {
+					ts.Sex = pasturePb.Genders_Male
+				}
+			case 5:
+				if d == "成母牛" {
+					ts.CowType = pasturePb.CowType_Breeding_Calf
+				} else if d == "犊牛" {
+					ts.CowType = pasturePb.CowType_Lactating_Calf
+				} else if d == "青年牛" {
+					ts.CowType = pasturePb.CowType_Youth_Calf
+				} else if d == "育成牛" {
+					ts.CowType = pasturePb.CowType_Reserve_Calf
+				}
+			case 6:
+				continue
+			case 7:
+				bat, _ := util.TimeParseLocal(model.LayoutTime2, d)
+				if !bat.IsZero() {
+					ts.BirthAt = int32(bat.Local().Unix())
+				}
+			case 8:
+				lact, _ := strconv.Atoi(d)
+				ts.Lact = int32(lact)
+			case 9:
+				eat, _ := util.TimeParseLocal(model.LayoutTime2, d)
+				if !eat.IsZero() {
+					ts.EnterAt = int32(eat.Local().Unix())
+				}
+			case 10:
+				ts.FatherNumber = d
+			case 11:
+				ts.MotherNumber = d
+			case 12:
+				mat, _ := util.TimeParseLocal(model.LayoutTime2, d)
+				if !mat.IsZero() {
+					ts.MatingAt = int32(mat.Local().Unix())
+				}
+			case 13:
+				continue
+			case 14:
+				continue
+			case 15:
+				mt, _ := strconv.Atoi(d)
+				ts.MatingTimes = int32(mt)
+			case 16:
+				continue
+			case 17:
+				pat, _ := util.TimeParseLocal(model.LayoutTime2, d)
+				if !pat.IsZero() {
+					ts.PregnancyCheckAt = int32(pat.Local().Unix())
+				}
+			case 18:
+				continue
+			case 19:
+				continue
+			case 20:
+				cat, _ := util.TimeParseLocal(model.LayoutTime2, d)
+				if !cat.IsZero() {
+					ts.CalvingAt = int32(cat.Local().Unix())
+				}
+			case 21:
+				continue
+			case 22:
+				continue
+			case 23:
+				continue
+			case 24:
+				ts.CowKind = pasturePb.CowKind_AGSN
 			}
 		}
-
-		// 创建或更新牛只信息
-		cow := &model.Cow{
-			PastureId: userModel.AppPasture.Id,
-			EarNumber: row["耳号"],
-			Sex:       sex,
-		}
-
-		// 检查牛只是否已存在
-		var existingCow model.Cow
-		if err = s.DB.Model(new(model.Cow)).
-			Where("pasture_id = ? AND ear_number = ?", userModel.AppPasture.Id, cow.EarNumber).
-			First(&existingCow).Error; err == nil {
-
-		} else {
-			cowList = append(cowList, cow)
-		}
+		eventEnterList = append(eventEnterList, ts)
+		break
 	}
 
-	zaplog.Info("ImportExcel", zap.Any("cowList", cowList))
-
+	if len(eventEnterList) <= 0 {
+		return nil
+	}
+	zaplog.Error("eventEnterList", zap.Any("eventEnterList", eventEnterList))
 	return nil
 }