// 思迅数据同步 package services import ( "fmt" "log" "os" "path/filepath" "time" "github.com/goccy/go-json" // 使用性能更好的JSON库 "github.com/spf13/viper" "fafa-crawler/src/beans" "fafa-crawler/src/mapper" "fafa-crawler/src/models" "fafa-crawler/src/util" "gorm.io/gorm" ) const ( BatchSize = 400 ) var () type SxGoodsService struct { db *gorm.DB // Add database connection } func NewSxGoodsService() *SxGoodsService { return sxGoodsService } // ProcessSiXunGoodsData 高效处理思迅商品数据的综合方法 // 执行顺序: 1. JSON数据转换 2. 图片文件过滤 3. 批量保存到数据库 4. 上传图片到COS func (s *SxGoodsService) ProcessSiXunGoodsData(path string) (srcDataCnt, filteredDataCnt, savedDataCnt, filteredImgCnt int64) { // 步骤1: JSON数据转换 srcDataCnt, targetDataCnt, sxBeanList := s.JsonDataConvBeans(path) if targetDataCnt == 0 || len(sxBeanList) == 0 { log.Printf("JSON数据转换失败或无有效数据") return srcDataCnt, 0, 0, 0 } log.Printf("JSON数据转换完成: 源数据%d条,转换成功%d条", srcDataCnt, targetDataCnt) // 步骤2: 图片文件过滤 filteredRecords, filteredDataCnt, filteredImgCnt := s.filterSiXunGoodsBeans(path, sxBeanList) if filteredDataCnt == 0 { log.Printf("图片文件过滤后无有效数据") return srcDataCnt, filteredDataCnt, 0, filteredImgCnt } log.Printf("图片文件过滤完成: 有效记录%d条,有效图片%d张", filteredDataCnt, filteredImgCnt) // 步骤3: 批量保存到数据库 savedDataCnt = s.BatchSaveSiXunProducts(filteredRecords) log.Printf("批量保存完成: 成功保存%d条记录", savedDataCnt) // 步骤4: 上传图片到COS if err := s.uploadImagesToCOS(path); err != nil { log.Printf("上传图片到COS失败: %v", err) } return srcDataCnt, filteredDataCnt, savedDataCnt, filteredImgCnt } // uploadImagesToCOS 上传图片到COS func (s *SxGoodsService) uploadImagesToCOS(path string) error { // 获取当前日期文件夹名称 currentTime := time.Now() dateFolderName := currentTime.Format("20060102") localImageDir := filepath.Join(path, dateFolderName) // 从配置文件读取COS配置 if err := s.loadCOSConfig(); err != nil { return fmt.Errorf("加载COS配置失败: %w", err) } // 获取COS工具类实例 cosConfig := util.COSConfig{ BucketURL: viper.GetString("cossdk.bucket_url"), SecretID: viper.GetString("cossdk.secret_id"), SecretKey: viper.GetString("cossdk.secret_key"), BasePath: viper.GetString("cossdk.base_path"), } cosUtil, err := util.GetCOSUtil(cosConfig) if err != nil { return fmt.Errorf("获取COS工具类实例失败: %w", err) } // 并发上传图片到COS if err := cosUtil.ConcurrentUploadDirectory(localImageDir, dateFolderName, 10); err != nil { return fmt.Errorf("上传图片到COS失败: %w", err) } log.Printf("图片上传到COS完成: 上传目录 %s", localImageDir) return nil } // loadCOSConfig 从配置文件加载COS配置 func (s *SxGoodsService) loadCOSConfig() error { viper.SetConfigFile("./config/config.toml") viper.SetConfigType("toml") if err := viper.ReadInConfig(); err != nil { return fmt.Errorf("读取配置文件失败: %w", err) } return nil } // JsonDataConvBeans 将JSON数据转换为SiXunGoodsBean对象数组 func (s *SxGoodsService) JsonDataConvBeans(path string) (srcDataCnt, targetDataCnt int64, sxBeanList []*beans.SiXunGoodsBean) { // 构建完整的文件路径 jsonFilePath := filepath.Join(path, "data", "product_data.json") // 读取JSON文件 data, err := os.ReadFile(jsonFilePath) if err != nil { log.Printf("读取JSON文件失败: %v", err) return 0, 0, nil } // 定义临时结构体来解析JSON数据 type tempImage struct { ProductID string `json:"product_id"` Barcode string `json:"barcode"` ImageURL string `json:"image_url"` Seq int32 `json:"seq"` IsMain int32 `json:"is_main"` } type tempProduct struct { ProductID string `json:"product_id"` Name string `json:"name"` Title string `json:"title"` Barcode string `json:"barcode"` Category1st string `json:"category_1st"` Category2nd string `json:"category_2nd"` Price float64 `json:"price"` Keywords string `json:"keywords"` Thumb string `json:"thumb"` Brand string `json:"brand"` Spec string `json:"spec"` Weight float64 `json:"weight"` WeightUnit string `json:"weight_unit"` Intro string `json:"intro"` ImagesList []tempImage `json:"images_list"` } // 解析JSON数据(使用高性能JSON库) var tempProducts []tempProduct if err := json.Unmarshal(data, &tempProducts); err != nil { log.Printf("解析JSON数据失败: %v", err) return 0, 0, nil } // 设置源数据计数 srcDataCnt = int64(len(tempProducts)) // 转换为SiXunGoodsBean对象数组 var siXunGoodsBeans []*beans.SiXunGoodsBean for _, tempProd := range tempProducts { // 创建LibraryProduct模型 product := &models.LibraryProduct{ Name: tempProd.Name, Sname: tempProd.Name, Title: tempProd.Title, Barcode: tempProd.Barcode, Category1St: tempProd.Category1st, Category2Nd: tempProd.Category2nd, Price: tempProd.Price, Keywords: tempProd.Keywords, Thumb: tempProd.Thumb, Brand: tempProd.Brand, Spec: tempProd.Spec, Weight: fmt.Sprintf("%.2f", tempProd.Weight), WeightUnit: tempProd.WeightUnit, Intro: tempProd.Intro, Source: fmt.Sprintf("sixun_%s", tempProd.ProductID), } // 如果Category2Nd为空,使用Category1st的值 if product.Category2Nd == "" { product.Category2Nd = product.Category1St } product.Category = product.Category2Nd // 如果Intro为空,使用Title的值 if product.Intro == "" { product.Intro = product.Title } // 创建LibraryProductImage模型列表 var productImages []*models.LibraryProductImage for _, tempImg := range tempProd.ImagesList { image := &models.LibraryProductImage{ ImageURL: tempImg.ImageURL, Seq: tempImg.Seq, IsMain: tempImg.IsMain, } productImages = append(productImages, image) } // 创建SiXunGoodsBean bean := &beans.SiXunGoodsBean{ Product: product, ImageList: productImages, } siXunGoodsBeans = append(siXunGoodsBeans, bean) } // 设置转换成功记录数 targetDataCnt = int64(len(siXunGoodsBeans)) return srcDataCnt, targetDataCnt, siXunGoodsBeans } // filterSiXunGoodsBeans 过滤思迅商品记录,检查图片文件是否存在 func (s *SxGoodsService) filterSiXunGoodsBeans(path string, records []*beans.SiXunGoodsBean) (filteredRecords []*beans.SiXunGoodsBean, targetDataCnt, targetImgCnt int64) { // 创建日期格式的文件夹 currentTime := time.Now() dateFolderName := currentTime.Format("20060102") dateFolderPath := filepath.Join(path, dateFolderName) // 确保日期文件夹存在 if err := os.MkdirAll(dateFolderPath, 0755); err != nil { log.Printf("创建日期文件夹失败: %v", err) return records, int64(len(records)), 0 } // 遍历所有记录 for _, record := range records { // 创建新的图片列表,只包含存在的图片 var validImages []*models.LibraryProductImage validImageCount := int64(0) // 检查每个图片是否存在 for _, image := range record.ImageList { imagePath := filepath.Join(path, "images", image.ImageURL) // 检查文件是否存在 if _, err := os.Stat(imagePath); err == nil { // 文件存在,添加到有效图片列表 validImages = append(validImages, image) validImageCount++ // 拷贝图片到日期文件夹(使用高性能文件拷贝工具) targetImagePath := filepath.Join(dateFolderPath, image.ImageURL) if err := util.CopyFile(imagePath, targetImagePath); err != nil { log.Printf("拷贝图片文件失败: %v", err) } // 更新图片路径为统一的实体示例路径 image.ImageURL = fmt.Sprintf("/media/images/goods_library/%s/%s", dateFolderName, image.ImageURL) } } // 更新缩略图路径 record.Product.Thumb = fmt.Sprintf("/media/images/goods_library/%s/%s", dateFolderName, record.Product.Thumb) // 如果有有效图片,则保留该记录 if len(validImages) > 0 { // 更新记录的图片列表 record.ImageList = validImages filteredRecords = append(filteredRecords, record) targetImgCnt += validImageCount } } targetDataCnt = int64(len(filteredRecords)) return filteredRecords, targetDataCnt, targetImgCnt } // BatchSaveSiXunProducts 高效批量添加思迅商品和商品图片记录 func (s *SxGoodsService) BatchSaveSiXunProducts(records []*beans.SiXunGoodsBean) int64 { if len(records) == 0 { return 0 } var successCount int64 totalRecords := len(records) // 分批处理记录 for i := 0; i < totalRecords; i += BatchSize { // 计算当前批次的结束索引 end := i + BatchSize if end > totalRecords { end = totalRecords } // 获取当前批次的数据 batch := records[i:end] // 处理当前批次 count := s.saveSiXunProductBatch(batch) successCount += count log.Printf("思迅商品批量保存进度: %d/%d (当前批次保存成功: %d)\n", end, totalRecords, count) } return successCount } // saveSiXunProductBatch 保存思迅商品批次数据 func (s *SxGoodsService) saveSiXunProductBatch(batch []*beans.SiXunGoodsBean) int64 { var successCount int64 // 使用事务确保数据一致性 err := s.db.Transaction(func(tx *gorm.DB) error { // 创建临时存储商品和图片的切片 var products []*models.LibraryProduct var allProductImages []*models.LibraryProductImage // 遍历批次中的每个商品 for _, bean := range batch { // 检查商品是否已存在 if s.IsExitsSiXun(bean.Product) { continue // 如果商品已存在,跳过 } // 添加商品到待插入列表 products = append(products, bean.Product) // 处理商品图片 for _, image := range bean.ImageList { // 设置商品ID(在创建商品后会更新) image.ProductID = bean.Product.ID allProductImages = append(allProductImages, image) } } // 如果没有需要保存的商品,直接返回 if len(products) == 0 { return nil } // 批量插入商品(使用批次大小) if err := tx.CreateInBatches(products, BatchSize).Error; err != nil { return fmt.Errorf("批量保存商品失败: %w", err) } // 更新图片中的商品ID并批量插入图片 if len(allProductImages) > 0 { // 根据商品的条形码或名称匹配更新图片的商品ID for i, product := range products { for _, image := range allProductImages { // 这里假设图片已经正确关联到商品 // 在实际应用中,可能需要更复杂的匹配逻辑 if image.ProductID == 0 { image.ProductID = product.ID } } // 更新原始数据中的商品ID(如果需要) if i < len(batch) { batch[i].Product.ID = product.ID } } // 批量插入图片(使用批次大小) if err := tx.CreateInBatches(allProductImages, BatchSize).Error; err != nil { return fmt.Errorf("批量保存商品图片失败: %w", err) } } successCount = int64(len(products)) return nil }) if err != nil { log.Printf("思迅商品批量保存失败: %v", err) return 0 } return successCount } // IsExitsSiXun 检查思迅商品是否已存在 func (s *SxGoodsService) IsExitsSiXun(record *models.LibraryProduct) bool { // 空记录直接返回不存在 if record == nil { return false } m := mapper.LibraryProduct q := m.Select(m.ID) // 优先使用条形码查询,无条形码时使用商品名称查询 if record.Barcode != "" { q = q.Where(m.Barcode.Eq(record.Barcode)) } else if record.Name != "" { q = q.Where(m.Name.Eq(record.Name)) } else { // 无有效查询条件 return false } // 执行数量统计查询 cnt, err := q.Count() if err != nil { log.Printf("查询思迅商品数量失败,错误: %v", err) return false } return cnt > 0 } // IsExitsSiXunByBarcode 根据条形码检查思迅商品是否已存在 func (s *SxGoodsService) IsExitsSiXunByBarcode(barcode string) bool { if barcode == "" { return false } m := mapper.LibraryProduct q := m.Select(m.ID).Where(m.Barcode.Eq(barcode)) // 执行数量统计查询 cnt, err := q.Count() if err != nil { log.Printf("根据条形码查询思迅商品数量失败,错误: %v", err) return false } return cnt > 0 }