From be5c82683fae0e6eb89da8c26c14ab5c0a366eda Mon Sep 17 00:00:00 2001 From: timerzz Date: Sun, 30 Mar 2025 17:36:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96Coach=20Outlet=20Exce?= =?UTF-8?q?l=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修改ExportCheapProduct方法,支持直接将Excel数据写入HTTP响应 2. 添加图片下载和嵌入功能,提升Excel报表可视化效果 3. 优化数据查询和处理逻辑,提高导出效率 4. 完善错误处理和日志记录 此次修改避免了临时文件的创建和管理,减少了内存使用,提高了响应速度。 --- build/build-chart.sh | 2 +- build/chart/Chart.yaml | 4 +- build/chart/values.yaml | 4 + cmd/article.go | 120 +++++----- go.mod | 15 ++ go.sum | 46 ++++ service/article_svc.go | 434 ++++++++++++++++++------------------ service/tools_svc.go | 72 ++++++ tools/coach-outlet/excel.go | 294 ++++++++++++++++++++++++ 9 files changed, 715 insertions(+), 276 deletions(-) create mode 100644 service/tools_svc.go create mode 100644 tools/coach-outlet/excel.go diff --git a/build/build-chart.sh b/build/build-chart.sh index 179eccc..40c65ef 100644 --- a/build/build-chart.sh +++ b/build/build-chart.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/bash # 打包 Helm Chart helm package chart/ diff --git a/build/chart/Chart.yaml b/build/chart/Chart.yaml index 70ea9b5..deb6f7c 100644 --- a/build/chart/Chart.yaml +++ b/build/chart/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.0 +version: 1.0.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.0.0" +appVersion: "v1.0.2" diff --git a/build/chart/values.yaml b/build/chart/values.yaml index 8511f9b..618713a 100644 --- a/build/chart/values.yaml +++ b/build/chart/values.yaml @@ -68,6 +68,8 @@ ingress: paths: - path: /api/v2/articles pathType: ImplementationSpecific + - path: /api/v2/tools/excel + pathType: ImplementationSpecific tls: [] # - secretName: chart-example-tls # hosts: @@ -125,6 +127,8 @@ volumes: items: - key: db path: db.yaml + - key: dw + path: dw.yaml defaultMode: 420 # - name: foo # secret: diff --git a/cmd/article.go b/cmd/article.go index d383b26..d83ded8 100644 --- a/cmd/article.go +++ b/cmd/article.go @@ -1,56 +1,64 @@ -package main - -import ( - "context" - "flag" - "os" - "os/signal" - - "gitea.timerzz.com/kedaya_haitao/article/service" - "gitea.timerzz.com/kedaya_haitao/common/pkg/database" - "gitea.timerzz.com/kedaya_haitao/common/pkg/web" - "gitea.timerzz.com/kedaya_haitao/common/structs/storage" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/cors" - "github.com/gofiber/fiber/v3/middleware/recover" - - "github.com/golang/glog" -) - -func main() { - flag.Parse() - glog.Info(">>> BEGIN INIT<<<") - ctx, cancel := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt) - defer cancel() - // 初始化数据库 - db, err := database.InitDefaultDatabase() - if err != nil { - glog.Fatalf("init database failed: %v", err) - } - // 初始化服务 - r := fiber.New(fiber.Config{ErrorHandler: web.ErrHandle}) - r.Use(cors.New(), recover.New()) - - stg := storage.NewStorage(db) - _ = stg.Article().AutoMigrate() - - svc := []web.Register{ - web.NewProbe(), - service.NewArticle(stg), - } - - for _, s := range svc { - s.Registry(r) - } - - port := os.Getenv("PORT") - if port == "" { - port = "80" - } - if err = r.Listen(":"+port, fiber.ListenConfig{ - EnablePrintRoutes: true, - GracefulContext: ctx, - }); err != nil { - glog.Warningf("service over: %v", err) - } -} +package main + +import ( + "context" + "flag" + "os" + "os/signal" + + "gitea.timerzz.com/kedaya_haitao/article/service" + "gitea.timerzz.com/kedaya_haitao/common/pkg/database" + "gitea.timerzz.com/kedaya_haitao/common/pkg/web" + "gitea.timerzz.com/kedaya_haitao/common/structs/storage" + dw_sdk "gitea.timerzz.com/kedaya_haitao/dw-sdk" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/cors" + "github.com/gofiber/fiber/v3/middleware/recover" + "github.com/sirupsen/logrus" + + "github.com/golang/glog" +) + +func main() { + flag.Parse() + glog.Info(">>> BEGIN INIT<<<") + ctx, cancel := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt) + defer cancel() + // 初始化数据库 + db, err := database.InitDefaultDatabase() + if err != nil { + glog.Fatalf("init database failed: %v", err) + } + + client, err := dw_sdk.InitDefaultDWClient() + if err != nil { + logrus.Fatalf("初始化redis失败:%v", err) + } + // 初始化服务 + r := fiber.New(fiber.Config{ErrorHandler: web.ErrHandle}) + r.Use(cors.New(), recover.New()) + + stg := storage.NewStorage(db) + _ = stg.Article().AutoMigrate() + + svc := []web.Register{ + web.NewProbe(), + service.NewArticle(stg), + service.NewTools(stg, client), // 添加工具服务 + } + + for _, s := range svc { + s.Registry(r) + } + + port := os.Getenv("PORT") + if port == "" { + port = "80" + } + if err = r.Listen(":"+port, fiber.ListenConfig{ + EnablePrintRoutes: true, + GracefulContext: ctx, + }); err != nil { + glog.Warningf("service over: %v", err) + } +} diff --git a/go.mod b/go.mod index a978439..f4f2c03 100644 --- a/go.mod +++ b/go.mod @@ -6,18 +6,25 @@ toolchain go1.24.1 require ( gitea.timerzz.com/kedaya_haitao/common v0.0.0-20250329125718-37b1ee0b6a4b + gitea.timerzz.com/kedaya_haitao/dw-sdk v0.0.0-20240904075121-552ef11ea87c github.com/gofiber/fiber/v3 v3.0.0-beta.4 github.com/golang/glog v1.2.2 github.com/samber/lo v1.49.1 + github.com/sirupsen/logrus v1.9.3 + github.com/xuri/excelize/v2 v2.9.0 gorm.io/gorm v1.25.10 ) require ( github.com/andybalholm/brotli v1.1.1 // indirect + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect github.com/expr-lang/expr v1.16.9 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gofiber/schema v1.2.0 // indirect github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -25,15 +32,23 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect github.com/tinylib/msgp v1.2.5 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.58.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect + github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect + golang.org/x/arch v0.9.0 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect diff --git a/go.sum b/go.sum index 3ab91db..37f70ac 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,17 @@ gitea.timerzz.com/kedaya_haitao/common v0.0.0-20250329125718-37b1ee0b6a4b h1:JPa3QxIOsgiKgT66ujH4EiAoIAHxp++Gacf/NpV1xds= gitea.timerzz.com/kedaya_haitao/common v0.0.0-20250329125718-37b1ee0b6a4b/go.mod h1:cfkwyDHbOjucM8xLLg8yIkZKz33kdVqvBZYrfNjM8oc= +gitea.timerzz.com/kedaya_haitao/dw-sdk v0.0.0-20240904075121-552ef11ea87c h1:pami6fFsRqKYJyJbSGqXaz2hsbgims7/3ma7cY9CwuY= +gitea.timerzz.com/kedaya_haitao/dw-sdk v0.0.0-20240904075121-552ef11ea87c/go.mod h1:Yfl45xbT2dTh4YHqz4E2i++TYq7fahTeSv9JWNFLjNo= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -17,8 +27,11 @@ github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/as github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -33,6 +46,10 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -42,23 +59,39 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= @@ -67,18 +100,30 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k= +golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -89,3 +134,4 @@ gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/service/article_svc.go b/service/article_svc.go index 7ae8a0b..dc386c4 100644 --- a/service/article_svc.go +++ b/service/article_svc.go @@ -1,217 +1,217 @@ -package service - -import ( - "fmt" - "strconv" - "strings" - - "gitea.timerzz.com/kedaya_haitao/common/pkg/web" - "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" - "github.com/gofiber/fiber/v3" - "github.com/samber/lo" - "gorm.io/gorm" -) - -type Article struct { - storage *storage.Storage -} - -func NewArticle(storage *storage.Storage) *Article { - return &Article{ - storage: storage, - } -} - -func (s *Article) Registry(r fiber.Router) { - api := r.Group("/api/v2") - api.Get("articles", s.List) - api.Get("articles/u/:id", s.Get) - api.Get("articles/dict/brand", s.BrandDict) - api.Patch("articles", s.Update) - api.Get("articles/provider/history/:id", s.ProviderHistory) - api.Get("articles/seller/history/:id", s.SellerHistory) - api.Patch("articles/provider", s.UpdateProviderArticle) - api.Patch("articles/seller", s.UpdateSellerArticle) - - //屏蔽相关操作 - api.Get("articles/ban", s.ListBanArticle) - api.Post("articles/ban/:ids", s.BanArticle) - api.Delete("articles/ban/:ids", s.LiftBanArticle) -} - -func (s *Article) List(c fiber.Ctx) error { - var q storage.FindArticleQuery - if err := c.Bind().Query(&q); err != nil { - return err - } - q.SetBan(false) - var query = storage.NewPageListQuery(&q) - if err := c.Bind().Query(query); err != nil { - return err - } - articles, total, err := s.storage.Article().List(*query, q.SortScope) - if err != nil { - return err - } - return c.JSON(web.NewResponse(web.NewListResponse(total, articles))) -} - -func (s *Article) Get(c fiber.Ctx) error { - id := c.Params("id") - if id == "" { - return fiber.NewError(fiber.StatusBadRequest, "id is required") - } - i, _ := strconv.Atoi(id) - query := storage.NewGetArticleQuery().SetID(uint(i)) - article, err := s.storage.Article().Get(query) - if err != nil { - return err - } - return c.JSON(web.NewResponse(article)) -} - -func (s *Article) Update(c fiber.Ctx) error { - var article v2.Article - if err := c.Bind().JSON(&article); err != nil { - return err - } - if article.ID == 0 { - return fiber.NewError(fiber.StatusBadRequest, "id is required") - } - if err := s.storage.Article().Update(article); err != nil { - return err - } - return c.JSON(web.NewResponse(article)) -} - -func (s *Article) BrandDict(c fiber.Ctx) error { - var dict = []utils.Dict{ - { - Key: string(v2.Brand_Coach), - Title: fmt.Sprintf("蔻驰/%s", v2.Brand_Coach), - Value: v2.Brand_Coach, - }, - } - return c.JSON(web.NewResponse(dict)) -} - -// 获取供应商历史价格 -func (s *Article) ProviderHistory(c fiber.Ctx) error { - i := c.Params("id") - id, _ := strconv.Atoi(i) - if id == 0 { - return fiber.NewError(fiber.StatusBadRequest, "id is required") - } - prices, err := s.storage.ProviderArticle().ProviderPrice(uint(id)) - if err != nil { - return err - } - return c.JSON(web.NewResponse(prices)) -} - -// 获取销售商历史价格 -func (s *Article) SellerHistory(c fiber.Ctx) error { - i := c.Params("id") - id, _ := strconv.Atoi(i) - if id == 0 { - return fiber.NewError(fiber.StatusBadRequest, "id is required") - } - prices, err := s.storage.SellerArticle().SellerPrice(uint(id)) - if err != nil { - return err - } - return c.JSON(web.NewResponse(prices)) -} - -func (s *Article) UpdateProviderArticle(c fiber.Ctx) error { - var article v2.ProviderArticle - if err := c.Bind().JSON(&article); err != nil { - return err - } - if err := s.storage.ProviderArticle().Update(article); err != nil { - return err - } - return c.JSON(web.NewResponse(article)) -} - -func (s *Article) UpdateSellerArticle(c fiber.Ctx) error { - var article v2.SellerArticle - if err := c.Bind().JSON(&article); err != nil { - return err - } - if err := s.storage.SellerArticle().Update(article); err != nil { - return err - } - return c.JSON(web.NewResponse(article)) -} - -// BanArticle 禁用商品 -func (s *Article) BanArticle(c fiber.Ctx) error { - ids := c.Params("ids") - if ids == "" { - return fiber.NewError(fiber.StatusBadRequest, "ids is required") - } - idList := lo.Map(strings.Split(ids, ","), func(item string, index int) int { - id, _ := strconv.Atoi(item) - return id - }) - - err := s.storage.DB().Transaction(func(tx *gorm.DB) (err error) { - if err = tx.Model(&v2.Article{}).Select("ban", "exclude").Where("id IN ?", idList).Updates(map[string]interface{}{"ban": true, "exclude": true}).Error; err != nil { - return err - } - if err = tx.Model(&v2.ProviderArticle{}).Where("article_id IN ?", idList).Update("exclude", true).Error; err != nil { - return err - } - return tx.Model(&v2.SellerArticle{}).Where("article_id IN ?", idList).Update("exclude", true).Error - }) - if err != nil { - return err - } - return c.JSON(web.NewResponse("ok")) -} - -// 解禁商品 -func (s *Article) LiftBanArticle(c fiber.Ctx) error { - ids := c.Params("ids") - if ids == "" { - return fiber.NewError(fiber.StatusBadRequest, "ids is required") - } - idList := lo.Map(strings.Split(ids, ","), func(item string, index int) int { - id, _ := strconv.Atoi(item) - return id - }) - - err := s.storage.DB().Transaction(func(tx *gorm.DB) (err error) { - if err = tx.Model(&v2.Article{}).Select("ban", "exclude").Where("id IN ?", idList).Updates(map[string]interface{}{"ban": false, "exclude": false}).Error; err != nil { - return err - } - if err = tx.Model(&v2.ProviderArticle{}).Where("article_id IN ?", idList).Update("exclude", false).Error; err != nil { - return err - } - return tx.Model(&v2.SellerArticle{}).Where("article_id IN ?", idList).Update("exclude", false).Error - }) - if err != nil { - return err - } - return c.JSON(web.NewResponse("ok")) -} - -func (s *Article) ListBanArticle(c fiber.Ctx) error { - var q storage.FindArticleQuery - if err := c.Bind().Query(&q); err != nil { - return err - } - q.SetBan(true) - var query = storage.NewPageListQuery(&q) - if err := c.Bind().Query(query); err != nil { - return err - } - articles, total, err := s.storage.Article().List(*query, q.SortScope) - if err != nil { - return err - } - return c.JSON(web.NewResponse(web.NewListResponse(total, articles))) -} +package service + +import ( + "fmt" + "strconv" + "strings" + + "gitea.timerzz.com/kedaya_haitao/common/pkg/web" + "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" + "github.com/gofiber/fiber/v3" + "github.com/samber/lo" + "gorm.io/gorm" +) + +type Article struct { + storage *storage.Storage +} + +func NewArticle(storage *storage.Storage) *Article { + return &Article{ + storage: storage, + } +} + +func (s *Article) Registry(r fiber.Router) { + api := r.Group("/api/v2") + api.Get("articles", s.List) + api.Get("articles/u/:id", s.Get) + api.Get("articles/dict/brand", s.BrandDict) + api.Patch("articles", s.Update) + api.Get("articles/provider/history/:id", s.ProviderHistory) + api.Get("articles/seller/history/:id", s.SellerHistory) + api.Patch("articles/provider", s.UpdateProviderArticle) + api.Patch("articles/seller", s.UpdateSellerArticle) + + //屏蔽相关操作 + api.Get("articles/ban", s.ListBanArticle) + api.Post("articles/ban/:ids", s.BanArticle) + api.Delete("articles/ban/:ids", s.LiftBanArticle) +} + +func (s *Article) List(c fiber.Ctx) error { + var q storage.FindArticleQuery + if err := c.Bind().Query(&q); err != nil { + return err + } + q.SetBan(false) + var query = storage.NewPageListQuery(&q) + if err := c.Bind().Query(query); err != nil { + return err + } + articles, total, err := s.storage.Article().List(*query, q.SortScope) + if err != nil { + return err + } + return c.JSON(web.NewResponse(web.NewListResponse(total, articles))) +} + +func (s *Article) Get(c fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return fiber.NewError(fiber.StatusBadRequest, "id is required") + } + i, _ := strconv.Atoi(id) + query := storage.NewGetArticleQuery().SetID(uint(i)) + article, err := s.storage.Article().Get(query) + if err != nil { + return err + } + return c.JSON(web.NewResponse(article)) +} + +func (s *Article) Update(c fiber.Ctx) error { + var article v2.Article + if err := c.Bind().JSON(&article); err != nil { + return err + } + if article.ID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "id is required") + } + if err := s.storage.Article().Update(article); err != nil { + return err + } + return c.JSON(web.NewResponse(article)) +} + +func (s *Article) BrandDict(c fiber.Ctx) error { + var dict = []utils.Dict{ + { + Key: string(v2.Brand_Coach), + Title: fmt.Sprintf("蔻驰/%s", v2.Brand_Coach), + Value: v2.Brand_Coach, + }, + } + return c.JSON(web.NewResponse(dict)) +} + +// 获取供应商历史价格 +func (s *Article) ProviderHistory(c fiber.Ctx) error { + i := c.Params("id") + id, _ := strconv.Atoi(i) + if id == 0 { + return fiber.NewError(fiber.StatusBadRequest, "id is required") + } + prices, err := s.storage.ProviderArticle().ProviderPrice(uint(id)) + if err != nil { + return err + } + return c.JSON(web.NewResponse(prices)) +} + +// 获取销售商历史价格 +func (s *Article) SellerHistory(c fiber.Ctx) error { + i := c.Params("id") + id, _ := strconv.Atoi(i) + if id == 0 { + return fiber.NewError(fiber.StatusBadRequest, "id is required") + } + prices, err := s.storage.SellerArticle().SellerPrice(uint(id)) + if err != nil { + return err + } + return c.JSON(web.NewResponse(prices)) +} + +func (s *Article) UpdateProviderArticle(c fiber.Ctx) error { + var article v2.ProviderArticle + if err := c.Bind().JSON(&article); err != nil { + return err + } + if err := s.storage.ProviderArticle().Update(article); err != nil { + return err + } + return c.JSON(web.NewResponse(article)) +} + +func (s *Article) UpdateSellerArticle(c fiber.Ctx) error { + var article v2.SellerArticle + if err := c.Bind().JSON(&article); err != nil { + return err + } + if err := s.storage.SellerArticle().Update(article); err != nil { + return err + } + return c.JSON(web.NewResponse(article)) +} + +// BanArticle 禁用商品 +func (s *Article) BanArticle(c fiber.Ctx) error { + ids := c.Params("ids") + if ids == "" { + return fiber.NewError(fiber.StatusBadRequest, "ids is required") + } + idList := lo.Map(strings.Split(ids, ","), func(item string, index int) int { + id, _ := strconv.Atoi(item) + return id + }) + + err := s.storage.DB().Transaction(func(tx *gorm.DB) (err error) { + if err = tx.Model(&v2.Article{}).Select("ban", "exclude").Where("id IN ?", idList).Updates(map[string]interface{}{"ban": true, "exclude": true}).Error; err != nil { + return err + } + if err = tx.Model(&v2.ProviderArticle{}).Where("article_id IN ?", idList).Update("exclude", true).Error; err != nil { + return err + } + return tx.Model(&v2.SellerArticle{}).Where("article_id IN ?", idList).Update("exclude", true).Error + }) + if err != nil { + return err + } + return c.JSON(web.NewResponse("ok")) +} + +// 解禁商品 +func (s *Article) LiftBanArticle(c fiber.Ctx) error { + ids := c.Params("ids") + if ids == "" { + return fiber.NewError(fiber.StatusBadRequest, "ids is required") + } + idList := lo.Map(strings.Split(ids, ","), func(item string, index int) int { + id, _ := strconv.Atoi(item) + return id + }) + + err := s.storage.DB().Transaction(func(tx *gorm.DB) (err error) { + if err = tx.Model(&v2.Article{}).Select("ban", "exclude").Where("id IN ?", idList).Updates(map[string]interface{}{"ban": false, "exclude": false}).Error; err != nil { + return err + } + if err = tx.Model(&v2.ProviderArticle{}).Where("article_id IN ?", idList).Update("exclude", false).Error; err != nil { + return err + } + return tx.Model(&v2.SellerArticle{}).Where("article_id IN ?", idList).Update("exclude", false).Error + }) + if err != nil { + return err + } + return c.JSON(web.NewResponse("ok")) +} + +func (s *Article) ListBanArticle(c fiber.Ctx) error { + var q storage.FindArticleQuery + if err := c.Bind().Query(&q); err != nil { + return err + } + q.SetBan(true) + var query = storage.NewPageListQuery(&q) + if err := c.Bind().Query(query); err != nil { + return err + } + articles, total, err := s.storage.Article().List(*query, q.SortScope) + if err != nil { + return err + } + return c.JSON(web.NewResponse(web.NewListResponse(total, articles))) +} diff --git a/service/tools_svc.go b/service/tools_svc.go new file mode 100644 index 0000000..b3c4f11 --- /dev/null +++ b/service/tools_svc.go @@ -0,0 +1,72 @@ +package service + +import ( + "fmt" + + coach_outlet "gitea.timerzz.com/kedaya_haitao/article/tools/coach-outlet" + "gitea.timerzz.com/kedaya_haitao/common/structs/storage" + dw_sdk "gitea.timerzz.com/kedaya_haitao/dw-sdk" + "github.com/gofiber/fiber/v3" + "github.com/sirupsen/logrus" +) + +type Tools struct { + storage *storage.Storage + dw *dw_sdk.Client + export *coach_outlet.Exporter +} + +// NewTools 创建工具服务实例 +func NewTools(storage *storage.Storage, dw *dw_sdk.Client) *Tools { + exporter, err := coach_outlet.NewExporter(storage, dw) + if err != nil { + logrus.Errorf("初始化Coach Outlet导出工具失败: %v", err) + } + + return &Tools{ + storage: storage, + dw: dw, + export: exporter, + } +} + +// Registry 注册路由 +func (s *Tools) Registry(r fiber.Router) { + api := r.Group("/api/v2") + // 添加新的Coach Outlet工具接口 + tools := api.Group("/tools") + tools.Get("/excel/coach-outlet/:provider_id", s.CoachOutletExcel) +} + +// CoachOutletExcel 导出Coach Outlet商品数据到Excel +func (s *Tools) CoachOutletExcel(c fiber.Ctx) error { + // 获取供应商ID参数 + providerId := c.Params("provider_id") + if providerId == "" { + providerId = coach_outlet.CACoachOutlet // 默认使用加拿大Coach Outlet + } + + // 验证供应商ID是否有效 + if providerId != coach_outlet.CACoachOutlet && providerId != coach_outlet.USCoachOutlet { + return fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("无效的供应商ID,必须是 %s 或 %s", coach_outlet.CACoachOutlet, coach_outlet.USCoachOutlet)) + } + + // 检查导出工具是否初始化成功 + if s.export == nil { + return fiber.NewError(fiber.StatusInternalServerError, "导出工具未初始化") + } + + // 设置文件名和响应头 + fileName := fmt.Sprintf("coach_outlet_%s.xlsx", providerId) + c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName)) + c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + // 直接将Excel数据写入响应 + if err := s.export.ExportCheapProduct(providerId, c); err != nil { + logrus.Errorf("导出Coach Outlet商品数据失败: %v", err) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("导出失败: %v", err)) + } + + return nil +} diff --git a/tools/coach-outlet/excel.go b/tools/coach-outlet/excel.go new file mode 100644 index 0000000..13646cf --- /dev/null +++ b/tools/coach-outlet/excel.go @@ -0,0 +1,294 @@ +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 +}