428 lines
12 KiB
Go
428 lines
12 KiB
Go
// 思迅数据同步
|
||
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
|
||
}
|