windows-pc hai 2 días
pai
achega
ddf1535c9a
Modificáronse 5 ficheiros con 777 adicións e 39 borrados
  1. 701 0
      sku3d/sku3d/api/a-excel.go
  2. 33 31
      sku3d/sku3d/api/a-service-img.go
  3. 7 2
      sku3d/sku3d/go.mod
  4. 19 6
      sku3d/sku3d/go.sum
  5. 17 0
      sku3d/sku3d/readme.md

+ 701 - 0
sku3d/sku3d/api/a-excel.go

@@ -0,0 +1,701 @@
+// 导入导出功能
+
+package api
+
+import (
+	"archive/zip"
+	"bytes"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path"
+	"path/filepath"
+	"sku3dweb/db/model"
+	"sku3dweb/db/repo"
+	"sku3dweb/log"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/gin-gonic/gin"
+	excelize "github.com/xuri/excelize/v2"
+	"go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// 固定Excel模板表头
+var fixedHeaders = []string{
+	"公司商品编号", "商品中文名", "商品英文名", "分类", "图片",
+	"单位包材毛重(KG)", "单位包材体积(CBM)", "长度(MM)", "门幅/宽(MM)", "厚度/高(MM)",
+	"备注", "首选供应商", "默认采购单价", "样品搜集人", "开发日期",
+	"出口属性", "内销属性", "内购属性", "委外属性",
+	"报关助记符", "报关商品编码", "报关商品中文名", "录入人名称", "适合的市场",
+	"供应商编号", "种类分类", "种类分类名称", "基布", "基布名称",
+	"表面工艺", "表面工艺名称", "产品克重(KG)", "运营周期", "商品单位体积",
+	"商品分类代码", "产品系列", "产品系列", "产品用途", "产品用途",
+	"样品编号", "留样册号", "原命名编号", "底布克重", "面布克重",
+	// "原始大图", "封面图", "商品图片",
+}
+
+func RegExcelRouter(router *GinRouter) {
+	// router.POSTJWT("/excel/import", ExcelImport)
+	// router.GETJWT("/excel/export", ExcelExport)
+	router.POSTJWT("/zip/import", ZipImport)
+}
+
+func ExcelImportWithImages(c *gin.Context, apictx *ApiSession, file io.Reader, goodsDir, textureDir string) (interface{}, error) {
+	// 读取Excel文件
+	xlsx, err := excelize.OpenReader(file)
+	if err != nil {
+		return nil, NewError("解析Excel文件失败")
+	}
+	defer func() {
+		if err := xlsx.Close(); err != nil {
+			log.Errorf("关闭Excel文件失败: %v", err)
+		}
+	}()
+
+	// 获取第一个sheet
+	sheetName := xlsx.GetSheetName(0)
+	rows, err := xlsx.GetRows(sheetName)
+	if err != nil {
+		return nil, NewError("读取Excel内容失败")
+	}
+
+	// 确保至少有表头和一行数据
+	if len(rows) < 2 {
+		return nil, NewError("Excel文件内容不足")
+	}
+
+	// 预热云函数
+	_, _ = QueryFassiImage("http://lymat.oss-cn-hangzhou.aliyuncs.com/images/1744072972845.png", 1, 0, 0)
+
+	// 获取分类配置
+	cat := []*model.Category{}
+	found, err := repo.RepoSeachDoc(apictx.CreateRepoCtx(), &repo.DocSearchOptions{
+		CollectName: repo.CollectionCategory,
+		Query:       repo.Map{},
+		Project:     []string{"name", "type", "children", "createTime"},
+	}, cat)
+	if err != nil || !found {
+		return nil, NewError("获取分类配置失败")
+	}
+	cates := cat[0].Children
+
+	// 创建导入结果记录
+	type ImportResult struct {
+		RowIndex     int
+		Status       string // "成功" 或 "失败"
+		ErrorMessage string
+		ImageID      string
+	}
+	importResults := make([]ImportResult, 0, len(rows)-1)
+
+	// 根据模板解析每列数据
+	for i, row := range rows {
+		// 跳过表头
+		if i == 0 {
+			continue
+		}
+
+		// 创建导入结果记录
+		result := ImportResult{
+			RowIndex: i,
+			Status:   "成功",
+		}
+
+		imageMat := model.MatImage{}
+		// 构建基础数据
+		imageMat.CusNum = row[0]
+		imageMat.NameCN = row[1]
+		imageMat.NameEN = row[2]
+		// 根据分类层级一的名字获取对应id,遍历一层获取对应数据
+		row3Cate := &model.Category{}
+		for _, cate := range cates {
+			if cate.Name == "商品分类" {
+				for _, c := range cate.Children {
+					if c.Name == row[3] {
+						// 记录该分类为其他自动做准备
+						row3Cate = c
+					}
+				}
+			}
+		}
+		if len(row3Cate.IdStr) <= 0 {
+			// 记录日志跳过
+			result.Status = "失败"
+			result.ErrorMessage = "商品分类未找到"
+			importResults = append(importResults, result)
+			continue
+		}
+
+		// 获取rowCate下的二级分类
+		var getRowCate2 = func(rowCate *model.Category, pName string, name string, cusNum string) *model.Category {
+			for _, cate := range rowCate.Children {
+				if cate.Name == pName {
+					for _, c := range cate.Children {
+						if c.Name == name {
+							if len(cusNum) > 0 {
+								if c.CusNum == cusNum {
+									return c
+								}
+								return nil
+							}
+							return c
+						}
+					}
+				}
+
+			}
+			return nil
+		}
+
+		imageMat.Categories = append(imageMat.Categories, row3Cate.IdStr)
+
+		// TODO 跳过图片 后面统一处理
+		var str2float64 = func(s string) float64 {
+			f, err := strconv.ParseFloat(s, 64)
+			if err != nil {
+				return 0 // 或 math.NaN() 表示无效值
+			}
+			return f
+		}
+		row5 := str2float64(row[5])
+		imageMat.PackageGrossWeight = &row5
+		row6 := str2float64(row[6])
+		imageMat.PackageVolume = &row6
+		row7 := str2float64(row[7])
+		imageMat.PhyHeight = &row7
+		row8 := str2float64(row[8])
+		imageMat.PhyWidth = &row8
+		row9 := str2float64(row[9])
+		imageMat.Thickness = &row9
+		imageMat.Remarks = row[10]
+		row11Cate := &model.Category{}
+		for _, cate := range cates {
+			if cate.Name == "首选供应商" {
+				for _, c := range cate.Children {
+					if c.Name == row[11] {
+						// 记录该分类为其他自动做准备
+						row11Cate = c
+					}
+				}
+			}
+		}
+		if len(row11Cate.IdStr) <= 0 {
+			// 记录日志跳过
+			result.Status = "失败"
+			result.ErrorMessage = "首选供应商未找到"
+			importResults = append(importResults, result)
+			continue
+		}
+		imageMat.Categories = append(imageMat.Categories, row11Cate.IdStr)
+
+		var str2int = func(s string) int {
+			f, err := strconv.Atoi(s)
+			if err != nil {
+				return 0 // 或 math.NaN() 表示无效值
+			}
+			return f
+		}
+		row12 := str2int(row[12])
+		imageMat.Price = &row12
+		staffName := row[14]
+		if len(staffName) > 0 {
+			staff := model.StaffUser{}
+			// 验证样品收集人是否预设
+			found, _ := repo.RepoSeachDoc(apictx.CreateRepoCtx(), &repo.DocSearchOptions{
+				CollectName: repo.CollectionStaffUser,
+				Query:       repo.Map{"name": row[14]},
+			}, &staff)
+			if found {
+				imageMat.From = staffName
+			} else {
+				// 记录日志
+				result.Status = "失败"
+				result.ErrorMessage = "样品收集人未预设"
+				importResults = append(importResults, result)
+				continue
+			}
+		}
+		//开发日期
+		if len(row[15]) > 0 {
+			layout := "2006/1/2"
+			// 解析字符串
+			devTime, _ := time.Parse(layout, row[15])
+			imageMat.DevTime = &devTime
+		}
+		var str2bool = func(s string) *bool {
+			result := false
+			if s == "True" {
+				result = true
+			}
+			return &result
+		}
+		// 出口属性
+		imageMat.ExportProperty = str2bool(row[16])
+		// 内销属性
+		imageMat.DomesticProperty = str2bool(row[17])
+		// 内购属性
+		imageMat.InpurchaseProperty = str2bool(row[18])
+		// 委外属性
+		imageMat.OutsourcedProperty = str2bool(row[19])
+
+		// 报关助记
+		// 根据分类层级一的名字获取对应id,遍历一层获取对应数据
+		row20Cate := &model.Category{}
+		for _, cate := range cates {
+			if cate.Name == "报关助记符" {
+				for _, c := range cate.Children {
+					if c.Name == row[20] {
+						// 记录该分类为其他自动做准备
+						row20Cate = c
+					}
+				}
+			}
+		}
+		if len(row20Cate.IdStr) <= 0 {
+			// 记录日志跳过
+			result.Status = "失败"
+			result.ErrorMessage = "报关助记符未找到"
+			importResults = append(importResults, result)
+			continue
+		}
+		imageMat.Categories = append(imageMat.Categories, row20Cate.IdStr)
+		imageMat.TaxNameCN = row[22]
+		// 录入人
+		imageMat.UserId, _ = primitive.ObjectIDFromHex(apictx.User.ID)
+		imageMat.FitMarket = row[24]
+		// 供应商编号
+		imageMat.SupplierID = row[25]
+
+		// 种类分类
+		row26Cate := getRowCate2(row3Cate, "种类分类", row[27], row[26])
+		if row26Cate == nil {
+			// 没有找到对应分类
+			result.Status = "失败"
+			result.ErrorMessage = "种类分类未找到"
+			importResults = append(importResults, result)
+			continue
+		}
+		imageMat.Categories = append(imageMat.Categories, row26Cate.IdStr)
+
+		// 基布
+		row28Cate := getRowCate2(row3Cate, "基布", row[29], row[28])
+		if row28Cate == nil {
+			// 没有找到对应分类
+			result.Status = "失败"
+			result.ErrorMessage = "基布未找到"
+			importResults = append(importResults, result)
+			continue
+		}
+		imageMat.Categories = append(imageMat.Categories, row28Cate.IdStr)
+
+		// 表面工艺
+		row30Cate := getRowCate2(row3Cate, "表面工艺", row[31], row[30])
+		if row30Cate == nil {
+			// 没有找到对应分类
+			result.Status = "失败"
+			result.ErrorMessage = "表面工艺未找到"
+			importResults = append(importResults, result)
+			continue
+		}
+		imageMat.Categories = append(imageMat.Categories, row30Cate.IdStr)
+
+		// 产品克重
+		pw := str2float64(row[32])
+		imageMat.ProductWeight = &pw
+		// 运营周期
+		imageMat.OperationCycle = row[33]
+		// 商品单位体积
+		pv := str2float64(row[34])
+		imageMat.ProductVolume = &pv
+
+		// ??? 产品分类代码
+
+		// 产品系列
+		row36Cate := getRowCate2(row3Cate, "产品系列", row[37], row[36])
+		if row36Cate == nil {
+			// 没有找到对应分类
+			result.Status = "失败"
+			result.ErrorMessage = "产品系列未找到"
+			importResults = append(importResults, result)
+			continue
+		}
+		imageMat.Categories = append(imageMat.Categories, row36Cate.IdStr)
+
+		// 产品用途
+		row38Cate := getRowCate2(row3Cate, "产品用途", row[39], row[38])
+		if row38Cate == nil {
+			// 没有找到对应分类
+			result.Status = "失败"
+			result.ErrorMessage = "产品用途未找到"
+			importResults = append(importResults, result)
+			continue
+		}
+		imageMat.Categories = append(imageMat.Categories, row38Cate.IdStr)
+
+		// 样品编号
+		imageMat.SampleNumber = row[40]
+		// 留样册号
+		imageMat.CatalogNumber = row[41]
+		// 原命名编号
+		imageMat.OriginalNumber = row[42]
+		// 底布克重
+		bw := str2float64(row[43])
+		imageMat.BackingWeight = &bw
+		// 面布克重
+		sw := str2float64(row[44])
+		imageMat.SurfaceWeight = &sw
+
+		imageName := row[42] // 不包含后缀 .jpg .png .jpeg
+		// 根据图片名称检查goods和texture中是否存在,图片可能是.jpg .png .jpeg后缀
+		// 上传图片到oss 获取对应url,赋值到imageMat(Thumbnail, RawImage, ProductImage)
+
+		// 在goods目录中查找图片
+		goodsImagePath := findImageFile(goodsDir, imageName)
+		textureImagePath := findImageFile(textureDir, imageName)
+
+		// 如果找到了图片,上传到OSS
+		if goodsImagePath != "" {
+			// 上传原始图片作为RawImage
+			goodsImage, err := uploadLocalImage(goodsImagePath, "goods", apictx)
+			if err != nil {
+				log.Errorf("上传图片失败: %v", err)
+
+			} else {
+				imageMat.ProductImage = goodsImage
+			}
+		}
+
+		if textureImagePath != "" {
+			// 上传原始图片作为RawImage
+			textureImage, err := uploadLocalImage(textureImagePath, "texture", apictx)
+			if err != nil {
+				log.Errorf("上传图片失败: %v", err)
+
+			} else {
+				imageMat.RawImage = textureImage
+				imageMat.Thumbnail = textureImage
+			}
+		}
+
+		// 设置创建时间
+		imageMat.CreateTime = time.Now()
+		imageMat.UpdateTime = time.Now()
+
+		// 写入到数据库中并获取创建记录的数据库id
+		imgId, err := repo.RepoAddDoc(apictx.CreateRepoCtx(), repo.CollectionMatImages, &imageMat)
+		if err != nil {
+			log.Errorf("写入记录失败: %v", err)
+			result.Status = "失败"
+			result.ErrorMessage = "数据库写入失败: " + err.Error()
+			importResults = append(importResults, result)
+			continue
+		}
+
+		result.ImageID = imgId
+
+		// 如果texture存在,调用AddFassiImage创建图片特征和id的关联
+		if textureImagePath != "" && imageMat.RawImage != nil {
+			objId, err := primitive.ObjectIDFromHex(imgId)
+			if err != nil {
+				log.Errorf("转换ID失败: %v", err)
+
+			} else {
+				err = AddFassiImage(objId, imageMat.RawImage.Url)
+				if err != nil {
+					log.Errorf("创建Fassi图片关联失败: %v", err)
+
+				}
+			}
+		}
+
+		importResults = append(importResults, result)
+	}
+
+	// 创建一个新的Excel文件用于导出结果
+	resultExcel := excelize.NewFile()
+	sheet := "Sheet1"
+
+	// 复制原始表头并添加状态列
+	headers := append(rows[0], "导入状态", "失败原因", "数据库ID")
+	for i, header := range headers {
+		colName := string(rune('A' + i))
+		resultExcel.SetCellValue(sheet, colName+"1", header)
+	}
+
+	// 写入每行数据和导入结果
+	for i, row := range rows {
+		if i == 0 {
+			continue // 跳过表头
+		}
+
+		// 查找对应的导入结果
+		var result ImportResult
+		for _, r := range importResults {
+			if r.RowIndex == i {
+				result = r
+				break
+			}
+		}
+
+		// 写入原始数据
+		for j, cell := range row {
+			colName := string(rune('A' + j))
+			resultExcel.SetCellValue(sheet, colName+strconv.Itoa(i+1), cell)
+		}
+
+		// 添加状态和错误信息
+		statusCol := string(rune('A' + len(row)))
+		errorCol := string(rune('A' + len(row) + 1))
+		idCol := string(rune('A' + len(row) + 2))
+
+		resultExcel.SetCellValue(sheet, statusCol+strconv.Itoa(i+1), result.Status)
+		resultExcel.SetCellValue(sheet, errorCol+strconv.Itoa(i+1), result.ErrorMessage)
+		resultExcel.SetCellValue(sheet, idCol+strconv.Itoa(i+1), result.ImageID)
+	}
+
+	// 设置列宽
+	for i := range headers {
+		colName := string(rune('A' + i))
+		resultExcel.SetColWidth(sheet, colName, colName, 15)
+	}
+
+	// 保存到内存缓冲区
+	buffer := bytes.Buffer{}
+	if err := resultExcel.Write(&buffer); err != nil {
+		return nil, NewError("生成导入结果文件失败")
+	}
+
+	// 设置响应头,使浏览器下载文件
+	c.Header("Content-Description", "File Transfer")
+	c.Header("Content-Disposition", "attachment; filename=import_result_"+time.Now().Format("20060102150405")+".xlsx")
+	c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buffer.Bytes())
+
+	// 统计导入结果
+	successCount := 0
+	failCount := 0
+	partialCount := 0
+
+	for _, result := range importResults {
+		switch result.Status {
+		case "成功":
+			successCount++
+		case "失败":
+			failCount++
+		case "部分成功":
+			partialCount++
+		}
+	}
+
+	return map[string]interface{}{
+		"total":   len(importResults),
+		"success": successCount,
+		"failed":  failCount,
+		"partial": partialCount,
+	}, nil
+}
+
+// 新增ZIP文件导入处理函数
+func ZipImport(c *gin.Context, apictx *ApiSession) (interface{}, error) {
+	// 获取上传的文件
+	file, header, err := c.Request.FormFile("file")
+	if err != nil {
+		return nil, NewError("获取上传文件失败")
+	}
+	defer file.Close()
+
+	// 检查文件大小
+	if header.Size > 100*1024*1024 { // 限制100MB
+		return nil, NewError("上传文件过大,请控制在100MB以内")
+	}
+
+	// 检查文件扩展名
+	if !strings.HasSuffix(strings.ToLower(header.Filename), ".zip") {
+		return nil, NewError("只支持上传ZIP格式的文件")
+	}
+
+	// 创建临时目录存放解压文件
+	tempDir, err := os.MkdirTemp("", "sku3d_import_"+time.Now().Format("20060102150405"))
+	if err != nil {
+		return nil, NewError("创建临时目录失败")
+	}
+	defer os.RemoveAll(tempDir) // 确保处理完成后删除临时目录
+
+	// 保存上传的ZIP文件
+	zipFilePath := path.Join(tempDir, "upload.zip")
+	tempZipFile, err := os.Create(zipFilePath)
+	if err != nil {
+		return nil, NewError("创建临时文件失败")
+	}
+
+	// 将上传的文件内容写入临时文件
+	_, err = io.Copy(tempZipFile, file)
+	tempZipFile.Close()
+	if err != nil {
+		return nil, NewError("保存上传文件失败")
+	}
+
+	// 解压ZIP文件
+	extractDir := path.Join(tempDir, "extract")
+	err = os.MkdirAll(extractDir, 0755)
+	if err != nil {
+		return nil, NewError("创建解压目录失败")
+	}
+
+	err = unzipFile(zipFilePath, extractDir)
+	if err != nil {
+		return nil, NewError("解压文件失败: " + err.Error())
+	}
+
+	// 查找Excel文件
+	excelFilePath, err := findExcelFile(extractDir)
+	if err != nil {
+		return nil, NewError("未找到有效的Excel文件: " + err.Error())
+	}
+
+	// 打开Excel文件
+	excelFile, err := os.Open(excelFilePath)
+	if err != nil {
+		return nil, NewError("打开Excel文件失败")
+	}
+	defer excelFile.Close()
+
+	// 商品图片目录和纹理图片目录
+	goodsDir := path.Join(extractDir, "goods")
+	textureDir := path.Join(extractDir, "texture")
+
+	// 检查目录是否存在
+	if _, err := os.Stat(goodsDir); os.IsNotExist(err) {
+		log.Errorf("商品图片目录不存在: %s", goodsDir)
+		goodsDir = ""
+	}
+	if _, err := os.Stat(textureDir); os.IsNotExist(err) {
+		log.Errorf("纹理图片目录不存在: %s", textureDir)
+		textureDir = ""
+	}
+
+	// 传递给Excel导入函数处理,并指定图片目录
+	return ExcelImportWithImages(c, apictx, excelFile, goodsDir, textureDir)
+}
+
+// 解压ZIP文件到指定目录
+func unzipFile(zipFile, destDir string) error {
+	r, err := zip.OpenReader(zipFile)
+	if err != nil {
+		return err
+	}
+	defer r.Close()
+
+	for _, f := range r.File {
+		// 处理路径安全问题
+		filePath := filepath.Join(destDir, f.Name)
+		if !strings.HasPrefix(filePath, filepath.Clean(destDir)+string(os.PathSeparator)) {
+			return fmt.Errorf("非法的ZIP文件路径: %s", f.Name)
+		}
+
+		if f.FileInfo().IsDir() {
+			// 创建目录
+			if err := os.MkdirAll(filePath, os.ModePerm); err != nil {
+				return err
+			}
+			continue
+		}
+
+		// 确保父目录存在
+		if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
+			return err
+		}
+
+		// 创建文件
+		outFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
+		if err != nil {
+			return err
+		}
+
+		// 打开压缩文件
+		rc, err := f.Open()
+		if err != nil {
+			outFile.Close()
+			return err
+		}
+
+		// 复制内容
+		_, err = io.Copy(outFile, rc)
+		outFile.Close()
+		rc.Close()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// 寻找目录中的Excel文件
+func findExcelFile(dir string) (string, error) {
+	var excelFiles []string
+	err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if !info.IsDir() {
+			lowerPath := strings.ToLower(path)
+			if strings.HasSuffix(lowerPath, ".xlsx") || strings.HasSuffix(lowerPath, ".xls") {
+				excelFiles = append(excelFiles, path)
+			}
+		}
+		return nil
+	})
+
+	if err != nil {
+		return "", err
+	}
+
+	if len(excelFiles) == 0 {
+		return "", fmt.Errorf("未找到Excel文件")
+	}
+
+	// 返回第一个找到的Excel文件
+	return excelFiles[0], nil
+}
+
+// 获取图片文件路径
+func findImageFile(dir, originalName string) string {
+	if dir == "" || originalName == "" {
+		return ""
+	}
+
+	// 检查不同扩展名的图片文件
+	for _, ext := range []string{".png", ".jpg", ".jpeg"} {
+		filePath := filepath.Join(dir, originalName+ext)
+		if _, err := os.Stat(filePath); err == nil {
+			return filePath
+		}
+	}
+
+	return ""
+}
+
+// 上传本地图片到OSS
+func uploadLocalImage(filePath string, prefix string, apictx *ApiSession) (*model.OssType, error) {
+	// 获取ObsClient
+	obsClient, err := CreateObsClient()
+	if err != nil {
+		return nil, fmt.Errorf("创建ObsClient失败: %v", err)
+	}
+	defer obsClient.Close()
+
+	// 上传图片到OSS
+	bucketName := apictx.Svc.Conf.Obs.Bucket
+	ossPath := fmt.Sprintf("u/%s/%s", apictx.User.ID, prefix)
+	return UploadFile(obsClient, bucketName, filePath, ossPath), nil
+}
+
+// 工具函数 - 获取图片URL
+func getImageUrl(oss *model.OssType) string {
+	if oss == nil {
+		return ""
+	}
+	return oss.Url
+}

