package coach_outlet import ( "fmt" "io" "net/http" "strconv" "strings" "time" "gitea.timerzz.com/kedaya_haitao/common/structs/storage" "gitea.timerzz.com/kedaya_haitao/common/structs/utils" v2 "gitea.timerzz.com/kedaya_haitao/common/structs/v2" dw_sdk "gitea.timerzz.com/kedaya_haitao/dw-sdk" "github.com/gofiber/fiber/v3" "github.com/samber/lo" "github.com/sirupsen/logrus" "github.com/xuri/excelize/v2" "gorm.io/gorm" ) const ( USCoachOutlet = "us-coach-outlet" CACoachOutlet = "ca-coach-outlet" ) type Exporter struct { storage *storage.Storage dw *dw_sdk.Client seller v2.Seller } func NewExporter(storage *storage.Storage, dw *dw_sdk.Client) (e *Exporter, err error) { e = &Exporter{storage: storage, dw: dw} e.seller, err = storage.Seller().GetBySellerId("dw-normal") return e, err } // ExportCheapProduct 导出价格较低的商品数据到Excel,直接写入到HTTP响应 func (e *Exporter) ExportCheapProduct(providerId string, c fiber.Ctx) error { // 创建Excel文件 f := excelize.NewFile() defer f.Close() // 创建工作表 sheetName := providerId _, err := f.NewSheet(sheetName) if err != nil { return fmt.Errorf("创建工作表失败: %w", err) } _ = f.DeleteSheet("Sheet1") // 删除默认的Sheet1 // 确定查询的供应商ID var cheapId, otherId string cheapId = providerId otherId = lo.Ternary(providerId == CACoachOutlet, USCoachOutlet, CACoachOutlet) // 查询符合条件的商品 var articles []*v2.Article rowIndex := 1 // 从第1行开始(标题行) setHeader(f, sheetName) err = e.storage.DB().Model(&v2.Article{}).Select("DISTINCT articles.*"). Preload("Providers", func(db *gorm.DB) *gorm.DB { return db.Where("provider_id IN ?", []string{cheapId, otherId}) }). Preload("Sellers", "seller_id = ?", "dw-normal"). Joins("JOIN provider_articles pa1 ON articles.id = pa1.article_id AND pa1.provider_id = ? AND pa1.available = true", cheapId). Joins("JOIN provider_articles pa2 ON articles.id = pa2.article_id AND pa2.provider_id = ?", otherId). Joins("JOIN seller_articles sa ON articles.id = sa.article_id AND (sa.seller_id = 'dw-normal' AND (sa.exclude = false OR sa.exclude IS NULL))"). Where("pa1.cost->>'finalPrice' <= pa2.cost->>'finalPrice'"). FindInBatches(&articles, 20, func(tx *gorm.DB, batch int) error { for _, article := range articles { // 更新DW价格(如果需要) dwSeller, idx, exist := lo.FindIndexOf(article.Sellers, func(seller v2.SellerArticle) bool { return seller.SellerId == "dw-normal" }) if exist && dwSeller.Sell.CreatedAt.Before(time.Now().AddDate(0, 0, -1)) { // 如果已经是一天前的数据,那么就要更新下 dwSeller = e.pullDwPrice(dwSeller) article.Sellers[idx] = dwSeller } // 计算利润率 utils.ProfitRate(article) // 添加到Excel rowIndex++ if err = addRow(f, sheetName, rowIndex, article); err != nil { logrus.Errorf("添加行失败: %v", err) continue } } return nil }).Error if err != nil { return fmt.Errorf("查询数据失败: %w", err) } // 保存Excel文件 filename := fmt.Sprintf("coach_outlet_%s.xlsx", providerId) if err = f.SaveAs(filename); err != nil { return fmt.Errorf("保存Excel文件失败: %w", err) } // 直接将Excel文件写入到HTTP响应 if err = f.Write(c.Response().BodyWriter()); err != nil { return fmt.Errorf("写入Excel数据失败: %w", err) } logrus.Infof("成功导出 %d 条数据", rowIndex-1) return nil } // 设置标题行 func setHeader(f *excelize.File, sheetName string) { // 设置列标题(如果是第一行) headers := []string{"商品名", "图片", "美国官网原价", "美国官网最终价格", "加拿大官网原价", "加拿大官网最终价格", "DW价格", "DW到手价格", "利润", "利润率"} for i, header := range headers { cell, _ := excelize.CoordinatesToCellName(i+1, 1) _ = f.SetCellValue(sheetName, cell, header) } // 设置列宽 _ = f.SetColWidth(sheetName, "A", "A", 30) _ = f.SetColWidth(sheetName, "B", "B", 20) // 增加图片列宽度 _ = f.SetColWidth(sheetName, "C", "J", 15) _ = f.SetRowHeight(sheetName, 1, 20) // 设置标题行高 // 设置样式 style, _ := f.NewStyle(&excelize.Style{ Font: &excelize.Font{Bold: true}, Alignment: &excelize.Alignment{Horizontal: "center"}, }) _ = f.SetRowStyle(sheetName, 1, 1, style) } // addRow 根据article信息添加一行到excel // 商品名,图片,美国官网原价,美国官网最终价格,加拿大官网原始价格,加拿大官网最终价格,dw价格, dw到手价格,利润,利润率 func addRow(f *excelize.File, sheetName string, rowIndex int, article *v2.Article) error { // 获取美国和加拿大的供应商信息 usProvider, _ := lo.Find(article.Providers, func(p v2.ProviderArticle) bool { return p.ProviderId == USCoachOutlet }) caProvider, _ := lo.Find(article.Providers, func(p v2.ProviderArticle) bool { return p.ProviderId == CACoachOutlet }) // 获取DW销售商信息 dwSeller, _, _ := lo.FindIndexOf(article.Sellers, func(s v2.SellerArticle) bool { return s.SellerId == "dw-normal" }) // 填充数据 cells := []interface{}{ article.Name, "", // 图片单元格留空,后面会添加图片 usProvider.Cost.OriginalPrice, usProvider.Cost.FinalPrice, caProvider.Cost.OriginalPrice, caProvider.Cost.FinalPrice, dwSeller.Sell.OriginalPrice, dwSeller.Sell.FinalPrice, article.GrossProfit, article.Rate, } // 写入数据 for i, value := range cells { cell, _ := excelize.CoordinatesToCellName(i+1, rowIndex) _ = f.SetCellValue(sheetName, cell, value) } // 设置行高以适应图片 _ = f.SetRowHeight(sheetName, rowIndex, 80) // 处理图片 if len(article.Image) > 0 { imgCell, _ := excelize.CoordinatesToCellName(2, rowIndex) // 下载图片 imgData, err := downloadImage(article.Image) if err != nil { logrus.Warnf("下载图片失败 %s: %v", article.Image, err) // 如果下载失败,至少显示URL _ = f.SetCellValue(sheetName, imgCell, article.Image) } else { // 添加图片到Excel if err := f.AddPictureFromBytes(sheetName, imgCell, &excelize.Picture{ Extension: getImageExtension(article.Image), File: imgData, Format: &excelize.GraphicOptions{ AutoFit: true, ScaleX: 0.5, ScaleY: 0.5, Positioning: "oneCell", }, }); err != nil { logrus.Warnf("添加图片到Excel失败: %v", err) // 如果添加失败,显示URL _ = f.SetCellValue(sheetName, imgCell, article.Image) } } } return nil } // downloadImage 下载图片并返回字节数据 func downloadImage(url string) ([]byte, error) { // 创建HTTP客户端,设置超时 client := &http.Client{ Timeout: 10 * time.Second, } // 发送GET请求 resp, err := client.Get(url) if err != nil { return nil, fmt.Errorf("HTTP请求失败: %w", err) } defer resp.Body.Close() // 检查状态码 if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP状态码错误: %d", resp.StatusCode) } // 读取响应体 imgData, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取图片数据失败: %w", err) } return imgData, nil } // getImageExtension 从URL获取图片扩展名 func getImageExtension(url string) string { // 默认扩展名 ext := ".jpg" // 从URL中提取扩展名 if strings.Contains(url, ".") { parts := strings.Split(url, ".") lastPart := parts[len(parts)-1] // 处理可能的查询参数 if strings.Contains(lastPart, "?") { lastPart = strings.Split(lastPart, "?")[0] } // 常见图片扩展名 switch strings.ToLower(lastPart) { case "jpg", "jpeg", "png", "gif", "bmp", "webp": ext = "." + strings.ToLower(lastPart) } } return ext } func (e *Exporter) pullDwPrice(sArticle v2.SellerArticle) v2.SellerArticle { skuId, _ := strconv.Atoi(sArticle.SkuID) resp, err := e.dw.NormalBidClient().LowestPrice(skuId) if err != nil { return sArticle } lowest, exist := lo.Find(resp.Items, func(item dw_sdk.LowestPriceItem) bool { return item.BiddingType == dw_sdk.BiddingType_Normal }) if !exist { // 没有拿到对应类型的价格,那就拿一个最低的 lowest = lo.MinBy(resp.Items, func(item1, item2 dw_sdk.LowestPriceItem) bool { return item1.LowestPrice < item2.LowestPrice && item1.LowestPrice > 0 }) } if lowest.LowestPrice == 0 { // 没有拿到价格,将exclude设置为true,如果后面要拉取,需要手动打开 return sArticle } sell := utils.CalculateSellerPrice(append(e.seller.CalculateProcess, sArticle.CalculateProcess...), map[string]float64{ "originalPrice": float64(lowest.LowestPrice / 100), }) if sell.OriginalPrice != sArticle.Sell.OriginalPrice { sArticle.Sell = sell sArticle.HistoryPrice = append(sArticle.HistoryPrice, sArticle.Sell) } if err = e.storage.SellerArticle().Upsert(sArticle); err != nil { logrus.Errorf("保存sellerArticle失败:%v", err) } return sArticle }