fafa-crawler/src/services/sx_goods_service.go

428 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 思迅数据同步
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
}