+ 33 - 31
sku3d/sku3d/api/a-service-img.go

@@ -127,37 +127,39 @@ func UpdateImage(c *gin.Context, apictx *ApiSession) (interface{}, error) {
 	if matImage.Id.IsZero() {
 		return nil, errors.New("id错误")
 	}
-	// searchMat := &model.MatImage{}
-	// // 如果更新的是面料图
-	// found, err := repo.RepoSeachDoc(apictx.CreateRepoCtx(), &repo.DocSearchOptions{
-	// 	CollectName: repo.CollectionMatImages,
-	// 	Query:       repo.Map{"_id": matImage.Id},
-	// 	Project:     []string{"rawImage"},
-	// }, &searchMat)
-	// if err != nil {
-	// 	return nil, err
-	// }
-	// // 没有找到面料数据
-	// if !found {
-	// 	return nil, NewError("未找到数据")
-	// }
-	// // 未更改图片
-	// if matImage.RawImage == nil {
-	// 	return repo.RepoUpdateSetDoc(apictx.CreateRepoCtx(), repo.CollectionMatImages, matImage.Id.Hex(), &matImage)
-	// }
-	// // 更新了面料原图 对应更新fassi 特征数据
-	// if searchMat.RawImage.Url != matImage.RawImage.Url {
-	// 	// 先删除
-	// 	_, err := RomoveFassiImage(matImage.Id.Hex())
-	// 	if err != nil {
-	// 		return nil, err
-	// 	}
-	// 	// 再新增
-	// 	err = AddFassiImage(matImage.Id, matImage.RawImage.Url)
-	// 	if err != nil {
-	// 		return nil, err
-	// 	}
-	// }
+	searchMat := &model.MatImage{}
+	// 如果更新的是面料图
+	found, err := repo.RepoSeachDoc(apictx.CreateRepoCtx(), &repo.DocSearchOptions{
+		CollectName: repo.CollectionMatImages,
+		Query:       repo.Map{"_id": matImage.Id},
+		Project:     []string{"rawImage"},
+	}, &searchMat)
+	if err != nil {
+		return nil, err
+	}
+	// 没有找到面料数据
+	if !found {
+		return nil, NewError("未找到数据")
+	}
+	// 未更改图片
+	if matImage.RawImage == nil || matImage.RawImage == searchMat.RawImage {
+		return repo.RepoUpdateSetDoc(apictx.CreateRepoCtx(), repo.CollectionMatImages, matImage.Id.Hex(), &matImage)
+	}
+	// 更新了面料原图 对应更新fassi 特征数据
+	if searchMat.RawImage.Url != matImage.RawImage.Url {
+		// 先删除
+		// 这里原本可能没有图片特征
+		_, err := RomoveFassiImage(matImage.Id.Hex())
+		if err != nil {
+			log.Error("更新时删除fassi image 错误:", err)
+		}
+		// 再新增
+		err = AddFassiImage(matImage.Id, matImage.RawImage.Url)
+		if err != nil {
+			log.Error("更新时添加fassi image 错误:", err)
+			return nil, errors.New("更新图片搜索特征错误,请重试!")
+		}
+	}
 
 	return repo.RepoUpdateSetDoc(apictx.CreateRepoCtx(), repo.CollectionMatImages, matImage.Id.Hex(), &matImage)
 }

