Merge branch 'v4' into analysis
This commit is contained in:
commit
f7afef05f7
|
|
@ -4,4 +4,6 @@
|
|||
docs
|
||||
cmd/server/wire_gen.go
|
||||
__debug*
|
||||
.bin/
|
||||
.bin/
|
||||
.idea/
|
||||
cache/
|
||||
|
|
@ -13,6 +13,7 @@ func main() {
|
|||
configPath := flag.String("config", "./config/config_test.yaml", "Path to configuration file")
|
||||
onBot := flag.String("bot", "", "bot start")
|
||||
flag.Parse()
|
||||
ctx := context.Background()
|
||||
bc, err := config.LoadConfig(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("加载配置失败: %v", err)
|
||||
|
|
@ -25,7 +26,9 @@ func main() {
|
|||
defer func() {
|
||||
cleanup()
|
||||
}()
|
||||
app.DingBotServer.Run(context.Background(), *onBot)
|
||||
|
||||
//钉钉机器人
|
||||
app.DingBotServer.Run(ctx, *onBot)
|
||||
//定时任务
|
||||
app.Cron.Run(ctx)
|
||||
log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,12 @@ redis:
|
|||
db:
|
||||
driver: mysql
|
||||
source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
oss:
|
||||
access_key: "LTAI5tGGZzjf3tvqWk8SQj2G"
|
||||
secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq"
|
||||
bucket: "attachment-public"
|
||||
domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com"
|
||||
endpoint: "https://oss-cn-hangzhou.aliyuncs.com"
|
||||
|
||||
tools:
|
||||
zltxOrderDetail:
|
||||
|
|
|
|||
|
|
@ -43,6 +43,12 @@ redis:
|
|||
db:
|
||||
driver: mysql
|
||||
source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
oss:
|
||||
access_key: "LTAI5tGGZzjf3tvqWk8SQj2G"
|
||||
secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq"
|
||||
bucket: "attachment-public"
|
||||
domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com"
|
||||
endpoint: "https://oss-cn-hangzhou.aliyuncs.com"
|
||||
|
||||
tools:
|
||||
zltxOrderDetail:
|
||||
|
|
|
|||
26
go.mod
26
go.mod
|
|
@ -17,7 +17,7 @@ require (
|
|||
github.com/faabiosr/cachego v0.26.0
|
||||
github.com/fastwego/dingding v1.0.0-beta.4
|
||||
github.com/gabriel-vasile/mimetype v1.4.11
|
||||
github.com/go-kratos/kratos/v2 v2.9.1
|
||||
github.com/go-kratos/kratos/v2 v2.9.2
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
|
|
@ -25,11 +25,13 @@ require (
|
|||
github.com/gofiber/websocket/v2 v2.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/ollama/ollama v0.12.7
|
||||
github.com/redis/go-redis/v9 v9.16.0
|
||||
github.com/spf13/viper v1.17.0
|
||||
github.com/tmc/langchaingo v0.1.13
|
||||
golang.org/x/sync v0.15.0
|
||||
github.com/xuri/excelize/v2 v2.10.0
|
||||
golang.org/x/sync v0.17.0
|
||||
google.golang.org/grpc v1.64.0
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.0
|
||||
|
|
@ -37,12 +39,14 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/gateway-dingtalk v1.0.2 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible // indirect
|
||||
github.com/aliyun/credentials-go v1.4.6 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
|
|
@ -69,7 +73,6 @@ require (
|
|||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
|
|
@ -86,33 +89,40 @@ require (
|
|||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.10.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.7.1 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
github.com/yargevad/filepathx v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.11.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
|
|
|||
51
go.sum
51
go.sum
|
|
@ -35,6 +35,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
|||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
|
|
@ -92,6 +94,8 @@ github.com/alibabacloud-go/tea-utils/v2 v2.0.6 h1:ZkmUlhlQbaDC+Eba/GARMPy6hKdCLi
|
|||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||
|
|
@ -193,6 +197,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
|
|||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kratos/kratos/v2 v2.9.1 h1:EGif6/S/aK/RCR5clIbyhioTNyoSrii3FC118jG40Z0=
|
||||
github.com/go-kratos/kratos/v2 v2.9.1/go.mod h1:a1MQLjMhIh7R0kcJS9SzJYR43BRI7EPzzN0J1Ksu2bA=
|
||||
github.com/go-kratos/kratos/v2 v2.9.2 h1:px8GJQBeLpquDKQWQ9zohEWiLA8n4D/pv7aH3asvUvo=
|
||||
github.com/go-kratos/kratos/v2 v2.9.2/go.mod h1:Jc7jaeYd4RAPjetun2C+oFAOO7HNMHTT/Z4LxpuEDJM=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
|
|
@ -368,8 +374,15 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
|
|||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
||||
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
|
|
@ -381,6 +394,8 @@ github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWR
|
|||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
|
|
@ -423,6 +438,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
|
||||
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
|
|
@ -440,6 +457,12 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/
|
|||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
|
||||
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
|
||||
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
|
@ -479,8 +502,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
|
|||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
|
@ -495,6 +518,8 @@ golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrC
|
|||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
|
@ -561,8 +586,8 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
|||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
|
@ -584,8 +609,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -640,8 +665,8 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
|
@ -650,8 +675,8 @@ golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
|||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
@ -664,11 +689,13 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
|
|
|||
|
|
@ -470,6 +470,24 @@ func (d *DingTalkBotBiz) HandleStreamRes(ctx context.Context, data *chatbot.BotC
|
|||
SenderStaffId: data.SenderStaffId,
|
||||
Title: data.Text.Content,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) GetReportLists(ctx context.Context) (contentChan chan string, err error) {
|
||||
contentChan = make(chan string, 10)
|
||||
defer close(contentChan)
|
||||
contentChan <- "截止今日23点利润亏损合计:127917.0866元,亏损500元以上的分销商和产品金额如下图:"
|
||||
contentChan <- ""
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) GetGroupInfo(ctx context.Context, groupId int) (group model.AiBotGroup, err error) {
|
||||
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"group_id": groupId})
|
||||
cond = cond.And(builder.Eq{"status": constants.Enable})
|
||||
err = d.botGroupImpl.GetOneBySearchToStrut(&cond, &group)
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/utils_vllm"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
|
|
@ -30,6 +31,7 @@ func (f *WithSys) CreatePrompt(ctx context.Context, rec *entitys.Recognize) (mes
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建提示消息列表,包含系统提示、助手回复和用户内容
|
||||
mes = append(prompt, api.Message{
|
||||
Role: "system", // 系统角色
|
||||
|
|
@ -41,7 +43,7 @@ func (f *WithSys) CreatePrompt(ctx context.Context, rec *entitys.Recognize) (mes
|
|||
Role: "user", // 用户角色
|
||||
Content: content.String(), // 用户输入内容
|
||||
})
|
||||
|
||||
fmt.Printf("[意图识别]最终prompt:%v", mes)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -84,6 +86,7 @@ func (f *WithSys) getUserContent(ctx context.Context, rec *entitys.Recognize) (c
|
|||
|
||||
// 解析结果回写到file
|
||||
file.FileRec = imageContent
|
||||
content.WriteString(file.FileRec)
|
||||
default:
|
||||
content.WriteString(file.FileRec)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,280 +0,0 @@
|
|||
package dingtalk
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/constants"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
dingtalkcard_1_0 "github.com/alibabacloud-go/dingtalk/card_1_0"
|
||||
dingtalkim_1_0 "github.com/alibabacloud-go/dingtalk/im_1_0"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const DefaultInterval = 100 * time.Millisecond
|
||||
const HeardBeatX = 50
|
||||
|
||||
type SendCardClient struct {
|
||||
Auth *Auth
|
||||
CardClient *sync.Map
|
||||
mu sync.RWMutex // 保护 CardClient 的并发访问
|
||||
logger log.AllLogger // 日志记录
|
||||
botOption *Bot
|
||||
}
|
||||
|
||||
func NewSendCardClient(auth *Auth, logger log.AllLogger) *SendCardClient {
|
||||
return &SendCardClient{
|
||||
Auth: auth,
|
||||
CardClient: &sync.Map{},
|
||||
logger: logger,
|
||||
botOption: &Bot{},
|
||||
}
|
||||
}
|
||||
|
||||
// initClient 初始化或复用 DingTalk 客户端
|
||||
func (s *SendCardClient) initClient(robotCode string) (*dingtalkcard_1_0.Client, error) {
|
||||
if client, ok := s.CardClient.Load(robotCode); ok {
|
||||
return client.(*dingtalkcard_1_0.Client), nil
|
||||
}
|
||||
s.botOption.BotCode = robotCode
|
||||
config := &openapi.Config{
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
client, err := dingtalkcard_1_0.NewClient(config)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to init DingTalk client")
|
||||
return nil, fmt.Errorf("init client failed: %w", err)
|
||||
}
|
||||
|
||||
s.CardClient.Store(robotCode, client)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (s *SendCardClient) NewCard(ctx context.Context, cardSend *CardSend) error {
|
||||
// 参数校验
|
||||
if (len(cardSend.ContentSlice) == 0 || cardSend.ContentSlice == nil) && cardSend.ContentChannel == nil {
|
||||
return errors.New("卡片内容不能为空")
|
||||
}
|
||||
if cardSend.UpdateInterval == 0 {
|
||||
cardSend.UpdateInterval = DefaultInterval // 默认更新间隔
|
||||
}
|
||||
if cardSend.Title == "" {
|
||||
cardSend.Title = "钉钉卡片"
|
||||
}
|
||||
//替换标题
|
||||
cardSend.Template = constants.CardTemp(strings.Replace(string(cardSend.Template), "${title}", cardSend.Title, 1))
|
||||
// 初始化客户端
|
||||
client, err := s.initClient(cardSend.RobotCode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化client失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成卡片实例ID
|
||||
cardInstanceId, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建uuid失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建初始请求
|
||||
request, err := s.buildBaseRequest(cardSend, cardInstanceId.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送初始卡片
|
||||
if _, err := s.SendInteractiveCard(ctx, request, cardSend.RobotCode, client); err != nil {
|
||||
return fmt.Errorf("发送初始卡片失败: %w", err)
|
||||
}
|
||||
|
||||
// 处理切片内容(同步)
|
||||
if len(cardSend.ContentSlice) > 0 {
|
||||
if err := s.processContentSlice(ctx, cardSend, cardInstanceId.String(), client); err != nil {
|
||||
return fmt.Errorf("内容同步失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理通道内容(异步)
|
||||
if cardSend.ContentChannel != nil {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s.processContentChannel(ctx, cardSend, cardInstanceId.String(), client)
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildBaseRequest 构建基础请求
|
||||
func (s *SendCardClient) buildBaseRequest(cardSend *CardSend, cardInstanceId string) (*dingtalkcard_1_0.StreamingUpdateRequest, error) {
|
||||
cardData := fmt.Sprintf(string(cardSend.Template), "") // 初始空内容
|
||||
request := &dingtalkcard_1_0.StreamingUpdateRequest{
|
||||
OutTrackId: tea.String("your-out-track-id"),
|
||||
Guid: tea.String("0F714542-0AFC-2B0E-CF14-E2D39F5BFFE8"),
|
||||
Key: tea.String("your-ai-param"),
|
||||
Content: tea.String("test"),
|
||||
IsFull: tea.Bool(false),
|
||||
IsFinalize: tea.Bool(false),
|
||||
IsError: tea.Bool(false),
|
||||
}
|
||||
|
||||
switch cardSend.ConversationType {
|
||||
case constants.ConversationTypeGroup:
|
||||
request.SetOpenConversationId(cardSend.ConversationId)
|
||||
case constants.ConversationTypeSingle:
|
||||
receiver, err := json.Marshal(map[string]string{"userId": cardSend.SenderStaffId})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("数据整理失败: %w", err)
|
||||
}
|
||||
request.SetSingleChatReceiver(string(receiver))
|
||||
default:
|
||||
return nil, errors.New("未知的聊天场景")
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// processContentChannel 处理通道内容(异步更新)
|
||||
func (s *SendCardClient) processContentChannel(ctx context.Context, cardSend *CardSend, cardInstanceId string, client *dingtalkim_1_0.Client) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
s.logger.Error("panic in processContentChannel")
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(cardSend.UpdateInterval)
|
||||
defer ticker.Stop()
|
||||
heartbeatTicker := time.NewTicker(time.Duration(HeardBeatX) * DefaultInterval)
|
||||
defer heartbeatTicker.Stop()
|
||||
|
||||
var (
|
||||
contentBuilder strings.Builder
|
||||
lastUpdate time.Time
|
||||
)
|
||||
for {
|
||||
|
||||
select {
|
||||
case content, ok := <-cardSend.ContentChannel:
|
||||
if !ok {
|
||||
// 通道关闭,发送最终内容
|
||||
if contentBuilder.Len() > 0 {
|
||||
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
|
||||
s.logger.Errorf("更新卡片失败1:%s", err.Error())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
contentBuilder.WriteString(content)
|
||||
if contentBuilder.Len() > 0 {
|
||||
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
|
||||
s.logger.Errorf("更新卡片失败2:%s", err.Error())
|
||||
}
|
||||
}
|
||||
lastUpdate = time.Now()
|
||||
|
||||
case <-heartbeatTicker.C:
|
||||
if time.Now().Unix()-lastUpdate.Unix() >= HeardBeatX {
|
||||
return
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
s.logger.Info("context canceled, stop channel processing")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// processContentSlice 处理切片内容(同步更新)
|
||||
func (s *SendCardClient) processContentSlice(ctx context.Context, cardSend *CardSend, cardInstanceId string, client *dingtalkim_1_0.Client) error {
|
||||
var contentBuilder strings.Builder
|
||||
for _, content := range cardSend.ContentSlice {
|
||||
contentBuilder.WriteString(content)
|
||||
err := s.updateCardRequest(ctx, &UpdateCardRequest{
|
||||
Template: string(cardSend.Template),
|
||||
Content: contentBuilder.String(),
|
||||
Client: client,
|
||||
RobotCode: cardSend.RobotCode,
|
||||
CardInstanceId: cardInstanceId,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新卡片失败: %w", err)
|
||||
}
|
||||
time.Sleep(cardSend.UpdateInterval) // 控制更新频率
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateCardContent 封装卡片更新逻辑
|
||||
func (s *SendCardClient) updateCardContent(ctx context.Context, cardSend *CardSend, cardInstanceId, content string, client *dingtalkim_1_0.Client) error {
|
||||
err := s.updateCardRequest(ctx, &UpdateCardRequest{
|
||||
Template: string(cardSend.Template),
|
||||
Content: content,
|
||||
Client: client,
|
||||
RobotCode: cardSend.RobotCode,
|
||||
CardInstanceId: cardInstanceId,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SendCardClient) updateCardRequest(ctx context.Context, updateCardRequest *UpdateCardRequest) error {
|
||||
|
||||
updateRequest := &dingtalkim_1_0.UpdateRobotInteractiveCardRequest{
|
||||
CardBizId: tea.String(updateCardRequest.CardInstanceId),
|
||||
CardData: tea.String(fmt.Sprintf(updateCardRequest.Template, updateCardRequest.Content)),
|
||||
}
|
||||
_, err := s.UpdateInteractiveCard(ctx, updateRequest, updateCardRequest.RobotCode, updateCardRequest.Client)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateInteractiveCard 更新交互卡片(封装错误处理)
|
||||
func (s *SendCardClient) UpdateInteractiveCard(ctx context.Context, request *dingtalkim_1_0.UpdateRobotInteractiveCardRequest, robotCode string, client *dingtalkim_1_0.Client) (*dingtalkim_1_0.UpdateRobotInteractiveCardResponse, error) {
|
||||
authInfo, err := s.Auth.GetTokenFromBotOption(ctx, WithBot(s.botOption))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get token failed: %w", err)
|
||||
}
|
||||
|
||||
headers := &dingtalkim_1_0.UpdateRobotInteractiveCardHeaders{
|
||||
XAcsDingtalkAccessToken: tea.String(authInfo.AccessToken),
|
||||
}
|
||||
|
||||
response, err := client.UpdateRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("API call failed: %w,request:%v", err, request.String())
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// SendInteractiveCard 发送交互卡片(封装错误处理)
|
||||
func (s *SendCardClient) SendInteractiveCard(ctx context.Context, request *dingtalkim_1_0.SendRobotInteractiveCardRequest, robotCode string, client *dingtalkim_1_0.Client) (res *dingtalkim_1_0.SendRobotInteractiveCardResponse, err error) {
|
||||
err = s.Auth.GetBotConfigFromModel(s.botOption)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化bot失败: %w", err)
|
||||
}
|
||||
authInfo, err := s.Auth.GetTokenFromBotOption(ctx, WithBot(s.botOption))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get token failed: %w", err)
|
||||
}
|
||||
|
||||
headers := &dingtalkim_1_0.SendRobotInteractiveCardHeaders{
|
||||
XAcsDingtalkAccessToken: tea.String(authInfo.AccessToken),
|
||||
}
|
||||
|
||||
response, err := client.SendRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("API call failed: %w", err)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"ai_scheduler/pkg"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ type Config struct {
|
|||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
Redis Redis `mapstructure:"redis"`
|
||||
DB DB `mapstructure:"db"`
|
||||
Oss Oss `mapstructure:"oss"`
|
||||
DefaultPrompt SysPrompt `mapstructure:"default_prompt"`
|
||||
PermissionConfig PermissionConfig `mapstructure:"permissionConfig"`
|
||||
LLM LLM `mapstructure:"llm"`
|
||||
|
|
@ -136,6 +138,15 @@ type DB struct {
|
|||
IsDebug bool `mapstructure:"isDebug"`
|
||||
}
|
||||
|
||||
// Oss 阿里云OSS配置
|
||||
type Oss struct {
|
||||
AccessKey string `mapstructure:"access_key"`
|
||||
SecretKey string `mapstructure:"secret_key"`
|
||||
Bucket string `mapstructure:"bucket"`
|
||||
Domain string `mapstructure:"domain"`
|
||||
Endpoint string `mapstructure:"endpoint"`
|
||||
}
|
||||
|
||||
// ToolsConfig 工具配置
|
||||
type ToolsConfig struct {
|
||||
Weather ToolConfig `mapstructure:"weather"`
|
||||
|
|
@ -230,10 +241,32 @@ func LoadConfig(configPath string) (*Config, error) {
|
|||
}
|
||||
|
||||
// 解析配置
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
var bc Config
|
||||
if err := viper.Unmarshal(&bc); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
return &bc, nil
|
||||
}
|
||||
|
||||
func LoadConfigWithTest() (*Config, error) {
|
||||
var bc Config
|
||||
modularDir, err := pkg.GetModuleDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
viper.SetConfigFile(modularDir + "/config/config_test.yaml")
|
||||
viper.SetConfigType("yaml")
|
||||
// 读取配置文件
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
// 解析配置
|
||||
|
||||
if err := viper.Unmarshal(&bc); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return &bc, nil
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type SafeChannelPool struct {
|
||||
pool chan chan entitys.ResponseData // 存储空闲 channel 的队列
|
||||
bufSize int // channel 缓冲大小
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewSafeChannelPool(c *config.Config) (*SafeChannelPool, func()) {
|
||||
pool := &SafeChannelPool{
|
||||
pool: make(chan chan entitys.ResponseData, c.Sys.ChannelPoolLen),
|
||||
bufSize: c.Sys.ChannelPoolSize,
|
||||
}
|
||||
|
||||
cleanup := pool.Close
|
||||
return pool, cleanup
|
||||
}
|
||||
|
||||
// 从池中获取 channel(若无空闲则创建新 channel)
|
||||
func (p *SafeChannelPool) Get() chan entitys.ResponseData {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.closed {
|
||||
return make(chan entitys.ResponseData, p.bufSize)
|
||||
}
|
||||
|
||||
select {
|
||||
case ch := <-p.pool: // 从池中取
|
||||
return ch
|
||||
default: // 池为空,创建新 channel
|
||||
return make(chan entitys.ResponseData, p.bufSize)
|
||||
}
|
||||
}
|
||||
|
||||
// 将 channel 放回池中(必须确保 channel 已清空!)
|
||||
func (p *SafeChannelPool) Put(ch chan entitys.ResponseData) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.closed {
|
||||
return
|
||||
}
|
||||
|
||||
// 清空 channel(防止复用时读取旧数据)
|
||||
go func() {
|
||||
for range ch {
|
||||
// 丢弃所有数据(或根据业务需求处理)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case p.pool <- ch: // 尝试放回池中
|
||||
default: // 池已满,直接关闭 channel(避免泄漏)
|
||||
close(ch)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭池(释放所有资源)
|
||||
func (p *SafeChannelPool) Close() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
p.closed = true
|
||||
close(p.pool) // 关闭池队列
|
||||
// 需额外逻辑关闭所有内部 channel(此处简化)
|
||||
}
|
||||
|
|
@ -6,8 +6,12 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func JsonStringIgonErr(data interface{}) string {
|
||||
|
|
@ -165,3 +169,132 @@ func SafeReplace(template string, replaceTag string, replacements ...string) (st
|
|||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
func StructToMapUsingJsoniter(obj interface{}) (map[string]string, error) {
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
// 转换为JSON
|
||||
jsonBytes, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析为map[string]interface{}
|
||||
var tempMap map[string]interface{}
|
||||
err = json.Unmarshal(jsonBytes, &tempMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为map[string]string
|
||||
result := make(map[string]string)
|
||||
for k, v := range tempMap {
|
||||
result[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 通用结构体转 Query 参数
|
||||
func StructToQuery(obj interface{}) (url.Values, error) {
|
||||
values := url.Values{}
|
||||
v := reflect.ValueOf(obj)
|
||||
t := reflect.TypeOf(obj)
|
||||
|
||||
// 如果是指针,获取指向的值
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
// 确保是结构体
|
||||
if v.Kind() != reflect.Struct {
|
||||
return values, fmt.Errorf("expected struct, got %v", v.Kind())
|
||||
}
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
fieldType := t.Field(i)
|
||||
|
||||
// 跳过零值字段(omitempty)
|
||||
tag := fieldType.Tag.Get("json")
|
||||
if strings.Contains(tag, "omitempty") && field.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取字段名
|
||||
fieldName := getFieldName(fieldType)
|
||||
if fieldName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理不同类型的字段
|
||||
addFieldToValues(values, fieldName, field)
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func getFieldName(field reflect.StructField) string {
|
||||
tag := field.Tag.Get("json")
|
||||
if tag != "" {
|
||||
parts := strings.Split(tag, ",")
|
||||
if parts[0] != "-" && parts[0] != "" {
|
||||
return parts[0]
|
||||
}
|
||||
if parts[0] == "-" {
|
||||
return "" // 跳过该字段
|
||||
}
|
||||
}
|
||||
return field.Name
|
||||
}
|
||||
|
||||
func addFieldToValues(values url.Values, name string, field reflect.Value) {
|
||||
if !field.IsValid() || field.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
values.Add(name, field.String())
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
values.Add(name, strconv.FormatInt(field.Int(), 10))
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
values.Add(name, strconv.FormatUint(field.Uint(), 10))
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
values.Add(name, strconv.FormatFloat(field.Float(), 'f', -1, 64))
|
||||
|
||||
case reflect.Bool:
|
||||
values.Add(name, strconv.FormatBool(field.Bool()))
|
||||
|
||||
case reflect.Slice:
|
||||
// 处理切片,特别是 []string
|
||||
if field.Type().Elem().Kind() == reflect.String {
|
||||
for i := 0; i < field.Len(); i++ {
|
||||
item := field.Index(i).String()
|
||||
// 特殊处理 ct 字段
|
||||
if name == "ct" {
|
||||
formatted := strings.Replace(item, " ", "+", 1)
|
||||
if i == 1 && field.Len() >= 2 {
|
||||
formatted = formatted + ".999"
|
||||
}
|
||||
values.Add("ct[]", formatted)
|
||||
} else {
|
||||
values.Add(fmt.Sprintf("%s[]", name), item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
// 处理 time.Time
|
||||
if t, ok := field.Interface().(time.Time); ok {
|
||||
values.Add(name, t.Format("2006-01-02+15:04:05"))
|
||||
}
|
||||
|
||||
default:
|
||||
values.Add(name, fmt.Sprintf("%v", field.Interface()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
package oss
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
config config.Oss
|
||||
client *oss.Client
|
||||
bucket *oss.Bucket
|
||||
}
|
||||
|
||||
// NewClient 初始化 OSS 客户端
|
||||
func NewClient(cfg config.Oss) (*Client, error) {
|
||||
client, err := oss.New(cfg.Endpoint, cfg.AccessKey, cfg.SecretKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oss new client failed: %v", err)
|
||||
}
|
||||
|
||||
bucket, err := client.Bucket(cfg.Bucket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oss get bucket failed: %v", err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
config: cfg,
|
||||
client: client,
|
||||
bucket: bucket,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadBytes 上传字节数组到 OSS
|
||||
// objectKey: OSS 中的文件路径,例如 "ai_scheduler/test.png"
|
||||
// fileBytes: 文件内容
|
||||
// 返回: 文件的访问 URL
|
||||
func (c *Client) UploadBytes(objectKey string, fileBytes []byte) (string, error) {
|
||||
err := c.bucket.PutObject(objectKey, bytes.NewReader(fileBytes))
|
||||
if err != nil {
|
||||
log.Errorf("oss PutObject failed: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 构造返回 URL
|
||||
var url string
|
||||
if c.config.Domain != "" {
|
||||
url = fmt.Sprintf("%s/%s", c.config.Domain, objectKey)
|
||||
} else {
|
||||
// 这里简单处理协议头
|
||||
url = fmt.Sprintf("https://%s.%s/%s", c.config.Bucket, c.config.Endpoint, objectKey)
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package pkg
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/internal/pkg/oss"
|
||||
"ai_scheduler/internal/pkg/utils_langchain"
|
||||
"ai_scheduler/internal/pkg/utils_ollama"
|
||||
"ai_scheduler/internal/pkg/utils_vllm"
|
||||
|
|
@ -15,8 +16,10 @@ var ProviderSetClient = wire.NewSet(
|
|||
utils_langchain.NewUtilLangChain,
|
||||
utils_ollama.NewClient,
|
||||
utils_vllm.NewClient,
|
||||
NewSafeChannelPool,
|
||||
|
||||
dingtalk.NewOldClient,
|
||||
dingtalk.NewContactClient,
|
||||
dingtalk.NewNotableClient,
|
||||
|
||||
oss.NewClient,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/services"
|
||||
"context"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type CronServer struct {
|
||||
Cron *cron.Cron
|
||||
jobs []*cronJob
|
||||
log log.AllLogger
|
||||
cronService *services.CronService
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
type cronJob struct {
|
||||
EntryId int32
|
||||
Func func(context.Context) error
|
||||
Name string
|
||||
Schedule string
|
||||
}
|
||||
|
||||
func NewCronServer(
|
||||
log log.AllLogger,
|
||||
cronService *services.CronService,
|
||||
) *CronServer {
|
||||
return &CronServer{
|
||||
Cron: cron.New(),
|
||||
log: log,
|
||||
cronService: cronService,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CronServer) InitJobs(ctx context.Context) {
|
||||
// 创建一个可用于所有定时任务的上下文(可以取消的上下文)
|
||||
c.ctx = ctx
|
||||
c.jobs = []*cronJob{
|
||||
{
|
||||
Func: c.cronService.CronReportSend,
|
||||
Name: "直连天下报表推送",
|
||||
Schedule: "@every 60s",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CronServer) Run(ctx context.Context) {
|
||||
// 先初始化任务
|
||||
if c.jobs == nil {
|
||||
c.InitJobs(ctx)
|
||||
}
|
||||
|
||||
for i, job := range c.jobs {
|
||||
// 复制变量到闭包内,避免闭包变量捕获问题
|
||||
job := job
|
||||
jobID := i + 1
|
||||
_, err := c.Cron.AddFunc(job.Schedule, func() {
|
||||
c.log.Infof("任务[%d]:%s开始执行", jobID, job.Name)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.log.Errorf("任务[%d]:%s执行时发生panic: %v", jobID, job.Name, r)
|
||||
}
|
||||
c.log.Infof("任务[%d]:%s执行结束", jobID, job.Name)
|
||||
}()
|
||||
|
||||
// 为每次执行创建新的上下文
|
||||
ctx := context.Background()
|
||||
err := job.Func(ctx)
|
||||
if err != nil {
|
||||
c.log.Errorf("任务[%d]:%s执行失败: %s", jobID, job.Name, err.Error())
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
c.log.Errorf("添加任务失败:%s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 启动cron调度器
|
||||
c.Cron.Start()
|
||||
c.log.Info("Cron调度器已启动")
|
||||
}
|
||||
|
||||
// Stop 停止cron调度器
|
||||
func (c *CronServer) Stop() {
|
||||
if c.Cron != nil {
|
||||
c.Cron.Stop()
|
||||
c.log.Info("Cron调度器已停止")
|
||||
}
|
||||
}
|
||||
|
|
@ -9,4 +9,5 @@ var ProviderSetServer = wire.NewSet(
|
|||
NewHTTPServer,
|
||||
ProvideAllDingBotServices,
|
||||
NewDingTalkBotServer,
|
||||
NewCronServer,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,18 +10,14 @@ type Servers struct {
|
|||
cfg *config.Config
|
||||
HttpServer *fiber.App
|
||||
DingBotServer *DingTalkBotServer
|
||||
Cron *CronServer
|
||||
}
|
||||
|
||||
func NewServers(cfg *config.Config, fiber *fiber.App, DingBotServer *DingTalkBotServer) *Servers {
|
||||
func NewServers(cfg *config.Config, fiber *fiber.App, DingBotServer *DingTalkBotServer, cron *CronServer) *Servers {
|
||||
return &Servers{
|
||||
HttpServer: fiber,
|
||||
cfg: cfg,
|
||||
DingBotServer: DingBotServer,
|
||||
Cron: cron,
|
||||
}
|
||||
}
|
||||
|
||||
//func DingBotServerInit(clientId string, clientSecret string, cfg *config.Config, handler *do.Handle, do *do.Do) (cli *client.StreamClient) {
|
||||
// cli = client.NewStreamClient(client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)))
|
||||
// cli.RegisterChatBotCallbackRouter(services.NewDingBotService(cfg, handler, do).OnChatBotMessageReceived)
|
||||
// return
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
"context"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||
)
|
||||
|
||||
type CronService struct {
|
||||
config *config.Config
|
||||
dingTalkBotBiz *biz.DingTalkBotBiz
|
||||
}
|
||||
|
||||
func NewCronService(config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz) *CronService {
|
||||
return &CronService{
|
||||
config: config,
|
||||
dingTalkBotBiz: dingTalkBotBiz,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *CronService) CronReportSend(ctx context.Context) error {
|
||||
reportChan, err := d.dingTalkBotBiz.GetReportLists(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
groupId := 23
|
||||
groupInfo, err := d.dingTalkBotBiz.GetGroupInfo(ctx, groupId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.dingTalkBotBiz.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{
|
||||
RobotCode: groupInfo.RobotCode,
|
||||
ConversationType: constants.ConversationTypeGroup,
|
||||
ConversationId: groupInfo.ConversationID,
|
||||
Text: chatbot.BotCallbackDataTextModel{
|
||||
Content: "报表",
|
||||
},
|
||||
}, reportChan)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package services
|
|||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"context"
|
||||
"log"
|
||||
|
|
@ -135,3 +136,24 @@ func (d *DingBotService) runBackgroundTasks(ctx context.Context, data *chatbot.B
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DingBotService) CronReportSend(ctx context.Context) error {
|
||||
reportChan, err := d.dingTalkBotBiz.GetReportLists(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
groupId := 23
|
||||
groupInfo, err := d.dingTalkBotBiz.GetGroupInfo(ctx, groupId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.dingTalkBotBiz.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{
|
||||
RobotCode: groupInfo.RobotCode,
|
||||
ConversationType: constants.ConversationTypeGroup,
|
||||
ConversationId: groupInfo.ConversationID,
|
||||
Text: chatbot.BotCallbackDataTextModel{
|
||||
Content: "报表",
|
||||
},
|
||||
}, reportChan)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"context"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||
)
|
||||
|
||||
type DingBotService struct {
|
||||
config *config.Config
|
||||
dingTalkBotBiz *biz.DingTalkBotBiz
|
||||
}
|
||||
|
||||
func NewDingBotService(config *config.Config, DingTalkBotBiz *biz.DingTalkBotBiz) *DingBotService {
|
||||
return &DingBotService{config: config, dingTalkBotBiz: DingTalkBotBiz}
|
||||
}
|
||||
|
||||
func (d *DingBotService) GetServiceCfg() ([]entitys.DingTalkBot, error) {
|
||||
return d.dingTalkBotBiz.GetDingTalkBotCfgList()
|
||||
}
|
||||
|
||||
func (d *DingBotService) OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) {
|
||||
var (
|
||||
lastErr error
|
||||
chat []string
|
||||
streamWG sync.WaitGroup
|
||||
resChan = make(chan string, 100) // 缓冲通道防止阻塞
|
||||
)
|
||||
|
||||
// 初始化请求
|
||||
requireData, err := d.dingTalkBotBiz.InitRequire(ctx, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建子上下文用于控制goroutine生命周期
|
||||
subCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// 启动流式处理goroutine
|
||||
streamWG.Add(1)
|
||||
go func() {
|
||||
defer streamWG.Done()
|
||||
err = d.dingTalkBotBiz.HandleStreamRes(subCtx, data, resChan)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// 启动业务处理goroutine
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- d.dingTalkBotBiz.Do(subCtx, requireData)
|
||||
}()
|
||||
|
||||
// 主处理循环
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
lastErr = ctx.Err()
|
||||
goto cleanup
|
||||
|
||||
case resp, ok := <-requireData.Ch:
|
||||
if !ok {
|
||||
goto cleanup
|
||||
}
|
||||
|
||||
// 处理不同类型响应
|
||||
switch resp.Type {
|
||||
case entitys.ResponseLog:
|
||||
// 忽略日志类型
|
||||
continue
|
||||
|
||||
//case entitys.ResponseText, entitys.ResponseJson:
|
||||
// chat = append(chat, resp.Content)
|
||||
// if err := d.dingTalkBotBiz.ReplyText(ctx, data.SessionWebhook, resp.Content); err != nil {
|
||||
// log.Printf("处理非流响应失败: %v", err)
|
||||
// lastErr = err
|
||||
// }
|
||||
|
||||
default:
|
||||
chat = append(chat, resp.Content)
|
||||
select {
|
||||
case resChan <- resp.Content:
|
||||
case <-ctx.Done():
|
||||
lastErr = ctx.Err()
|
||||
goto cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
streamWG.Wait()
|
||||
// 关闭流式通道
|
||||
close(resChan)
|
||||
|
||||
// 保存历史记录
|
||||
if saveErr := d.dingTalkBotBiz.SaveHis(ctx, requireData, chat); saveErr != nil {
|
||||
log.Printf("保存历史记录失败: %v", saveErr)
|
||||
if lastErr == nil {
|
||||
lastErr = saveErr
|
||||
}
|
||||
}
|
||||
|
||||
// 等待业务处理完成(带超时)
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
log.Printf("业务处理失败: %v", err)
|
||||
if lastErr == nil {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
case <-time.After(3 * time.Second): // 增加超时时间
|
||||
log.Println("警告:等待业务处理超时,可能发生goroutine泄漏")
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
return []byte("success"), nil
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/biz/do"
|
||||
dingtalk2 "ai_scheduler/internal/biz/handle/dingtalk"
|
||||
"ai_scheduler/internal/biz/llm_service"
|
||||
"ai_scheduler/internal/biz/tools_regis"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/domain/component"
|
||||
"ai_scheduler/internal/domain/component/callback"
|
||||
"ai_scheduler/internal/domain/repo"
|
||||
"ai_scheduler/internal/domain/workflow"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/internal/pkg/utils_ollama"
|
||||
"ai_scheduler/internal/pkg/utils_vllm"
|
||||
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
func Test_Report(t *testing.T) {
|
||||
run()
|
||||
a := dingBotService.CronReportSend(context.Background())
|
||||
t.Log(a)
|
||||
}
|
||||
|
||||
var (
|
||||
configConfig *config.Config
|
||||
err error
|
||||
dingBotService *DingBotService
|
||||
)
|
||||
|
||||
// run 函数是程序的入口函数,负责初始化和配置各个组件
|
||||
func run() {
|
||||
// 加载测试配置
|
||||
configConfig, err = config.LoadConfigWithTest()
|
||||
// 初始化数据库连接
|
||||
db, _ := utils.NewGormDb(configConfig)
|
||||
// 初始化各种实现层组件
|
||||
sysImpl := impl.NewSysImpl(db)
|
||||
taskImpl := impl.NewTaskImpl(db)
|
||||
chatHisImpl := impl.NewChatHisImpl(db)
|
||||
sessionImpl := impl.NewSessionImpl(db)
|
||||
botConfigImpl := impl.NewBotConfigImpl(db)
|
||||
botGroupImpl := impl.NewBotGroupImpl(db)
|
||||
botUserImpl := impl.NewBotUserImpl(db)
|
||||
// 初始化Do业务对象
|
||||
doDo := do.NewDo(sysImpl, taskImpl, chatHisImpl, configConfig)
|
||||
// 初始化Ollama客户端
|
||||
client, _, _ := utils_ollama.NewClient(configConfig)
|
||||
// 初始化vLLM客户端
|
||||
utils_vllmClient, _, _ := utils_vllm.NewClient(configConfig)
|
||||
// 初始化Redis数据库连接
|
||||
rdb := utils.NewRdb(configConfig)
|
||||
// 初始化仓库层
|
||||
repos := repo.NewRepos(sessionImpl, rdb)
|
||||
// 初始化包级别的Redis连接
|
||||
pkgRdb := pkg.NewRdb(configConfig)
|
||||
|
||||
// 初始化机器人工具实现层
|
||||
botToolsImpl := impl.NewBotToolsImpl(db)
|
||||
// 初始化机器人部门实现层
|
||||
botDeptImpl := impl.NewBotDeptImpl(db)
|
||||
// 初始化Redis管理器
|
||||
redisManager := callback.NewRedisManager(pkgRdb)
|
||||
// 初始化组件
|
||||
components := component.NewComponents(redisManager)
|
||||
// 初始化工作流注册表
|
||||
registry := workflow.NewRegistry(configConfig, client, repos, components)
|
||||
// 初始化钉钉旧版客户端
|
||||
oldClient := dingtalk.NewOldClient(configConfig)
|
||||
// 初始化Ollama服务
|
||||
ollamaService := llm_service.NewOllamaGenerate(client, utils_vllmClient, configConfig, chatHisImpl)
|
||||
// 初始化工具管理器
|
||||
manager := tools.NewManager(configConfig, client)
|
||||
// 初始化钉钉联系人客户端
|
||||
contactClient, _ := dingtalk.NewContactClient(configConfig)
|
||||
// 初始化钉钉记事本客户端
|
||||
notableClient, _ := dingtalk.NewNotableClient(configConfig)
|
||||
// 初始化工具注册
|
||||
toolRegis := tools_regis.NewToolsRegis(botToolsImpl)
|
||||
// 初始化机器人聊天历史实现层
|
||||
botChatHisImpl := impl.NewBotChatHisImpl(db)
|
||||
// 初始化钉钉认证
|
||||
auth := dingtalk2.NewAuth(configConfig, rdb, botConfigImpl)
|
||||
// 初始化部门服务
|
||||
dept := dingtalk2.NewDept(botDeptImpl, auth)
|
||||
// 初始化用户服务
|
||||
user := dingtalk2.NewUser(botUserImpl, auth, dept)
|
||||
// 初始化发送卡片客户端
|
||||
sendCardClient := dingtalk2.NewSendCardClient(auth, log.DefaultLogger())
|
||||
// 初始化处理器
|
||||
handle := do.NewHandle(ollamaService, manager, configConfig, sessionImpl, registry, oldClient, contactClient, notableClient)
|
||||
// 初始化钉钉机器人业务逻辑
|
||||
dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, toolRegis, botChatHisImpl, manager, configConfig, sendCardClient)
|
||||
// 初始化钉钉机器人服务
|
||||
dingBotService = NewDingBotService(configConfig, dingTalkBotBiz)
|
||||
}
|
||||
|
|
@ -14,4 +14,5 @@ var ProviderSetServices = wire.NewSet(
|
|||
NewDingBotService,
|
||||
NewHistoryService,
|
||||
NewCapabilityService,
|
||||
NewCronService,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,207 @@
|
|||
package bbxt
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type StatisOursProductLossSumReq struct {
|
||||
ResellerId int `json:"reseller_id,omitempty"`
|
||||
Ct []string `json:"ct,omitempty"`
|
||||
}
|
||||
|
||||
type StatisOursProductLossSumRes struct {
|
||||
List []*StatisOursProductLossSumResponse `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"`
|
||||
}
|
||||
|
||||
type StatisOursProductLossSumResponse struct {
|
||||
OursProductId int32 `json:"oursProductId,omitempty"`
|
||||
OursProductName string `json:"oursProductName,omitempty"`
|
||||
ResellerName string `json:"resellerName,omitempty"`
|
||||
ResellerId int32 `json:"resellerId,omitempty"`
|
||||
Loss float64 `json:"loss,omitempty"`
|
||||
}
|
||||
|
||||
const Base = "https://reportapi.1688sup.com/api"
|
||||
|
||||
// StatisOursProductLossSumApi 负利润分析
|
||||
func StatisOursProductLossSumApi(param *StatisOursProductLossSumReq) (*StatisOursProductLossSumRes, error) {
|
||||
url := "/dataanalytics/statisOursProductLossSum"
|
||||
var res StatisOursProductLossSumRes
|
||||
if err := request(url, param, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
type GetProfitRankingSumRequest struct {
|
||||
Ct []string `protobuf:"bytes,1,rep,name=ct,proto3" json:"ct,omitempty"`
|
||||
Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"`
|
||||
Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"`
|
||||
ResellerIds []int32 `protobuf:"varint,5,rep,packed,name=reseller_ids,json=resellerIds,proto3" json:"reseller_ids,omitempty"`
|
||||
}
|
||||
|
||||
type GetProfitRankingSumResponse struct {
|
||||
List []*ProfitRankingSumResponse `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"`
|
||||
DataCount int32 `protobuf:"varint,2,opt,name=data_count,json=dataCount,proto3" json:"data_count,omitempty"`
|
||||
}
|
||||
|
||||
type ProfitRankingSumResponse struct {
|
||||
// 分销商ID
|
||||
ResellerId string `protobuf:"bytes,1,opt,name=reseller_id,json=resellerId,proto3" json:"ResellerId,omitempty"`
|
||||
// 分销商名称
|
||||
ResellerName string `protobuf:"bytes,2,opt,name=reseller_name,json=resellerName,proto3" json:"ResellerName,omitempty"`
|
||||
// 当前利润
|
||||
CurrentProfit float64 `protobuf:"fixed64,3,opt,name=current_profit,json=currentProfit,proto3" json:"CurrentProfit,omitempty"`
|
||||
// 昨日同比利润
|
||||
HistoryOneProfit float64 `protobuf:"fixed64,4,opt,name=history_one_profit,json=historyOneProfit,proto3" json:"HistoryOneProfit,omitempty"`
|
||||
// 上周同比利润
|
||||
HistoryTwoProfit float64 `protobuf:"fixed64,5,opt,name=history_two_profit,json=historyTwoProfit,proto3" json:"HistoryTwoProfit,omitempty"`
|
||||
// 昨日同比利润差值
|
||||
HistoryOneDiff float64 `protobuf:"fixed64,6,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"HistoryOneDiff,omitempty"`
|
||||
// 上周同比利润差值
|
||||
HistoryTwoDiff float64 `protobuf:"fixed64,7,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"HistoryTwoDiff,omitempty"`
|
||||
}
|
||||
|
||||
// GetProfitRankingSumApi 利润同比分销商排行榜
|
||||
func GetProfitRankingSumApi(param *GetProfitRankingSumRequest) (*GetProfitRankingSumResponse, error) {
|
||||
url := "/dataanalytics/profitRankingSum"
|
||||
var res GetProfitRankingSumResponse
|
||||
if err := request(url, param, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
type GetStatisOfficialProductSumRequest struct {
|
||||
Ct []string `protobuf:"bytes,1,rep,name=ct,proto3" json:"ct,omitempty"`
|
||||
DownwardValue int32 `protobuf:"varint,4,opt,name=downward_value,json=downwardValue,proto3" json:"downward_value,omitempty"`
|
||||
Page int32 `protobuf:"varint,5,opt,name=page,proto3" json:"page,omitempty"`
|
||||
Limit int32 `protobuf:"varint,6,opt,name=limit,proto3" json:"limit,omitempty"`
|
||||
OfficialProductId []int32 `protobuf:"varint,7,rep,packed,name=official_product_id,json=officialProductId,proto3" json:"official_product_id,omitempty"`
|
||||
}
|
||||
|
||||
type GetStatisOfficialProductSumResponse struct {
|
||||
OfficialProductSum []*GetStatisOfficialProductSum `protobuf:"bytes,1,rep,name=official_product_sum,json=officialProductSum,proto3" json:"official_product_sum,omitempty"`
|
||||
DataCount int32 `protobuf:"varint,2,opt,name=data_count,json=dataCount,proto3" json:"data_count,omitempty"`
|
||||
}
|
||||
|
||||
type GetStatisOfficialProductSum struct {
|
||||
OfficialProductId int32 `protobuf:"varint,1,opt,name=official_product_id,json=officialProductId,proto3" json:"official_product_id,omitempty"`
|
||||
OfficialProductName string `protobuf:"bytes,2,opt,name=official_product_name,json=officialProductName,proto3" json:"official_product_name,omitempty"`
|
||||
CurrentNum int32 `protobuf:"varint,3,opt,name=current_num,json=currentNum,proto3" json:"current_num,omitempty"`
|
||||
HistoryOneNum int32 `protobuf:"varint,4,opt,name=history_one_num,json=historyOneNum,proto3" json:"history_one_num,omitempty"`
|
||||
HistoryTwoNum int32 `protobuf:"varint,5,opt,name=history_two_num,json=historyTwoNum,proto3" json:"history_two_num,omitempty"`
|
||||
HistoryOneDiff int32 `protobuf:"varint,6,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"history_one_diff,omitempty"`
|
||||
HistoryTwoDiff int32 `protobuf:"varint,7,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"history_two_diff,omitempty"`
|
||||
}
|
||||
|
||||
// GetStatisOfficialProductSumApi 销量同比分析
|
||||
func GetStatisOfficialProductSumApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumResponse, error) {
|
||||
url := "/dataanalytics/statisOfficialProduct"
|
||||
var res GetStatisOfficialProductSumResponse
|
||||
if err := request(url, param, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
type GetStatisOfficialProductSumDeclineResponse struct {
|
||||
OfficialProductSumDecline []*GetStatisOfficialProductSumDecline `protobuf:"bytes,1,rep,name=official_product_sum_decline,json=officialProductSumDecline,proto3" json:"official_product_sum_decline,omitempty"`
|
||||
DataCount int32 `protobuf:"varint,2,opt,name=data_count,json=dataCount,proto3" json:"data_count,omitempty"`
|
||||
}
|
||||
|
||||
type GetStatisOfficialProductSumDecline struct {
|
||||
ResellerId int32 `protobuf:"varint,1,opt,name=reseller_id,json=resellerId,proto3" json:"reseller_id,omitempty"`
|
||||
OfficialProductId int32 `protobuf:"varint,2,opt,name=official_product_id,json=officialProductId,proto3" json:"official_product_id,omitempty"`
|
||||
OfficialProductName string `protobuf:"bytes,3,opt,name=official_product_name,json=officialProductName,proto3" json:"official_product_name,omitempty"`
|
||||
ResellerName string `protobuf:"bytes,4,opt,name=reseller_name,json=resellerName,proto3" json:"reseller_name,omitempty"`
|
||||
CurrentNum int32 `protobuf:"varint,5,opt,name=current_num,json=currentNum,proto3" json:"current_num,omitempty"`
|
||||
HistoryOneNum int32 `protobuf:"varint,6,opt,name=history_one_num,json=historyOneNum,proto3" json:"history_one_num,omitempty"`
|
||||
HistoryTwoNum int32 `protobuf:"varint,7,opt,name=history_two_num,json=historyTwoNum,proto3" json:"history_two_num,omitempty"`
|
||||
HistoryOneDiff int32 `protobuf:"varint,8,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"history_one_diff,omitempty"`
|
||||
HistoryTwoDiff int32 `protobuf:"varint,9,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"history_two_diff,omitempty"`
|
||||
}
|
||||
|
||||
// GetStatisOfficialProductSumDeclineApi 销量同比分析
|
||||
func GetStatisOfficialProductSumDeclineApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumDeclineResponse, error) {
|
||||
url := "/dataanalytics/statisOfficialProductDecline"
|
||||
var res GetStatisOfficialProductSumDeclineResponse
|
||||
if err := request(url, param, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
type resCode struct {
|
||||
Code int `json:"code"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type GetStatisFilterOfficialProductRequest struct {
|
||||
OfficialProductId int32 `protobuf:"varint,1,opt,name=official_product_id,json=officialProductId,proto3" json:"official_product_id,omitempty"`
|
||||
}
|
||||
|
||||
type GetStatisFilterOfficialProductResponse struct {
|
||||
List []*StatisFilterOfficialProductResponse `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"`
|
||||
}
|
||||
|
||||
type StatisFilterOfficialProductResponse struct {
|
||||
OfficialProductId int32 `protobuf:"varint,1,opt,name=official_product_id,json=officialProductId,proto3" json:"OfficialProductId,omitempty"`
|
||||
OfficialProductName string `protobuf:"bytes,2,opt,name=official_product_name,json=officialProductName,proto3" json:"OfficialProductName,omitempty"`
|
||||
}
|
||||
|
||||
// GetStatisFilterOfficialProductApi 官方商品列表
|
||||
func GetStatisFilterOfficialProductApi(param *GetStatisFilterOfficialProductRequest) (*GetStatisFilterOfficialProductResponse, error) {
|
||||
url := "/dataanalytics/statisFilterOfficialProduct"
|
||||
var res GetStatisFilterOfficialProductResponse
|
||||
if err := request(url, param, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func request(url string, reqData interface{}, resData interface{}) error {
|
||||
|
||||
reqParam, err := pkg.StructToQuery(reqData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &l_request.Request{
|
||||
Url: Base + url + "?" + customEncode(reqParam),
|
||||
Method: http.MethodGet,
|
||||
}
|
||||
res, err := req.Send()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("request failed, status code: %d,resion: %s", res.StatusCode, res.Reason)
|
||||
}
|
||||
var code resCode
|
||||
if err = json.Unmarshal(res.Content, &code); err != nil {
|
||||
return fmt.Errorf("返回结构异常:%s", string(res.Content))
|
||||
}
|
||||
if code.Code != 200 {
|
||||
return fmt.Errorf("返回状态异常:%s", string(code.Error))
|
||||
}
|
||||
if err = json.Unmarshal(code.Data, resData); err != nil {
|
||||
return fmt.Errorf("返回数据异常:%s", string(res.Content))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func customEncode(params url.Values) string {
|
||||
encoded := params.Encode()
|
||||
|
||||
// 解码我们想要保留的字符
|
||||
encoded = strings.ReplaceAll(encoded, "%5B", "[") // 恢复 [
|
||||
encoded = strings.ReplaceAll(encoded, "%5D", "]") // 恢复 ]
|
||||
encoded = strings.ReplaceAll(encoded, "%2B", "+") // 恢复 +
|
||||
|
||||
return encoded
|
||||
}
|
||||
|
|
@ -0,0 +1,410 @@
|
|||
package bbxt
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/pkg/oss"
|
||||
"ai_scheduler/pkg"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type BbxtTools struct {
|
||||
cacheDir string
|
||||
excelTempDir string
|
||||
ossClient *oss.Client
|
||||
}
|
||||
|
||||
func NewBbxtTools(ossClient *oss.Client) (*BbxtTools, error) {
|
||||
cache, err := pkg.GetCacheDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tempDir, err := pkg.GetTmplDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BbxtTools{
|
||||
cacheDir: cache,
|
||||
excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir),
|
||||
ossClient: ossClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BbxtTools) DailyReport(now time.Time) (err error) {
|
||||
|
||||
err = b.StatisOursProductLossSum([]string{
|
||||
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
|
||||
time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StatisOursProductLossSumTotal 负利润分析
|
||||
func (b *BbxtTools) StatisOursProductLossSum(ct []string) (err error) {
|
||||
data, err := StatisOursProductLossSumApi(&StatisOursProductLossSumReq{
|
||||
Ct: ct,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var (
|
||||
resellerMap = make(map[int32]*ResellerLoss)
|
||||
total [][]string
|
||||
gt []*ResellerLoss
|
||||
)
|
||||
|
||||
for _, info := range data.List {
|
||||
// 检查经销商是否已存在
|
||||
if _, ok := resellerMap[info.ResellerId]; !ok {
|
||||
// 创建新的经销商记录
|
||||
resellerMap[info.ResellerId] = &ResellerLoss{
|
||||
ResellerId: info.ResellerId,
|
||||
ResellerName: info.ResellerName,
|
||||
Total: 0, // 初始化为0,后续累加
|
||||
ProductLoss: make(map[int32]ProductLoss), // 初始化map
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前经销商
|
||||
reseller := resellerMap[info.ResellerId]
|
||||
|
||||
// 累加经销商总亏损
|
||||
reseller.Total += info.Loss
|
||||
|
||||
// 检查产品是否已存在
|
||||
if _, ok := reseller.ProductLoss[info.OursProductId]; !ok {
|
||||
// 创建新的产品亏损记录
|
||||
reseller.ProductLoss[info.OursProductId] = ProductLoss{
|
||||
ProductId: info.OursProductId,
|
||||
ProductName: info.OursProductName,
|
||||
Loss: info.Loss, // 初始化为当前产品的亏损
|
||||
}
|
||||
} else {
|
||||
// 已存在产品记录,累加亏损
|
||||
productLoss := reseller.ProductLoss[info.OursProductId]
|
||||
productLoss.Loss += info.Loss
|
||||
reseller.ProductLoss[info.OursProductId] = productLoss
|
||||
}
|
||||
}
|
||||
|
||||
// 按经销商总亏损排序
|
||||
resellers := make([]*ResellerLoss, 0, len(resellerMap))
|
||||
for _, v := range resellerMap {
|
||||
resellers = append(resellers, v)
|
||||
}
|
||||
sort.Slice(resellers, func(i, j int) bool {
|
||||
return resellers[i].Total < resellers[j].Total
|
||||
})
|
||||
|
||||
// 构建分组
|
||||
for _, v := range resellers {
|
||||
if v.Total <= -100 {
|
||||
total = append(total, []string{
|
||||
fmt.Sprintf("%s", v.ResellerName),
|
||||
fmt.Sprintf("%.2f", v.Total),
|
||||
})
|
||||
}
|
||||
if v.Total <= -500 {
|
||||
gt = append(gt, v)
|
||||
}
|
||||
}
|
||||
//总量生成excel
|
||||
if len(total) > 0 {
|
||||
filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
|
||||
err = b.SimpleFillExcel(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total)
|
||||
}
|
||||
|
||||
if len(gt) > 0 {
|
||||
filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
|
||||
// err = b.resellerDetailFillExcel(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt)
|
||||
err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetProfitRankingSum 利润同比分销商排行榜
|
||||
func (b *BbxtTools) GetProfitRankingSum(now time.Time) (err error) {
|
||||
ct := []string{
|
||||
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
|
||||
now.Format(time.DateTime),
|
||||
}
|
||||
|
||||
data, err := GetProfitRankingSumApi(&GetProfitRankingSumRequest{
|
||||
Ct: ct,
|
||||
})
|
||||
timeCh := now.Format("1月2日15点")
|
||||
title := "截至" + timeCh + "利润同比分销商排行榜"
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//排序
|
||||
sort.Slice(data.List, func(i, j int) bool {
|
||||
return data.List[i].HistoryOneDiff > data.List[j].HistoryOneDiff
|
||||
})
|
||||
//取前20和后20
|
||||
var (
|
||||
total [][]string
|
||||
top = data.List[:20]
|
||||
bottom = data.List[len(data.List)-20:]
|
||||
)
|
||||
//合并前20和后20
|
||||
top = append(top, bottom...)
|
||||
|
||||
// 构建分组
|
||||
for _, v := range top {
|
||||
var diff string
|
||||
if v.HistoryOneDiff > 0 {
|
||||
diff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%.4f", v.HistoryOneDiff)
|
||||
} else {
|
||||
diff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%.4f", v.HistoryOneDiff)
|
||||
}
|
||||
total = append(total, []string{
|
||||
fmt.Sprintf("%s", v.ResellerName),
|
||||
fmt.Sprintf("%.4f", v.CurrentProfit),
|
||||
fmt.Sprintf("%.4f", v.HistoryOneProfit),
|
||||
diff,
|
||||
})
|
||||
}
|
||||
//总量生成excel
|
||||
if len(total) > 0 {
|
||||
filePath := b.cacheDir + "/lrtb_rank" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx"
|
||||
err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"lrtb_rank.xlsx", filePath, total, title)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetStatisOfficialProductSum 利润同比分销商排行榜
|
||||
func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []string) (err error) {
|
||||
ct := []string{
|
||||
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
|
||||
now.Format(time.DateTime),
|
||||
}
|
||||
var ids []int32
|
||||
if len(productName) > 0 {
|
||||
ids, err = b.getProductIdFromProductName(productName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
reqParam := &GetStatisOfficialProductSumRequest{
|
||||
Ct: ct,
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
reqParam.OfficialProductId = ids
|
||||
}
|
||||
data, err := GetStatisOfficialProductSumApi(reqParam)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var total [][]string
|
||||
for _, v := range data.OfficialProductSum {
|
||||
var (
|
||||
yeterDatyDiff string
|
||||
lastWeekDiff string
|
||||
)
|
||||
if v.HistoryOneDiff > 0 {
|
||||
yeterDatyDiff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%d", v.HistoryOneDiff)
|
||||
} else {
|
||||
yeterDatyDiff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%d", v.HistoryOneDiff)
|
||||
}
|
||||
if v.HistoryTwoDiff > 0 {
|
||||
lastWeekDiff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%d", v.HistoryTwoDiff)
|
||||
} else {
|
||||
lastWeekDiff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%d", v.HistoryTwoDiff)
|
||||
}
|
||||
total = append(total, []string{
|
||||
fmt.Sprintf("%s", v.OfficialProductName),
|
||||
fmt.Sprintf("%d", v.CurrentNum),
|
||||
fmt.Sprintf("%d", v.HistoryOneNum),
|
||||
yeterDatyDiff,
|
||||
fmt.Sprintf("%d", v.HistoryTwoNum),
|
||||
lastWeekDiff,
|
||||
})
|
||||
}
|
||||
|
||||
timeCh := now.Format("1月2日15点")
|
||||
title := "截至" + timeCh + "销售同比分析"
|
||||
//总量生成excel
|
||||
if len(total) > 0 {
|
||||
filePath := b.cacheDir + "/xstb_ana" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx"
|
||||
err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"xstb_ana.xlsx", filePath, total, title)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *BbxtTools) getProductIdFromProductName(productNames []string) ([]int32, error) {
|
||||
data, err := GetStatisFilterOfficialProductApi(&GetStatisFilterOfficialProductRequest{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var product2IdMap = make(map[string]int32)
|
||||
for _, v := range data.List {
|
||||
product2IdMap[v.OfficialProductName] = v.OfficialProductId
|
||||
}
|
||||
var ids []int32
|
||||
for _, v := range productNames {
|
||||
if id, ok := product2IdMap[v]; ok {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (b *BbxtTools) SimpleFillExcelWithTitle(templatePath, outputPath string, dataSlice interface{}, title string) error {
|
||||
// 1. 打开模板
|
||||
f, err := excelize.OpenFile(templatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sheet := f.GetSheetName(0)
|
||||
|
||||
// 1.1 获取第三行模板样式
|
||||
templateRow := 3
|
||||
styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", templateRow))
|
||||
if err != nil {
|
||||
log.Errorf("获取模板样式失败: %v", err)
|
||||
styleID = 0
|
||||
}
|
||||
|
||||
// 1.2 获取模板行高
|
||||
rowHeight, err := f.GetRowHeight(sheet, templateRow)
|
||||
if err != nil {
|
||||
log.Errorf("获取模板行高失败: %v", err)
|
||||
rowHeight = 31 // 默认高度
|
||||
}
|
||||
|
||||
// 2. 写入标题到第一行
|
||||
f.SetCellValue(sheet, "A1", title)
|
||||
|
||||
// 3. 反射获取切片数据
|
||||
v := reflect.ValueOf(dataSlice)
|
||||
if v.Kind() != reflect.Slice {
|
||||
return fmt.Errorf("dataSlice must be a slice")
|
||||
}
|
||||
|
||||
if v.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 4. 从第三行开始填充数据(第二行留空或作为标题行)
|
||||
startRow := 3
|
||||
pattern := `\$\{(.*?)\}`
|
||||
re := regexp.MustCompile(pattern)
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
currentRow := startRow + i
|
||||
|
||||
// 获取当前行数据
|
||||
item := v.Index(i)
|
||||
|
||||
// 处理不同类型的切片
|
||||
var rowData []interface{}
|
||||
|
||||
if item.Kind() == reflect.Slice || item.Kind() == reflect.Array {
|
||||
// 处理 []string 或 [][]string 中的一行
|
||||
for j := 0; j < item.Len(); j++ {
|
||||
if item.Index(j).CanInterface() {
|
||||
rowData = append(rowData, item.Index(j).Interface())
|
||||
}
|
||||
}
|
||||
} else if item.Kind() == reflect.Interface {
|
||||
// 处理 interface{} 类型
|
||||
if actualValue, ok := item.Interface().([]string); ok {
|
||||
for _, val := range actualValue {
|
||||
rowData = append(rowData, val)
|
||||
}
|
||||
} else {
|
||||
rowData = []interface{}{item.Interface()}
|
||||
}
|
||||
} else {
|
||||
rowData = []interface{}{item.Interface()}
|
||||
}
|
||||
|
||||
// 4.1 设置行高
|
||||
f.SetRowHeight(sheet, currentRow, rowHeight)
|
||||
|
||||
// 5. 填充数据到Excel
|
||||
for col, value := range rowData {
|
||||
cell := fmt.Sprintf("%c%d", 'A'+col, currentRow)
|
||||
// 5.1 应用模板样式到整行(根据实际列数)
|
||||
if styleID != 0 && len(rowData) > 0 {
|
||||
startCol := "A"
|
||||
endCol := fmt.Sprintf("%c", 'A'+len(rowData)-1)
|
||||
endCell := fmt.Sprintf("%s%d", endCol, currentRow)
|
||||
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("%s%d", startCol, currentRow),
|
||||
endCell, styleID)
|
||||
}
|
||||
switch value.(type) {
|
||||
case string:
|
||||
var style = value.(string)
|
||||
if re.MatchString(style) {
|
||||
matches := re.FindStringSubmatch(style)
|
||||
styleMap := make(map[string]string)
|
||||
//matches = strings.Replace(matches, "$", "", 1)
|
||||
if len(matches) != 2 {
|
||||
continue
|
||||
}
|
||||
for _, kv := range strings.Split(matches[1], ";") {
|
||||
kvParts := strings.Split(kv, ":")
|
||||
if len(kvParts) == 2 {
|
||||
styleMap[strings.TrimSpace(kvParts[0])] = strings.TrimSpace(kvParts[1])
|
||||
}
|
||||
}
|
||||
fontStyleID, _err := SetStyle(styleMap, f)
|
||||
if _err == nil {
|
||||
f.SetCellStyle(sheet, cell, cell, fontStyleID)
|
||||
}
|
||||
|
||||
value = re.ReplaceAllString(style, "")
|
||||
|
||||
}
|
||||
f.SetCellValue(sheet, cell, value)
|
||||
default:
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 6. 保存
|
||||
return f.SaveAs(outputPath)
|
||||
}
|
||||
|
||||
func SetStyle(styleMap map[string]string, f *excelize.File) (int, error) {
|
||||
|
||||
var style = &excelize.Style{}
|
||||
if colorHex, exists := styleMap["color"]; exists {
|
||||
style.Font = &excelize.Font{
|
||||
Color: colorHex,
|
||||
}
|
||||
}
|
||||
if horizontal, exists := styleMap["horizontal"]; exists {
|
||||
if style.Alignment == nil {
|
||||
style.Alignment = &excelize.Alignment{}
|
||||
}
|
||||
style.Alignment.Horizontal = horizontal
|
||||
}
|
||||
|
||||
if vertical, exists := styleMap["vertical"]; exists {
|
||||
if style.Alignment == nil {
|
||||
style.Alignment = &excelize.Alignment{}
|
||||
}
|
||||
style.Alignment.Vertical = vertical
|
||||
}
|
||||
|
||||
return f.NewStyle(style)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package bbxt
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/oss"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
|
||||
ossClient, err := oss.NewClient(config.Oss{
|
||||
AccessKey: "LTAI5tGGZzjf3tvqWk8SQj2G",
|
||||
SecretKey: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq",
|
||||
Bucket: "attachment-public",
|
||||
Domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com",
|
||||
Endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
o, err := NewBbxtTools(ossClient)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = o.DailyReport(time.Date(2025, 12, 30, 0, 0, 0, 0, time.Local))
|
||||
|
||||
t.Log(err)
|
||||
|
||||
}
|
||||
|
||||
func Test_GetProfitRankingSum(t *testing.T) {
|
||||
o, err := NewBbxtTools()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = o.GetProfitRankingSum(time.Now())
|
||||
|
||||
t.Log(err)
|
||||
|
||||
}
|
||||
|
||||
func Test_GetStatisOfficialProductSum(t *testing.T) {
|
||||
o, err := NewBbxtTools()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = o.GetStatisOfficialProductSum(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"})
|
||||
|
||||
t.Log(err)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package bbxt
|
||||
|
||||
type ResellerLoss struct {
|
||||
ResellerId int32
|
||||
ResellerName string
|
||||
Total float64
|
||||
ProductLoss map[int32]ProductLoss
|
||||
}
|
||||
|
||||
type ProductLoss struct {
|
||||
ProductId int32
|
||||
ProductName string
|
||||
Loss float64
|
||||
}
|
||||
|
|
@ -0,0 +1,444 @@
|
|||
package bbxt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// 最简单的通用函数
|
||||
func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice interface{}) error {
|
||||
// 1. 打开模板
|
||||
f, err := excelize.OpenFile(templatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sheet := f.GetSheetName(0)
|
||||
|
||||
// 1.1 获取第二行模板样式
|
||||
resellerTplRow := 2
|
||||
styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow))
|
||||
if err != nil {
|
||||
log.Errorf("获取分销商总计样式失败: %v", err)
|
||||
styleIDReseller = 0
|
||||
}
|
||||
// 1.2 获取分销商总计行高
|
||||
rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow)
|
||||
if err != nil {
|
||||
log.Errorf("获取分销商总计行高失败: %v", err)
|
||||
rowHeightReseller = 31 // 默认高度
|
||||
}
|
||||
|
||||
// 2. 反射获取切片数据
|
||||
v := reflect.ValueOf(dataSlice)
|
||||
if v.Kind() != reflect.Slice {
|
||||
return fmt.Errorf("dataSlice must be a slice")
|
||||
}
|
||||
|
||||
// 3. 从第2行开始填充
|
||||
row := 2
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
item := v.Index(i).Interface()
|
||||
currentRow := row + i
|
||||
|
||||
// 4. 将item转换为一行数据
|
||||
var rowData []interface{}
|
||||
|
||||
// 如果是切片
|
||||
if reflect.TypeOf(item).Kind() == reflect.Slice {
|
||||
itemV := reflect.ValueOf(item)
|
||||
for j := 0; j < itemV.Len(); j++ {
|
||||
rowData = append(rowData, itemV.Index(j).Interface())
|
||||
}
|
||||
} else if reflect.TypeOf(item).Kind() == reflect.Struct {
|
||||
itemV := reflect.ValueOf(item)
|
||||
for j := 0; j < itemV.NumField(); j++ {
|
||||
if itemV.Field(j).CanInterface() {
|
||||
rowData = append(rowData, itemV.Field(j).Interface())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rowData = []interface{}{item}
|
||||
}
|
||||
// 4.1 设置行高
|
||||
f.SetRowHeight(sheet, currentRow, rowHeightReseller)
|
||||
|
||||
// 5. 填充到Excel
|
||||
for col, value := range rowData {
|
||||
cell := fmt.Sprintf("%c%d", 'A'+col, currentRow)
|
||||
f.SetCellValue(sheet, cell, value)
|
||||
}
|
||||
|
||||
// 5.1 使用第二行模板样式
|
||||
if styleIDReseller != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller)
|
||||
}
|
||||
}
|
||||
|
||||
excelBytes, err := f.WriteToBuffer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("write to bytes failed: %v", err)
|
||||
}
|
||||
|
||||
picBytes, err := b.excel2picPy(templatePath, excelBytes.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("excel2picPy failed: %v", err)
|
||||
}
|
||||
// b.savePic("temp.png", picBytes) // 本地生成图片,仅测试
|
||||
// outputPath 提取文件名(不包含扩展名)
|
||||
filename := filepath.Base(outputPath)
|
||||
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||
imgUrl := b.uploadToOSS(filename, picBytes)
|
||||
log.Infof("imgUrl: %s", imgUrl)
|
||||
|
||||
// 6. 保存
|
||||
return f.SaveAs(outputPath)
|
||||
}
|
||||
|
||||
// 分销商负利润详情填充excel
|
||||
// 1.使用模板文件作为输出文件
|
||||
// 2.分销商总计使用第二行样式(宽高、背景、颜色等)
|
||||
// 3.商品详情使用第三行样式(宽高、背景、颜色等)
|
||||
// 4.保存为新文件
|
||||
func (b *BbxtTools) resellerDetailFillExcel(templatePath, outputPath string, dataSlice []*ResellerLoss) error {
|
||||
// 1. 读取模板
|
||||
f, err := excelize.OpenFile(templatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sheet := f.GetSheetName(0)
|
||||
|
||||
// 获取模板样式1:第二行-分销商总计
|
||||
resellerTplRow := 2
|
||||
styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow))
|
||||
if err != nil {
|
||||
log.Errorf("获取分销商总计样式失败: %v", err)
|
||||
styleIDReseller = 0
|
||||
}
|
||||
rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow)
|
||||
if err != nil {
|
||||
log.Errorf("获取分销商总计行高失败: %v", err)
|
||||
rowHeightReseller = 31 // 默认高度
|
||||
}
|
||||
// 获取模板样式2:第三行-产品亏损明细
|
||||
productTplRow := 3
|
||||
styleIDProduct, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", productTplRow))
|
||||
if err != nil {
|
||||
log.Errorf("获取商品详情样式失败: %v", err)
|
||||
styleIDProduct = 0
|
||||
}
|
||||
rowHeightProduct, err := f.GetRowHeight(sheet, productTplRow)
|
||||
if err != nil {
|
||||
log.Errorf("获取商品详情行高失败: %v", err)
|
||||
rowHeightProduct = 25 // 默认高度
|
||||
}
|
||||
|
||||
currentRow := 2
|
||||
|
||||
for _, reseller := range dataSlice {
|
||||
// 3. 填充经销商数据 (ResellerName, Total)
|
||||
// 设置行高
|
||||
f.SetRowHeight(sheet, currentRow, rowHeightReseller)
|
||||
|
||||
// 设置单元格值
|
||||
f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), reseller.ResellerName)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), reseller.Total)
|
||||
|
||||
// 应用样式
|
||||
if styleIDReseller != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller)
|
||||
}
|
||||
|
||||
currentRow++
|
||||
|
||||
// 4. 填充产品亏损明细
|
||||
// 先对 ProductLoss 进行排序
|
||||
var products []ProductLoss
|
||||
for _, p := range reseller.ProductLoss {
|
||||
products = append(products, p)
|
||||
}
|
||||
// 按 Loss 升序排序 (亏损越多越靠前,负数越小)
|
||||
sort.Slice(products, func(i, j int) bool {
|
||||
return products[i].Loss < products[j].Loss
|
||||
})
|
||||
|
||||
for _, p := range products {
|
||||
// 设置行高
|
||||
f.SetRowHeight(sheet, currentRow, rowHeightProduct)
|
||||
|
||||
// 设置单元格值
|
||||
f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("·%s", p.ProductName))
|
||||
f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.Loss)
|
||||
|
||||
// 应用样式
|
||||
if styleIDProduct != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDProduct)
|
||||
}
|
||||
|
||||
currentRow++
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 保存
|
||||
return f.SaveAs(outputPath)
|
||||
}
|
||||
|
||||
// 分销商负利润详情填充excel-V2
|
||||
// 1.使用模板文件作为输出文件,从第二行开始填充
|
||||
// 2.整体为3列:1.分销商名称(以ResellerName为分组,分销商名称列使用的样式为) 2.商品名称(p.ProductName) 3.亏损金额(p.Loss)
|
||||
// 3.分销商名称列使用的样式为 A2;商品名称、亏损金额使用的样式为 B2、C2;样式包括宽高、背景、颜色等
|
||||
// 4.以ResellerName分组,合并单元格
|
||||
// 5.在文件末尾使用“合计”,合计行样式为模板第四行
|
||||
// 6.保存为新文件
|
||||
func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, dataSlice []*ResellerLoss) error {
|
||||
// 1. 读取模板
|
||||
f, err := excelize.OpenFile(templatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sheet := f.GetSheetName(0)
|
||||
|
||||
// ---------------- 样式获取 ----------------
|
||||
// 模板第2行:数据行样式
|
||||
tplRowData := 2
|
||||
styleA2, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowData))
|
||||
if err != nil {
|
||||
styleA2 = 0
|
||||
}
|
||||
// B2和C2通常样式一致,这里取B2作为明细列样式
|
||||
styleB2, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowData))
|
||||
if err != nil {
|
||||
styleB2 = 0
|
||||
}
|
||||
styleC2, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowData))
|
||||
if err != nil {
|
||||
styleC2 = 0
|
||||
}
|
||||
|
||||
rowHeightData, err := f.GetRowHeight(sheet, tplRowData)
|
||||
if err != nil {
|
||||
rowHeightData = 20
|
||||
}
|
||||
|
||||
// 模板第4行:合计行样式
|
||||
tplRowTotal := 4
|
||||
styleTotalA, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowTotal))
|
||||
if err != nil {
|
||||
styleTotalA = 0
|
||||
}
|
||||
styleTotalB, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowTotal))
|
||||
if err != nil {
|
||||
styleTotalB = 0
|
||||
}
|
||||
styleTotalC, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowTotal))
|
||||
if err != nil {
|
||||
styleTotalC = 0
|
||||
}
|
||||
rowHeightTotal, err := f.GetRowHeight(sheet, tplRowTotal)
|
||||
if err != nil {
|
||||
rowHeightTotal = 30
|
||||
}
|
||||
// ----------------------------------------
|
||||
|
||||
currentRow := 2
|
||||
totalLoss := 0.0
|
||||
|
||||
for _, reseller := range dataSlice {
|
||||
// 排序 ProductLoss
|
||||
var products []ProductLoss
|
||||
for _, p := range reseller.ProductLoss {
|
||||
products = append(products, p)
|
||||
}
|
||||
sort.Slice(products, func(i, j int) bool {
|
||||
return products[i].Loss < products[j].Loss
|
||||
})
|
||||
|
||||
startRow := currentRow
|
||||
|
||||
// 填充该经销商的所有产品
|
||||
for _, p := range products {
|
||||
// 设置行高
|
||||
f.SetRowHeight(sheet, currentRow, rowHeightData)
|
||||
|
||||
// 设置值
|
||||
f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), reseller.ResellerName)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.ProductName)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), p.Loss)
|
||||
|
||||
// 设置样式
|
||||
if styleA2 != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleA2)
|
||||
}
|
||||
if styleB2 != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleB2)
|
||||
}
|
||||
if styleC2 != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleC2)
|
||||
}
|
||||
|
||||
totalLoss += p.Loss
|
||||
currentRow++
|
||||
}
|
||||
|
||||
endRow := currentRow - 1
|
||||
// 合并单元格 (如果多于1行)
|
||||
if endRow > startRow {
|
||||
f.MergeCell(sheet, fmt.Sprintf("A%d", startRow), fmt.Sprintf("A%d", endRow))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 填充合计行 ----------------
|
||||
// 四舍五入保留四位小数
|
||||
totalLoss, _ = decimal.NewFromFloat(totalLoss).Round(4).Float64()
|
||||
// 设置行高
|
||||
f.SetRowHeight(sheet, currentRow, rowHeightTotal)
|
||||
|
||||
f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), "合计")
|
||||
// B列留空,C列填充总亏损
|
||||
f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), totalLoss)
|
||||
|
||||
// 设置合计行样式
|
||||
if styleTotalA != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleTotalA)
|
||||
}
|
||||
if styleTotalB != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleTotalB)
|
||||
}
|
||||
if styleTotalC != 0 {
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleTotalC)
|
||||
}
|
||||
// 取消合并合计行的A、B列
|
||||
// f.MergeCell(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow))
|
||||
|
||||
excelBytes, err := f.WriteToBuffer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("write to bytes failed: %v", err)
|
||||
}
|
||||
|
||||
picBytes, err := b.excel2picPy(templatePath, excelBytes.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("excel2picPy failed: %v", err)
|
||||
}
|
||||
// b.savePic("temp.png", picBytes) // 本地生成图片,仅测试
|
||||
// outputPath 提取文件名(不包含扩展名)
|
||||
filename := filepath.Base(outputPath)
|
||||
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||
imgUrl := b.uploadToOSS(filename, picBytes)
|
||||
log.Infof("imgUrl: %s", imgUrl)
|
||||
|
||||
// 6. 保存
|
||||
return f.SaveAs(outputPath)
|
||||
}
|
||||
|
||||
// excel2picPy 将excel转换为图片python
|
||||
// python 接口如下:
|
||||
// curl --location --request POST 'http://192.168.6.109:8010/api/v1/convert' \
|
||||
// --header 'Content-Type: multipart/form-data; boundary=--------------------------952147881043913664015069' \
|
||||
// --form 'file=@"C:\\Users\\Administrator\\Downloads\\销售同比分析2025-12-29 0-12点.xlsx"' \
|
||||
// --form 'sheet_name="销售同比分析"'
|
||||
func (b *BbxtTools) excel2picPy(templatePath string, excelBytes []byte) ([]byte, error) {
|
||||
// 1. 获取 Sheet Name
|
||||
// 尝试从 excelBytes 解析,如果失败则使用默认值 "Sheet1"
|
||||
sheetName := "Sheet1"
|
||||
f, err := excelize.OpenReader(bytes.NewReader(excelBytes))
|
||||
if err == nil {
|
||||
sheetName = f.GetSheetName(0)
|
||||
if sheetName == "" {
|
||||
sheetName = "Sheet1"
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// 2. 构造 Multipart 请求
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
// 添加文件字段
|
||||
// 使用 templatePath 的文件名作为上传文件名,如果没有则用 default.xlsx
|
||||
filename := "default.xlsx"
|
||||
if templatePath != "" {
|
||||
filename = filepath.Base(templatePath)
|
||||
}
|
||||
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create form file failed: %v", err)
|
||||
}
|
||||
if _, err = part.Write(excelBytes); err != nil {
|
||||
return nil, fmt.Errorf("write file part failed: %v", err)
|
||||
}
|
||||
|
||||
// 添加 sheet_name 字段
|
||||
if err = writer.WriteField("sheet_name", sheetName); err != nil {
|
||||
return nil, fmt.Errorf("write field sheet_name failed: %v", err)
|
||||
}
|
||||
|
||||
if err = writer.Close(); err != nil {
|
||||
return nil, fmt.Errorf("close writer failed: %v", err)
|
||||
}
|
||||
|
||||
// 3. 发送 HTTP POST 请求
|
||||
url := "http://192.168.6.109:8010/api/v1/convert"
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request failed: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("send request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("api request failed with status: %d, body: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
// 4. 读取响应 Body (图片内容)
|
||||
picBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response body failed: %v", err)
|
||||
}
|
||||
|
||||
return picBytes, nil
|
||||
}
|
||||
|
||||
// savePic 保存图片到本地
|
||||
func (b *BbxtTools) savePic(outputPath string, picBytes []byte) error {
|
||||
dir := filepath.Dir(outputPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("create directory failed: %v", err)
|
||||
}
|
||||
return os.WriteFile(outputPath, picBytes, 0644)
|
||||
}
|
||||
|
||||
// uploadToOSS 上传至 oss 返回图片url
|
||||
func (b *BbxtTools) uploadToOSS(fileName string, fileBytes []byte) string {
|
||||
objectKey := fmt.Sprintf("ai-scheduler/data-analytics/images/%s.png", fileName)
|
||||
url, err := b.ossClient.UploadBytes(objectKey, fileBytes)
|
||||
if err != nil {
|
||||
log.Errorf("oss upload failed: %v", err)
|
||||
return ""
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package zltx
|
||||
|
||||
import (
|
||||
config2 "ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_task(t *testing.T) {
|
||||
c := NewZltxOrderDetailTool(config2.ToolConfig{}, nil)
|
||||
err :=
|
||||
c.Execute(context.Background(), &entitys.Recognize{
|
||||
Match: &entitys.Match{
|
||||
Parameters: `{"order_number": 859393216068067329}`,
|
||||
},
|
||||
})
|
||||
t.Log(err)
|
||||
}
|
||||
|
|
@ -98,11 +98,17 @@ func (w *ZltxOrderDetailTool) Execute(ctx context.Context, rec *entitys.Recogniz
|
|||
|
||||
// getMockZltxOrderDetail 获取模拟直连天下订单详情数据
|
||||
func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number interface{}) (err error) {
|
||||
log.Infof("订单编号:%v", number)
|
||||
log.Infof("订单编号:%v,类型:%v")
|
||||
var orderNum string
|
||||
switch number.(type) {
|
||||
case int, int32, int64:
|
||||
number = fmt.Sprintf("%d", number)
|
||||
orderNum = fmt.Sprintf("%d", number)
|
||||
case float64:
|
||||
orderNum = fmt.Sprintf("%d", int(number.(float64)))
|
||||
case string:
|
||||
orderNum = number.(string)
|
||||
default:
|
||||
orderNum = fmt.Sprintf("%v", number)
|
||||
}
|
||||
|
||||
ext, err := rec_extra.GetTaskRecExt(rec)
|
||||
|
|
@ -111,7 +117,7 @@ func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number
|
|||
}
|
||||
//查询订单详情
|
||||
req := l_request.Request{
|
||||
Url: fmt.Sprintf(w.config.BaseURL, number),
|
||||
Url: fmt.Sprintf(w.config.BaseURL, orderNum),
|
||||
Headers: map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", ext.Auth),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func (z ZltxOrderStatisticsTool) Definition() entitys.ToolDefinition {
|
|||
}
|
||||
|
||||
type ZltxOrderStatisticsRequest struct {
|
||||
Number string `json:"number"`
|
||||
Number interface{} `json:"number"`
|
||||
}
|
||||
|
||||
func (z ZltxOrderStatisticsTool) Execute(ctx context.Context, rec *entitys.Recognize) error {
|
||||
|
|
@ -53,7 +53,7 @@ func (z ZltxOrderStatisticsTool) Execute(ctx context.Context, rec *entitys.Recog
|
|||
if err := json.Unmarshal([]byte(rec.Match.Parameters), &req); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Number == "" {
|
||||
if req.Number == nil {
|
||||
return fmt.Errorf("number is required")
|
||||
}
|
||||
return z.getZltxOrderStatistics(req.Number, rec)
|
||||
|
|
@ -76,14 +76,13 @@ type ZltxOrderStatisticsData struct {
|
|||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
func (z ZltxOrderStatisticsTool) getZltxOrderStatistics(number string, rec *entitys.Recognize) error {
|
||||
func (z ZltxOrderStatisticsTool) getZltxOrderStatistics(number interface{}, rec *entitys.Recognize) error {
|
||||
ext, err := rec_extra.GetTaskRecExt(rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//查询订单详情
|
||||
|
||||
url := fmt.Sprintf("%s%s", z.config.BaseURL, number)
|
||||
url := fmt.Sprintf("%s%s", z.config.BaseURL, fmt.Sprintf("%v", number))
|
||||
req := l_request.Request{
|
||||
Url: url,
|
||||
Headers: map[string]string{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func GetModuleDir() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for {
|
||||
modPath := filepath.Join(dir, "go.mod")
|
||||
if _, err := os.Stat(modPath); err == nil {
|
||||
return dir, nil // 找到 go.mod
|
||||
}
|
||||
|
||||
// 向上查找父目录
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break // 到达根目录,未找到
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("go.mod not found in current directory or parents")
|
||||
}
|
||||
|
||||
// GetCacheDir 用于获取缓存目录路径
|
||||
// 如果缓存目录不存在,则会自动创建
|
||||
// 返回值:
|
||||
// - string: 缓存目录的路径
|
||||
// - error: 如果获取模块目录失败或创建缓存目录失败,则返回错误信息
|
||||
func GetCacheDir() (string, error) {
|
||||
// 获取模块目录
|
||||
modDir, err := GetModuleDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 拼接缓存目录路径
|
||||
path := fmt.Sprintf("%s/cache", modDir)
|
||||
// 创建目录(包括所有必要的父目录),权限设置为0755
|
||||
err = os.MkdirAll(path, 0755)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
// 返回成功创建的缓存目录路径
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func GetTmplDir() (string, error) {
|
||||
modDir, err := GetModuleDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := fmt.Sprintf("%s/tmpl", modDir)
|
||||
err = os.MkdirAll(path, 0755)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func ReverseSliceNew[T any](s []T) []T {
|
||||
result := make([]T, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
result[i] = s[len(s)-1-i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue