timerzz 9e5e4d7d27
All checks were successful
Build image / build (push) Successful in 1m5s
update 不在尝试添加图片
2025-03-30 21:29:26 +08:00

275 lines
8.6 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 coach_outlet
import (
"fmt"
"io"
"net/http"
"strconv"
"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/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) (string, error) {
// 创建Excel文件
f := excelize.NewFile()
defer func() {
if err := f.Close(); err != nil {
fmt.Println(err)
}
}()
// 创建工作表
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 {
pArticle, _ := lo.Find(article.Providers, func(p v2.ProviderArticle) bool {
return p.ProviderId == v2.ProviderId(cheapId)
})
if pArticle.Cost.OriginalPrice != 0 {
// 更新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)
}
logrus.Infof("成功导出 %d 条数据", rowIndex-1)
return filename, 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,
article.Image, // 图片单元格留空,后面会添加图片
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)
// 处理图片
linkCell, _ := excelize.CoordinatesToCellName(1, rowIndex)
_ = f.SetCellHyperLink(sheetName, linkCell, caProvider.Link, "External")
//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: ".jpg",
// 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
}
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
}