+ 7 - 2
sku3d/sku3d/go.mod

@@ -24,6 +24,7 @@ require (
 	github.com/natefinch/lumberjack v2.0.0+incompatible
 	github.com/nats-io/nats.go v1.39.1
 	github.com/spf13/viper v1.9.0
+	github.com/xuri/excelize/v2 v2.9.0
 	go.mongodb.org/mongo-driver v1.11.1
 	go.uber.org/dig v1.12.0
 	go.uber.org/zap v1.17.0
@@ -86,12 +87,15 @@ require (
 	github.com/mitchellh/mapstructure v1.4.2 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
 	github.com/nats-io/nkeys v0.4.10 // indirect
 	github.com/nats-io/nuid v1.0.1 // indirect
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pelletier/go-toml v1.9.4 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
+	github.com/richardlehane/mscfb v1.0.4 // indirect
+	github.com/richardlehane/msoleps v1.0.4 // indirect
 	github.com/robfig/cron/v3 v3.0.1 // indirect
 	github.com/rs/xid v1.2.1 // indirect
 	github.com/sirupsen/logrus v1.8.1 // indirect
@@ -100,19 +104,20 @@ require (
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/streadway/amqp v1.0.0 // indirect
-	github.com/stretchr/testify v1.8.0 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
 	github.com/tjfoc/gmsm v1.4.1 // indirect
 	github.com/ugorji/go/codec v1.1.7 // indirect
 	github.com/xdg-go/pbkdf2 v1.0.0 // indirect
 	github.com/xdg-go/scram v1.1.1 // indirect
 	github.com/xdg-go/stringprep v1.0.3 // indirect
+	github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
+	github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
 	github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
 	go.opencensus.io v0.23.0 // indirect
 	go.uber.org/atomic v1.7.0 // indirect
 	go.uber.org/multierr v1.8.0 // indirect
 	golang.org/x/crypto v0.36.0 // indirect
-	golang.org/x/net v0.25.0 // indirect
+	golang.org/x/net v0.30.0 // indirect
 	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
 	golang.org/x/sync v0.12.0 // indirect
 	golang.org/x/text v0.23.0 // indirect

+ 19 - 6
sku3d/sku3d/go.sum

@@ -470,6 +470,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
 github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
@@ -516,6 +518,11 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
 github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
+github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
+github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
+github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
 github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@@ -555,16 +562,14 @@ github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1Sd
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
 github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
@@ -591,6 +596,12 @@ github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCO
 github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
 github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
 github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
+github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
+github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
+github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
+github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
+github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -665,6 +676,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
+golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -743,8 +756,8 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

+ 17 - 0
sku3d/sku3d/readme.md

@@ -0,0 +1,17 @@
+这是一个已存在的项目,现在要添加根据模板excel文件批量导入和导出相关数据到mongodb的功能。
+
+    可能用到的实现: api/a-oss-upload.go,api/a-service-fassi.go,api/a-service-img.go,api/a-user.go,db/model/a-matimage.go
+    要求:根据模板文件template.xls,数据模型db/model/a-matimage.go和示例项目中example.json为参考,在api/a-excel.go文件中实现导入和导出功能。
+    特别说明:
+        1. 因为导入和导出这个都会用到template.xls这个模板。template.xls模板是固定的,读取template.xls中示例来让你确定导入导出数据填充。程序中并不需要每次读取。
+            1. 导入时template.xls中的数据应该填充到各个数据表中,并且做好对应的数据id关联。
+            2. 导出时template.xls表头下方为空的,数据应该从各个数据表中读取解析后逐行填充到对应表头下的单元格中。
+        2. 表格中图片需要上传到oss,并返回url。然后将url填入到mongodb中。调用api/a-service-img.go createImg 并在写入成功后调用AddFassiImage(imgId, data.RawImage.Url)api将图片向量特征和id关联。
+        3. 因为使用云函数处理图片需要预热,所以在调用AddFassiImage时需要等待云函数预热完成,这里通过提前触发main.go中的QueryFassiImage来实现。
+        4. 当没有图片时跳过AddFassiImage调用。
+        5. 表格中多个数据源来自category表,category列表全局只有一条数据,取下标0,需要解析image中的categories对应id匹配到相关信息。
+        6. 样品收集对应staffUser
+
+
+1. 上传的文件是个.zi压缩文件,其中包括一个.xls或者.xlsx文件,记录着数据;一个goods目录里面存放的是.xls文件中原命名编号为名的商品图片(.png,.jpg,.jpeg);一个texture目录里面存放的是.xls文件中原命名编号为名的纹理图片(.png,.jpg,.jpeg)。我们需要上传这个.zip文件然后解压后,解析这个.xlsx或.xls文件,找到原命名编号。然后从goods目录和texture目录中找到对应的图片,上传到oss,并返回url。然后将纹理图片url填入到mongodb中。调用api/a-service-img.go createImg 并在写入成功后调用AddFassiImage(imgId, data.RawImage.Url)api将纹理图片向量特征和id关联。当图片不存在时跳过上传oss和纹理图片向量特征和id关联。处理完成后删除解压的文件
+2. 当我们检查到分类或者收集人不存在时,记录导入失败日志到数据库用于用户查看(第几条数据错误,错误原因)