111
This commit is contained in:
parent
4538a300d7
commit
38e1995f86
|
|
@ -1 +1 @@
|
||||||
[{"name":"loadts","value":"1775587764105","domain":".xiaohongshu.com","path":"/","expires":1807123764,"size":19,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"sec_poison_id","value":"e2a04087-a39c-4880-8ae0-b5e883c79cf9","domain":".xiaohongshu.com","path":"/","expires":1775587813,"size":49,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"websectiga","value":"7750c37de43b7be9de8ed9ff8ea0e576519e8cd2157322eb972ecb429a7735d4","domain":".xiaohongshu.com","path":"/","expires":1775846408,"size":74,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"a1","value":"19d683c737fwjbhd74tlf1z8pn3k0yzvdu3kqz8qk30000423644","domain":".xiaohongshu.com","path":"/","expires":1807106285,"size":54,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"access-token-creator.xiaohongshu.com","value":"customer.creator.AT-68c517626043192484708355gq1bgv9gaekvksnp","domain":".xiaohongshu.com","path":"/","expires":1778168281.361146,"size":96,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"acw_tc","value":"0a0d096b17755871571385002e93f7de908303182e2b596681bf8035f47188","domain":"creator.xiaohongshu.com","path":"/","expires":1775588693.515565,"size":68,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customerClientId","value":"104269762757230","domain":".xiaohongshu.com","path":"/","expires":1810130303.328316,"size":31,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy_creator_session_id","value":"p2OOUhXT3z4D6OTUiVUO4G9xfaqTgRUYLlRG","domain":".xiaohongshu.com","path":"/","expires":1778168282.361207,"size":61,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy.creator.beaker.session.id","value":"1775576545935015586770","domain":".xiaohongshu.com","path":"/","expires":1778168282.361281,"size":54,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"x-user-id-creator.xiaohongshu.com","value":"65d74a4c0000000005032a98","domain":".xiaohongshu.com","path":"/","expires":1810136282.361083,"size":57,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customer-sso-sid","value":"68c517626017512875294727zy1y0kednadz2ql9","domain":".xiaohongshu.com","path":"/","expires":1776175103.360961,"size":56,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"gid","value":"yjfKY48YfqfyyjfKYqSWq7uYWiEVDxfW491iqATyuYUIqTq8T8vuhA8884JqK44824iWfq0q","domain":".xiaohongshu.com","path":"/","expires":1810137369.281301,"size":75,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"webId","value":"4afc571d437c6b9cde5f42ac3bd5ab88","domain":".xiaohongshu.com","path":"/","expires":1807106285,"size":37,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"ets","value":"1775570285237","domain":".xiaohongshu.com","path":"/","expires":1778162285.237612,"size":16,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"xsecappid","value":"ugc","domain":".xiaohongshu.com","path":"/","expires":1807123764,"size":12,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443}]
|
[{"name":"loadts","value":"1775643436062","domain":".xiaohongshu.com","path":"/","expires":1807179436,"size":19,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"sec_poison_id","value":"c61fcdd3-c715-4d43-b5b0-ca6c29a621ba","domain":".xiaohongshu.com","path":"/","expires":1775643877,"size":49,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"websectiga","value":"f47eda31ec99545da40c2f731f0630efd2b0959e1dd10d5fedac3dce0bd1e04d","domain":".xiaohongshu.com","path":"/","expires":1775902472,"size":74,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"a1","value":"19d6b2f0b04zdps8dison8k8oibcyzaju3ji7d03d30000118748","domain":".xiaohongshu.com","path":"/","expires":1807155738,"size":54,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"access-token-creator.xiaohongshu.com","value":"customer.creator.AT-68c517626228786611519495vggizcwmifuvnaaw","domain":".xiaohongshu.com","path":"/","expires":1778211754.091569,"size":96,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"acw_tc","value":"0a0d09d017756432743251523e05604bffad685b0b4bb5706dbccf32be49f5","domain":"creator.xiaohongshu.com","path":"/","expires":1775645071.571978,"size":68,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customerClientId","value":"231145420384063","domain":".xiaohongshu.com","path":"/","expires":1810179755.091552,"size":31,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy_creator_session_id","value":"nWC5PzTFCCSJsLYiRFLFORIWUoUf5c4Egr3v","domain":".xiaohongshu.com","path":"/","expires":1778211755.091586,"size":61,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy.creator.beaker.session.id","value":"1775619757404082990054","domain":".xiaohongshu.com","path":"/","expires":1778211755.091605,"size":54,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"x-user-id-creator.xiaohongshu.com","value":"65d74a4c0000000005032a98","domain":".xiaohongshu.com","path":"/","expires":1810179755.091531,"size":57,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customer-sso-sid","value":"68c5176262287866115194944uxxdffhcemiwvwt","domain":".xiaohongshu.com","path":"/","expires":1776224554.091467,"size":56,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"gid","value":"yjfKDJiJ0J7SyjfKDJi8Dqf884ufUMYf3MlIVqfYTYl3D3q8Svu0VW888yyYW4Y8JD0j8Yd2","domain":".xiaohongshu.com","path":"/","expires":1810179744.008933,"size":75,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"webId","value":"f840cc8b10e0a01b0bdb839482af19d1","domain":".xiaohongshu.com","path":"/","expires":1807155738,"size":37,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"ets","value":"1775619738206","domain":".xiaohongshu.com","path":"/","expires":1778211738.206388,"size":16,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"xsecappid","value":"ugc","domain":".xiaohongshu.com","path":"/","expires":1807179436,"size":12,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443}]
|
||||||
18
go.mod
18
go.mod
|
|
@ -3,7 +3,6 @@ module geo
|
||||||
go 1.26.1
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/WhityGhost/gh0ffice v1.0.0
|
|
||||||
github.com/go-kratos/kratos/v2 v2.9.2
|
github.com/go-kratos/kratos/v2 v2.9.2
|
||||||
github.com/go-playground/validator/v10 v10.30.2
|
github.com/go-playground/validator/v10 v10.30.2
|
||||||
github.com/go-rod/rod v0.116.2
|
github.com/go-rod/rod v0.116.2
|
||||||
|
|
@ -17,20 +16,12 @@ require (
|
||||||
xorm.io/builder v0.3.13
|
xorm.io/builder v0.3.13
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/nguyenthenguyen/docx v0.0.0-20230621112118-9c8e795a11db // indirect
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/lipgloss v0.10.0 // indirect
|
|
||||||
github.com/charmbracelet/log v0.4.0 // indirect
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect
|
|
||||||
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
|
|
@ -38,19 +29,11 @@ require (
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
|
||||||
github.com/mattetti/filebuffer v1.0.1 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594 // indirect
|
|
||||||
github.com/muesli/reflow v0.3.0 // indirect
|
|
||||||
github.com/muesli/termenv v0.15.2 // indirect
|
|
||||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
|
||||||
github.com/richardlehane/msoleps v1.0.3 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/thedatashed/xlsxreader v1.2.8 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
|
@ -61,7 +44,6 @@ require (
|
||||||
github.com/ysmood/leakless v0.9.0 // indirect
|
github.com/ysmood/leakless v0.9.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/crypto v0.49.0 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
37
go.sum
37
go.sum
|
|
@ -2,37 +2,23 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||||
github.com/WhityGhost/gh0ffice v1.0.0 h1:caSkJ90733riUlaqJffTjutoUUGdRi1M0A+x/qqQdKc=
|
|
||||||
github.com/WhityGhost/gh0ffice v1.0.0/go.mod h1:+bSISwCkGiY+vUMvdHHyaYvp4vKtvIt9iovKrrMGvLY=
|
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
|
|
||||||
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
|
|
||||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
|
||||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a h1:c5k29baTzznteWs+9dxrtqpNxgtQ3V5NbU8d6laLK9Q=
|
|
||||||
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a/go.mod h1:xbpgo9r3xURoPa/l3sLKLGcnWlkz9UkfFsQ7lW0S6h8=
|
|
||||||
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 h1:n+nk0bNe2+gVbRI8WRbLFVwwcBQ0rr5p+gzkKb6ol8c=
|
|
||||||
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7/go.mod h1:GPpMrAfHdb8IdQ1/R2uIRBsNfnPnwsYE9YYI5WyY1zw=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/go-kratos/kratos/v2 v2.9.2 h1:px8GJQBeLpquDKQWQ9zohEWiLA8n4D/pv7aH3asvUvo=
|
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-kratos/kratos/v2 v2.9.2/go.mod h1:Jc7jaeYd4RAPjetun2C+oFAOO7HNMHTT/Z4LxpuEDJM=
|
||||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
|
||||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
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/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
|
@ -63,36 +49,17 @@ github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBF
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
||||||
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
|
|
||||||
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594 h1:oRA3NxvX2usoIybfgLP37H1G7VVBDbnwseE7uKnK7lo=
|
|
||||||
github.com/moipa-cn/pptx v0.0.0-20220526133451-f2b53ab6e594/go.mod h1:eH2lLq9oH3EoCtPcCpsfPDpFKzknE2Hj5mUdlTivH8Q=
|
|
||||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
|
||||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
|
||||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
|
||||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
|
||||||
github.com/nguyenthenguyen/docx v0.0.0-20230621112118-9c8e795a11db h1:v0cW/tTMrJQyZr7r6t+t9+NhH2OBAjydHisVYxuyObc=
|
|
||||||
github.com/nguyenthenguyen/docx v0.0.0-20230621112118-9c8e795a11db/go.mod h1:BZyH8oba3hE/BTt2FfBDGPOHhXiKs9RFmUvvXRdzrhM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||||
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.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
|
|
||||||
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
|
@ -100,8 +67,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/thedatashed/xlsxreader v1.2.8 h1:8aGbkXIPEThQbA8KzUZqIa4v4oqFrJFKLQ36vWePI5U=
|
|
||||||
github.com/thedatashed/xlsxreader v1.2.8/go.mod h1:wZyb/2xF1+rkZ2ujhC72tuuOWBY574QvcXHFls+5AXc=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||||
|
|
@ -128,8 +93,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ func LoadConfig() (*Config, error) {
|
||||||
},
|
},
|
||||||
Sys: Sys{
|
Sys: Sys{
|
||||||
MaxConcurrent: 1,
|
MaxConcurrent: 1,
|
||||||
TaskTimeout: 60,
|
TaskTimeout: 200,
|
||||||
SessionTimeout: 300,
|
SessionTimeout: 300,
|
||||||
MaxImageSize: 5 * 1024 * 1024,
|
MaxImageSize: 5 * 1024 * 1024,
|
||||||
LogsDir: filepath.Join(BaseDir, "logs"),
|
LogsDir: filepath.Join(BaseDir, "logs"),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package entitys
|
||||||
|
|
||||||
|
type PublishTaskDetail struct {
|
||||||
|
// publish 表字段
|
||||||
|
RequestID string `db:"request_id"`
|
||||||
|
PlatIndex string `db:"plat_index"`
|
||||||
|
Title string `db:"title"`
|
||||||
|
Tag string `db:"tag"`
|
||||||
|
UserIndex string `db:"user_index"`
|
||||||
|
URL string `db:"url"`
|
||||||
|
Img string `db:"img"`
|
||||||
|
PublishTime string `db:"publish_time"`
|
||||||
|
Status int `db:"status"`
|
||||||
|
Msg string `db:"msg"`
|
||||||
|
TokenID int `db:"token_id"`
|
||||||
|
|
||||||
|
// plat 表字段
|
||||||
|
PlatName string `db:"plat_name"`
|
||||||
|
PlatType int `db:"plat_type"`
|
||||||
|
PlatStatus int `db:"plat_status"`
|
||||||
|
PlatContentFormat string `db:"content_format"`
|
||||||
|
LoginUrl string `db:"login_url"`
|
||||||
|
EditUrl string `db:"edit_url"`
|
||||||
|
LoginedUrl string `db:"logined_url"`
|
||||||
|
Desc string `db:"desc"`
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"geo/internal/config"
|
"geo/internal/config"
|
||||||
|
"geo/internal/entitys"
|
||||||
"geo/internal/publisher"
|
"geo/internal/publisher"
|
||||||
"geo/pkg"
|
"geo/pkg"
|
||||||
|
"geo/utils"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -12,25 +15,45 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"geo/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 任务状态常量
|
||||||
|
StatusPending = 1
|
||||||
|
StatusProcessing = 2
|
||||||
|
StatusFailed = 3
|
||||||
|
StatusSuccess = 4
|
||||||
|
|
||||||
|
// 批处理间隔
|
||||||
|
BatchInterval = 30 * time.Second
|
||||||
|
|
||||||
|
// 日志格式
|
||||||
|
LogSeparator = "================================================================"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PublishManager 发布管理器
|
||||||
type PublishManager struct {
|
type PublishManager struct {
|
||||||
AutoStatus bool
|
AutoStatus bool
|
||||||
Conf *config.Config
|
Conf *config.Config
|
||||||
TokenID int
|
TokenID int
|
||||||
running bool
|
running bool
|
||||||
mu sync.Mutex
|
mu sync.RWMutex // 使用读写锁优化并发读取
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
currentPublisher interface{}
|
db *utils.Db
|
||||||
db *utils.Db
|
stopOnce sync.Once // 确保stopCh只关闭一次
|
||||||
}
|
}
|
||||||
|
|
||||||
var publishManager *PublishManager
|
var (
|
||||||
var once sync.Once
|
publishManager *PublishManager
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetPublishManager 获取单例实例(优化:添加nil检查)
|
||||||
|
func GetPublishManager(config *config.Config, db *utils.Db) (*PublishManager, error) {
|
||||||
|
if config == nil || db == nil {
|
||||||
|
return nil, fmt.Errorf("config和db参数不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
func GetPublishManager(config *config.Config, db *utils.Db) *PublishManager {
|
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
publishManager = &PublishManager{
|
publishManager = &PublishManager{
|
||||||
AutoStatus: false,
|
AutoStatus: false,
|
||||||
|
|
@ -39,12 +62,16 @@ func GetPublishManager(config *config.Config, db *utils.Db) *PublishManager {
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return publishManager
|
return publishManager, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTaskLogger 获取任务专属日志记录器(同一个文件)
|
// getTaskLogger 获取任务专属日志记录器(优化:添加参数验证和错误恢复)
|
||||||
func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File, error) {
|
func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File, error) {
|
||||||
// 确定日志目录
|
if requestID == "" {
|
||||||
|
return nil, nil, fmt.Errorf("requestID不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定日志目录,提供默认值
|
||||||
logsDir := pm.Conf.Sys.LogsDir
|
logsDir := pm.Conf.Sys.LogsDir
|
||||||
if logsDir == "" {
|
if logsDir == "" {
|
||||||
logsDir = "./logs"
|
logsDir = "./logs"
|
||||||
|
|
@ -57,7 +84,7 @@ func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File
|
||||||
return nil, nil, fmt.Errorf("创建日志目录失败: %v", err)
|
return nil, nil, fmt.Errorf("创建日志目录失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建以requestId命名的日志文件
|
// 使用requestID作为文件名,添加随机数避免重复(可选)
|
||||||
logPath := filepath.Join(taskLogDir, fmt.Sprintf("%s.log", requestID))
|
logPath := filepath.Join(taskLogDir, fmt.Sprintf("%s.log", requestID))
|
||||||
|
|
||||||
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
|
@ -67,8 +94,6 @@ func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File
|
||||||
|
|
||||||
// 创建写入器:同时写入文件和标准输出
|
// 创建写入器:同时写入文件和标准输出
|
||||||
multiWriter := io.MultiWriter(logFile, os.Stdout)
|
multiWriter := io.MultiWriter(logFile, os.Stdout)
|
||||||
|
|
||||||
// 创建专用的logger
|
|
||||||
taskLogger := log.New(multiWriter, "", log.LstdFlags|log.Lmicroseconds)
|
taskLogger := log.New(multiWriter, "", log.LstdFlags|log.Lmicroseconds)
|
||||||
|
|
||||||
// 写入任务开始分隔线
|
// 写入任务开始分隔线
|
||||||
|
|
@ -79,22 +104,26 @@ func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File
|
||||||
return taskLogger, logFile, nil
|
return taskLogger, logFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start 启动自动发布(优化:使用读锁检查状态,写锁修改状态)
|
||||||
func (pm *PublishManager) Start(tokenID int) bool {
|
func (pm *PublishManager) Start(tokenID int) bool {
|
||||||
pm.mu.Lock()
|
pm.mu.Lock()
|
||||||
defer pm.mu.Unlock()
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
if pm.AutoStatus {
|
if pm.AutoStatus {
|
||||||
|
log.Printf("自动发布服务已在运行中,tokenID=%d", tokenID)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
pm.TokenID = tokenID
|
pm.TokenID = tokenID
|
||||||
pm.AutoStatus = true
|
pm.AutoStatus = true
|
||||||
pm.stopCh = make(chan struct{})
|
pm.stopCh = make(chan struct{}) // 重新创建stopCh
|
||||||
|
|
||||||
go pm.autoPublishLoop()
|
go pm.autoPublishLoop()
|
||||||
|
log.Printf("自动发布服务已启动,tokenID=%d", tokenID)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop 停止自动发布(优化:使用sync.Once确保stopCh只关闭一次)
|
||||||
func (pm *PublishManager) Stop() bool {
|
func (pm *PublishManager) Stop() bool {
|
||||||
pm.mu.Lock()
|
pm.mu.Lock()
|
||||||
defer pm.mu.Unlock()
|
defer pm.mu.Unlock()
|
||||||
|
|
@ -104,12 +133,15 @@ func (pm *PublishManager) Stop() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
pm.AutoStatus = false
|
pm.AutoStatus = false
|
||||||
close(pm.stopCh)
|
pm.stopOnce.Do(func() {
|
||||||
|
close(pm.stopCh)
|
||||||
|
})
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// autoPublishLoop 自动发布循环(优化:添加退出日志)
|
||||||
func (pm *PublishManager) autoPublishLoop() {
|
func (pm *PublishManager) autoPublishLoop() {
|
||||||
log.Println("自动发布服务已启动")
|
log.Println("自动发布服务已启动,开始循环执行")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
@ -118,235 +150,433 @@ func (pm *PublishManager) autoPublishLoop() {
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
pm.batchPublish()
|
pm.batchPublish()
|
||||||
time.Sleep(30 * time.Second)
|
time.Sleep(BatchInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// batchPublish 批量发布
|
||||||
func (pm *PublishManager) batchPublish() {
|
func (pm *PublishManager) batchPublish() {
|
||||||
if !pm.AutoStatus {
|
if !pm.isAutoStatus() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
publishData := pm.getPendingPublish()
|
publishData, err := pm.getPendingPublish()
|
||||||
if publishData == nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 defer recover 防止 panic 导致整个循环崩溃
|
// 使用context实现超时控制
|
||||||
defer func() {
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(pm.Conf.Sys.TaskTimeout)*time.Second)
|
||||||
if r := recover(); r != nil {
|
defer cancel()
|
||||||
log.Printf("批处理发布发生 panic: %v", r)
|
|
||||||
}
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("批处理发布发生 panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
pm.processSingleTask(publishData)
|
||||||
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
pm.processSingleTask(publishData)
|
select {
|
||||||
|
case <-done:
|
||||||
|
// 正常完成
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Printf("任务执行超时: %v", ctx.Err())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *PublishManager) getPendingPublish() map[string]interface{} {
|
// getPendingPublish 获取待发布任务(返回结构体)
|
||||||
|
func (pm *PublishManager) getPendingPublish() (*entitys.PublishTaskDetail, error) {
|
||||||
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
// SQL查询,明确指定字段
|
||||||
sql := `
|
sql := `
|
||||||
SELECT p.*, pl.*
|
SELECT
|
||||||
|
p.request_id,
|
||||||
|
p.plat_index,
|
||||||
|
p.title,
|
||||||
|
p.tag,
|
||||||
|
p.user_index,
|
||||||
|
p.url,
|
||||||
|
p.img,
|
||||||
|
p.publish_time,
|
||||||
|
p.status,
|
||||||
|
pl.index as plat_index_value,
|
||||||
|
pl.status as plat_status
|
||||||
|
pl.login_url,
|
||||||
|
pl.edit_url,
|
||||||
|
pl.logined_url,
|
||||||
|
pl.desc
|
||||||
FROM publish p
|
FROM publish p
|
||||||
INNER JOIN plat pl ON p.plat_index = pl.index AND pl.status = 1
|
INNER JOIN plat pl ON p.plat_index = pl.index AND pl.status = 1
|
||||||
WHERE p.token_id = ? AND p.status = 1 AND p.publish_time <= ?
|
WHERE p.token_id = ? AND p.status = ? AND p.publish_time <= ?
|
||||||
ORDER BY p.publish_time DESC
|
ORDER BY p.publish_time ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
result, err := pm.db.GetOne(sql, pm.TokenID, currentTime)
|
var task entitys.PublishTaskDetail
|
||||||
|
err := pm.db.GetOneToStruct(sql, &task, pm.TokenID, StatusPending, currentTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("查询待发布任务失败: token_id=%d, error=%v", pm.TokenID, err)
|
log.Printf("查询待发布任务失败: token_id=%d, error=%v", pm.TokenID, err)
|
||||||
return nil
|
return nil, err
|
||||||
}
|
|
||||||
if result == nil {
|
|
||||||
log.Printf("没有待发布任务: token_id=%d, current_time=%s", pm.TokenID, currentTime)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestID := pkg.GetString(result, "request_id")
|
// 检查是否为空记录(根据你的db实现,可能需要判断task.RequestID是否为空)
|
||||||
log.Printf("获取到待发布任务: token_id=%d, request_id=%s", pm.TokenID, requestID)
|
if task.RequestID == "" {
|
||||||
return result
|
log.Printf("没有待发布任务: token_id=%d, current_time=%s", pm.TokenID, currentTime)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("获取到待发布任务: token_id=%d, request_id=%s", pm.TokenID, task.RequestID)
|
||||||
|
return &task, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *PublishManager) GetTaskByRequestID(requestID string) (map[string]interface{}, error) {
|
// isAutoStatus 获取自动状态(优化:使用读锁)
|
||||||
|
func (pm *PublishManager) isAutoStatus() bool {
|
||||||
|
pm.mu.RLock()
|
||||||
|
defer pm.mu.RUnlock()
|
||||||
|
return pm.AutoStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTaskByRequestID 根据RequestID获取任务(优化:添加缓存机制可选项)
|
||||||
|
func (pm *PublishManager) GetTaskByRequestID(requestID string) (*entitys.PublishTaskDetail, error) {
|
||||||
|
if requestID == "" {
|
||||||
|
return nil, fmt.Errorf("requestID不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
sql := `
|
sql := `
|
||||||
SELECT p.*, pl.*
|
SELECT
|
||||||
|
p.request_id,
|
||||||
|
p.plat_index,
|
||||||
|
p.title,
|
||||||
|
p.tag,
|
||||||
|
p.user_index,
|
||||||
|
p.url,
|
||||||
|
p.img,
|
||||||
|
p.publish_time,
|
||||||
|
p.status,
|
||||||
|
pl.index as plat_index_value,
|
||||||
|
pl.status as plat_status,
|
||||||
|
pl.login_url,
|
||||||
|
pl.edit_url,
|
||||||
|
pl.logined_url,
|
||||||
|
pl.desc
|
||||||
FROM publish p
|
FROM publish p
|
||||||
INNER JOIN plat pl ON p.plat_index COLLATE utf8mb4_unicode_ci = pl.index AND pl.status = 1
|
INNER JOIN plat pl ON p.plat_index COLLATE utf8mb4_unicode_ci = pl.index AND pl.status = 1
|
||||||
WHERE p.request_id = ?
|
WHERE p.request_id = ?
|
||||||
`
|
`
|
||||||
return pm.db.GetOne(sql, requestID)
|
var task entitys.PublishTaskDetail
|
||||||
|
err := pm.db.GetOneToStruct(sql, &task, requestID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 检查是否为空记录(根据你的db实现,可能需要判断task.RequestID是否为空)
|
||||||
|
if task.RequestID == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &task, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *PublishManager) processSingleTask(publishData map[string]interface{}) (result map[string]interface{}) {
|
type SingleResult struct {
|
||||||
requestID := pkg.GetString(publishData, "request_id")
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
RequestId string `json:"request_id"`
|
||||||
|
}
|
||||||
|
|
||||||
// 获取任务专属日志(同一个文件)
|
func (pm *PublishManager) processSingleTask(publishData *entitys.PublishTaskDetail) (result *SingleResult) {
|
||||||
taskLogger, logFile, err := pm.getTaskLogger(requestID)
|
if publishData == nil {
|
||||||
|
return &SingleResult{
|
||||||
|
Success: false,
|
||||||
|
Message: "publishData不能为空",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if publishData.RequestID == "" {
|
||||||
|
|
||||||
|
return &SingleResult{
|
||||||
|
Success: false,
|
||||||
|
Message: "requestID不能为空",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务专属日志
|
||||||
|
taskLogger, logFile, err := pm.getTaskLogger(publishData.RequestID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[任务 %s] 创建日志文件失败: %v,使用全局日志", requestID, err)
|
log.Printf("[任务 %s] 创建日志文件失败: %v,使用全局日志", publishData.RequestID, err)
|
||||||
taskLogger = log.Default()
|
taskLogger = log.Default()
|
||||||
}
|
}
|
||||||
if logFile != nil {
|
if logFile != nil {
|
||||||
defer logFile.Close()
|
defer logFile.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局defer用于捕获panic并记录到同一个日志文件
|
// 全局defer用于捕获panic并记录
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
errMsg := fmt.Sprintf("任务执行发生panic: %v", r)
|
errMsg := fmt.Sprintf("任务执行发生panic: %v", r)
|
||||||
taskLogger.Printf("❌ CRITICAL: %s", errMsg)
|
taskLogger.Printf("❌ CRITICAL: %s", errMsg)
|
||||||
taskLogger.Printf(strings.Repeat("=", 80))
|
taskLogger.Printf(strings.Repeat("=", 80))
|
||||||
taskLogger.Printf("任务异常结束 | RequestID: %s | 时间: %s", requestID, time.Now().Format("2006-01-02 15:04:05.000"))
|
taskLogger.Printf("任务异常结束 | RequestID: %s | 时间: %s", publishData.RequestID, time.Now().Format("2006-01-02 15:04:05.000"))
|
||||||
taskLogger.Printf(strings.Repeat("=", 80))
|
taskLogger.Printf(strings.Repeat("=", 80))
|
||||||
result = map[string]interface{}{
|
|
||||||
"success": false,
|
result = &SingleResult{
|
||||||
"message": errMsg,
|
Success: false,
|
||||||
"request_id": requestID,
|
Message: errMsg,
|
||||||
|
RequestId: publishData.RequestID,
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
taskLogger.Printf("[任务 %s] 开始处理", requestID)
|
taskLogger.Printf("[任务 %s] 开始处理", publishData.RequestID)
|
||||||
|
|
||||||
platIndex := pkg.GetString(publishData, "plat_index")
|
// 提取任务参数
|
||||||
title := pkg.GetString(publishData, "title")
|
params, sourceUrl := pm.extractTaskParams(publishData, taskLogger)
|
||||||
tagRaw := pkg.GetString(publishData, "tag")
|
if params == nil {
|
||||||
userIndex := pkg.GetString(publishData, "user_index")
|
return &SingleResult{
|
||||||
url := pkg.GetString(publishData, "url")
|
Success: false,
|
||||||
imgURL := pkg.GetString(publishData, "img")
|
Message: "提取任务参数失败",
|
||||||
|
RequestId: publishData.RequestID,
|
||||||
taskLogger.Printf("[任务 %s] 任务详情 - 平台:%s,标题:%s,用户:%s", requestID, platIndex, title, userIndex)
|
}
|
||||||
taskLogger.Printf("[任务 %s] 文档URL: %s", requestID, url)
|
|
||||||
taskLogger.Printf("[任务 %s] 图片URL: %s", requestID, imgURL)
|
|
||||||
|
|
||||||
// 更新状态为发布中
|
|
||||||
pm.updatePublishStatus(requestID, 2, "")
|
|
||||||
taskLogger.Printf("[任务 %s] 状态已更新为发布中", requestID)
|
|
||||||
|
|
||||||
// 下载文件
|
|
||||||
taskLogger.Printf("[任务 %s] 开始下载文档...", requestID)
|
|
||||||
docPath, err := pkg.DownloadFile(url, pm.Conf.Sys.DocsDir, requestID+".docx")
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Sprintf("下载文档失败: %v", err)
|
|
||||||
taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg)
|
|
||||||
pm.updatePublishStatus(requestID, 3, errMsg)
|
|
||||||
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if docPath != "" {
|
|
||||||
pkg.DeleteFile(docPath)
|
|
||||||
taskLogger.Printf("[任务 %s] 已删除文档文件: %s", requestID, docPath)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
taskLogger.Printf("[任务 %s] ✅ 文档下载成功: %s", requestID, docPath)
|
|
||||||
|
|
||||||
// 下载图片
|
|
||||||
taskLogger.Printf("[任务 %s] 开始下载图片...", requestID)
|
|
||||||
imgPath, err := pkg.DownloadImage(imgURL, requestID, pm.Conf.Sys.UploadDir)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if imgPath != "" {
|
|
||||||
pkg.DeleteFile(imgPath)
|
|
||||||
taskLogger.Printf("[任务 %s] 已删除图片文件: %s", requestID, imgPath)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Sprintf("下载图片失败: %v", err)
|
|
||||||
taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg)
|
|
||||||
pm.updatePublishStatus(requestID, 3, errMsg)
|
|
||||||
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
|
|
||||||
}
|
|
||||||
taskLogger.Printf("[任务 %s] ✅ 图片下载成功: %s", requestID, imgPath)
|
|
||||||
|
|
||||||
// 解析标签
|
|
||||||
tags := pkg.ParseTags(tagRaw)
|
|
||||||
taskLogger.Printf("[任务 %s] 标签解析完成: %v", requestID, tags)
|
|
||||||
|
|
||||||
// 获取发布器
|
// 获取发布器
|
||||||
publisherClass := GetPublisherClass(platIndex)
|
publisherClass := GetPublisherClass(params.PlatIndex)
|
||||||
if publisherClass == nil {
|
if publisherClass == nil {
|
||||||
errMsg := fmt.Sprintf("不支持的平台: %s", platIndex)
|
errMsg := fmt.Sprintf("不支持的平台: %s", params.PlatIndex)
|
||||||
taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg)
|
taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg)
|
||||||
pm.updatePublishStatus(requestID, 3, errMsg)
|
pm.updatePublishStatus(publishData.RequestID, StatusFailed, errMsg)
|
||||||
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
|
return &SingleResult{
|
||||||
}
|
Success: false,
|
||||||
|
Message: errMsg,
|
||||||
// 提取内容
|
RequestId: publishData.RequestID,
|
||||||
taskLogger.Printf("[任务 %s] 开始提取文档内容...", requestID)
|
|
||||||
var content string
|
|
||||||
if publisherClass.Type == 1 {
|
|
||||||
content, err = pkg.ExtractWordContent(docPath, publisherClass.ContentFormat)
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Sprintf("提取文档内容失败: %v", err)
|
|
||||||
taskLogger.Printf("[任务 %s] ❌ %s", requestID, errMsg)
|
|
||||||
pm.updatePublishStatus(requestID, 3, errMsg)
|
|
||||||
return map[string]interface{}{"success": false, "message": errMsg, "request_id": requestID}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
taskLogger.Printf("[任务 %s] ✅ 内容提取成功,长度: %d", requestID, len(content))
|
// 更新状态为发布中
|
||||||
taskLogger.Printf("[任务 %s] 创建发布器...", requestID)
|
if err := pm.updatePublishStatus(publishData.RequestID, StatusProcessing, ""); err != nil {
|
||||||
pub := publisherClass.InitMethod(false, title, content, tags, userIndex, platIndex, requestID, imgPath, docPath, publishData, pm.Conf, taskLogger)
|
taskLogger.Printf("[任务 %s] ❌ 更新状态失败: %v", publishData.RequestID, err)
|
||||||
taskLogger.Printf("[任务 %s] 创建%s发布器", publisherClass.Name, requestID)
|
}
|
||||||
taskLogger.Printf("[任务 %s] 开始执行发布...", requestID)
|
|
||||||
|
// 下载并处理文档
|
||||||
|
params.SourcePath, params.ImagePath, err = pm.downloadAndPrepareFiles(params.RequestID, sourceUrl, taskLogger, publisherClass)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("准备文件失败: %v", err)
|
||||||
|
taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg)
|
||||||
|
pm.updatePublishStatus(publishData.RequestID, StatusFailed, errMsg)
|
||||||
|
return &SingleResult{
|
||||||
|
Success: false,
|
||||||
|
Message: errMsg,
|
||||||
|
RequestId: publishData.RequestID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保文件清理
|
||||||
|
defer pm.cleanupFiles(params.SourcePath, params.ImagePath, taskLogger, publishData.RequestID)
|
||||||
|
|
||||||
|
// 提取内容
|
||||||
|
params.Content, err = pm.extractContent(params.SourcePath, publisherClass, taskLogger, publishData.RequestID)
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("提取文档内容失败: %v", err)
|
||||||
|
taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg)
|
||||||
|
pm.updatePublishStatus(publishData.RequestID, StatusFailed, errMsg)
|
||||||
|
return &SingleResult{
|
||||||
|
Success: false,
|
||||||
|
Message: errMsg,
|
||||||
|
RequestId: publishData.RequestID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行发布
|
||||||
|
taskLogger.Printf("[任务 %s] ✅ 内容提取成功,长度: %d", publishData.RequestID, len(params.Content))
|
||||||
|
taskLogger.Printf("[任务 %s] 创建发布器...", publishData.RequestID)
|
||||||
|
pub := publisherClass.InitMethod(params, pm.Conf, taskLogger)
|
||||||
|
taskLogger.Printf("[任务 %s] 创建%s发布器", publisherClass.Name, publishData.RequestID)
|
||||||
|
taskLogger.Printf("[任务 %s] 开始执行发布...", publishData.RequestID)
|
||||||
success, message := pub.PublishNote()
|
success, message := pub.PublishNote()
|
||||||
|
|
||||||
|
// 更新最终状态
|
||||||
if success {
|
if success {
|
||||||
taskLogger.Printf("[任务 %s] ✅ 发布成功: %s", requestID, message)
|
taskLogger.Printf("[任务 %s] ✅ 发布成功: %s", publishData.RequestID, message)
|
||||||
pm.updatePublishStatus(requestID, 4, message)
|
pm.updatePublishStatus(publishData.RequestID, StatusSuccess, message)
|
||||||
} else {
|
} else {
|
||||||
taskLogger.Printf("[任务 %s] ❌ 发布失败: %s", requestID, message)
|
taskLogger.Printf("[任务 %s] ❌ 发布失败: %s", publishData.RequestID, message)
|
||||||
pm.updatePublishStatus(requestID, 3, message)
|
pm.updatePublishStatus(publishData.RequestID, StatusFailed, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录任务结束
|
||||||
taskLogger.Printf(strings.Repeat("=", 80))
|
taskLogger.Printf(strings.Repeat("=", 80))
|
||||||
taskLogger.Printf("任务结束 | RequestID: %s | 结果: %v | 时间: %s", requestID, success, time.Now().Format("2006-01-02 15:04:05.000"))
|
taskLogger.Printf("任务结束 | RequestID: %s | 结果: %v | 时间: %s", publishData.RequestID, success, time.Now().Format("2006-01-02 15:04:05.000"))
|
||||||
taskLogger.Printf(strings.Repeat("=", 80))
|
taskLogger.Printf(strings.Repeat("=", 80))
|
||||||
|
|
||||||
return map[string]interface{}{
|
return &SingleResult{
|
||||||
"success": success,
|
Success: success,
|
||||||
"message": message,
|
Message: message,
|
||||||
"request_id": requestID,
|
RequestId: publishData.RequestID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *PublishManager) updatePublishStatus(requestID string, status int, message string) {
|
type fileUrl struct {
|
||||||
|
url string
|
||||||
|
imgURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *PublishManager) extractTaskParams(publishData *entitys.PublishTaskDetail, taskLogger *log.Logger) (*publisher.TaskParams, *fileUrl) {
|
||||||
|
|
||||||
|
taskLogger.Printf("[任务 %s] 任务详情 - 平台:%s,标题:%s,用户:%s", publishData.RequestID, publishData.PlatIndex, publishData.Title, publishData.UserIndex)
|
||||||
|
|
||||||
|
// 解析标签
|
||||||
|
tags := pkg.ParseTags(publishData.Tag)
|
||||||
|
taskLogger.Printf("[任务 %s] 标签解析完成: %v", publishData.RequestID, tags)
|
||||||
|
|
||||||
|
return &publisher.TaskParams{
|
||||||
|
RequestID: publishData.RequestID,
|
||||||
|
PlatIndex: publishData.PlatIndex,
|
||||||
|
Title: publishData.Title,
|
||||||
|
TagRaw: publishData.Tag,
|
||||||
|
UserIndex: publishData.UserIndex,
|
||||||
|
Tags: tags,
|
||||||
|
PublishData: publishData,
|
||||||
|
}, &fileUrl{
|
||||||
|
url: publishData.URL,
|
||||||
|
imgURL: publishData.Img,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *PublishManager) downloadAndPrepareFiles(requestId string, params *fileUrl, taskLogger *log.Logger, publishClass *publisher.PublisherValue) (docPath, imgPath string, err error) {
|
||||||
|
// 下载文档
|
||||||
|
taskLogger.Printf("[任务 %s] 开始下载文档...", requestId)
|
||||||
|
docPath, err = pkg.DownloadFile(params.url, pm.Conf.Sys.DocsDir, requestId+".docx")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("下载文档失败: %v", err)
|
||||||
|
}
|
||||||
|
taskLogger.Printf("[任务 %s] ✅ 文档下载成功: %s", requestId, docPath)
|
||||||
|
|
||||||
|
// 下载图片
|
||||||
|
taskLogger.Printf("[任务 %s] 开始下载图片...", requestId)
|
||||||
|
imgPath, err = pkg.DownloadImage(params.imgURL, requestId, pm.Conf.Sys.UploadDir)
|
||||||
|
if err != nil {
|
||||||
|
// 如果图片下载失败,清理已下载的文档
|
||||||
|
if docPath != "" {
|
||||||
|
pkg.DeleteFile(docPath)
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("下载图片失败: %v", err)
|
||||||
|
}
|
||||||
|
taskLogger.Printf("[任务 %s] ✅ 图片下载成功: %s", requestId, imgPath)
|
||||||
|
|
||||||
|
if publishClass.Type == 1 && publishClass.WordContainImg {
|
||||||
|
// 如果文档中包含图片,则将图片复制到文档目录
|
||||||
|
if err = pkg.CopyImageToDoc(docPath, imgPath); err != nil {
|
||||||
|
return docPath, imgPath, fmt.Errorf("复制图片到文档失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return docPath, imgPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupFiles 清理临时文件
|
||||||
|
func (pm *PublishManager) cleanupFiles(docPath, imgPath string, taskLogger *log.Logger, requestID string) {
|
||||||
|
if docPath != "" {
|
||||||
|
pkg.DeleteFile(docPath)
|
||||||
|
taskLogger.Printf("[任务 %s] 已删除文档文件: %s", requestID, docPath)
|
||||||
|
}
|
||||||
|
if imgPath != "" {
|
||||||
|
pkg.DeleteFile(imgPath)
|
||||||
|
taskLogger.Printf("[任务 %s] 已删除图片文件: %s", requestID, imgPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractContent 提取文档内容
|
||||||
|
func (pm *PublishManager) extractContent(docPath string, publisherClass *publisher.PublisherValue, taskLogger *log.Logger, requestID string) (string, error) {
|
||||||
|
if publisherClass.Type != 1 {
|
||||||
|
return "", nil // 不需要提取内容
|
||||||
|
}
|
||||||
|
|
||||||
|
taskLogger.Printf("[任务 %s] 开始提取文档内容...", requestID)
|
||||||
|
content, err := pkg.ExtractWordContent(docPath, publisherClass.ContentFormat)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
taskLogger.Printf("[任务 %s] ✅ 内容提取成功,长度: %d", requestID, len(content))
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updatePublishStatus 更新发布状态(优化:添加错误处理)
|
||||||
|
func (pm *PublishManager) updatePublishStatus(requestID string, status int, message string) error {
|
||||||
|
if requestID == "" {
|
||||||
|
return fmt.Errorf("requestID不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
if message != "" {
|
if message != "" {
|
||||||
pm.db.Execute("UPDATE publish SET status = ?, msg = ? WHERE request_id = ?", status, message, requestID)
|
_, err = pm.db.Execute("UPDATE publish SET status = ?, msg = ? WHERE request_id = ?", status, message, requestID)
|
||||||
} else {
|
} else {
|
||||||
pm.db.Execute("UPDATE publish SET status = ? WHERE request_id = ?", status, requestID)
|
_, err = pm.db.Execute("UPDATE publish SET status = ? WHERE request_id = ?", status, requestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("更新发布状态失败: requestID=%s, status=%d, error=%v", requestID, status, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *PublishManager) ExecuteOnce(tokenId int32) map[string]interface{} {
|
// RetryTask 重试任务(优化:添加任务状态检查)
|
||||||
publishData := pm.getPendingPublish()
|
func (pm *PublishManager) RetryTask(requestID string) *SingleResult {
|
||||||
if publishData == nil {
|
if requestID == "" {
|
||||||
return map[string]interface{}{"success": false, "message": "没有待发布任务"}
|
return &SingleResult{
|
||||||
|
Success: false,
|
||||||
|
Message: "requestID不能为空",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return pm.processSingleTask(publishData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *PublishManager) RetryTask(requestID string) map[string]interface{} {
|
|
||||||
publishData, err := pm.GetTaskByRequestID(requestID)
|
publishData, err := pm.GetTaskByRequestID(requestID)
|
||||||
if err != nil || publishData == nil {
|
if err != nil || publishData == nil {
|
||||||
return map[string]interface{}{"success": false, "message": "任务不存在"}
|
|
||||||
|
return &SingleResult{
|
||||||
|
Success: false,
|
||||||
|
Message: "任务不存在",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只允许重试失败的任务
|
||||||
|
//
|
||||||
|
//if publishData.Status != StatusFailed {
|
||||||
|
// return &SingleResult{
|
||||||
|
// Success: false,
|
||||||
|
// Message: fmt.Sprintf("只能重试失败的任务,当前状态: %d", publishData.Status),
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
return pm.processSingleTask(publishData)
|
return pm.processSingleTask(publishData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetStatus 获取状态(优化:使用读锁)
|
||||||
func (pm *PublishManager) GetStatus() map[string]interface{} {
|
func (pm *PublishManager) GetStatus() map[string]interface{} {
|
||||||
|
pm.mu.RLock()
|
||||||
|
defer pm.mu.RUnlock()
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"auto_status": pm.AutoStatus,
|
"auto_status": pm.AutoStatus,
|
||||||
"max_concurrent": pm.Conf.Sys.MaxConcurrent,
|
"max_concurrent": pm.Conf.Sys.MaxConcurrent,
|
||||||
"task_timeout": pm.Conf.Sys.TaskTimeout,
|
"task_timeout": pm.Conf.Sys.TaskTimeout,
|
||||||
|
"token_id": pm.TokenID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPublisherClass 获取发布器类(优化:添加默认值处理)
|
||||||
func GetPublisherClass(platIndex string) *publisher.PublisherValue {
|
func GetPublisherClass(platIndex string) *publisher.PublisherValue {
|
||||||
|
if platIndex == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return publisher.PublisherMap[platIndex]
|
if publisherClass, exists := publisher.PublisherMap[platIndex]; exists {
|
||||||
|
return publisherClass
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("未找到平台 %s 对应的发布器", platIndex)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ package publisher
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"geo/internal/config"
|
"geo/internal/config"
|
||||||
"geo/pkg"
|
|
||||||
"log"
|
"log"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-rod/rod"
|
"github.com/go-rod/rod"
|
||||||
|
|
@ -13,25 +13,10 @@ import (
|
||||||
|
|
||||||
type BaijiahaoPublisher struct {
|
type BaijiahaoPublisher struct {
|
||||||
*BasePublisher
|
*BasePublisher
|
||||||
Category string
|
|
||||||
ArticleType string
|
|
||||||
IsTop bool
|
|
||||||
maxRetries int
|
|
||||||
retryDelay int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBaijiahaoPublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
func NewBaijiahaoPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||||||
base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg, logger)
|
return &BaijiahaoPublisher{NewBasePublisher(task, cfg, logger)}
|
||||||
if platInfo != nil {
|
|
||||||
base.LoginURL = pkg.GetString(platInfo, "login_url")
|
|
||||||
base.EditorURL = pkg.GetString(platInfo, "edit_url")
|
|
||||||
base.LoginedURL = pkg.GetString(platInfo, "logined_url")
|
|
||||||
}
|
|
||||||
return &BaijiahaoPublisher{
|
|
||||||
BasePublisher: base,
|
|
||||||
maxRetries: 5,
|
|
||||||
retryDelay: 2,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BaijiahaoPublisher) CheckLoginStatus() bool {
|
func (p *BaijiahaoPublisher) CheckLoginStatus() bool {
|
||||||
|
|
@ -141,8 +126,6 @@ func (p *BaijiahaoPublisher) PublishNote() (bool, string) {
|
||||||
|
|
||||||
func (p *BaijiahaoPublisher) doPublish() (bool, string) {
|
func (p *BaijiahaoPublisher) doPublish() (bool, string) {
|
||||||
p.LogInfo("开始发布百家号文章...")
|
p.LogInfo("开始发布百家号文章...")
|
||||||
p.Sleep(3)
|
|
||||||
|
|
||||||
steps := []struct {
|
steps := []struct {
|
||||||
name string
|
name string
|
||||||
fn func() error
|
fn func() error
|
||||||
|
|
@ -214,10 +197,6 @@ func (p *BaijiahaoPublisher) inputTitle() error {
|
||||||
if currentTitle != "" {
|
if currentTitle != "" {
|
||||||
p.LogInfo(fmt.Sprintf("清空当前标题: %s", currentTitle[:min(50, len(currentTitle))]))
|
p.LogInfo(fmt.Sprintf("清空当前标题: %s", currentTitle[:min(50, len(currentTitle))]))
|
||||||
p.ClearContentEditable(titleInput)
|
p.ClearContentEditable(titleInput)
|
||||||
p.SleepMs(300)
|
|
||||||
titleInput.Input("\u0001")
|
|
||||||
p.SleepMs(200)
|
|
||||||
titleInput.Input("\u007F")
|
|
||||||
p.SleepMs(200)
|
p.SleepMs(200)
|
||||||
}
|
}
|
||||||
titleInput.Input(p.Title)
|
titleInput.Input(p.Title)
|
||||||
|
|
@ -234,35 +213,142 @@ func (p *BaijiahaoPublisher) inputTitle() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BaijiahaoPublisher) inputContent() error {
|
func (p *BaijiahaoPublisher) inputContent() error {
|
||||||
p.LogInfo("输入文章内容...")
|
p.LogInfo("开始导入文档内容...")
|
||||||
titleInput, err := p.Page.Element("[contenteditable='true']")
|
|
||||||
if err != nil || titleInput == nil {
|
// 1. 找到 id="edui41" 的 div 并 hover
|
||||||
return fmt.Errorf("未找到标题输入框")
|
edui41, err := p.WaitForElement("#edui41", 10)
|
||||||
}
|
|
||||||
p.LogInfo("从标题框按 Tab 键切换到内容编辑器")
|
|
||||||
titleInput.Click(proto.InputMouseButtonLeft, 1)
|
|
||||||
p.SleepMs(500)
|
|
||||||
titleInput.Input("\t")
|
|
||||||
p.LogInfo("已按 Tab 键")
|
|
||||||
p.SleepMs(1500)
|
|
||||||
contentEditor, err := p.Page.Element(".ProseMirror")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
contentEditor, err = p.Page.Element("[contenteditable='true']")
|
return fmt.Errorf("未找到编辑器工具栏: %v", err)
|
||||||
}
|
}
|
||||||
if contentEditor == nil {
|
|
||||||
return fmt.Errorf("未找到内容编辑器")
|
// 鼠标 hover
|
||||||
|
if err := edui41.Hover(); err != nil {
|
||||||
|
return fmt.Errorf("hover 失败: %v", err)
|
||||||
}
|
}
|
||||||
contentEditor.Click(proto.InputMouseButtonLeft, 1)
|
p.LogInfo("已 hover 到编辑器工具栏")
|
||||||
p.SleepMs(500)
|
p.SleepMs(500)
|
||||||
p.ClearContentEditable(contentEditor)
|
|
||||||
p.SleepMs(300)
|
// 2. 查找并点击"导入文档"
|
||||||
p.SetContentEditable(contentEditor, p.Content)
|
var importDocBtn *rod.Element
|
||||||
p.SleepMs(2000)
|
// 等待 popover 出现
|
||||||
inputContent, _ := contentEditor.Text()
|
for i := 0; i < 10; i++ {
|
||||||
if len(inputContent) == 0 {
|
// 查找 class 包含 cheetah-popover 的元素
|
||||||
contentEditor.Input(p.Content)
|
popover, err := p.Page.Element("[class*='cheetah-popover']")
|
||||||
p.SleepMs(2000)
|
if err != nil || popover == nil {
|
||||||
|
p.SleepMs(500)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 popover 内查找 class 包含 "-label" 且文本为"导入文档"的 div
|
||||||
|
// 使用正则匹配 class 包含随机字符-label 的模式
|
||||||
|
importDocBtn, err = popover.ElementX("//div[contains(@class, '-label') and contains(text(), '导入文档')]")
|
||||||
|
if err == nil && importDocBtn != nil {
|
||||||
|
p.LogInfo("找到导入文档按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备用查找方式:直接在整个页面中查找
|
||||||
|
importDocBtn, err = p.Page.ElementX("//div[contains(@class, '-label') and contains(text(), '导入文档')]")
|
||||||
|
if err == nil && importDocBtn != nil {
|
||||||
|
p.LogInfo("通过 XPath 找到导入文档按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if importDocBtn == nil {
|
||||||
|
return fmt.Errorf("未找到导入文档按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击导入文档按钮
|
||||||
|
if err := p.JSClick(importDocBtn); err != nil {
|
||||||
|
return fmt.Errorf("点击导入文档按钮失败: %v", err)
|
||||||
|
}
|
||||||
|
p.LogInfo("已点击导入文档按钮")
|
||||||
|
p.SleepMs(1000)
|
||||||
|
|
||||||
|
// 3. 查找 dialog 中的文件上传 input
|
||||||
|
var fileInput *rod.Element
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
// 查找 role="dialog" 的元素
|
||||||
|
dialog, err := p.Page.Element("[role='dialog']")
|
||||||
|
if err != nil || dialog == nil {
|
||||||
|
p.SleepMs(500)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 dialog 内查找 name="file" 的 input
|
||||||
|
fileInput, err = dialog.Element("input[name='file']")
|
||||||
|
if err == nil && fileInput != nil {
|
||||||
|
p.LogInfo("找到文件上传输入框")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备用:直接在整个页面中查找
|
||||||
|
fileInput, err = p.Page.Element("input[name='file']")
|
||||||
|
if err == nil && fileInput != nil {
|
||||||
|
p.LogInfo("通过全局选择器找到文件上传输入框")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInput == nil {
|
||||||
|
return fmt.Errorf("未找到文件上传输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 上传文档
|
||||||
|
if p.SourcePath == "" {
|
||||||
|
return fmt.Errorf("未提供文档路径")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fileInput.SetFiles([]string{p.SourcePath}); err != nil {
|
||||||
|
return fmt.Errorf("上传文档失败: %v", err)
|
||||||
|
}
|
||||||
|
p.LogInfo(fmt.Sprintf("已上传文档: %s", p.SourcePath))
|
||||||
|
|
||||||
|
// 5. 等待导入成功
|
||||||
|
// 提取文件名(不含路径)
|
||||||
|
fileName := filepath.Base(p.SourcePath)
|
||||||
|
|
||||||
|
// 等待导入成功的提示
|
||||||
|
for i := 0; i < 30; i++ {
|
||||||
|
// 查找包含文件名的成功提示
|
||||||
|
successMsg, err := p.Page.ElementX(fmt.Sprintf("//*[contains(text(), '%s') and (contains(text(), '成功') or contains(text(), '导入'))]", fileName))
|
||||||
|
if err == nil && successMsg != nil {
|
||||||
|
text, _ := successMsg.Text()
|
||||||
|
p.LogInfo(fmt.Sprintf("文档导入成功: %s", text))
|
||||||
|
p.SleepMs(2000) // 等待内容加载完成
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用成功提示查找
|
||||||
|
successMsg, err = p.Page.ElementX("//*[contains(text(), '导入成功')]")
|
||||||
|
if err == nil && successMsg != nil {
|
||||||
|
text, _ := successMsg.Text()
|
||||||
|
p.LogInfo(fmt.Sprintf("文档导入成功: %s", text))
|
||||||
|
p.SleepMs(2000)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找是否有错误提示
|
||||||
|
errorMsg, err := p.Page.ElementX("//*[contains(text(), '失败') or contains(text(), '错误')]")
|
||||||
|
if err == nil && errorMsg != nil {
|
||||||
|
text, _ := errorMsg.Text()
|
||||||
|
if strings.Contains(text, fileName) || strings.Contains(text, "导入") {
|
||||||
|
return fmt.Errorf("文档导入失败: %s", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 虽然没有明确的成功提示,但等待几秒让内容加载
|
||||||
|
p.LogInfo("等待内容加载完成...")
|
||||||
|
p.SleepMs(3000)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,7 +358,6 @@ func (p *BaijiahaoPublisher) uploadImage() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
p.LogInfo("设置文章封面...")
|
p.LogInfo("设置文章封面...")
|
||||||
p.SleepMs(2000)
|
|
||||||
|
|
||||||
// 查找并点击封面选择区域
|
// 查找并点击封面选择区域
|
||||||
coverSelectors := []string{
|
coverSelectors := []string{
|
||||||
|
|
@ -300,130 +385,135 @@ func (p *BaijiahaoPublisher) uploadImage() error {
|
||||||
p.SleepMs(2000)
|
p.SleepMs(2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找并点击上传区域
|
//// 查找并点击上传区域
|
||||||
uploadSelectors := []string{
|
//uploadSelectors := []string{
|
||||||
"div[class*='cheetah-upload']",
|
// "div[class*='cheetah-upload']",
|
||||||
".cheetah-upload",
|
// ".cheetah-upload",
|
||||||
"div[class*='upload']",
|
// "div[class*='upload']",
|
||||||
".upload-area",
|
// ".upload-area",
|
||||||
"._73a3a52aab7e3a36-content",
|
// "._73a3a52aab7e3a36-content",
|
||||||
"._93c3fe2a3121c388-item",
|
// "._93c3fe2a3121c388-item",
|
||||||
}
|
//}
|
||||||
var uploadArea *rod.Element
|
//var uploadArea *rod.Element
|
||||||
for _, selector := range uploadSelectors {
|
//for _, selector := range uploadSelectors {
|
||||||
elements, _ := p.Page.Elements(selector)
|
// elements, _ := p.Page.Elements(selector)
|
||||||
for _, elem := range elements {
|
// for _, elem := range elements {
|
||||||
visible, _ := elem.Visible()
|
// visible, _ := elem.Visible()
|
||||||
if visible {
|
// if visible {
|
||||||
uploadArea = elem
|
// uploadArea = elem
|
||||||
p.LogInfo(fmt.Sprintf("找到上传区域: %s", selector))
|
// p.LogInfo(fmt.Sprintf("找到上传区域: %s", selector))
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if uploadArea != nil {
|
// if uploadArea != nil {
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
if uploadArea != nil {
|
//if uploadArea != nil {
|
||||||
p.ScrollToElement(uploadArea)
|
// p.ScrollToElement(uploadArea)
|
||||||
p.SleepMs(500)
|
// p.SleepMs(500)
|
||||||
p.JSClick(uploadArea)
|
// p.JSClick(uploadArea)
|
||||||
p.LogInfo("已点击图片上传区域")
|
// p.LogInfo("已点击图片上传区域")
|
||||||
p.SleepMs(1000)
|
// p.SleepMs(1000)
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
// 查找cheetah-upload组件
|
//// 查找cheetah-upload组件
|
||||||
componentSelectors := []string{
|
//componentSelectors := []string{
|
||||||
"div[class*='cheetah-upload']",
|
// "div[class*='cheetah-upload']",
|
||||||
".cheetah-upload",
|
// ".cheetah-upload",
|
||||||
"div[class*='upload']",
|
// "div[class*='upload']",
|
||||||
}
|
//}
|
||||||
var uploadComponent *rod.Element
|
//var uploadComponent *rod.Element
|
||||||
for _, selector := range componentSelectors {
|
//for _, selector := range componentSelectors {
|
||||||
elements, _ := p.Page.Elements(selector)
|
// elements, _ := p.Page.Elements(selector)
|
||||||
for _, elem := range elements {
|
// for _, elem := range elements {
|
||||||
visible, _ := elem.Visible()
|
// visible, _ := elem.Visible()
|
||||||
if visible {
|
// if visible {
|
||||||
uploadComponent = elem
|
// uploadComponent = elem
|
||||||
p.LogInfo(fmt.Sprintf("找到cheetah-upload组件: %s", selector))
|
// p.LogInfo(fmt.Sprintf("找到cheetah-upload组件: %s", selector))
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if uploadComponent != nil {
|
// if uploadComponent != nil {
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
if uploadComponent != nil {
|
//if uploadComponent != nil {
|
||||||
p.ScrollToElement(uploadComponent)
|
// p.ScrollToElement(uploadComponent)
|
||||||
p.SleepMs(500)
|
// p.SleepMs(500)
|
||||||
p.JSClick(uploadComponent)
|
// p.JSClick(uploadComponent)
|
||||||
p.LogInfo("已点击cheetah-upload上传组件")
|
// p.LogInfo("已点击cheetah-upload上传组件")
|
||||||
p.SleepMs(2000)
|
// p.SleepMs(2000)
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
// 查找文件上传输入框
|
//// 查找文件上传输入框
|
||||||
var fileInput *rod.Element
|
//var fileInput *rod.Element
|
||||||
for i := 0; i < 10; i++ {
|
//for i := 0; i < 10; i++ {
|
||||||
fileInput, _ = p.Page.Element("input[name='media'][type='file'][accept='image/*']")
|
// fileInput, _ = p.Page.Element("input[name='media'][type='file'][accept='image/*']")
|
||||||
if fileInput != nil {
|
// if fileInput != nil {
|
||||||
p.LogInfo("找到文件上传输入框")
|
// p.LogInfo("找到文件上传输入框")
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
fileInput, _ = p.Page.Element("input[type='file'][accept*='image']")
|
// fileInput, _ = p.Page.Element("input[type='file'][accept*='image']")
|
||||||
if fileInput != nil {
|
// if fileInput != nil {
|
||||||
p.LogInfo("通过备用选择器找到文件上传输入框")
|
// p.LogInfo("通过备用选择器找到文件上传输入框")
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
p.SleepMs(500)
|
// p.SleepMs(500)
|
||||||
}
|
//}
|
||||||
if fileInput != nil {
|
//if fileInput != nil {
|
||||||
fileInput.SetFiles([]string{p.ImagePath})
|
// fileInput.SetFiles([]string{p.ImagePath})
|
||||||
p.LogInfo(fmt.Sprintf("图片上传成功: %s", p.ImagePath))
|
// p.LogInfo(fmt.Sprintf("图片上传成功: %s", p.ImagePath))
|
||||||
p.Sleep(3)
|
// p.Sleep(3)
|
||||||
}
|
//}
|
||||||
|
|
||||||
|
// 查找并点击确认按钮
|
||||||
|
// 查找并点击确认按钮
|
||||||
// 查找并点击确认按钮
|
// 查找并点击确认按钮
|
||||||
var confirmBtn *rod.Element
|
var confirmBtn *rod.Element
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < p.MaxRetries; i++ {
|
||||||
confirmBtn, _ = p.Page.ElementX("//button[contains(text(), '确定')]")
|
p.LogInfo("正在查找确认按钮...")
|
||||||
|
|
||||||
|
// 精确匹配:button 包含 cheetah-btn-primary 类,且 span 文本为"确定 (1)"
|
||||||
|
confirmBtn, _ = p.Page.ElementX("//button[contains(@class, 'cheetah-btn-primary')]//span[text()='确定 (1)']/..")
|
||||||
|
|
||||||
if confirmBtn != nil {
|
if confirmBtn != nil {
|
||||||
visible, _ := confirmBtn.Visible()
|
visible, _ := confirmBtn.Visible()
|
||||||
if visible {
|
if visible {
|
||||||
p.LogInfo("通过文本找到确认按钮")
|
p.LogInfo("找到确认按钮")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
confirmBtn, _ = p.Page.Element(".cheetah-btn-primary")
|
|
||||||
|
// 备选:只匹配 span 文本
|
||||||
|
confirmBtn, _ = p.Page.ElementX("//span[text()='确定 (1)']/..")
|
||||||
if confirmBtn != nil {
|
if confirmBtn != nil {
|
||||||
text, _ := confirmBtn.Text()
|
visible, _ := confirmBtn.Visible()
|
||||||
if strings.Contains(text, "确定") {
|
|
||||||
p.LogInfo(fmt.Sprintf("通过CSS选择器找到确认按钮: %s", text))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buttons, _ := p.Page.Elements("button[class*='cheetah-btn']")
|
|
||||||
for _, btn := range buttons {
|
|
||||||
visible, _ := btn.Visible()
|
|
||||||
if visible {
|
if visible {
|
||||||
text, _ := btn.Text()
|
p.LogInfo("通过 span 文本找到确认按钮")
|
||||||
if strings.Contains(text, "确定") || strings.Contains(text, "确认") {
|
break
|
||||||
confirmBtn = btn
|
|
||||||
p.LogInfo(fmt.Sprintf("通过遍历按钮找到确认按钮: %s", text))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 备选:文本包含"确定"和数字
|
||||||
|
confirmBtn, _ = p.Page.ElementX("//button[contains(@class, 'cheetah-btn-primary') and contains(., '确定')]")
|
||||||
if confirmBtn != nil {
|
if confirmBtn != nil {
|
||||||
break
|
visible, _ := confirmBtn.Visible()
|
||||||
|
if visible {
|
||||||
|
p.LogInfo("通过文本内容找到确认按钮")
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
p.Sleep(1)
|
|
||||||
|
p.SleepMs(p.RetryDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
if confirmBtn != nil {
|
if confirmBtn != nil {
|
||||||
p.ScrollToElement(confirmBtn)
|
|
||||||
p.SleepMs(500)
|
|
||||||
p.JSClick(confirmBtn)
|
p.JSClick(confirmBtn)
|
||||||
p.LogInfo("已点击确认按钮")
|
p.LogInfo("已点击确认按钮")
|
||||||
p.SleepMs(2000)
|
p.SleepMs(2000)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("未找到确认按钮")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ package publisher
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"geo/internal/entitys"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"geo/internal/config"
|
"geo/internal/config"
|
||||||
|
|
@ -16,15 +18,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type BasePublisher struct {
|
type BasePublisher struct {
|
||||||
Headless bool
|
Headless bool
|
||||||
Title string
|
Title string
|
||||||
Content string
|
Content string
|
||||||
Tags []string
|
Tags []string
|
||||||
TenantID string
|
UserIndex string
|
||||||
PlatIndex string
|
PlatIndex string
|
||||||
RequestID string
|
RequestID string
|
||||||
ImagePath string
|
ImagePath string
|
||||||
WordPath string
|
SourcePath string
|
||||||
|
|
||||||
Browser *rod.Browser
|
Browser *rod.Browser
|
||||||
Page *rod.Page
|
Page *rod.Page
|
||||||
|
|
@ -37,15 +39,33 @@ type BasePublisher struct {
|
||||||
LoginedURL string
|
LoginedURL string
|
||||||
CookiesFile string
|
CookiesFile string
|
||||||
|
|
||||||
PlatInfo map[string]interface{}
|
PlatInfo *entitys.PublishTaskDetail
|
||||||
config *config.Config
|
config *config.Config
|
||||||
|
|
||||||
|
MaxRetries int
|
||||||
|
RetryDelay int
|
||||||
|
}
|
||||||
|
|
||||||
|
// taskParams 任务参数结构体
|
||||||
|
type TaskParams struct {
|
||||||
|
Headless bool
|
||||||
|
Title string
|
||||||
|
TagRaw string
|
||||||
|
UserIndex string
|
||||||
|
PlatIndex string
|
||||||
|
RequestID string
|
||||||
|
ImagePath string
|
||||||
|
SourcePath string
|
||||||
|
Content string
|
||||||
|
Tags []string
|
||||||
|
PublishData *entitys.PublishTaskDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBasePublisher 构造函数,增加 logger 参数
|
// NewBasePublisher 构造函数,增加 logger 参数
|
||||||
func NewBasePublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, config *config.Config, logger *log.Logger) *BasePublisher {
|
func NewBasePublisher(task *TaskParams, config *config.Config, logger *log.Logger) *BasePublisher {
|
||||||
cookiesDir := filepath.Join(config.Sys.CookiesDir, tenantID)
|
cookiesDir := filepath.Join(config.Sys.CookiesDir, task.UserIndex)
|
||||||
os.MkdirAll(cookiesDir, 0755)
|
os.MkdirAll(cookiesDir, 0755)
|
||||||
cookiesFile := filepath.Join(cookiesDir, platIndex+".json")
|
cookiesFile := filepath.Join(cookiesDir, task.PlatIndex+".json")
|
||||||
|
|
||||||
var baseLogger *log.Logger
|
var baseLogger *log.Logger
|
||||||
var logFile *os.File
|
var logFile *os.File
|
||||||
|
|
@ -61,34 +81,49 @@ func NewBasePublisher(headless bool, title, content string, tags []string, tenan
|
||||||
logsDir = "./logs"
|
logsDir = "./logs"
|
||||||
}
|
}
|
||||||
os.MkdirAll(logsDir, 0755)
|
os.MkdirAll(logsDir, 0755)
|
||||||
logFile, _ = os.Create(filepath.Join(logsDir, requestID+".log"))
|
logFile, _ = os.Create(filepath.Join(logsDir, task.RequestID+".log"))
|
||||||
baseLogger = log.New(logFile, "", log.LstdFlags)
|
baseLogger = log.New(logFile, "", log.LstdFlags)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &BasePublisher{
|
return &BasePublisher{
|
||||||
Headless: headless,
|
Headless: task.Headless,
|
||||||
Title: title,
|
Title: task.Title,
|
||||||
Content: content,
|
Content: task.Content,
|
||||||
Tags: tags,
|
Tags: task.Tags,
|
||||||
TenantID: tenantID,
|
UserIndex: task.UserIndex,
|
||||||
PlatIndex: platIndex,
|
PlatIndex: task.PlatIndex,
|
||||||
RequestID: requestID,
|
RequestID: task.RequestID,
|
||||||
ImagePath: imagePath,
|
ImagePath: task.ImagePath,
|
||||||
WordPath: wordPath,
|
SourcePath: task.SourcePath,
|
||||||
Logger: baseLogger,
|
Logger: baseLogger,
|
||||||
LogFile: logFile,
|
LogFile: logFile,
|
||||||
CookiesFile: cookiesFile,
|
CookiesFile: cookiesFile,
|
||||||
PlatInfo: platInfo,
|
PlatInfo: task.PublishData,
|
||||||
config: config,
|
config: config,
|
||||||
|
LoginURL: task.PublishData.LoginUrl,
|
||||||
|
EditorURL: task.PublishData.EditUrl,
|
||||||
|
LoginedURL: task.PublishData.LoginedUrl,
|
||||||
|
MaxRetries: 3,
|
||||||
|
RetryDelay: 200,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BasePublisher) SetupDriver() error {
|
func (b *BasePublisher) SetupDriver() error {
|
||||||
|
headless := false
|
||||||
l := launcher.New()
|
l := launcher.New()
|
||||||
l.Headless(b.Headless)
|
l.Headless(headless)
|
||||||
|
if headless {
|
||||||
|
// 无头模式专用参数
|
||||||
|
l.Set("headless", "new") // 使用新版无头模式
|
||||||
|
l.Set("disable-gpu")
|
||||||
|
l.Set("disable-software-rasterizer")
|
||||||
|
l.Set("disable-dev-shm-usage")
|
||||||
|
l.Set("no-sandbox")
|
||||||
|
// 禁用虚拟滚动,避免等待
|
||||||
|
l.Set("disable-scroll-to-text-fragment")
|
||||||
|
}
|
||||||
// 设置用户数据目录
|
// 设置用户数据目录
|
||||||
userDataDir := filepath.Join(b.config.Sys.ChromeDataDir, b.TenantID)
|
userDataDir := filepath.Join(b.config.Sys.ChromeDataDir, b.UserIndex)
|
||||||
os.MkdirAll(userDataDir, 0755)
|
os.MkdirAll(userDataDir, 0755)
|
||||||
l.UserDataDir(userDataDir)
|
l.UserDataDir(userDataDir)
|
||||||
|
|
||||||
|
|
@ -104,6 +139,12 @@ func (b *BasePublisher) SetupDriver() error {
|
||||||
l.Set("disable-setuid-sandbox")
|
l.Set("disable-setuid-sandbox")
|
||||||
l.Set("remote-debugging-port", "9222")
|
l.Set("remote-debugging-port", "9222")
|
||||||
|
|
||||||
|
// 关键:禁用后台限制,让页面在后台也能正常执行
|
||||||
|
l.Set("disable-background-timer-throttling")
|
||||||
|
l.Set("disable-backgrounding-occluded-windows")
|
||||||
|
l.Set("disable-renderer-backgrounding")
|
||||||
|
l.Set("disable-ipc-flooding-protection")
|
||||||
|
|
||||||
// 窗口大小
|
// 窗口大小
|
||||||
l.Set("window-size", "1920,1080")
|
l.Set("window-size", "1920,1080")
|
||||||
l.Set("lang", "zh-CN")
|
l.Set("lang", "zh-CN")
|
||||||
|
|
@ -117,9 +158,6 @@ func (b *BasePublisher) SetupDriver() error {
|
||||||
b.Browser = rod.New().ControlURL(url).MustConnect()
|
b.Browser = rod.New().ControlURL(url).MustConnect()
|
||||||
b.Page = b.Browser.MustPage()
|
b.Page = b.Browser.MustPage()
|
||||||
|
|
||||||
// 删除这行!!!!
|
|
||||||
// b.Page.MustSetViewport(1920, 1080, 1, false)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,23 +238,15 @@ func (b *BasePublisher) WaitForElementClickable(selector string, timeout int) (*
|
||||||
|
|
||||||
func (b *BasePublisher) JSClick(element *rod.Element) error {
|
func (b *BasePublisher) JSClick(element *rod.Element) error {
|
||||||
if element == nil {
|
if element == nil {
|
||||||
|
b.Logger.Printf("element is nil")
|
||||||
return fmt.Errorf("element is nil")
|
return fmt.Errorf("element is nil")
|
||||||
}
|
}
|
||||||
|
err := element.Click(proto.InputMouseButtonLeft, 1)
|
||||||
// 方法1:使用 rod 自带的 Click
|
// 方法1:使用 rod 自带的 Click
|
||||||
return element.Click(proto.InputMouseButtonLeft, 1)
|
if err != nil {
|
||||||
|
b.Logger.Printf("click fail:" + err.Error())
|
||||||
//// 方法2:使用 JavaScript 点击(修复版)
|
}
|
||||||
//_, err := element.Evaluate(&rod.EvalOptions{
|
return err
|
||||||
// JS: `function(el) {
|
|
||||||
// if(el && el.click) {
|
|
||||||
// el.click();
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
// return false;
|
|
||||||
// }(this)`,
|
|
||||||
//})
|
|
||||||
//return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BasePublisher) ScrollToElement(element *rod.Element) error {
|
func (b *BasePublisher) ScrollToElement(element *rod.Element) error {
|
||||||
|
|
@ -314,3 +344,13 @@ func (b *BasePublisher) ClearInput(element *rod.Element) error {
|
||||||
func (b *BasePublisher) SleepMs(milliseconds int) {
|
func (b *BasePublisher) SleepMs(milliseconds int) {
|
||||||
time.Sleep(time.Duration(milliseconds) * time.Millisecond)
|
time.Sleep(time.Duration(milliseconds) * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartNote 开始任务日志
|
||||||
|
|
||||||
|
func (b *BasePublisher) StartNote() {
|
||||||
|
b.LogInfo(strings.Repeat("=", 50))
|
||||||
|
b.LogInfo(fmt.Sprintf("开始执行:%s", b.PlatIndex))
|
||||||
|
b.LogInfo(fmt.Sprintf("标题: %s", b.Title))
|
||||||
|
b.LogInfo(fmt.Sprintf("标签: %v", b.Tags))
|
||||||
|
b.LogInfo(strings.Repeat("=", 50))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,40 +11,42 @@ type PublisherInerface interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type NewPublisher func(
|
type NewPublisher func(
|
||||||
headless bool,
|
param *TaskParams,
|
||||||
title string,
|
|
||||||
content string,
|
|
||||||
tags []string,
|
|
||||||
tenantID string,
|
|
||||||
platIndex string,
|
|
||||||
requestID string,
|
|
||||||
imagePath string,
|
|
||||||
wordPath string,
|
|
||||||
platInfo map[string]interface{},
|
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
logger *log.Logger) PublisherInerface
|
logger *log.Logger) PublisherInerface
|
||||||
|
|
||||||
type PublisherValue struct {
|
type PublisherValue struct {
|
||||||
Name string
|
Name string
|
||||||
InitMethod NewPublisher
|
InitMethod NewPublisher
|
||||||
ContentFormat string //需要的文章格式,html,text,markdown
|
ContentFormat string //需要的文章格式,html,text,markdown
|
||||||
ImgNeed int8 //是否需要图片,1需要,2非必须,3不要
|
ImgNeed int8 //是否需要图片,1需要,2非必须,3不要
|
||||||
Type int8 //类型:1文章2视频
|
Type int8 //类型:1文章2视频
|
||||||
|
WordContainImg bool //文章带上头图
|
||||||
}
|
}
|
||||||
|
|
||||||
var PublisherMap = map[string]*PublisherValue{
|
var PublisherMap = map[string]*PublisherValue{
|
||||||
"xhs": {
|
"xhs": {
|
||||||
Name: "小红书",
|
Name: "小红书",
|
||||||
InitMethod: NewXiaohongshuPublisher,
|
InitMethod: NewXiaohongshuPublisher,
|
||||||
ContentFormat: "text",
|
ContentFormat: "text",
|
||||||
ImgNeed: 3,
|
ImgNeed: 3,
|
||||||
Type: 1,
|
Type: 1,
|
||||||
|
WordContainImg: false,
|
||||||
},
|
},
|
||||||
"bjh": {
|
"bjh": {
|
||||||
Name: "百家号",
|
Name: "百家号",
|
||||||
InitMethod: NewBaijiahaoPublisher,
|
InitMethod: NewBaijiahaoPublisher,
|
||||||
ContentFormat: "text",
|
ContentFormat: "text",
|
||||||
ImgNeed: 1,
|
ImgNeed: 1,
|
||||||
Type: 1,
|
Type: 1,
|
||||||
|
WordContainImg: true,
|
||||||
|
},
|
||||||
|
"toutiao": {
|
||||||
|
Name: "今日头条",
|
||||||
|
InitMethod: NewToutiaoPublisher,
|
||||||
|
ContentFormat: "text",
|
||||||
|
ImgNeed: 1,
|
||||||
|
Type: 1,
|
||||||
|
WordContainImg: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,580 @@
|
||||||
|
package publisher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"geo/internal/config"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/go-rod/rod/lib/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ToutiaoPublisher struct {
|
||||||
|
*BasePublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewToutiaoPublisher 构造函数
|
||||||
|
func NewToutiaoPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||||||
|
return &ToutiaoPublisher{NewBasePublisher(task, cfg, logger)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ToutiaoPublisher) CheckLogin() (bool, string) {
|
||||||
|
p.LogInfo("检查登录状态...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.Sleep(2)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return true, "已登录"
|
||||||
|
}
|
||||||
|
return false, "未登录"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ToutiaoPublisher) CheckLoginStatus() bool {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
// 如果在登录页面,未登录
|
||||||
|
if strings.Contains(currentURL, p.LoginURL) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ToutiaoPublisher) WaitLogin() (bool, string) {
|
||||||
|
p.LogInfo("开始等待登录...")
|
||||||
|
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
// 先尝试访问已登录页面
|
||||||
|
p.Page.MustNavigate(p.LoginedURL)
|
||||||
|
p.Sleep(3)
|
||||||
|
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
p.LogInfo("已有登录状态")
|
||||||
|
return true, "already_logged_in"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未登录,跳转到登录页
|
||||||
|
p.Page.MustNavigate(p.LoginURL)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
p.LogInfo("请扫码登录...")
|
||||||
|
|
||||||
|
// 等待登录完成,最多120秒
|
||||||
|
for i := 0; i < 120; i++ {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
if strings.Contains(currentURL, p.LoginedURL) {
|
||||||
|
p.SaveCookies()
|
||||||
|
p.LogInfo("登录成功")
|
||||||
|
return true, "login_success"
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "登录超时,请检查网络或账号状态"
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeCloseBtn 关闭页面上的关闭按钮(class="close-btn"的svg)
|
||||||
|
func (p *ToutiaoPublisher) closeCloseBtn() {
|
||||||
|
p.LogInfo("检查并关闭页面上的关闭按钮...")
|
||||||
|
|
||||||
|
// 查找所有 class="close-btn" 的元素
|
||||||
|
closeBtns, err := p.Page.Elements(".close-btn")
|
||||||
|
if err != nil {
|
||||||
|
p.LogInfo("查找关闭按钮失败或无关闭按钮")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(closeBtns) == 0 {
|
||||||
|
p.LogInfo("未找到关闭按钮")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("找到 %d 个关闭按钮,尝试点击...", len(closeBtns)))
|
||||||
|
|
||||||
|
for _, btn := range closeBtns {
|
||||||
|
if btn == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查元素是否可见
|
||||||
|
visible, err := btn.Visible()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if visible {
|
||||||
|
p.LogInfo("点击关闭按钮...")
|
||||||
|
// 使用 JavaScript 强制点击
|
||||||
|
if err := p.JSClick(btn); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("点击关闭按钮失败: %v", err))
|
||||||
|
} else {
|
||||||
|
p.LogInfo("成功点击关闭按钮")
|
||||||
|
p.SleepMs(500) // 等待弹窗关闭动画
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputTitle 输入标题
|
||||||
|
func (p *ToutiaoPublisher) inputTitle() error {
|
||||||
|
p.LogInfo("输入文章标题...")
|
||||||
|
|
||||||
|
// 尝试多种选择器查找标题输入框
|
||||||
|
titleSelectors := []string{
|
||||||
|
".publish-editor-title textarea",
|
||||||
|
"#txtTitle",
|
||||||
|
".title-input textarea",
|
||||||
|
"textarea[placeholder*='标题']",
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleInput *rod.Element
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, selector := range titleSelectors {
|
||||||
|
titleInput, err = p.WaitForElementVisible(selector, 5)
|
||||||
|
if err == nil && titleInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if titleInput == nil {
|
||||||
|
return fmt.Errorf("未找到标题输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击获取焦点
|
||||||
|
if err := titleInput.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
return fmt.Errorf("点击标题框失败: %v", err)
|
||||||
|
}
|
||||||
|
p.SleepMs(300)
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
if err := p.ClearInput(titleInput); err != nil {
|
||||||
|
titleInput.Input("")
|
||||||
|
}
|
||||||
|
p.SleepMs(300)
|
||||||
|
|
||||||
|
// 输入标题
|
||||||
|
if err := p.SetInputValue(titleInput, p.Title); err != nil {
|
||||||
|
titleInput.Input(p.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title))
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputContent 通过导入文件输入内容
|
||||||
|
func (p *ToutiaoPublisher) inputContent() error {
|
||||||
|
p.LogInfo("开始导入文章内容...")
|
||||||
|
// 查找所有 class="close-btn" 的元素
|
||||||
|
p.closeCloseBtn()
|
||||||
|
p.SleepMs(500)
|
||||||
|
// 1. 找到并点击导入按钮(class="syl-toolbar-button")
|
||||||
|
p.LogInfo("查找导入按钮...")
|
||||||
|
p.LogInfo("查找导入按钮...")
|
||||||
|
|
||||||
|
// 先找 class 包含 doc-import 的 div
|
||||||
|
docImportDiv, err := p.WaitForElementVisible("[class*='doc-import']", 10)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("未找到包含doc-import的元素: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("找到包含doc-import的元素,开始查找其中的button...")
|
||||||
|
|
||||||
|
// 在该 div 下查找 button
|
||||||
|
importBtn, err := docImportDiv.Element("button")
|
||||||
|
if err != nil {
|
||||||
|
// 尝试查找 button 的多种可能选择器
|
||||||
|
importBtn, err = docImportDiv.Element("button:first-child")
|
||||||
|
if err != nil {
|
||||||
|
importBtn, err = docImportDiv.Element(".syl-toolbar-button")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("在doc-import元素下未找到按钮: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if importBtn == nil {
|
||||||
|
return fmt.Errorf("未找到导入按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("找到导入按钮,准备点击...")
|
||||||
|
|
||||||
|
// 点击导入按钮
|
||||||
|
if err := p.JSClick(importBtn); err != nil {
|
||||||
|
return fmt.Errorf("点击导入按钮失败: %v", err)
|
||||||
|
}
|
||||||
|
p.LogInfo("已点击导入按钮")
|
||||||
|
p.SleepMs(1000)
|
||||||
|
|
||||||
|
// 2. 找到文件上传输入框并上传文件
|
||||||
|
p.LogInfo("查找文件上传输入框...")
|
||||||
|
|
||||||
|
// 文件上传输入框的选择器
|
||||||
|
fileInputSelectors := []string{
|
||||||
|
"input[type='file'][accept*='.doc']",
|
||||||
|
"input[type='file'][accept*='application']",
|
||||||
|
"input[type='file']",
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInput *rod.Element
|
||||||
|
for _, selector := range fileInputSelectors {
|
||||||
|
fileInput, err = p.Page.Element(selector)
|
||||||
|
if err == nil && fileInput != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInput == nil {
|
||||||
|
return fmt.Errorf("未找到文件上传输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有可用的文件路径
|
||||||
|
if p.SourcePath == "" && p.ImagePath == "" {
|
||||||
|
return fmt.Errorf("未提供要导入的文件路径")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用 SourcePath(Word文档路径),其次使用 ImagePath
|
||||||
|
filePath := p.SourcePath
|
||||||
|
if filePath == "" {
|
||||||
|
filePath = p.ImagePath
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo(fmt.Sprintf("开始上传文件: %s", filePath))
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
if err := fileInput.SetFiles([]string{filePath}); err != nil {
|
||||||
|
return fmt.Errorf("上传文件失败: %v", err)
|
||||||
|
}
|
||||||
|
p.LogInfo("文件已上传,等待导入处理...")
|
||||||
|
|
||||||
|
// 3. 等待导入成功的弹窗出现
|
||||||
|
p.LogInfo("等待导入成功弹窗...")
|
||||||
|
|
||||||
|
// 等待导入成功的提示出现
|
||||||
|
successSelectors := []string{
|
||||||
|
".byte-message-success",
|
||||||
|
".toast-success",
|
||||||
|
"[class*='success']",
|
||||||
|
"[class*='导入成功']",
|
||||||
|
".syl-toast-success",
|
||||||
|
}
|
||||||
|
|
||||||
|
var successMsg *rod.Element
|
||||||
|
for attempt := 0; attempt < 30; attempt++ {
|
||||||
|
for _, selector := range successSelectors {
|
||||||
|
successMsg, err = p.Page.Element(selector)
|
||||||
|
if err == nil && successMsg != nil {
|
||||||
|
text, _ := successMsg.Text()
|
||||||
|
if strings.Contains(text, "成功") || strings.Contains(text, "导入") {
|
||||||
|
p.LogInfo(fmt.Sprintf("检测到导入成功提示: %s", text))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if successMsg != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.SleepMs(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待弹窗消失(导入完成的标志)
|
||||||
|
p.LogInfo("等待导入完成,弹窗消失...")
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
// 等待内容加载完成
|
||||||
|
p.LogInfo("等待内容加载到编辑器...")
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
// 验证内容是否已导入
|
||||||
|
contentEditor, err := p.WaitForElementVisible(".ProseMirror", 10)
|
||||||
|
if err == nil {
|
||||||
|
text, _ := contentEditor.Text()
|
||||||
|
if len(text) > 0 {
|
||||||
|
p.LogInfo(fmt.Sprintf("内容导入成功,内容长度: %d", len(text)))
|
||||||
|
} else {
|
||||||
|
p.LogWarning("内容可能未正确导入,编辑器为空")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.LogWarning("未找到内容编辑器,无法验证导入结果")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogInfo("文章内容导入完成")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContentEditableHTML 设置 contenteditable 元素的 HTML 内容
|
||||||
|
func (p *ToutiaoPublisher) SetContentEditableHTML(element *rod.Element, html string) error {
|
||||||
|
_, err := element.Evaluate(&rod.EvalOptions{
|
||||||
|
JS: `(el, val) => { el.innerHTML = val; el.dispatchEvent(new Event('input', {bubbles: true})); }`,
|
||||||
|
JSArgs: []interface{}{html},
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// findFileInput 查找文件上传输入框
|
||||||
|
func (p *ToutiaoPublisher) findFileInput() (*rod.Element, error) {
|
||||||
|
selectors := []string{
|
||||||
|
"input[type='file']",
|
||||||
|
".byte-drawer-inner input[type='file']",
|
||||||
|
"input[accept*='image']",
|
||||||
|
".upload-input input[type='file']",
|
||||||
|
"button input[type='file']",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, selector := range selectors {
|
||||||
|
el, err := p.Page.Element(selector)
|
||||||
|
if err == nil && el != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector))
|
||||||
|
return el, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("未找到文件上传输入框")
|
||||||
|
}
|
||||||
|
|
||||||
|
// clickConfirmButton 点击确认按钮(带重试机制)
|
||||||
|
func (p *ToutiaoPublisher) clickConfirmButton() error {
|
||||||
|
for attempt := 1; attempt <= 10; attempt++ {
|
||||||
|
p.LogInfo(fmt.Sprintf("第 %d 次尝试点击确认按钮", attempt))
|
||||||
|
|
||||||
|
// 查找确认按钮
|
||||||
|
confirmBtn, err := p.Page.Element("button[data-e2e='imageUploadConfirm-btn']")
|
||||||
|
if err == nil && confirmBtn != nil {
|
||||||
|
// 检查按钮是否可用
|
||||||
|
if err := p.JSClick(confirmBtn); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("点击确认按钮失败: %v", err))
|
||||||
|
} else {
|
||||||
|
p.LogInfo("已点击确认按钮")
|
||||||
|
p.SleepMs(1000)
|
||||||
|
|
||||||
|
// 检查弹窗是否已关闭
|
||||||
|
if p.isDrawerClosed() {
|
||||||
|
p.LogInfo("弹窗已成功关闭,封面设置完成")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.LogInfo(fmt.Sprintf("第 %d 次尝试:未找到确认按钮", attempt))
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempt < 10 {
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最终检查弹窗状态
|
||||||
|
if p.isDrawerClosed() {
|
||||||
|
p.LogInfo("弹窗已关闭,封面设置成功")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("确认按钮点击后弹窗未关闭")
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDrawerClosed 检查弹窗是否已关闭
|
||||||
|
func (p *ToutiaoPublisher) isDrawerClosed() bool {
|
||||||
|
drawerWrappers, err := p.Page.Elements(".byte-drawer-wrapper")
|
||||||
|
if err != nil {
|
||||||
|
// 没找到弹窗元素,可能已关闭
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, wrapper := range drawerWrappers {
|
||||||
|
className, err := wrapper.Attribute("class")
|
||||||
|
if err == nil && className != nil && strings.Contains(*className, "byte-drawer-wrapper-hide") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// clickPublish 点击发布按钮
|
||||||
|
func (p *ToutiaoPublisher) clickPublish() error {
|
||||||
|
p.LogInfo("点击发布按钮...")
|
||||||
|
|
||||||
|
// 查找发布按钮
|
||||||
|
publishBtn, err := p.WaitForElementClickable(".publish-btn-last, .publish-footer .byte-btn-primary", 5)
|
||||||
|
if err != nil {
|
||||||
|
// 尝试其他选择器
|
||||||
|
publishBtn, err = p.Page.Element("button:contains('预览并发布')")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("未找到发布按钮: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到按钮位置
|
||||||
|
if err := p.ScrollToElement(publishBtn); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("滚动到发布按钮失败: %v", err))
|
||||||
|
}
|
||||||
|
p.SleepMs(500)
|
||||||
|
|
||||||
|
// 点击第一次发布按钮
|
||||||
|
if err := p.JSClick(publishBtn); err != nil {
|
||||||
|
return fmt.Errorf("点击发布按钮失败: %v", err)
|
||||||
|
}
|
||||||
|
p.LogInfo("已点击第一次发布按钮")
|
||||||
|
p.SleepMs(2000)
|
||||||
|
|
||||||
|
// 第二次点击确认发布
|
||||||
|
p.LogInfo("查找第二次确认发布按钮...")
|
||||||
|
secondPublishBtn, err := p.WaitForElementClickable(".publish-btn.publish-btn-last", 5)
|
||||||
|
if err != nil {
|
||||||
|
secondPublishBtn, err = p.Page.Element("button:contains('预览并发布')")
|
||||||
|
if err != nil {
|
||||||
|
p.LogInfo("未找到第二次发布确认按钮,可能已经发布")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if secondPublishBtn != nil {
|
||||||
|
if err := p.ScrollToElement(secondPublishBtn); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("滚动到确认按钮失败: %v", err))
|
||||||
|
}
|
||||||
|
p.SleepMs(500)
|
||||||
|
if err := p.JSClick(secondPublishBtn); err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("点击第二次发布确认按钮失败: %v", err))
|
||||||
|
} else {
|
||||||
|
p.LogInfo("已点击第二次发布确认按钮")
|
||||||
|
p.SleepMs(3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForPublishResult 等待发布结果
|
||||||
|
func (p *ToutiaoPublisher) waitForPublishResult() (bool, string) {
|
||||||
|
p.LogInfo("等待发布结果...")
|
||||||
|
|
||||||
|
for attempt := 0; attempt < 60; attempt++ {
|
||||||
|
currentURL := p.GetCurrentURL()
|
||||||
|
p.LogInfo(fmt.Sprintf("第 %d 次检查 - URL: %s", attempt+1, currentURL))
|
||||||
|
|
||||||
|
// 检查是否发布成功
|
||||||
|
if strings.Contains(currentURL, "success") || !strings.Contains(currentURL, "publish") {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功!URL: %s", currentURL))
|
||||||
|
return true, "发布成功"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查错误提示
|
||||||
|
errorSelectors := []string{
|
||||||
|
"[class*='error']",
|
||||||
|
"[class*='toast-error']",
|
||||||
|
".byte-message-error",
|
||||||
|
}
|
||||||
|
for _, selector := range errorSelectors {
|
||||||
|
errorMsgs, err := p.Page.Elements(selector)
|
||||||
|
if err == nil {
|
||||||
|
for _, elem := range errorMsgs {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if text != "" && (strings.Contains(text, "失败") || strings.Contains(strings.ToLower(text), "error")) {
|
||||||
|
p.LogError(fmt.Sprintf("发布失败: %s", text))
|
||||||
|
return false, fmt.Sprintf("发布失败: %s", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查成功提示
|
||||||
|
successSelectors := []string{
|
||||||
|
"[class*='success']",
|
||||||
|
".byte-message-success",
|
||||||
|
}
|
||||||
|
for _, selector := range successSelectors {
|
||||||
|
successMsgs, err := p.Page.Elements(selector)
|
||||||
|
if err == nil {
|
||||||
|
for _, elem := range successMsgs {
|
||||||
|
text, _ := elem.Text()
|
||||||
|
if text != "" && strings.Contains(text, "成功") {
|
||||||
|
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
|
||||||
|
return true, text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SleepMs(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.LogWarning("发布结果未知")
|
||||||
|
return false, "发布结果未知"
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitPage 初始化页面
|
||||||
|
func (p *ToutiaoPublisher) InitPage() error {
|
||||||
|
// 访问发布页面
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.Sleep(2)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
|
||||||
|
// 尝试加载cookies并检查登录状态
|
||||||
|
if err := p.LoadCookies(); err == nil {
|
||||||
|
p.RefreshPage()
|
||||||
|
p.Sleep(2)
|
||||||
|
if p.CheckLoginStatus() {
|
||||||
|
p.SaveCookies()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("需要登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishNote 发布文章
|
||||||
|
func (p *ToutiaoPublisher) PublishNote() (bool, string) {
|
||||||
|
p.StartNote()
|
||||||
|
|
||||||
|
// 初始化浏览器
|
||||||
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
return false, fmt.Sprintf("浏览器启动失败: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
// 执行发布流程
|
||||||
|
steps := []struct {
|
||||||
|
name string
|
||||||
|
fn func() error
|
||||||
|
}{
|
||||||
|
{"初始化页面", p.InitPage},
|
||||||
|
{"输入内容", p.inputContent},
|
||||||
|
//{"上传封面", p.uploadCover},
|
||||||
|
{"输入标题", p.inputTitle},
|
||||||
|
{"点击发布", p.clickPublish},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, step := range steps {
|
||||||
|
if err := step.fn(); err != nil {
|
||||||
|
// 失败时截图
|
||||||
|
screenshotFile := fmt.Sprintf("screenshot_%s_%d.png", step.name, time.Now().Unix())
|
||||||
|
p.Screenshot(screenshotFile)
|
||||||
|
p.LogStep(step.name, false, err.Error())
|
||||||
|
return false, fmt.Sprintf("%s失败: %v", step.name, err)
|
||||||
|
}
|
||||||
|
p.LogStep(step.name, true, "")
|
||||||
|
p.SleepMs(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待发布结果
|
||||||
|
return p.waitForPublishResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogWarning 记录警告日志
|
||||||
|
func (p *ToutiaoPublisher) LogWarning(message string) {
|
||||||
|
p.Logger.Printf("⚠️ %s", message)
|
||||||
|
}
|
||||||
|
|
@ -2,33 +2,22 @@ package publisher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"geo/pkg"
|
"geo/internal/config"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"geo/internal/config"
|
|
||||||
|
|
||||||
"github.com/go-rod/rod"
|
"github.com/go-rod/rod"
|
||||||
"github.com/go-rod/rod/lib/proto"
|
"github.com/go-rod/rod/lib/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type XiaohongshuPublisher struct {
|
type XiaohongshuPublisher struct {
|
||||||
*BasePublisher
|
*BasePublisher
|
||||||
maxRetries int
|
|
||||||
retryDelay int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewXiaohongshuPublisher 构造函数,增加 logger 参数
|
// NewXiaohongshuPublisher 构造函数,增加 logger 参数
|
||||||
func NewXiaohongshuPublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
func NewXiaohongshuPublisher(task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
|
||||||
base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg, logger)
|
return &XiaohongshuPublisher{NewBasePublisher(task, cfg, logger)}
|
||||||
if platInfo != nil {
|
|
||||||
base.LoginURL = pkg.GetString(platInfo, "login_url")
|
|
||||||
base.EditorURL = pkg.GetString(platInfo, "edit_url")
|
|
||||||
base.LoginedURL = pkg.GetString(platInfo, "logined_url")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &XiaohongshuPublisher{BasePublisher: base, maxRetries: 5, retryDelay: 200}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *XiaohongshuPublisher) CheckLogin() (bool, string) {
|
func (p *XiaohongshuPublisher) CheckLogin() (bool, string) {
|
||||||
|
|
@ -254,7 +243,7 @@ func (p *XiaohongshuPublisher) clickPublish() error {
|
||||||
p.SleepMs(1000)
|
p.SleepMs(1000)
|
||||||
|
|
||||||
// 查找并点击 next-btn(对应Python中的第一步)
|
// 查找并点击 next-btn(对应Python中的第一步)
|
||||||
for attempt := 0; attempt < p.maxRetries; attempt++ {
|
for attempt := 0; attempt < p.MaxRetries; attempt++ {
|
||||||
nextBtns, err := p.Page.Elements("button[class*='next-btn']")
|
nextBtns, err := p.Page.Elements("button[class*='next-btn']")
|
||||||
if err != nil || len(nextBtns) == 0 {
|
if err != nil || len(nextBtns) == 0 {
|
||||||
nextBtns, err = p.Page.Elements(".next-btn")
|
nextBtns, err = p.Page.Elements(".next-btn")
|
||||||
|
|
@ -269,12 +258,12 @@ func (p *XiaohongshuPublisher) clickPublish() error {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.SleepMs(p.retryDelay)
|
p.SleepMs(p.RetryDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进入发布设置页面,点击submit按钮
|
// 进入发布设置页面,点击submit按钮
|
||||||
p.LogInfo("进入发布设置页面...")
|
p.LogInfo("进入发布设置页面...")
|
||||||
for attempt := 0; attempt < p.maxRetries; attempt++ {
|
for attempt := 0; attempt < p.MaxRetries; attempt++ {
|
||||||
submitBtn, err := p.WaitForElement("button[class*='submit']", 3)
|
submitBtn, err := p.WaitForElement("button[class*='submit']", 3)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
submitBtn, err = p.WaitForElement("button.submit", 3)
|
submitBtn, err = p.WaitForElement("button.submit", 3)
|
||||||
|
|
@ -290,7 +279,7 @@ func (p *XiaohongshuPublisher) clickPublish() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.LogInfo(fmt.Sprintf("未找到submit按钮,第%d次重试...", attempt+1))
|
p.LogInfo(fmt.Sprintf("未找到submit按钮,第%d次重试...", attempt+1))
|
||||||
p.SleepMs(p.retryDelay)
|
p.SleepMs(p.RetryDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输入话题标签
|
// 输入话题标签
|
||||||
|
|
@ -318,7 +307,7 @@ func (p *XiaohongshuPublisher) clickPublish() error {
|
||||||
|
|
||||||
// 最终发布
|
// 最终发布
|
||||||
p.LogInfo("最终发布...")
|
p.LogInfo("最终发布...")
|
||||||
for attempt := 0; attempt < p.maxRetries; attempt++ {
|
for attempt := 0; attempt < p.MaxRetries; attempt++ {
|
||||||
if p.CheckElementExists(".publish-page-publish-btn", 2) {
|
if p.CheckElementExists(".publish-page-publish-btn", 2) {
|
||||||
publishDiv, err := p.Page.Element(".publish-page-publish-btn")
|
publishDiv, err := p.Page.Element(".publish-page-publish-btn")
|
||||||
if err == nil && publishDiv != nil {
|
if err == nil && publishDiv != nil {
|
||||||
|
|
@ -333,7 +322,7 @@ func (p *XiaohongshuPublisher) clickPublish() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.SleepMs(p.retryDelay)
|
p.SleepMs(p.RetryDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -392,13 +381,47 @@ func (p *XiaohongshuPublisher) CheckLoginStatus() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *XiaohongshuPublisher) ClickUploadBotton() error {
|
||||||
|
// 执行发布流程之前,先点击上传区域的第一个按钮
|
||||||
|
uploadDiv, err := p.WaitForElement(".upload-content", 10)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("未找到上传区域: %v", err)
|
||||||
|
} else {
|
||||||
|
buttons, err := uploadDiv.Elements("button")
|
||||||
|
if err != nil {
|
||||||
|
p.LogInfo(fmt.Sprintf("查找按钮失败: %v", err))
|
||||||
|
} else if len(buttons) > 0 {
|
||||||
|
if err := p.JSClick(buttons[0]); err != nil {
|
||||||
|
return fmt.Errorf("JS点击按钮失败: %v", err)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
p.LogInfo("已点击上传按钮")
|
||||||
|
p.Sleep(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *XiaohongshuPublisher) InitPage() error {
|
||||||
|
// 访问发布页面
|
||||||
|
p.Page.MustNavigate(p.EditorURL)
|
||||||
|
p.WaitForPageReady(5)
|
||||||
|
// 尝试加载cookies并检查登录状态
|
||||||
|
if err := p.LoadCookies(); err == nil {
|
||||||
|
p.RefreshPage()
|
||||||
|
p.Sleep(2)
|
||||||
|
}
|
||||||
|
// 统一检查登录状态
|
||||||
|
if !p.CheckLoginStatus() {
|
||||||
|
p.LogInfo("未登录或登录已过期,需要重新登录")
|
||||||
|
return fmt.Errorf("需要登录")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
|
func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
|
||||||
p.LogInfo(strings.Repeat("=", 50))
|
p.StartNote()
|
||||||
p.LogInfo("开始发布小红书笔记...")
|
|
||||||
p.LogInfo(fmt.Sprintf("标题: %s", p.Title))
|
|
||||||
p.LogInfo(fmt.Sprintf("内容长度: %d", len(p.Content)))
|
|
||||||
p.LogInfo(fmt.Sprintf("标签: %v", p.Tags))
|
|
||||||
p.LogInfo(strings.Repeat("=", 50))
|
|
||||||
|
|
||||||
// 初始化浏览器
|
// 初始化浏览器
|
||||||
if err := p.SetupDriver(); err != nil {
|
if err := p.SetupDriver(); err != nil {
|
||||||
|
|
@ -406,62 +429,17 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
|
||||||
}
|
}
|
||||||
defer p.Close()
|
defer p.Close()
|
||||||
|
|
||||||
// 访问发布页面
|
|
||||||
p.Page.MustNavigate(p.LoginedURL)
|
|
||||||
|
|
||||||
p.WaitForPageReady(5)
|
|
||||||
|
|
||||||
// 尝试加载cookies
|
|
||||||
if err := p.LoadCookies(); err == nil {
|
|
||||||
p.RefreshPage()
|
|
||||||
p.Sleep(2)
|
|
||||||
if p.CheckLoginStatus() {
|
|
||||||
p.LogInfo("使用cookies登录成功")
|
|
||||||
} else {
|
|
||||||
p.LogInfo("cookies已过期,需要重新登录")
|
|
||||||
return false, "需要登录"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查登录状态
|
|
||||||
if !p.CheckLoginStatus() {
|
|
||||||
return false, "需要登录"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存cookies
|
|
||||||
p.SaveCookies()
|
|
||||||
|
|
||||||
// 访问发布页面
|
|
||||||
p.Page.MustNavigate(p.EditorURL)
|
|
||||||
|
|
||||||
p.WaitForPageReady(5)
|
|
||||||
|
|
||||||
// 执行发布流程之前,先点击上传区域的第一个按钮
|
|
||||||
p.LogInfo("点击上传按钮...")
|
|
||||||
uploadDiv, err := p.WaitForElement(".upload-content", 10)
|
|
||||||
if err != nil {
|
|
||||||
p.LogInfo(fmt.Sprintf("未找到上传区域: %v", err))
|
|
||||||
} else {
|
|
||||||
buttons, err := uploadDiv.Elements("button")
|
|
||||||
if err != nil {
|
|
||||||
p.LogInfo(fmt.Sprintf("查找按钮失败: %v", err))
|
|
||||||
} else if len(buttons) > 0 {
|
|
||||||
if err := p.JSClick(buttons[0]); err != nil {
|
|
||||||
p.LogInfo(fmt.Sprintf("JS点击按钮失败: %v", err))
|
|
||||||
} else {
|
|
||||||
p.LogInfo("已点击上传按钮")
|
|
||||||
p.Sleep(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行发布流程
|
// 执行发布流程
|
||||||
steps := []struct {
|
steps := []struct {
|
||||||
name string
|
name string
|
||||||
fn func() error
|
fn func() error
|
||||||
}{
|
}{
|
||||||
|
{"初始化", p.InitPage},
|
||||||
|
{"保存cookie", p.SaveCookies},
|
||||||
|
{"点击上传按钮", p.ClickUploadBotton},
|
||||||
{"输入内容", p.inputContent},
|
{"输入内容", p.inputContent},
|
||||||
{"输入标题", p.inputTitle},
|
{"输入标题", p.inputTitle},
|
||||||
|
{"点击发布", p.clickPublish},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, step := range steps {
|
for _, step := range steps {
|
||||||
|
|
@ -472,12 +450,6 @@ func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
|
||||||
p.LogStep(step.name, true, "")
|
p.LogStep(step.name, true, "")
|
||||||
p.SleepMs(500)
|
p.SleepMs(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击发布
|
|
||||||
if err := p.clickPublish(); err != nil {
|
|
||||||
return false, err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待发布结果
|
// 等待发布结果
|
||||||
return p.waitForPublishResult()
|
return p.waitForPublishResult()
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ func (m *AppModule) Register(router fiber.Router) {
|
||||||
router.Post("/publish_on", vali(m.publishService.PublishOn, &entitys.PublishOnRequest{}))
|
router.Post("/publish_on", vali(m.publishService.PublishOn, &entitys.PublishOnRequest{}))
|
||||||
router.Post("/publish_off", vali(m.publishService.PublishOff, &entitys.PublishOffRequest{}))
|
router.Post("/publish_off", vali(m.publishService.PublishOff, &entitys.PublishOffRequest{}))
|
||||||
router.Post("/publish_status", vali(m.publishService.PublishStatus, &entitys.PublishStatusRequest{}))
|
router.Post("/publish_status", vali(m.publishService.PublishStatus, &entitys.PublishStatusRequest{}))
|
||||||
router.Post("/publish_execute_once", vali(m.publishService.PublishExecuteOnce, &entitys.PublishExecuteOnceRequest{}))
|
//router.Post("/publish_execute_once", vali(m.publishService.PublishExecuteOnce, &entitys.PublishExecuteOnceRequest{}))
|
||||||
router.Post("/publish_execute_retry", vali(m.publishService.PublishExecuteRetry, &entitys.PublishExecuteRetryRequest{}))
|
router.Post("/publish_execute_retry", vali(m.publishService.PublishExecuteRetry, &entitys.PublishExecuteRetryRequest{}))
|
||||||
router.Post("/get_publish_list", vali(m.publishService.GetPublishList, &entitys.GetPublishListRequest{}))
|
router.Post("/get_publish_list", vali(m.publishService.GetPublishList, &entitys.GetPublishListRequest{}))
|
||||||
router.Post("/login_platform", vali(m.loginService.LoginPlatform, &entitys.LoginPlatformRequest{}))
|
router.Post("/login_platform", vali(m.loginService.LoginPlatform, &entitys.LoginPlatformRequest{}))
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,10 @@ func (a *AppService) GetUserAndAutoStatus(c *fiber.Ctx, req *entitys.GetUserAndA
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pm := manager.GetPublishManager(a.cfg, a.tokenImpl.GetDb())
|
pm, err := manager.GetPublishManager(a.cfg, a.tokenImpl.GetDb())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return pkg.HandleResponse(c, fiber.Map{
|
return pkg.HandleResponse(c, fiber.Map{
|
||||||
"user": users,
|
"user": users,
|
||||||
"auto_status": pm.AutoStatus,
|
"auto_status": pm.AutoStatus,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"geo/internal/manager"
|
"geo/internal/manager"
|
||||||
|
"geo/internal/publisher"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
|
@ -43,14 +44,21 @@ func (s *LoginService) LoginPlatform(c *fiber.Ctx, req *entitys.LoginPlatformReq
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建发布器
|
// 创建发布器
|
||||||
platMap := map[string]interface{}{
|
|
||||||
"login_url": platInfo.LoginURL,
|
|
||||||
"edit_url": platInfo.EditURL,
|
|
||||||
"logined_url": platInfo.LoginedURL,
|
|
||||||
}
|
|
||||||
|
|
||||||
publisherClass := manager.GetPublisherClass(req.PlatIndex)
|
publisherClass := manager.GetPublisherClass(req.PlatIndex)
|
||||||
pub := publisherClass.InitMethod(false, "", "", nil, req.UserIndex, req.PlatIndex, "", "", "", platMap, s.cfg, nil)
|
if publisherClass == nil {
|
||||||
|
return errcode.NotFound("平台不存在")
|
||||||
|
}
|
||||||
|
task := &publisher.TaskParams{
|
||||||
|
PlatIndex: req.PlatIndex,
|
||||||
|
UserIndex: req.UserIndex,
|
||||||
|
PublishData: &entitys.PublishTaskDetail{
|
||||||
|
LoginedUrl: platInfo.LoginedURL,
|
||||||
|
EditUrl: platInfo.EditURL,
|
||||||
|
LoginUrl: platInfo.LoginURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pub := publisherClass.InitMethod(task, s.cfg, nil)
|
||||||
success, msg := pub.WaitLogin()
|
success, msg := pub.WaitLogin()
|
||||||
if !success {
|
if !success {
|
||||||
return errcode.SysErr(msg)
|
return errcode.SysErr(msg)
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,10 @@ func (s *PublishService) PublishOn(c *fiber.Ctx, req *entitys.PublishOnRequest)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pm := manager.GetPublishManager(s.cfg, s.db)
|
pm, err := manager.GetPublishManager(s.cfg, s.db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if pm.Start(int(tokenInfo.ID)) {
|
if pm.Start(int(tokenInfo.ID)) {
|
||||||
return pkg.HandleResponse(c, fiber.Map{
|
return pkg.HandleResponse(c, fiber.Map{
|
||||||
"auto_status": pm.AutoStatus,
|
"auto_status": pm.AutoStatus,
|
||||||
|
|
@ -99,7 +102,10 @@ func (s *PublishService) PublishOff(c *fiber.Ctx, req *entitys.PublishOffRequest
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pm := manager.GetPublishManager(s.cfg, s.db)
|
pm, err := manager.GetPublishManager(s.cfg, s.db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if pm.Stop() {
|
if pm.Stop() {
|
||||||
return pkg.HandleResponse(c, fiber.Map{})
|
return pkg.HandleResponse(c, fiber.Map{})
|
||||||
}
|
}
|
||||||
|
|
@ -112,8 +118,10 @@ func (s *PublishService) PublishStatus(c *fiber.Ctx, req *entitys.PublishStatusR
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pm := manager.GetPublishManager(s.cfg, s.db)
|
pm, err := manager.GetPublishManager(s.cfg, s.db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if req.RequestID != "" {
|
if req.RequestID != "" {
|
||||||
// 查询单个任务
|
// 查询单个任务
|
||||||
task, err := s.publishBiz.GetTaskByRequestID(c.UserContext(), req.RequestID)
|
task, err := s.publishBiz.GetTaskByRequestID(c.UserContext(), req.RequestID)
|
||||||
|
|
@ -127,16 +135,16 @@ func (s *PublishService) PublishStatus(c *fiber.Ctx, req *entitys.PublishStatusR
|
||||||
return pkg.HandleResponse(c, pm.GetStatus())
|
return pkg.HandleResponse(c, pm.GetStatus())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PublishService) PublishExecuteOnce(c *fiber.Ctx, req *entitys.PublishExecuteOnceRequest) error {
|
//func (s *PublishService) PublishExecuteOnce(c *fiber.Ctx, req *entitys.PublishExecuteOnceRequest) error {
|
||||||
tokenInfo, err := s.publishBiz.ValidateAccessToken(c.UserContext(), req.AccessToken)
|
// tokenInfo, err := s.publishBiz.ValidateAccessToken(c.UserContext(), req.AccessToken)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
pm := manager.GetPublishManager(s.cfg, s.db)
|
// pm := manager.GetPublishManager(s.cfg, s.db)
|
||||||
result := pm.ExecuteOnce(tokenInfo.ID)
|
// result := pm.ExecuteOnce(tokenInfo.ID)
|
||||||
return pkg.HandleResponse(c, result)
|
// return pkg.HandleResponse(c, result)
|
||||||
}
|
//}
|
||||||
|
|
||||||
func (s *PublishService) PublishExecuteRetry(c *fiber.Ctx, req *entitys.PublishExecuteRetryRequest) error {
|
func (s *PublishService) PublishExecuteRetry(c *fiber.Ctx, req *entitys.PublishExecuteRetryRequest) error {
|
||||||
_, err := s.publishBiz.ValidateAccessToken(c.UserContext(), req.AccessToken)
|
_, err := s.publishBiz.ValidateAccessToken(c.UserContext(), req.AccessToken)
|
||||||
|
|
@ -144,7 +152,10 @@ func (s *PublishService) PublishExecuteRetry(c *fiber.Ctx, req *entitys.PublishE
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pm := manager.GetPublishManager(s.cfg, s.db)
|
pm, err := manager.GetPublishManager(s.cfg, s.db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
result := pm.RetryTask(req.RequestID)
|
result := pm.RetryTask(req.RequestID)
|
||||||
return pkg.HandleResponse(c, result)
|
return pkg.HandleResponse(c, result)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
pkg/doc.go
47
pkg/doc.go
|
|
@ -57,3 +57,50 @@ func ExtractWordContent(filePath, format string) (string, error) {
|
||||||
|
|
||||||
return result.Content, nil
|
return result.Content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CopyImageToDoc 将图片插入到word头部
|
||||||
|
func CopyImageToDoc(docPath, imgPath string) error {
|
||||||
|
baseDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取工作目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exePath := filepath.Join(baseDir, "plugins", "insert_img_to_word.exe")
|
||||||
|
|
||||||
|
// 检查exe是否存在
|
||||||
|
if _, err = os.Stat(exePath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("exe不存在: %s", exePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查docx文件是否存在
|
||||||
|
if _, err = os.Stat(docPath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("文件不存在: %s", docPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查图片文件是否存在
|
||||||
|
if _, err = os.Stat(imgPath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("图片不存在: %s", imgPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(exePath, docPath, imgPath)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("执行失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(output, &result); err != nil {
|
||||||
|
return fmt.Errorf("解析结果失败: %v, 输出: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Success {
|
||||||
|
return fmt.Errorf("插入图片失败: %s", result.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -131,3 +131,8 @@ func (d *Db) ExecuteMany(sql string, argsList [][]interface{}) (int64, error) {
|
||||||
}
|
}
|
||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetOneToStruct
|
||||||
|
func (d *Db) GetOneToStruct(sql string, data interface{}, args ...interface{}) error {
|
||||||
|
return d.Client.Raw(sql, args...).Scan(data).Error
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue