Selaa lähdekoodia

Merge branch 'refs/heads/eta_2.0.8_ly_0804@guomengyuan'

# Conflicts:
#	go.sum
gmy 5 kuukautta sitten
vanhempi
commit
d9936280f3

+ 1 - 0
go.mod

@@ -11,6 +11,7 @@ require (
 	github.com/gonum/stat v0.0.0-20181125101827-41a0da705a5b
 	github.com/mozillazg/go-pinyin v0.20.0
 	github.com/patrickmn/go-cache v2.1.0+incompatible
+	github.com/rdlucklib/rdluck_tools v1.0.3
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/shakinm/xlsReader v0.9.12
 	github.com/shopspring/decimal v1.3.1

+ 143 - 0
go.sum

@@ -1,13 +1,29 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
 github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
+github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
 github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
 github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
+github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA=
 github.com/beego/bee/v2 v2.1.0 h1:4WngbAnkvVOyKy74WXcRH3clon76wkjhuzrV2mx2fQU=
 github.com/beego/bee/v2 v2.1.0/go.mod h1:wDhKy5TNxv46LHKsK2gyxo38ObCOm9PbCN89lWHK3EU=
 github.com/beego/beego/v2 v2.1.4 h1:99d8+sUmQyfaArQIjjzWGE+KYU9M1GkfWec74nXQxFY=
 github.com/beego/beego/v2 v2.1.4/go.mod h1:0J0RQVIpepnRUfu6ax+kLVVB1FcdYryHK9lpRl5wvbY=
+github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
+github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
+github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732 h1:XYUCaZrW8ckGWlCRJKCSoh/iFwlpX316a8yY9IFEzv8=
@@ -16,22 +32,53 @@ github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93
 github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
 github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
 github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
+github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
+github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
+github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
+github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
+github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
 github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/garyburd/redigo v1.6.3/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw=
+github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
 github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
 github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
 github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
 github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
 github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
 github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac h1:Q0Jsdxl5jbxouNs1TQYt0gxesYMU4VXRbsTlgDloZ50=
 github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc=
 github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 h1:EvokxLQsaaQjcWVWSV38221VAK7qc2zhaO17bKys/18=
@@ -46,12 +93,22 @@ github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9 h1:V2IgdyerlBa/MxaEFR
 github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw=
 github.com/gonum/stat v0.0.0-20181125101827-41a0da705a5b h1:fbskpz/cPqWH8VqkQ7LJghFkl2KPAiIFUHrTJ2O3RGk=
 github.com/gonum/stat v0.0.0-20181125101827-41a0da705a5b/go.mod h1:Z4GIJBJO3Wa4gD4vbwQxXXZ+WHmW6E9ixmNrwvs0iZs=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -59,35 +116,69 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
+github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
 github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/metakeule/fmtdate v1.1.2 h1:n9M7H9HfAqp+6OA98wXGMdcAr6omshSNVct65Bks1lQ=
 github.com/metakeule/fmtdate v1.1.2/go.mod h1:2JyMFlKxeoGy1qS6obQukT0AL0Y4iNANQL8scbSdT4E=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
 github.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+GnMFQ=
 github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
 github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
 github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
 github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
 github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
 github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
 github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
 github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
+github.com/rdlucklib/rdluck_tools v1.0.3 h1:iOtK2QPlPQ6CL6c1htCk5VnFCHzyG6DCfJtunrMswK0=
+github.com/rdlucklib/rdluck_tools v1.0.3/go.mod h1:9Onw9o4w19C8KE5lxb8GyxgRBbZweRVkQSc79v38EaA=
 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=
@@ -98,25 +189,44 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/shakinm/xlsReader v0.9.12 h1:F6GWYtCzfzQqdIuqZJ0MU3YJ7uwH1ofJtmTKyWmANQk=
 github.com/shakinm/xlsReader v0.9.12/go.mod h1:ME9pqIGf+547L4aE4YTZzwmhsij+5K9dR+k84OO6WSs=
+github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
 github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik=
 github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
 github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
 github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
+github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s=
+github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
+github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
 github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
 github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
+github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
+github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
 github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca h1:uvPMDVyP7PXMMioYdyPH+0O+Ta/UO1WFfNYMO3Wz0eg=
 github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
 github.com/xuri/excelize/v2 v2.8.0 h1:Vd4Qy809fupgp1v7X+nCS/MioeQmYVVzi495UCTqB7U=
 github.com/xuri/excelize/v2 v2.8.0/go.mod h1:6iA2edBTKxKbZAa7X5bDhcCg51xdOn1Ar5sfoXRGrQg=
 github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a h1:Mw2VNrNNNjDtw68VsEj2+st+oCSn4Uz7vZw6TbhcV1o=
 github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+github.com/ylywyn/jpush-api-go-client v0.0.0-20190906031852-8c4466c6e369/go.mod h1:Nv7wKD2/bCdKUFNKcJRa99a+1+aSLlCRJFriFYdjz/I=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
 golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
@@ -125,6 +235,10 @@ golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
 golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -134,11 +248,22 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
 golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -172,13 +297,31 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
+gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 8 - 1
models/base.go

@@ -26,8 +26,15 @@ type BaseResponseResult struct {
 	Data    string `description:"返回数据,json格式字符串"`
 }
 
+type RequestResponse[T any] struct {
+	Ret     int    `json:"Ret"`
+	Success bool   `json:"Success"`
+	Data    T      `json:"Data"` // 这里是你要获取的关键字段
+	Msg     string `json:"Msg"`
+}
+
 func (r *BaseResponse) Init() *BaseResponse {
-	return &BaseResponse{Ret: 403,IsSendEmail: true}
+	return &BaseResponse{Ret: 403, IsSendEmail: true}
 }
 
 type BaseRequest struct {

+ 12 - 0
models/base_from_ly_classify.go

@@ -0,0 +1,12 @@
+// @Author gmy 2024/8/7 9:26:00
+package models
+
+type BaseFromLyClassify struct {
+	BaseFromLyClassifyId int    `orm:"column(base_from_ly_classify_id);pk"` // 分类ID
+	CreateTime           string `orm:"column(create_time)"`                 // 创建时间
+	ModifyTime           string `orm:"column(modify_time)"`                 // 修改时间
+	ClassifyName         string `orm:"column(classify_name)"`               // 分类名称
+	ParentId             int    `orm:"column(parent_id)"`                   // 上级id
+	Sort                 int    `orm:"column(sort)"`                        // 排序字段,越小越靠前
+	ClassifyNameEn       string `orm:"column(classify_name_en)"`            // 英文分类名称
+}

+ 12 - 0
models/base_from_ly_data.go

@@ -0,0 +1,12 @@
+// @Author gmy 2024/8/7 9:50:00
+package models
+
+type BaseFromLyData struct {
+	BaseFromLyDataId  int     `orm:"column(base_from_ly_data_id);pk"` // 数据ID
+	CreateTime        string  `orm:"column(create_time)"`             // 创建时间
+	ModifyTime        string  `orm:"column(modify_time)"`             // 修改时间
+	BaseFromLyIndexId int     `orm:"column(base_from_ly_index_id)"`   // 指标id
+	IndexCode         string  `orm:"column(index_code)"`              // 指标编码
+	DataTime          string  `orm:"column(data_time)"`               // 数据日期
+	Value             float64 `orm:"column(value)"`                   // 数据值
+}

+ 15 - 0
models/base_from_ly_index.go

@@ -0,0 +1,15 @@
+// Package models
+// @Author gmy 2024/8/7 9:38:00
+package models
+
+type BaseFromLyIndex struct {
+	BaseFromLyIndexId    int    `orm:"column(base_from_ly_index_id);pk"` // 指标ID
+	CreateTime           string `orm:"column(create_time)"`              // 创建时间
+	ModifyTime           string `orm:"column(modify_time)"`              // 修改时间
+	BaseFromLyClassifyId int    `orm:"column(base_from_ly_classify_id)"` // 原始数据指标分类id
+	IndexCode            string `orm:"column(index_code)"`               // 指标编码
+	IndexName            string `orm:"column(index_name)"`               // 指标名称
+	Frequency            string `orm:"column(frequency)"`                // 频度
+	Unit                 string `orm:"column(unit)"`                     // 单位
+	EdbExist             int    `orm:"column(edb_exist)"`                // 指标库是否已添加:0-否;1-是
+}

+ 13 - 0
models/base_from_ly_index_record.go

@@ -0,0 +1,13 @@
+// Package models
+// @Author gmy 2024/8/7 9:38:00
+package models
+
+type BaseFromLyIndexRecord struct {
+	BaseFromLyIndexRecordId int    `orm:"column(base_from_ly_index_record_id);pk"` // 指标记录ID
+	CreateTime              string `orm:"column(create_time)"`                     // 创建时间
+	ModifyTime              string `orm:"column(modify_time)"`                     // 修改时间
+	Product                 string `orm:"column(product)"`                         // 产品
+	Category                string `orm:"column(category)"`                        // 分类
+	Url                     string `orm:"column(url)"`                             // 指标页面地址
+	DataTime                string `orm:"column(data_time)"`                       // 数据日期
+}

+ 13 - 0
models/edb_data_ly.go

@@ -0,0 +1,13 @@
+// @Author gmy 2024/9/14 16:13:00
+package models
+
+type EdbDataLy struct {
+	edbDataId     int     `orm:"column(edb_data_id);pk"` // 数据ID
+	CreateTime    string  `orm:"column(create_time)"`    // 创建时间
+	ModifyTime    string  `orm:"column(modify_time)"`    // 修改时间
+	EdbInfoId     int     `orm:"column(edb_info_id)"`    // 指标id
+	EdbCode       string  `orm:"column(edb_code)"`       // 指标编码
+	DataTime      string  `orm:"column(data_time)"`      // 数据日期
+	Value         float64 `orm:"column(value)"`          // 数据值
+	DataTimestamp uint64  `orm:"column(data_timestamp)"` // 数据日期时间戳
+}

+ 54 - 0
models/edb_info.go

@@ -0,0 +1,54 @@
+// @Author gmy 2024/9/18 17:29:00
+package models
+
+import "time"
+
+type EdbInfo struct {
+	EdbInfoId        int    `orm:"column(edb_info_id);pk"`
+	EdbInfoType      int    `description:"指标类型,0:普通指标,1:预测指标"`
+	SourceName       string `description:"来源名称"`
+	Source           int    `description:"来源id"`
+	EdbCode          string `description:"指标编码"`
+	EdbName          string `description:"指标名称"`
+	EdbNameEn        string `description:"英文指标名称"`
+	EdbNameSource    string `description:"指标名称来源"`
+	Frequency        string `description:"频率"`
+	Unit             string `description:"单位"`
+	UnitEn           string `description:"英文单位"`
+	StartDate        string `description:"起始日期"`
+	EndDate          string `description:"终止日期"`
+	ClassifyId       int    `description:"分类id"`
+	SysUserId        int
+	SysUserRealName  string
+	UniqueCode       string `description:"指标唯一编码"`
+	CreateTime       time.Time
+	ModifyTime       time.Time
+	MinValue         float64 `description:"指标最小值"`
+	MaxValue         float64 `description:"指标最大值"`
+	CalculateFormula string  `description:"计算公式"`
+	EdbType          int     `description:"指标类型:1:基础指标,2:计算指标"`
+	IsUpdate         int     `description:"当天是否已更新,1:未更新,2:已更新"`
+	Sort             int     `description:"排序字段"`
+	LatestDate       string  `description:"数据最新日期(实际日期)"`
+	LatestValue      float64 `description:"数据最新值(实际值)"`
+	EndValue         float64 `description:"数据的最新值(预测日期的最新值)"`
+	MoveType         int     `description:"移动方式:1:领先(默认),2:滞后"`
+	MoveFrequency    string  `description:"移动频度"`
+	NoUpdate         int8    `description:"是否停止更新,0:继续更新;1:停止更新"`
+	ServerUrl        string  `description:"服务器地址"`
+	ChartImage       string  `description:"图表图片"`
+	Calendar         string  `description:"公历/农历" orm:"default(公历);"`
+	EmptyType        int     `description:"空值处理类型(0查找前后35天,1不计算,2前值填充,3后值填充,4等于0)"`
+	MaxEmptyType     int     `description:"MAX、MIN公式空值处理类型(1、等于0;2、跳过空值)"`
+	DataDateType     string  `orm:"column(data_date_type);size(255);null;default(交易日)"`
+	ManualSave       int     `description:"是否有手动保存过上下限: 0-否; 1-是"`
+	TerminalCode     string  `description:"终端编码,用于配置在机器上"`
+	DataUpdateTime   string  `description:"最近一次数据发生变化的时间"`
+	ErDataUpdateDate string  `description:"本次更新,数据发生变化的最早日期"`
+	SourceIndexName  string  `description:"数据源中的指标名称"`
+	SubSource        int     `description:"子数据来源:0:经济数据库,1:日期序列"`
+	SubSourceName    string  `description:"子数据来源名称"`
+	IndicatorCode    string  `description:"指标代码"`
+	StockCode        string  `description:"证券代码"`
+	Extra            string  `description:"指标的额外配置"`
+}

+ 29 - 0
services/alarm_msg/alarm_msg.go

@@ -0,0 +1,29 @@
+package alarm_msg
+
+import (
+	"encoding/json"
+	"eta/eta_data_analysis/utils"
+	"github.com/rdlucklib/rdluck_tools/http"
+)
+
+var (
+	AlarmMsgUrl = "http://127.0.0.1:8606/api/alarm/send"
+)
+
+// projectName-项目名称
+// runMode-运行模式
+// msgBody-消息内容
+// level:消息基本,1:提示消息,2:警告消息,3:严重错误信息,默认为1 提示消息
+func SendAlarmMsg(msgBody string, level int) {
+	params := make(map[string]interface{})
+	params["ProjectName"] = utils.APPNAME
+	params["RunMode"] = utils.RunMode
+	params["MsgBody"] = msgBody
+	params["Level"] = level
+	param, err := json.Marshal(params)
+	if err != nil {
+		utils.FileLog.Info("SendAlarmMsg json.Marshal Err:" + err.Error())
+		return
+	}
+	http.Post(AlarmMsgUrl, string(param))
+}

+ 595 - 0
services/liangyou/commodity_liangyou.go

@@ -0,0 +1,595 @@
+package liangyou
+
+import (
+	"context"
+	"encoding/json"
+	"eta/eta_data_analysis/models"
+	"eta/eta_data_analysis/services/alarm_msg"
+	"eta/eta_data_analysis/utils"
+	"fmt"
+	"github.com/beego/beego/v2/core/logs"
+	"github.com/chromedp/cdproto/cdp"
+	"log"
+	"os"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/chromedp/chromedp"
+)
+
+var (
+	lyLoginPath = "https://www.fao.com.cn/"
+)
+
+func LyDataDeal(cont context.Context) (err error) {
+
+	// 读取 JSON 文件
+	configFile, err := os.ReadFile(utils.LY_JSON_PATH)
+	if err != nil {
+		fmt.Printf("读取配置文件错误: %v\n", err)
+		return
+	}
+
+	// 定义通用的 map 结构体来解析 JSON
+	var data map[string]map[string]map[string][]string
+
+	// 解析 JSON 文件内容
+	err = json.Unmarshal(configFile, &data)
+	if err != nil {
+		fmt.Printf("解析配置文件错误: %v\n", err)
+		return
+	}
+
+	// 打印解析后的数据以验证
+	fmt.Printf("%+v\n", data)
+
+	// 创建 chromedp 执行上下文
+	options := []chromedp.ExecAllocatorOption{
+		chromedp.Flag("headless", false),
+		chromedp.Flag("disable-blink-features", "AutomationControlled"),
+		chromedp.UserAgent(`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36`),
+	}
+
+	allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), options...)
+	defer cancel()
+	ctx, cancel := chromedp.NewContext(allocCtx)
+	defer cancel()
+
+	// 登录操作
+	err = login(ctx)
+	if err != nil {
+		fmt.Printf("登录错误: %v\n", err)
+		return
+	}
+
+	// 遍历配置并爬取数据
+	for product, productData := range data {
+		for category, categoryData := range productData {
+			for report, keywords := range categoryData {
+				fmt.Printf("正在获取数据: %s -> %s -> %s\n", product, category, report)
+				err = fetchReportData(ctx, product, category, report, keywords)
+				if err != nil {
+					fmt.Printf("获取数据错误: %s -> %s -> %s: %v\n", product, category, report, err)
+					// 您看文章的速度太快了,歇一会再看吧
+					if strings.Contains(err.Error(), "您看文章的速度太快了,歇一会再看吧") {
+						return
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func login(ctx context.Context) error {
+
+	return chromedp.Run(ctx,
+		chromedp.Navigate(lyLoginPath),
+		chromedp.Sleep(5*time.Second),
+		chromedp.Click(`a[id="btnLogin"]`, chromedp.ByQuery),
+		chromedp.Sleep(2*time.Second),
+		chromedp.SetValue(`input[id="userName"]`, utils.LY_USERNAME, chromedp.ByQuery),
+		chromedp.SetValue(`input[id="pwd"]`, utils.LY_PASSWORD, chromedp.ByQuery),
+		chromedp.Sleep(2*time.Second),
+		chromedp.Click(`input[id="btn_Login"]`, chromedp.ByQuery),
+		chromedp.Sleep(5*time.Second),
+	)
+}
+
+func fetchReportData(ctx context.Context, product, category, report string, keywords []string) error {
+	// Navigate to the main page
+	err := chromedp.Run(ctx,
+		chromedp.Navigate(lyLoginPath),
+		chromedp.Sleep(5*time.Second),
+	)
+	if err != nil {
+		return err
+	}
+
+	// Navigate to the product page
+	productPageURL, err := fillProductPageURL(ctx, product, category)
+	if err != nil {
+		return err
+	}
+
+	// Navigate to the category page
+	var categoryPageURL string
+	err = chromedp.Run(ctx,
+		chromedp.Navigate(productPageURL),
+		chromedp.Sleep(5*time.Second),
+		chromedp.Click(fmt.Sprintf(`//div[contains(@class, "newBox")]//a[contains(text(), '%s')]`, category), chromedp.BySearch),
+		chromedp.Sleep(5*time.Second),
+		chromedp.Location(&categoryPageURL),
+	)
+	if err != nil {
+		return err
+	}
+	logs.Info("categoryPageURL: %s: %s: %s", product, category, categoryPageURL)
+
+	//var allReportURLs []string
+	var allReportURLMap = make(map[string]string)
+	for {
+		var htmlContent string
+		err = chromedp.Run(ctx,
+			chromedp.Navigate(categoryPageURL),
+			chromedp.Sleep(5*time.Second),
+			chromedp.OuterHTML("html", &htmlContent),
+		)
+		if err != nil {
+			return err
+		}
+		fmt.Printf("页面内容: %s\n", htmlContent)
+
+		// Extract report URLs containing the partial keyword
+		reportURLMap := extractReportURLs(htmlContent, report)
+		//allReportURLs = append(allReportURLs, reportURLs...)
+		for key, value := range reportURLMap {
+			allReportURLMap[key] = value
+		}
+
+		//  测试环境跑部分数据,上线放开
+		//break
+		// Check if next page button is disabled
+		//  测试环境跑部分数据,上线放开
+		var nextPageDisabled bool
+		err = chromedp.Run(ctx,
+			chromedp.Evaluate(`document.querySelector('div.my-page-next').classList.contains('my-page-forbid')`, &nextPageDisabled),
+		)
+		if err != nil {
+			return err
+		}
+
+		if nextPageDisabled {
+			break
+		}
+
+		// Click the next page button
+		err = chromedp.Run(ctx,
+			chromedp.Click(`div.my-page-next`, chromedp.ByQuery),
+			chromedp.Sleep(5*time.Second),
+			chromedp.Location(&categoryPageURL),
+		)
+		if err != nil {
+			return err
+		}
+
+	}
+
+	logs.Info("所有报告 URLs: %s: %s: %v", product, category, allReportURLMap)
+
+	if len(allReportURLMap) == 0 {
+		return fmt.Errorf("未找到报告 URL")
+	}
+
+	// 处理报告数据
+	for key, value := range allReportURLMap {
+		// 查询报告是否已经处理  这里只对近7天的数据进行处理
+		paramsLib := make(map[string]interface{})
+		paramsLib["Url"] = key
+		postEdbLib, err := utils.PostEdbLibRequest(paramsLib, utils.GET_LY_INDEX_RECORD_BY_URL)
+		if err != nil {
+			// 有错误就不继续执行
+			log.Printf("postEdbLib err: %v", err)
+			continue
+		}
+		var requestResponse models.RequestResponse[models.BaseFromLyIndexRecord]
+		err = json.Unmarshal(postEdbLib, &requestResponse)
+		lyIndexRecord := requestResponse.Data
+		if lyIndexRecord.DataTime != "" {
+			toTime, err := utils.StringToTime(lyIndexRecord.DataTime + " 00:00:00")
+			if err != nil {
+				logs.Error("时间格式转换错误: %s: %s: %s: %s: %v", product, category, report, key, err)
+				continue
+			}
+
+			if time.Now().Sub(toTime) > 7*24*time.Hour {
+				logs.Info("报告已处理: %s: %s: %s: %s", product, category, report, key)
+				continue
+			}
+		}
+
+		// 随机睡眠
+		rand := utils.RangeRand(20, 100)
+		fmt.Println(report+";sleep:", strconv.Itoa(int(rand)))
+		time.Sleep(time.Duration(rand) * time.Second)
+
+		err = processReport(ctx, product, category, key, keywords)
+		if err != nil {
+			logs.Error("处理报告错误: %s: %s: %s: %s: %v", product, category, report, key, err)
+			if strings.Contains(err.Error(), "您看文章的速度太快了,歇一会再看吧") {
+				// 如果报告内容包含 “您看文章的速度太快了,歇一会再看吧” 则停止处理,发短信通知
+				// 发送短信通知
+				alarm_msg.SendAlarmMsg(fmt.Sprintf("粮油商务网-爬取指标数据被限制,请稍后重试, ErrMsg: %s", err.Error()), 1)
+				return nil
+			}
+			continue
+		}
+
+		format, err := utils.ConvertTimeFormat(value)
+		if err != nil {
+			logs.Error("时间格式转换错误: %s, %s, %v: %v", product, category, value, err)
+			continue
+		}
+
+		// 处理报告成功,将维护指标数据读取进度到数据库,避免后面重复读取
+		record := &models.BaseFromLyIndexRecord{
+			CreateTime: utils.GetCurrentTime(),
+			ModifyTime: utils.GetCurrentTime(),
+			Product:    product,
+			Category:   category,
+			Url:        key,
+			DataTime:   format,
+		}
+		// 转换成json
+		marshal, err := json.Marshal(record)
+		if err != nil {
+			logs.Error("维护指标数据读取进度错误: %s, %s, %v: %v", product, category, key, err)
+			continue
+		}
+		// json 转 interface
+		var result map[string]interface{}
+		err = json.Unmarshal(marshal, &result)
+
+		postEdbLib, err = utils.PostEdbLibRequest(result, utils.ADD_LY_INDEX_RECORD)
+		if err != nil {
+			// 有错误就不继续执行
+			logs.Error("维护指标数据读取进度错误: %s, %s, %v: %v", product, category, key, err)
+			continue
+		}
+		logs.Info("维护指标数据读取进度成功: %s, %s, %v", product, category, key)
+	}
+
+	return nil
+}
+
+func fillProductPageURL(ctx context.Context, product string, category string) (string, error) {
+	// 选择 dl 标签下所有 a 标签的 XPath
+	selector := `//dl[contains(@class, 'dl_hot')]//a`
+	logs.Info("选择器表达式: %s", selector)
+
+	var nodes []*cdp.Node
+	var productPageURL string
+
+	// 获取 dl 标签下的所有 a 标签节点
+	err := chromedp.Run(ctx,
+		chromedp.WaitReady(selector, chromedp.BySearch),
+		chromedp.Nodes(selector, &nodes, chromedp.BySearch),
+	)
+	if err != nil {
+		return "", err
+	}
+
+	// 提取并打印所有 a 标签的 OuterHTML
+	var targetURL string
+	for _, node := range nodes {
+		var outerHTML string
+
+		// 获取 a 标签的 OuterHTML
+		err = chromedp.Run(ctx,
+			chromedp.OuterHTML(node.FullXPath(), &outerHTML, chromedp.BySearch),
+		)
+		if err != nil {
+			return "", err
+		}
+
+		// 打印获取的 OuterHTML 内容
+		logs.Info("Link OuterHTML: %s", outerHTML)
+
+		// 从 OuterHTML 中提取 href 和文本内容
+		// 使用正则或字符串处理提取 href 和文本内容
+		href, linkText := extractHrefAndText(outerHTML)
+
+		// 打印提取的 href 和文本内容
+		logs.Info("Link Text: %s, Href: %s", linkText, href)
+
+		// 如果文本内容匹配目标产品
+		if linkText == product {
+			// 拼接完整的 URL
+			/*if !strings.HasPrefix(href, "http") {
+				href = lyLoginPath + href
+			}*/
+			targetURL = href
+			break
+		}
+	}
+
+	if targetURL == "" {
+		return "", fmt.Errorf("未找到匹配的产品链接")
+	}
+
+	// 显示更多内容
+	err = chromedp.Run(ctx,
+		chromedp.Evaluate(`document.getElementById("moreSpeList").style.display = "block";`, nil),
+	)
+	if err != nil {
+		return "", err
+	}
+
+	// 点击目标产品的链接
+	clickSelector := fmt.Sprintf(`//a[@href='%s']`, targetURL)
+	err = chromedp.Run(ctx,
+		chromedp.WaitReady(clickSelector, chromedp.BySearch),
+		chromedp.Click(clickSelector, chromedp.BySearch),
+		chromedp.Sleep(5*time.Second),
+		chromedp.Location(&productPageURL),
+	)
+	if err != nil {
+		return "", err
+	}
+
+	// 返回点击后的页面URL
+	logs.Info("productPageURL: %s", productPageURL)
+	return productPageURL, nil
+}
+
+// extractHrefAndText 从 OuterHTML 提取 href 和文本内容的辅助函数
+func extractHrefAndText(outerHTML string) (string, string) {
+	// 使用正则表达式或其他字符串处理方法提取 href 和文本内容
+	// 这里只是一个简单的例子,具体实现需要根据 HTML 结构来调整
+	hrefRegex := `href="([^"]+)"`
+	textRegex := `>([^<]+)<`
+
+	hrefMatches := regexp.MustCompile(hrefRegex).FindStringSubmatch(outerHTML)
+	textMatches := regexp.MustCompile(textRegex).FindStringSubmatch(outerHTML)
+
+	href := ""
+	linkText := ""
+	if len(hrefMatches) > 1 {
+		href = hrefMatches[1]
+	}
+	if len(textMatches) > 1 {
+		linkText = textMatches[1]
+	}
+
+	return href, linkText
+}
+
+// Extract report URLs from the HTML content
+func extractReportURLs(htmlContent, keyword string) map[string]string {
+	//var reportURLs []string
+	var reportURLMap = make(map[string]string)
+	var reportURL string
+
+	// Find all occurrences of the keyword and extract report URLs
+	content := htmlContent
+	for {
+		startIdx := strings.Index(content, keyword)
+		if startIdx == -1 {
+			break
+		}
+		startIdx += len(keyword)
+
+		// Extract the URL from the HTML content
+		urlStartIdx := strings.LastIndex(content[:startIdx], `href="`) + len(`href="`)
+		urlEndIdx := strings.Index(content[urlStartIdx:], `"`) + urlStartIdx
+		if urlStartIdx > 0 && urlEndIdx > urlStartIdx {
+			reportURL = content[urlStartIdx:urlEndIdx]
+			//reportURLs = append(reportURLs, content[urlStartIdx:urlEndIdx])
+		}
+
+		content = content[startIdx:]
+
+		// Now extract the content inside the first <div class="short_right">
+		divStartIdx := strings.Index(content, `<div class="short_right">`)
+		if divStartIdx != -1 {
+			divStartIdx += len(`<div class="short_right">`)
+			divEndIdx := strings.Index(content[divStartIdx:], `</div>`) + divStartIdx
+			if divEndIdx > divStartIdx {
+				shortRightContent := content[divStartIdx:divEndIdx]
+
+				// Extract the first <div> content inside <div class="short_right">
+				innerDivStartIdx := strings.Index(shortRightContent, `<div>`)
+				if innerDivStartIdx != -1 {
+					innerDivStartIdx += len(`<div>`)
+					//innerDivEndIdx := strings.Index(shortRightContent[innerDivStartIdx:], `</div>`) + innerDivStartIdx
+					innerDivContent := shortRightContent[innerDivStartIdx:]
+					fmt.Println("Inner Div Content:", innerDivContent)
+					reportURLMap[reportURL] = innerDivContent
+				}
+			}
+		}
+	}
+
+	return reportURLMap
+}
+
+// Process the report data
+func processReport(ctx context.Context, product string, category string, reportURL string, keywords []string) error {
+	// Navigate to the report page
+	var reportContent string
+	err := chromedp.Run(ctx,
+		chromedp.Navigate(lyLoginPath+reportURL),
+		chromedp.WaitVisible("body", chromedp.ByQuery), // 等待 body 元素可见,确保页面已加载
+		chromedp.Sleep(5*time.Second),                  // 等待额外时间,以确保动态内容加载
+		chromedp.OuterHTML("html", &reportContent),     // 获取页面 HTML 内容
+	)
+	if err != nil {
+		return err
+	}
+
+	// 如果文章内容包含 “您看文章的速度太快了,歇一会再看吧” 则返回指定错误
+	if strings.Contains(reportContent, "您看文章的速度太快了,歇一会再看吧") {
+		return fmt.Errorf("您看文章的速度太快了,歇一会再看吧")
+	}
+
+	var lyIndexDataList []models.BaseFromLyData
+	var edbDataLyList []models.EdbDataLy
+	// Process the data based on keywords
+	for _, keyword := range keywords {
+		partialKeyword := strings.Split(keyword, ":")
+		// Select appropriate processor based on product and category
+		processor, err := GetProcessor(product, category)
+		if err != nil {
+			return err
+		}
+
+		// 查询报告所属分类
+		paramsLib := make(map[string]interface{})
+		paramsLib["CategoryName"] = product
+		postEdbLib, err := utils.PostEdbLibRequest(paramsLib, utils.GET_LY_CLASSIFY_BY_NAME)
+		if err != nil {
+			// 有错误就不继续执行
+			log.Printf("postEdbLib err: %v", err)
+			continue
+		}
+		var requestResponse models.RequestResponse[models.BaseFromLyClassify]
+		err = json.Unmarshal(postEdbLib, &requestResponse)
+		if err != nil {
+			return err
+		}
+		classify := requestResponse.Data
+
+		// Process the report content using the selected processor
+		baseFromLyDataList, err := processor.Process(ctx, product, reportContent, partialKeyword, classify.BaseFromLyClassifyId)
+		if err != nil {
+			return err
+		}
+		if len(baseFromLyDataList) > 0 {
+			for _, baseFromLyData := range baseFromLyDataList {
+				if baseFromLyData.DataTime != "" && baseFromLyData.IndexCode != "" && baseFromLyData.IndexCode != "lysww" {
+					baseFromLyData.CreateTime = utils.GetCurrentTime()
+					baseFromLyData.ModifyTime = utils.GetCurrentTime()
+
+					// 补充 判断是否存在于指标库
+					paramsLib = make(map[string]interface{})
+					paramsLib["IndexCode"] = baseFromLyData.IndexCode
+					paramsLib["Source"] = utils.DATA_SOURCE_LY
+					postEdbLib, err := httpRequestFill(paramsLib, utils.GET_EDB_INFO_BY_INDEX_CODE)
+					if err != nil {
+						// 有错误就不继续执行
+						log.Printf("postEdbLib err: %v", err)
+						continue
+					}
+					var requestResponse models.RequestResponse[models.EdbInfo]
+					err = json.Unmarshal(postEdbLib, &requestResponse)
+					if err != nil {
+						log.Printf("postEdbLib err: %v", err)
+						continue
+					}
+
+					lyIndexDataList = append(lyIndexDataList, baseFromLyData)
+
+					edbIndexData := requestResponse.Data
+					if edbIndexData.EdbCode == "" {
+						// 不存在 不用维护指标库数据
+						continue
+					}
+
+					// 存在 则同步新增指标库中的指标数据
+					edbDataLy := models.EdbDataLy{
+						CreateTime:    utils.GetCurrentTime(),
+						ModifyTime:    utils.GetCurrentTime(),
+						EdbInfoId:     edbIndexData.EdbInfoId,
+						EdbCode:       edbIndexData.EdbCode,
+						DataTime:      baseFromLyData.DataTime,
+						Value:         baseFromLyData.Value,
+						DataTimestamp: uint64(time.Now().UnixNano() / int64(time.Millisecond)),
+					}
+					edbDataLyList = append(edbDataLyList, edbDataLy)
+				}
+			}
+		}
+	}
+	// 新增指标数据
+	if len(lyIndexDataList) > 0 {
+		// 转换成json
+		marshal, err := json.Marshal(lyIndexDataList)
+		if err != nil {
+			log.Printf("postEdbLib err: %v", err)
+			return err
+		}
+		_, err = utils.HttpPostRequest(utils.EDB_LIB_URL+utils.ADD_LY_DATA_LIST, string(marshal), "application/json")
+		if err != nil {
+			// 有错误就不继续执行
+			log.Printf("postEdbLib err: %v", err)
+			return err
+		}
+	}
+
+	// 新增指标库数据
+	if len(edbDataLyList) > 0 {
+		// 转换成json
+		marshal, err := json.Marshal(edbDataLyList)
+		if err != nil {
+			log.Printf("postEdbLib err: %v", err)
+			return err
+		}
+		_, err = utils.HttpPostRequest(utils.EDB_LIB_URL+utils.ADD_BATCH_LY_EDB_DATA, string(marshal), "application/json")
+		//_, err := httpRequestFill(edbDataLyList, utils.ADD_BATCH_LY_EDB_DATA)
+		if err != nil {
+			// 有错误就不继续执行
+			log.Printf("postEdbLib err: %v", err)
+			return err
+		}
+	}
+
+	return nil
+}
+
+func addLyIndex(classifyId int, indexCode string, indexName string, unit string, frequency string) (int, error) {
+	// 添加指标
+	index := &models.BaseFromLyIndex{
+		CreateTime:           utils.GetCurrentTime(),
+		ModifyTime:           utils.GetCurrentTime(),
+		BaseFromLyClassifyId: classifyId,
+		IndexCode:            indexCode,
+		IndexName:            indexName,
+		Frequency:            frequency,
+		Unit:                 unit,
+		EdbExist:             0,
+	}
+	postEdbLib, err := httpRequestFill(index, utils.ADD_LY_INDEX)
+	if err != nil {
+		// 有错误就不继续执行
+		log.Printf("postEdbLib err: %v", err)
+		return 0, err
+	}
+	var requestResponse models.RequestResponse[int64]
+	err = json.Unmarshal(postEdbLib, &requestResponse)
+	indexId := requestResponse.Data
+	return int(indexId), nil
+}
+
+func httpRequestFill(data interface{}, urlMethod string) (postEdbLib []byte, err error) {
+	// 转换成json
+	marshal, err := json.Marshal(data)
+	if err != nil {
+		return nil, err
+	}
+	// json 转 interface
+	var result map[string]interface{}
+	err = json.Unmarshal(marshal, &result)
+	if err != nil {
+		return nil, err
+	}
+
+	postEdbLib, err = utils.PostEdbLibRequest(result, urlMethod)
+	if err != nil {
+		// 有错误就不继续执行
+		log.Printf("postEdbLib err: %v", err)
+		return nil, err
+	}
+	return postEdbLib, nil
+}

+ 2456 - 0
services/liangyou/processor_business_logic.go

@@ -0,0 +1,2456 @@
+// @Author gmy 2024/8/6 10:50:00
+package liangyou
+
+import (
+	"context"
+	"encoding/json"
+	"eta/eta_data_analysis/models"
+	"eta/eta_data_analysis/utils"
+	"fmt"
+	"github.com/PuerkitoBio/goquery"
+	"github.com/beego/beego/v2/core/logs"
+	"github.com/chromedp/chromedp"
+	"log"
+	"regexp"
+	"strconv"
+	"strings"
+	"unicode"
+)
+
+var (
+	sourceName = "lysww" // 粮油商务网
+)
+
+// TableData 用于存储表格的数据
+type TableData struct {
+	Headers []string   `json:"headers"`
+	Rows    [][]string `json:"rows"`
+}
+
+// ImportCostProcessor
+// @Description: 进口成本处理器
+type ImportCostProcessor struct{}
+
+func (p *ImportCostProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing import cost...")
+
+	// 解析关键字
+	if len(keywords) < 5 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingImportCostProcessor Process() : keywords must contain at least 5 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[len(keywords)-4]
+	rowVariety := keywords[0]
+	rowPort := keywords[len(keywords)-3]
+	indexNamePrefix := keywords[:1]
+	indexNameSuffix := keywords[1:]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 解析日期并计算当前月份
+	var targetMonths []string
+	if product == "油菜籽" {
+		targetMonths, err = utils.ParseDateAndMonthColzaOil(format)
+	} else {
+		targetMonths, err = utils.ParseDateAndMonth(dateText)
+	}
+	if err != nil {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingImportCostProcessor Process() : Failed to parse date: %v", err)
+	}
+	fmt.Printf("Target Month: %s\n", targetMonths)
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnName) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("ProcessingImportCostProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 处理表格中的每一行
+		//var flag bool = true
+		var previousRowVariety string
+		var previousRowPort string
+		for rowIndex, row := range tableRows {
+			if len(row) == len(tableHeaders) {
+				previousRowVariety = row[0]
+				previousRowPort = row[1]
+			} else if len(row) == len(tableHeaders)-1 {
+				previousRowPort = row[0]
+				row = append([]string{previousRowVariety}, row...)
+				tableRows[rowIndex] = row
+			} else if len(row) == len(tableHeaders)-2 {
+				row = append([]string{previousRowVariety, previousRowPort}, row...)
+				tableRows[rowIndex] = row
+			}
+			for _, targetMonth := range targetMonths {
+				if len(row) >= len(tableHeaders) && strings.Contains(rowVariety, row[0]) && row[1] == targetMonth && row[len(row)-1] == rowPort {
+					if columnIdx < len(row) {
+						// 指标名称
+						indexNameList := append(indexNamePrefix, append([]string{targetMonth}, indexNameSuffix...)...)
+						indexName := strings.Join(indexNameList[:len(keywords)-2], ":")
+						// 指标编码
+						indexCode := utils.GenerateIndexCode(sourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+						if err != nil {
+							logs.Error("ProcessingImportCostProcessor Process() : Failed to get index id: %v", err)
+							continue
+						}
+						paramsLib := make(map[string]interface{})
+						paramsLib["IndexId"] = indexId
+						paramsLib["DataTime"] = format
+						postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+						if err != nil {
+							logs.Error("ProcessingImportCostProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+						err = json.Unmarshal(postEdbLib, &requestResponse)
+
+						indexData := requestResponse.Data
+						if len(indexData) > 0 {
+							logs.Info("ProcessingImportCostProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+							continue
+						}
+
+						valueStr := row[columnIdx]
+						value, err := strconv.ParseFloat(valueStr, 64)
+						if err != nil {
+							return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+						}
+						// 创建并添加到结果列表
+						baseFromLyData := models.BaseFromLyData{
+							DataTime:          format,
+							Value:             value,
+							BaseFromLyIndexId: indexId,
+							IndexCode:         indexCode,
+						}
+						result = append(result, baseFromLyData)
+					} else {
+						log.Printf("ProcessingImportCostProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, rowPort)
+					}
+					break
+				}
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// ProcessingProfitProcessor
+// @Description: 加工利润处理器
+type ProcessingProfitProcessor struct{}
+
+func (p *ProcessingProfitProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	fmt.Println("Processing processing profit...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingProfitProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[1]
+	rowVariety := keywords[0]
+	indexNamePrefix := keywords[:1]
+	indexNameSuffix := keywords[1:]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 解析日期并计算当前月份 和 后两月
+	yearMonths, err := utils.ConvertTimeFormatToYearMonth(format)
+	if err != nil {
+		return nil, err
+	}
+	fmt.Printf("Target yearMonth: %s\n", yearMonths)
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(columnName, header) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("ProcessingProfitProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 处理表格中的每一行
+		var previousRowVariety string
+		for rowIndex, row := range tableRows {
+			if len(row) == len(tableHeaders) {
+				previousRowVariety = row[0]
+			} else if len(row) == len(tableHeaders)-1 {
+				row = append([]string{previousRowVariety}, row...)
+				tableRows[rowIndex] = row
+			}
+
+			for _, targetMonth := range yearMonths {
+				if len(row) >= len(tableHeaders) && row[0] == rowVariety && row[1] == targetMonth {
+					if columnIdx < len(row) {
+						// 指标名称
+						indexNameList := append(indexNamePrefix, append([]string{targetMonth}, indexNameSuffix...)...)
+						indexName := strings.Join(indexNameList[:len(keywords)-1], ":")
+						// 指标编码
+						indexCode := utils.GenerateIndexCode(sourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+						if err != nil {
+							logs.Error("ProcessingProfitProcessor Process() : Failed to get index id: %v", err)
+							continue
+						}
+
+						paramsLib := make(map[string]interface{})
+						paramsLib["IndexId"] = indexId
+						paramsLib["DataTime"] = format
+						postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+						if err != nil {
+							logs.Error("ProcessingProfitProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+						err = json.Unmarshal(postEdbLib, &requestResponse)
+
+						indexData := requestResponse.Data
+						if len(indexData) > 0 {
+							logs.Info("ProcessingProfitProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+							continue
+						}
+
+						valueStr := row[columnIdx]
+						value, err := strconv.ParseFloat(valueStr, 64)
+						if err != nil {
+							return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+						}
+						// 创建并添加到结果列表
+						baseFromLyData := models.BaseFromLyData{
+							DataTime:          format,
+							Value:             value,
+							BaseFromLyIndexId: indexId,
+							IndexCode:         indexCode,
+						}
+						result = append(result, baseFromLyData)
+					} else {
+						log.Printf("ProcessingProfitProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnName)
+					}
+					break
+				}
+			}
+
+		}
+	}
+
+	return result, nil
+}
+
+// ShippingCostProcessor
+// @Description: 船运费用处理器
+type ShippingCostProcessor struct{}
+
+func (p *ShippingCostProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	fmt.Println("Processing processing profit...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ShippingCostProcessor Process() : keywords must contain at least 5 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[len(keywords)-3]
+	rowVariety := keywords[0]
+	rowDestination := keywords[1]
+	rowShipType := keywords[2]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnName) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("ShippingCostProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 处理表格中的每一行
+		for rowIndex, row := range tableRows {
+			if len(row) == len(tableHeaders)-1 {
+				row = append([]string{rowVariety}, row...)
+				tableRows[rowIndex] = row
+				rowShipType, err = extractValueInParentheses(rowVariety)
+				if err != nil {
+					logs.Error("ShippingCostProcessor Process() : Failed to extract value in parentheses: %v", err)
+					continue
+				}
+
+			}
+			if len(row) >= len(tableHeaders) && row[0] == rowVariety && (row[1] == rowDestination || strings.Contains(row[0], row[1])) && row[2] == rowShipType {
+				if columnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-3], `:`)
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("ShippingCostProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					paramsLib := make(map[string]interface{})
+					paramsLib["IndexId"] = indexId
+					paramsLib["DataTime"] = format
+					postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+					if err != nil {
+						logs.Error("ShippingCostProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+					err = json.Unmarshal(postEdbLib, &requestResponse)
+
+					indexData := requestResponse.Data
+					if len(indexData) > 0 {
+						logs.Info("ShippingCostProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						continue
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("ShippingCostProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnName)
+				}
+				break
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// SupplyDemandBalanceProcessor
+// @Description: 供需平衡处理器
+type SupplyDemandBalanceProcessor struct{}
+
+func (p *SupplyDemandBalanceProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	// https://www.fao.com.cn/art/gG7gKTCNDHLJNsq9QRYjoQ==.htm
+	logs.Info("Processing processing report...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	var columnName string
+	rowVariety := keywords[1]
+
+	// 提取所有表格数据
+	tableData := getTableData(reportContent, true)
+	logs.Info("SupplyDemandBalanceProcessor Process() : Table data: %v", tableData)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	currentYearAndNextYear, err := utils.GetCurrentYearAndNextYear(format)
+	if err != nil {
+		return nil, err
+	}
+
+	month, err := utils.GetCurrentMonth(format)
+	if err != nil {
+		return nil, err
+	}
+	monthSuffix := "预估"
+	logs.Info("SupplyDemandBalanceProcessor Process() : Target Year: %s:%s\n", currentYearAndNextYear, month+monthSuffix)
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	headers := tableData.Headers
+	rows := tableData.Rows
+
+	for _, year := range currentYearAndNextYear {
+		columnName = year + month + monthSuffix
+		isCurrentYear, err := utils.IsCurrentYear(year)
+		if err != nil {
+			logs.Error("SupplyDemandBalanceProcessor Process() : Failed to determine if year is current year: %v", err)
+			continue
+		}
+		if !isCurrentYear {
+			format, err = utils.GetNextYearLastDay(format)
+			if err != nil {
+				logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get next year last day: %v", err)
+				continue
+			}
+		}
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range headers {
+			if strings.Contains(columnName, header) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			logs.Error("SupplyDemandBalanceProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+		// 处理表格中的每一行
+		for _, row := range rows {
+			if len(row) >= len(headers) && row[0] == rowVariety {
+				if columnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-2], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+					}
+					yearMonth, err := utils.GetYearMonth(format)
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get year month: %v", err)
+						continue
+					}
+
+					paramsLib := make(map[string]interface{})
+					paramsLib["IndexId"] = indexId
+					paramsLib["YearMonth"] = format
+					postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME_YM)
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+					err = json.Unmarshal(postEdbLib, &requestResponse)
+
+					indexData := requestResponse.Data
+					if len(indexData) > 0 {
+						logs.Info("SupplyDemandBalanceProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 存在走更新逻辑 主要更新今年在去年的预估值
+						indexData := indexData[0]
+						if indexData.Value != value {
+							time, err := utils.StringToTime(indexData.ModifyTime)
+							if err != nil {
+								return nil, err
+							}
+
+							timeZero, err := utils.StringToTimeZero(format)
+							if err != nil {
+								return nil, err
+							}
+
+							if time.Before(timeZero) {
+								// 更新指标数据
+								paramsLib := make(map[string]interface{})
+								paramsLib["Id"] = indexData.BaseFromLyDataId
+								paramsLib["Value"] = value
+								_, err := httpRequestFill(paramsLib, utils.UPDATE_LY_DATA_BY_ID)
+								if err != nil {
+									return nil, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+								}
+								// 更新指标库数据
+								paramsLib = make(map[string]interface{})
+								paramsLib["IndexCode"] = indexData.IndexCode
+								paramsLib["DataTime"] = yearMonth
+								postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_EDB_DATA_BY_INDEX_CODE_AND_DATA_TIME)
+								if err != nil {
+									logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get data by index id and date: %v", err)
+									continue
+								}
+								var requestResponse models.RequestResponse[[]models.EdbDataLy]
+								err = json.Unmarshal(postEdbLib, &requestResponse)
+								if err != nil {
+									return nil, err
+								}
+
+								edbIndexData := requestResponse.Data
+								if len(edbIndexData) > 0 {
+									paramsLib := make(map[string]interface{})
+									paramsLib["Id"] = edbIndexData[0].EdbInfoId
+									paramsLib["Value"] = value
+									_, err := httpRequestFill(paramsLib, utils.UPDATE_LY_EDB_DATA_BY_ID)
+									if err != nil {
+										return nil, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+									}
+								}
+							}
+						}
+						continue
+					}
+
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("SupplyDemandBalanceProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnName)
+				}
+				break
+			}
+		}
+	}
+	return result, nil
+}
+
+// PurchaseShippingProcessor
+// @Description: 采购装船处理器
+type PurchaseShippingProcessor struct{}
+
+func (p *PurchaseShippingProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing purchase shipping...")
+	// 解析关键字
+	if len(keywords) < 3 {
+		return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : keywords must contain at least 3 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[len(keywords)-3]
+
+	// 提取所有表格数据
+	tableData := getPurchaseShippingTableData(reportContent)
+	logs.Info("SupplyDemandBalanceProcessor Process() : Table data: %v", tableData)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	headers := tableData.Headers
+	rows := tableData.Rows
+
+	// 查找目标列
+	columnIdx := -1
+	for i, header := range headers {
+		if strings.Contains(columnName, header) {
+			columnIdx = i
+			break
+		}
+	}
+	if columnIdx == -1 {
+		log.Printf("SupplyDemandBalanceProcessor Process() : Column '%s' not found in table", columnName)
+	} else {
+		// 处理表格中的每一行
+		for _, row := range rows {
+			if len(row) >= len(headers) {
+				if columnIdx < len(row) {
+					if !isNumber(row[columnIdx]) {
+						continue
+					}
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-3], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+					var yearMonth string
+					number, err := utils.ConvertMonthToNumber(row[1])
+					if err != nil {
+						return nil, err
+					}
+					yearMonth = row[0] + "-" + number
+					isSameMonth, err := utils.IsSameMonth(format, yearMonth)
+					if err != nil {
+						return nil, err
+					}
+					if isSameMonth {
+						yearMonth = format
+					} else {
+						lastDayOfMonth, err := utils.GetLastDayOfMonth(yearMonth)
+						if err != nil {
+							return nil, err
+						}
+						yearMonth = lastDayOfMonth
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+					}
+
+					month, err := utils.GetYearMonth(yearMonth)
+					if err != nil {
+						return nil, err
+					}
+
+					paramsLib := make(map[string]interface{})
+					paramsLib["IndexId"] = indexId
+					paramsLib["YearMonth"] = month
+					postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME_YM)
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+					err = json.Unmarshal(postEdbLib, &requestResponse)
+
+					indexData := requestResponse.Data
+					if len(indexData) > 0 {
+						if indexData[0].Value != value {
+							logs.Info("SupplyDemandBalanceProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+
+							lyData := indexData[0]
+							time, err := utils.StringToTime(lyData.ModifyTime)
+							if err != nil {
+								return nil, err
+							}
+
+							timeZero, err := utils.StringToTimeZero(format)
+							if err != nil {
+								return nil, err
+							}
+
+							if time.Before(timeZero) {
+								// 更新指标数据
+								paramsLib := make(map[string]interface{})
+								paramsLib["Id"] = lyData.BaseFromLyDataId
+								paramsLib["Value"] = value
+								_, err := httpRequestFill(paramsLib, utils.UPDATE_LY_DATA_BY_ID)
+								if err != nil {
+									return nil, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+								}
+
+								// 同步更新指标库数据 须根据指标编码和日期更新
+								paramsLib = make(map[string]interface{})
+								paramsLib["IndexCode"] = lyData.IndexCode
+								paramsLib["DataTime"] = yearMonth
+								postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_EDB_DATA_BY_INDEX_CODE_AND_DATA_TIME)
+								if err != nil {
+									logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get data by index id and date: %v", err)
+									continue
+								}
+								var requestResponse models.RequestResponse[[]models.EdbDataLy]
+								err = json.Unmarshal(postEdbLib, &requestResponse)
+
+								edbIndexData := requestResponse.Data
+								if err != nil {
+									return nil, err
+								}
+								if len(edbIndexData) > 0 {
+									paramsLib := make(map[string]interface{})
+									paramsLib["Id"] = edbIndexData[0].EdbInfoId
+									paramsLib["Value"] = value
+									_, err := httpRequestFill(paramsLib, utils.UPDATE_LY_EDB_DATA_BY_ID)
+									if err != nil {
+										return nil, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+									}
+								}
+							}
+						}
+						continue
+					}
+
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          yearMonth,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+					continue
+				} else {
+					log.Printf("SupplyDemandBalanceProcessor Process() : Column index out of range for row '%s'", columnName)
+				}
+				break
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// ProcessingReportProcessor
+// @Description: 加工报告处理器
+type ProcessingReportProcessor struct {
+}
+
+func (p *ProcessingReportProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing processing report...")
+	// 解析关键字
+	if len(keywords) < 3 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingReportProcessor Process() : keywords must contain at least 3 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[0]
+	rowName := keywords[1]
+
+	// 提取所有表格数据
+	tableData := getAllTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+	indexName := strings.Join(keywords[:len(keywords)-2], ":")
+	// 指标编码
+	indexCode := utils.GenerateIndexCode(sourceName, indexName)
+	// 指标id获取
+	indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+	if err != nil {
+		return nil, err
+	}
+
+	// 校验指标数据是否存在 根据指标id和日期 存在则跳过,不存在正常往下走
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	paramsLib := make(map[string]interface{})
+	paramsLib["IndexId"] = indexId
+	paramsLib["DataTime"] = format
+	postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+	if err != nil {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingReportProcessor Process() : Failed to get data by index id and date: %v", err)
+	}
+	var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+	err = json.Unmarshal(postEdbLib, &requestResponse)
+
+	indexData := requestResponse.Data
+	if len(indexData) > 0 {
+		logs.Info("ProcessingReportProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+		// 不必做更新处理,报告每周刷新,即使本周和上周数据一致,也需要每周记录
+		return []models.BaseFromLyData{}, nil
+	}
+
+	// 解析日期并计算当前周数
+	targetWeek, err := utils.ParseDateAndWeek(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingReportProcessor Process() : Failed to parse date: %v", err)
+	}
+
+	fmt.Printf("Target Week: %s\n", targetWeek)
+
+	var result []models.BaseFromLyData
+	// 处理提取的表格数据
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			headerString := extractChinese(header)
+			if strings.Contains(columnName, headerString) {
+				// 这个表格不是很好处理,这里写的有些僵硬,后续需要优化
+				if columnName == "国内大豆开机率" {
+					i = i + 2
+				}
+				columnIdx = i
+				break
+			}
+		}
+
+		if columnIdx == -1 {
+			logs.Error("ProcessingReportProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 查找本周的列位置
+		weekIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, targetWeek) && i > columnIdx {
+				weekIdx = i
+				break
+			}
+		}
+
+		if weekIdx == -1 {
+			fmt.Printf("Week column '%s' not found in table\n", targetWeek)
+			continue
+		}
+
+		// 查找目标行
+		for _, row := range tableRows {
+			if strings.Contains(row[0], rowName) {
+				if columnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-2], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("ProcessingReportProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					paramsLib := make(map[string]interface{})
+					paramsLib["IndexId"] = indexId
+					paramsLib["DataTime"] = format
+					postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+					if err != nil {
+						logs.Error("ProcessingReportProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+					err = json.Unmarshal(postEdbLib, &requestResponse)
+
+					indexData := requestResponse.Data
+					if len(indexData) > 0 {
+						logs.Info("ProcessingReportProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 无需走更新逻辑,报告每日更新,即使今天和昨天数据一致,也需要每天记录,如果到这里也只是说,今天这个报告被读取了两次
+						continue
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("ProcessingReportProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+					continue
+				} else {
+					log.Printf("ProcessingReportProcessor Process() : Column index out of range for row '%s', '%s'", rowName, columnName)
+				}
+				break
+			}
+
+			/*if len(row) > 0 && strings.Contains(row[0], rowName) {
+				if weekIdx < len(row) {
+					logs.Info("Value in column '%s' - '%s': %s", columnName, rowName, row[columnIdx])
+					numFlag := isNumeric(row[columnIdx])
+					if numFlag {
+						value, err := strconv.ParseFloat(row[columnIdx], 64)
+						if err != nil {
+							logs.Error("ProcessingReportProcessor Process() : Error converting value to float64: %v", err)
+							return []models.BaseFromLyData{}, err
+						}
+						// 返回BaseFromLyData对象的数据
+						baseFromLyData := models.BaseFromLyData{
+							DataTime: dateText,
+							Value:    value,
+						}
+						result = append(result, baseFromLyData)
+					}
+				} else {
+					logs.Error("ProcessingReportProcessor Process() : Column index out of range")
+				}
+			}*/
+		}
+	}
+
+	return result, nil
+}
+
+// InventoryAnalysisProcessor
+// @Description: 库存分析处理器
+type InventoryAnalysisProcessor struct{}
+
+func (p *InventoryAnalysisProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	// https://www.fao.com.cn/art/yg1IKj9FpPEIDv2LefnPhQ==.htm
+	logs.Info("Processing inventory analysis...")
+
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[0]
+	rowVariety := keywords[1]
+
+	columnSuffix := "本周"
+	columnName = columnName + columnSuffix
+
+	// 提取所有表格数据
+	tableData := getTableData(reportContent, true)
+	logs.Info("SupplyDemandBalanceProcessor Process() : Table data: %v", tableData)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	headers := tableData.Headers
+	rows := tableData.Rows
+
+	// 查找目标列
+	columnIdx := -1
+	for i, header := range headers {
+		header := removeParentheses(header)
+		if strings.Contains(columnName, header) {
+			columnIdx = i
+			break
+		}
+	}
+	if columnIdx == -1 {
+		logs.Error("SupplyDemandBalanceProcessor Process() : Column '%s' not found in table", columnName)
+	} else {
+		// 处理表格中的每一行
+		for _, row := range rows {
+			if len(row) >= len(headers) && strings.Contains(row[0], rowVariety) {
+				if columnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-2], ":")
+					indexName = removeParentheses(indexName)
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					paramsLib := make(map[string]interface{})
+					paramsLib["IndexId"] = indexId
+					paramsLib["DataTime"] = format
+					postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+					err = json.Unmarshal(postEdbLib, &requestResponse)
+
+					indexData := requestResponse.Data
+					if len(indexData) > 0 {
+						logs.Info("SupplyDemandBalanceProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 无需走更新逻辑,报告每周更新,即使本周和上周数据一致,也需要每周记录,如果到这里也只是说,今天这个报告被读取了两次
+						continue
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+					continue
+				} else {
+					log.Printf("SupplyDemandBalanceProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnName)
+				}
+				break
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// PriceSpreadArbitrageProcessor
+// @Description: 价差套利处理器
+type PriceSpreadArbitrageProcessor struct{}
+
+func (p *PriceSpreadArbitrageProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	fmt.Println("Processing processing profit...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingProfitProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	var columnDate string
+	rowVariety := keywords[0]
+	rowBourse := keywords[1]
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+	day, err := utils.ConvertTimeFormatToYearMonthDay(format)
+	if err != nil {
+		return nil, err
+	}
+	columnDate = day
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnDate) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("ProcessingProfitProcessor Process() : Column '%s' not found in table", columnDate)
+			continue
+		}
+
+		// 处理表格中的每一行
+		for _, row := range tableRows {
+			if len(row) >= len(tableHeaders) && row[0] == rowVariety && row[1] == rowBourse {
+				if columnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-2], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("ProcessingProfitProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					paramsLib := make(map[string]interface{})
+					paramsLib["IndexId"] = indexId
+					paramsLib["DataTime"] = format
+					postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+					if err != nil {
+						logs.Error("ProcessingProfitProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+					err = json.Unmarshal(postEdbLib, &requestResponse)
+
+					indexData := requestResponse.Data
+					if len(indexData) > 0 {
+						logs.Info("ProcessingProfitProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 无需走更新逻辑,报告每天更新,即使今天和每天数据一致,也需要每天记录,如果到这里也只是说,今天这个报告被读取了两次
+						continue
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("ProcessingProfitProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnDate)
+				}
+				break
+			}
+
+		}
+	}
+
+	return result, nil
+}
+
+// DailyTransactionProcessor
+// @Description: 每日成交处理器
+type DailyTransactionProcessor struct{}
+
+func (p *DailyTransactionProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	fmt.Println("Processing processing profit...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("DailyTransactionProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 获取第一个表格
+	areaTableDataList := getNoHeadTableData(reportContent)
+	if len(areaTableDataList) == 0 {
+		return []models.BaseFromLyData{}, fmt.Errorf("DailyTransactionProcessor Process() : No table data found")
+	}
+	areaTableData := areaTableDataList[0]
+	// 获取第二个表格
+	blocTableData := getTableData(reportContent, false)
+	if blocTableData.Headers == nil {
+		return []models.BaseFromLyData{}, fmt.Errorf("DailyTransactionProcessor Process() : No table data found")
+
+	}
+	logs.Info("SupplyDemandBalanceProcessor Process() : areaTableData data: %v, blocTableData data: %v", areaTableData, blocTableData)
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	areaHeaders := areaTableData.Headers
+	areaRows := areaTableData.Rows
+
+	// 第一个表格
+	// 拿到 行关键字和列关键字
+	columnArea := keywords[1]
+	var rowAreaMonthDays []string
+	rowWeek := "地区总计"
+
+	monthDay, err := utils.GetWeekdaysInSameWeek(format)
+	if err != nil {
+		return nil, err
+	}
+	rowAreaMonthDays = monthDay
+
+	// 查找目标列
+	areaColumnIdx := -1
+	for i, header := range areaHeaders {
+		if strings.Contains(header, columnArea) {
+			areaColumnIdx = i
+			break
+		}
+	}
+	if areaColumnIdx == -1 {
+		log.Printf("DailyTransactionProcessor Process() : One Column '%s' not found in table", columnArea)
+	} else if !strings.Contains(strings.Join(keywords[:len(keywords)-3], ":"), "主要集团") {
+		for _, row := range areaRows {
+			if len(row) >= len(areaHeaders) && row[0] == rowWeek {
+				if areaColumnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-3], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("DailyTransactionProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					paramsLib := make(map[string]interface{})
+					paramsLib["IndexId"] = indexId
+					paramsLib["DataTime"] = format
+					postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+					if err != nil {
+						logs.Error("DailyTransactionProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+					err = json.Unmarshal(postEdbLib, &requestResponse)
+
+					indexData := requestResponse.Data
+					if len(indexData) > 0 {
+						logs.Info("DailyTransactionProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 无需走更新逻辑,报告每周更新,一周出来一周中每天得数据,即使本周和上周数据一致,也需要每天记录,如果到这里也只是说,今天这个报告被读取了两次
+						continue
+					}
+
+					valueStr := row[areaColumnIdx]
+					isChinese := IsChinese(valueStr)
+					if isChinese {
+						continue
+					}
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					var dealDate string
+					if row[0] == rowWeek {
+						dealDate = format
+					} else {
+						date, err := utils.ConvertToDate(row[0])
+						if err != nil {
+							return nil, err
+						}
+						dealDate = date
+					}
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          dealDate,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("DailyTransactionProcessor Process() : Column index out of range for row '%s', '%s'", monthDay, columnArea)
+				}
+				break
+			} else {
+				for _, monthDay := range rowAreaMonthDays {
+					if len(row) >= len(areaHeaders) && (row[0] == monthDay && !strings.Contains(strings.Join(keywords[:len(keywords)-3], ":"), "周度")) {
+						if areaColumnIdx < len(row) {
+							// 指标名称
+							indexName := strings.Join(keywords[:len(keywords)-3], ":")
+							// 指标编码
+							indexCode := utils.GenerateIndexCode(sourceName, indexName)
+							// 指标id获取
+							indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+							if err != nil {
+								logs.Error("DailyTransactionProcessor Process() : Failed to get index id: %v", err)
+								continue
+							}
+
+							paramsLib := make(map[string]interface{})
+							paramsLib["IndexId"] = indexId
+							paramsLib["DataTime"] = format
+							postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+							if err != nil {
+								logs.Error("DailyTransactionProcessor Process() : Failed to get data by index id and date: %v", err)
+								continue
+							}
+							var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+							err = json.Unmarshal(postEdbLib, &requestResponse)
+
+							indexData := requestResponse.Data
+							if len(indexData) > 0 {
+								logs.Info("DailyTransactionProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+								// 无需走更新逻辑,报告每周更新,即使本周和上周数据一致,也需要每周记录,如果到这里也只是说,今天这个报告被读取了两次
+								continue
+							}
+
+							valueStr := row[areaColumnIdx]
+							isChinese := IsChinese(valueStr)
+							if isChinese {
+								continue
+							}
+							value, err := strconv.ParseFloat(valueStr, 64)
+							if err != nil {
+								return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+							}
+							// 创建并添加到结果列表
+							var dealDate string
+							if row[0] == rowWeek {
+								dealDate = format
+							} else {
+								date, err := utils.ConvertToDate(row[0])
+								if err != nil {
+									return nil, err
+								}
+								dealDate = date
+							}
+							baseFromLyData := models.BaseFromLyData{
+								DataTime:          dealDate,
+								Value:             value,
+								BaseFromLyIndexId: indexId,
+								IndexCode:         indexCode,
+							}
+
+							result = append(result, baseFromLyData)
+						} else {
+							log.Printf("DailyTransactionProcessor Process() : Column index out of range for row '%s', '%s'", monthDay, columnArea)
+						}
+						break
+					}
+				}
+			}
+		}
+	}
+
+	// 第二个表格
+	// 拿到 行关键字和列关键字
+	columnBloc := keywords[len(keywords)-3]
+	rowBloc := keywords[1]
+
+	blocHeaders := blocTableData.Headers
+	blocRows := blocTableData.Rows
+
+	// 查找目标列
+	blocColumnIdx := -1
+	for i, header := range blocHeaders {
+		if strings.Contains(header, columnBloc) {
+			blocColumnIdx = i
+			break
+		}
+	}
+
+	if blocColumnIdx == -1 {
+		log.Printf("DailyTransactionProcessor Process() : Two Column '%s' not found in table", columnBloc)
+	} else {
+		// 处理表格中的每一行
+		for _, row := range blocRows {
+			if len(row) >= len(blocHeaders) && strings.Contains(row[0], rowBloc) {
+				if blocColumnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-3], ":")
+					indexName = removeParentheses(indexName)
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					paramsLib := make(map[string]interface{})
+					paramsLib["IndexId"] = indexId
+					paramsLib["DataTime"] = format
+					postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+					err = json.Unmarshal(postEdbLib, &requestResponse)
+
+					indexData := requestResponse.Data
+					if len(indexData) > 0 {
+						logs.Info("SupplyDemandBalanceProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						continue
+					}
+
+					valueStr := row[blocColumnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("SupplyDemandBalanceProcessor Process() : Column index out of range for row '%s', '%s'", rowBloc, columnBloc)
+				}
+				break
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// PalmOilImportCostProcessor 棕榈油进口成本
+type PalmOilImportCostProcessor struct{}
+
+func (p *PalmOilImportCostProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing palm oil import cost...")
+	// 解析关键字
+	if len(keywords) < 5 {
+		return []models.BaseFromLyData{}, fmt.Errorf("PalmOilImportCostProcessor Process() : keywords must contain at least 5 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[len(keywords)-4]
+	rowVariety := keywords[0]
+	rowPort := keywords[len(keywords)-3]
+	indexNamePrefix := keywords[:1]
+	indexNameSuffix := keywords[1:]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 解析日期并计算当前月份
+	targetMonths, err := utils.GetYearMonthNoYear(format)
+	if err != nil {
+		return []models.BaseFromLyData{}, fmt.Errorf("PalmOilImportCostProcessor Process() : Failed to parse date: %v", err)
+	}
+	fmt.Printf("Target Month: %s\n", targetMonths)
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnName) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("PalmOilImportCostProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 处理表格中的每一行
+		//var flag bool = true
+		var previousRowVariety string
+		var previousRowPort string
+		var previousRowFob string
+		for rowIndex, row := range tableRows {
+			if len(row) == len(tableHeaders) {
+				previousRowVariety = row[0]
+				previousRowPort = row[1]
+				previousRowFob = row[2]
+			} else if len(row) == len(tableHeaders)-1 {
+				previousRowPort = row[0]
+				previousRowFob = row[1]
+				row = append([]string{previousRowVariety}, row...)
+				tableRows[rowIndex] = row
+			} else if len(row) == len(tableHeaders)-2 {
+				// 这段这里不需要。。。先保留吧
+				previousRowFob = row[0]
+				row = append([]string{previousRowVariety, previousRowPort}, row...)
+				tableRows[rowIndex] = row
+			} else if len(row) == len(tableHeaders)-3 {
+				row = append([]string{previousRowVariety, previousRowPort, previousRowFob}, row...)
+				tableRows[rowIndex] = row
+			}
+			for _, targetMonth := range targetMonths {
+				if len(row) >= len(tableHeaders) && strings.Contains(rowVariety, row[0]) && row[1] == targetMonth && row[len(row)-1] == rowPort {
+					if columnIdx < len(row) {
+						// 指标名称
+						indexNameList := append(indexNamePrefix, append([]string{targetMonth}, indexNameSuffix...)...)
+						indexName := strings.Join(indexNameList[:len(keywords)-2], ":")
+						// 指标编码
+						indexCode := utils.GenerateIndexCode(sourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+						if err != nil {
+							logs.Error("PalmOilImportCostProcessor Process() : Failed to get index id: %v", err)
+							continue
+						}
+
+						paramsLib := make(map[string]interface{})
+						paramsLib["IndexId"] = indexId
+						paramsLib["DataTime"] = format
+						postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+						if err != nil {
+							logs.Error("PalmOilImportCostProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+						err = json.Unmarshal(postEdbLib, &requestResponse)
+
+						indexData := requestResponse.Data
+						if len(indexData) > 0 {
+							logs.Info("PalmOilImportCostProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+							continue
+						}
+
+						valueStr := row[columnIdx]
+						value, err := strconv.ParseFloat(valueStr, 64)
+						if err != nil {
+							return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+						}
+						// 创建并添加到结果列表
+						baseFromLyData := models.BaseFromLyData{
+							DataTime:          format,
+							Value:             value,
+							BaseFromLyIndexId: indexId,
+							IndexCode:         indexCode,
+						}
+						result = append(result, baseFromLyData)
+					} else {
+						log.Printf("PalmOilImportCostProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, rowPort)
+					}
+					break
+				}
+			}
+
+		}
+
+	}
+
+	return result, nil
+}
+
+// ImportEstimateProcessor
+// @Description: 进口预估处理器
+type ImportEstimateProcessor struct{}
+
+func (p *ImportEstimateProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing import estimate...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ImportEstimateProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	var columnDates []string
+	rowVariety := keywords[1]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	columnDates, err = utils.GetNextThreeMonthsNoYear(format)
+	if err != nil {
+		return nil, err
+	}
+
+	monthsLastDay, err := utils.GetNextThreeMonthsLastDay(format)
+	if err != nil {
+		return nil, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		for _, columnDate := range columnDates {
+			columnIdx := -1
+			for i, tableHeader := range tableHeaders {
+				if strings.Contains(tableHeader, columnDate) {
+					columnIdx = i
+					break
+				}
+			}
+
+			if columnIdx == -1 {
+				log.Printf("ImportEstimateProcessor Process() : Column '%s' not found in table", columnDate)
+				continue
+			} else {
+				// 处理表格中的每一行
+				for _, row := range tableRows {
+					if len(row) >= len(tableHeaders) && strings.Contains(row[0], rowVariety) && isNumber(row[columnIdx]) {
+						if columnIdx < len(row) {
+							// 指标名称
+							indexName := strings.Join(keywords[:len(keywords)-2], `:`)
+							// 指标编码
+							indexCode := utils.GenerateIndexCode(sourceName, indexName)
+							// 指标id获取
+							indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+							if err != nil {
+								logs.Error("ImportEstimateProcessor Process() : Failed to get index id: %v", err)
+								continue
+							}
+							toNumber, err := utils.ConvertMonthToNumber(columnDate)
+							if err != nil {
+								logs.Error("ImportEstimateProcessor Process() : Failed to convert month to number: %v", err)
+								continue
+							}
+							slice, err := utils.GetElementInSlice(monthsLastDay, toNumber)
+							if err != nil {
+								logs.Error("ImportEstimateProcessor Process() : Failed to get element in slice: %v", err)
+								continue
+							}
+
+							paramsLib := make(map[string]interface{})
+							paramsLib["IndexId"] = indexId
+							paramsLib["DataTime"] = format
+							postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+							if err != nil {
+								logs.Error("ImportEstimateProcessor Process() : Failed to get data by index id and date: %v", err)
+								continue
+							}
+							var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+							err = json.Unmarshal(postEdbLib, &requestResponse)
+
+							indexData := requestResponse.Data
+
+							valueStr := row[columnIdx]
+							value, err := strconv.ParseFloat(valueStr, 64)
+							if err != nil {
+								return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+							}
+
+							if len(indexData) > 0 {
+								if indexData[0].Value != value {
+									logs.Info("ImportEstimateProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+
+									lyData := indexData[0]
+									time, err := utils.StringToTime(lyData.ModifyTime)
+									if err != nil {
+										return nil, err
+									}
+
+									timeZero, err := utils.StringToTimeZero(format)
+									if err != nil {
+										return nil, err
+									}
+
+									if lyData.Value != value && time.Before(timeZero) {
+										// 更新指标数据
+										paramsLib := make(map[string]interface{})
+										paramsLib["Id"] = lyData.BaseFromLyDataId
+										paramsLib["Value"] = value
+										_, err := httpRequestFill(paramsLib, utils.UPDATE_LY_DATA_BY_ID)
+										if err != nil {
+											return nil, fmt.Errorf("ImportEstimateProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+										}
+
+										// 同步更新指标库数据
+										paramsLib = make(map[string]interface{})
+										paramsLib["IndexCode"] = lyData.IndexCode
+										paramsLib["DataTime"] = lyData.DataTime
+										postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_EDB_DATA_BY_INDEX_CODE_AND_EXACT_DATA_TIME)
+										if err != nil {
+											logs.Error("ImportEstimateProcessor Process() : Failed to get ly edb data by index code and exact data time: %v", err)
+											continue
+										}
+										var requestResponse models.RequestResponse[[]models.EdbDataLy]
+										err = json.Unmarshal(postEdbLib, &requestResponse)
+										if err != nil {
+											return nil, err
+										}
+
+										lyEdbIndexData := requestResponse.Data
+										if len(lyEdbIndexData) > 0 {
+											paramsLib := make(map[string]interface{})
+											paramsLib["Id"] = lyEdbIndexData[0].EdbInfoId
+											paramsLib["Value"] = value
+											_, err := httpRequestFill(paramsLib, utils.UPDATE_LY_EDB_DATA_BY_ID)
+											if err != nil {
+												return nil, fmt.Errorf("ImportEstimateProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+											}
+										}
+									}
+								}
+
+								continue
+							}
+
+							// 创建并添加到结果列表
+							baseFromLyData := models.BaseFromLyData{
+								DataTime:          slice,
+								Value:             value,
+								BaseFromLyIndexId: indexId,
+								IndexCode:         indexCode,
+							}
+							result = append(result, baseFromLyData)
+						} else {
+							log.Printf("ImportEstimateProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnDate)
+						}
+						break
+					}
+				}
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// InternationalPriceProcessor
+// @Description: 国际价格处理器
+type InternationalPriceProcessor struct{}
+
+func (p *InternationalPriceProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing international price...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("InternationalPriceProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[1]
+	rowVariety := keywords[0]
+	indexNamePrefix := keywords[:1]
+	indexNameSuffix := keywords[1:]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 解析日期并计算当前月份 和 后两月
+	yearMonths, err := utils.ConvertTimeFormatToYearMonth(format)
+	if err != nil {
+		return nil, err
+	}
+	fmt.Printf("Target yearMonth: %s\n", yearMonths)
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnName) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("InternationalPriceProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 处理表格中的每一行
+		var previousRowVariety string
+		for rowIndex, row := range tableRows {
+			if len(row) == len(tableHeaders) {
+				previousRowVariety = row[0]
+			} else if len(row) == len(tableHeaders)-1 {
+				row = append([]string{previousRowVariety}, row...)
+				tableRows[rowIndex] = row
+			}
+
+			for _, targetMonth := range yearMonths {
+				if len(row) >= len(tableHeaders) && row[0] == rowVariety && row[1] == targetMonth {
+					if columnIdx < len(row) {
+						// 指标名称
+						indexNameList := append(indexNamePrefix, append([]string{targetMonth}, indexNameSuffix...)...)
+						indexName := strings.Join(indexNameList[:len(keywords)-2], ":")
+						// 指标编码
+						indexCode := utils.GenerateIndexCode(sourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+						if err != nil {
+							logs.Error("InternationalPriceProcessor Process() : Failed to get index id: %v", err)
+							continue
+						}
+
+						paramsLib := make(map[string]interface{})
+						paramsLib["IndexId"] = indexId
+						paramsLib["DataTime"] = format
+						postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+						if err != nil {
+							logs.Error("InternationalPriceProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+						err = json.Unmarshal(postEdbLib, &requestResponse)
+
+						indexData := requestResponse.Data
+						if len(indexData) > 0 {
+							logs.Info("InternationalPriceProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+							// 无需更新 指标展示本月和后两月的数据,报告每天更新,每天的值可能会改变,即使今天和每天数据一致,也需要每天记录,如果到这里也只是说,今天这个报告被读取了两次
+							continue
+						}
+
+						valueStr := row[columnIdx]
+						value, err := strconv.ParseFloat(valueStr, 64)
+						if err != nil {
+							return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+						}
+						// 创建并添加到结果列表
+						baseFromLyData := models.BaseFromLyData{
+							DataTime:          format,
+							Value:             value,
+							BaseFromLyIndexId: indexId,
+							IndexCode:         indexCode,
+						}
+						result = append(result, baseFromLyData)
+					} else {
+						log.Printf("InternationalPriceProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnName)
+					}
+					break
+				}
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// CanadaStatisticsBureauProcessor
+// @Description: 加拿大统计局处理器
+type CanadaStatisticsBureauProcessor struct{}
+
+func (p *CanadaStatisticsBureauProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing Canada statistics bureau...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("CanadaStatisticsBureauProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnDate := "本周"
+	rowVariety := keywords[1]
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnDate) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("CanadaStatisticsBureauProcessor Process() : Column '%s' not found in table", columnDate)
+			continue
+		}
+
+		// 处理表格中的每一行
+		for _, row := range tableRows {
+			if len(row) >= len(tableHeaders) {
+				if columnIdx < len(row) {
+					if row[0] != rowVariety {
+						continue
+					}
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-2], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("CanadaStatisticsBureauProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					paramsLib := make(map[string]interface{})
+					paramsLib["IndexId"] = indexId
+					paramsLib["DataTime"] = format
+					postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+					if err != nil {
+						logs.Error("CanadaStatisticsBureauProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+					err = json.Unmarshal(postEdbLib, &requestResponse)
+
+					indexData := requestResponse.Data
+					if len(indexData) > 0 {
+						logs.Info("CanadaStatisticsBureauProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 无需更新 指标展示本周的数据,报告每周更新,即使本周和上周数据一致,也需要每周记录,如果到这里也只是说,今天这个报告被读取了两次
+						continue
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("CanadaStatisticsBureauProcessor Process() : Column index out of range for row '%s'", columnDate)
+				}
+				break
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// ImportExportAnalysisProcessor
+// @Description: 进出口分析处理器
+type ImportExportAnalysisProcessor struct{}
+
+func (p *ImportExportAnalysisProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	fmt.Println("Processing processing profit...")
+	// 解析关键字
+	if len(keywords) < 3 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingProfitProcessor Process() : keywords must contain at least 3 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	var columnDates []string
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 2025年1月可能才出2024年12月的数据,所以往前取一年
+	columnDates, err = utils.GetCurrentYearAndLastYear(format)
+	if err != nil {
+		return nil, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		for _, columnDate := range columnDates {
+			// 查找目标列
+			columnIdx := -1
+			for i, header := range tableHeaders {
+				if strings.Contains(header, columnDate) {
+					columnIdx = i
+					break
+				}
+			}
+
+			if columnIdx == -1 {
+				log.Printf("ProcessingProfitProcessor Process() : Column '%s' not found in table", columnDate)
+				continue
+			}
+
+			// 处理表格中的每一行
+			for _, row := range tableRows {
+				if len(row) >= len(tableHeaders) {
+					if columnIdx < len(row) && isNumber(row[columnIdx]) && isNumber(row[0]) {
+						// 指标名称
+						indexName := strings.Join(keywords[:len(keywords)-2], ":")
+						// 指标编码
+						indexCode := utils.GenerateIndexCode(sourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+						if err != nil {
+							logs.Error("ProcessingProfitProcessor Process() : Failed to get index id: %v", err)
+							continue
+						}
+						atoi, err := strconv.Atoi(row[0])
+						if err != nil {
+							return nil, err
+						}
+						date := columnDate[:4] + "-" + fmt.Sprintf("%02d", atoi)
+						lastDayOfMonth, err := utils.GetLastDayOfMonth(date)
+						if err != nil {
+							return nil, err
+						}
+
+						paramsLib := make(map[string]interface{})
+						paramsLib["IndexId"] = indexId
+						paramsLib["DataTime"] = format
+						postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME)
+						if err != nil {
+							logs.Error("ProcessingProfitProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						var requestResponse models.RequestResponse[[]models.BaseFromLyData]
+						err = json.Unmarshal(postEdbLib, &requestResponse)
+
+						indexData := requestResponse.Data
+						if len(indexData) > 0 {
+							logs.Info("ProcessingProfitProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+							continue
+						}
+
+						valueStr := row[columnIdx]
+						value, err := strconv.ParseFloat(valueStr, 64)
+						if err != nil {
+							return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+						}
+						// 创建并添加到结果列表
+						baseFromLyData := models.BaseFromLyData{
+							DataTime:          lastDayOfMonth,
+							Value:             value,
+							BaseFromLyIndexId: indexId,
+							IndexCode:         indexCode,
+						}
+						result = append(result, baseFromLyData)
+						continue
+					} else {
+						log.Printf("ProcessingProfitProcessor Process() : Column index out of range for row '%s'", columnDate)
+					}
+					break
+				}
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// ExtractValueInParentheses 从字符串中提取括号中的值
+func extractValueInParentheses(input string) (string, error) {
+	re := regexp.MustCompile(`(([^)]+))`)
+	matches := re.FindStringSubmatch(input)
+
+	if len(matches) > 1 {
+		return matches[1], nil
+	}
+
+	return "", fmt.Errorf("no value found in parentheses")
+}
+
+// 获取指标id,根据指标名称判断,没有插入指标生成返回
+func getIndexId(indexCode string, indexName string, classifyId int, sourceName string, frequency string, unit string) (int, error) {
+	if indexCode == "lysww" {
+		return 0, fmt.Errorf("indexCode is error")
+	}
+
+	// 判断指标是否存在
+	var indexId int
+	paramsLib := make(map[string]interface{})
+	paramsLib["IndexCode"] = indexCode
+	postEdbLib, err := httpRequestFill(paramsLib, utils.GET_LY_INDEX_BY_CODE)
+	if err != nil {
+		return 0, fmt.Errorf("getIndexId() : Failed to get ly index by code: %v", err)
+	}
+	var requestResponse models.RequestResponse[models.BaseFromLyIndex]
+	err = json.Unmarshal(postEdbLib, &requestResponse)
+	if err != nil {
+		return 0, fmt.Errorf("getIndexId() : Failed to unmarshal response: %v", err)
+	}
+	indexInfo := requestResponse.Data
+	if indexInfo.IndexName == "" {
+		// 新增指标
+		index, err := addLyIndex(classifyId, indexCode, indexName, frequency, unit)
+		if err != nil {
+			return 0, err
+		}
+		indexId = index
+	} else {
+		indexId = indexInfo.BaseFromLyIndexId
+	}
+	return indexId, nil
+}
+
+// 获取页面时间信息
+func getDateInfo(ctx context.Context) (string, error) {
+	var dateText string
+	err := chromedp.Run(ctx,
+		chromedp.Evaluate(`document.querySelector('div.a_date span').innerText`, &dateText),
+	)
+	if err != nil {
+		return "", fmt.Errorf("processing Process() : Failed to extract report date: %v", err)
+	}
+
+	logs.Info("Processing Process() : Report Extracted Date: %s", dateText)
+	return dateText, nil
+}
+
+// 获取所有表格数据 获取表格中有thead标签的数据
+func getAllTableData(reportContent string) []TableData {
+	var tableData []TableData
+
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// 选择 id 为 "a_content" 的 div
+	doc.Find("#a_content").Each(func(index int, item *goquery.Selection) {
+		item.Find("table").Each(func(index int, table *goquery.Selection) {
+			var headers []string
+			var rows [][]string
+
+			// 提取表头
+			table.Find("thead th").Each(func(index int, th *goquery.Selection) {
+				headers = append(headers, th.Text())
+			})
+
+			// 提取表格行数据
+			table.Find("tbody tr").Each(func(index int, row *goquery.Selection) {
+				var rowData []string
+				row.Find("td").Each(func(index int, td *goquery.Selection) {
+					rowData = append(rowData, td.Text())
+				})
+				rows = append(rows, rowData)
+			})
+
+			// 仅在表头存在时添加到结果中
+			if len(headers) > 0 {
+				tableData = append(tableData, TableData{
+					Headers: headers,
+					Rows:    rows,
+				})
+			}
+		})
+	})
+	return tableData
+}
+
+// 获取无头表格数据
+func getNoHeadTableData(reportContent string) []TableData {
+	var tableData []TableData
+
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Find the div with id "a_content"
+	doc.Find("#a_content").Each(func(index int, div *goquery.Selection) {
+		// Find all tables within the div
+		div.Find("table").Each(func(index int, table *goquery.Selection) {
+			var headers []string
+			var rows [][]string
+
+			// Extract table headers if any
+			table.Find("tr").Each(func(index int, tr *goquery.Selection) {
+				var rowData []string
+				tr.Find("td, th").Each(func(index int, cell *goquery.Selection) {
+					cellText := cell.Text()
+					rowData = append(rowData, cellText)
+				})
+
+				if index == 0 && len(rowData) > 0 {
+					// The first row is treated as the header row
+					headers = rowData
+				} else if len(rowData) > 0 {
+					// Add the row data to the rows slice
+					rows = append(rows, rowData)
+				}
+			})
+
+			// Only add table data if headers are present
+			if len(headers) > 0 {
+				tableData = append(tableData, TableData{
+					Headers: headers,
+					Rows:    rows,
+				})
+			}
+		})
+	})
+
+	return tableData
+}
+
+// 获取表格数据 获取id 为 a_content 的 div 中的第一个表格 左上角那个单元格会拼在第一个,会拼上列上的合并单元格
+func getTableData(reportContent string, isFirst bool) TableData {
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	tableData := &TableData{}
+
+	// 只提取 id 为 a_content 的 div 中的第一个表格
+	var firstTable *goquery.Selection
+	if isFirst {
+		firstTable = doc.Find("div#a_content table").First()
+	} else {
+		firstTable = doc.Find("div#a_content table").Last()
+	}
+
+	var combinedHeaders []string
+
+	// 提取表头
+	firstTable.Find("tr").Each(func(i int, row *goquery.Selection) {
+		if i == 0 {
+			// 第一行处理合并单元格,保存到 combinedHeaders
+			row.Find("td,th").Each(func(j int, cell *goquery.Selection) {
+				if j == 0 {
+					// 把左上角的“年度(10/9月)”放入 Headers 第一个元素
+					tableData.Headers = append(tableData.Headers, strings.TrimSpace(cell.Text()))
+				} else {
+					// 处理其他单元格
+					colspan, exists := cell.Attr("colspan")
+					if exists {
+						spanCount := 0
+						fmt.Sscanf(colspan, "%d", &spanCount)
+						for k := 0; k < spanCount; k++ {
+							combinedHeaders = append(combinedHeaders, strings.TrimSpace(cell.Text()))
+						}
+					} else {
+						combinedHeaders = append(combinedHeaders, strings.TrimSpace(cell.Text()))
+					}
+				}
+			})
+		} else if i == 1 {
+			// 第二行处理具体标题,组合后保存到 Headers
+			row.Find("td,th").Each(func(j int, cell *goquery.Selection) {
+				if j < len(combinedHeaders) {
+					fullHeader := combinedHeaders[j] + strings.TrimSpace(cell.Text())
+					tableData.Headers = append(tableData.Headers, fullHeader)
+				}
+			})
+		} else {
+			// 处理数据行
+			var rowData []string
+			row.Find("td").Each(func(j int, cell *goquery.Selection) {
+				rowData = append(rowData, strings.TrimSpace(cell.Text()))
+			})
+			if len(rowData) > 0 {
+				tableData.Rows = append(tableData.Rows, rowData)
+			}
+		}
+	})
+
+	return *tableData
+}
+
+// 获取采购装船表格数据
+func getPurchaseShippingTableData(reportContent string) TableData {
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	var tableData TableData
+
+	// 只提取 id 为 a_content 的 div 中的第一个表格
+	firstTable := doc.Find("div#a_content table").First()
+
+	// 提取表头
+	var headers []string
+	var subHeaders []string
+
+	firstTable.Find("thead tr").Each(func(i int, row *goquery.Selection) {
+		row.Find("th").Each(func(j int, cell *goquery.Selection) {
+			headerText := strings.TrimSpace(cell.Text())
+
+			if i == 0 {
+				// 处理第一行表头
+				colspan, exists := cell.Attr("colspan")
+				if exists {
+					spanCount := 0
+					fmt.Sscanf(colspan, "%d", &spanCount)
+					for k := 0; k < spanCount; k++ {
+						headers = append(headers, headerText)
+					}
+				} else {
+					headers = append(headers, headerText)
+				}
+			} else if i == 1 {
+				// 处理第二行表头
+				subHeaders = append(subHeaders, headerText)
+			}
+		})
+	})
+
+	// 合并第一行和第二行表头信息
+	if len(subHeaders) > 0 {
+		for i := 0; i < len(subHeaders); i++ {
+			// 从第四个单元格开始拼接
+			headers[3+i] = headers[3+i] + subHeaders[i]
+		}
+	}
+
+	tableData.Headers = headers
+
+	// 处理数据行
+	firstTable.Find("tbody tr").Each(func(i int, row *goquery.Selection) {
+		var rowData []string
+		row.Find("td").Each(func(j int, cell *goquery.Selection) {
+			rowData = append(rowData, strings.TrimSpace(cell.Text()))
+		})
+		if len(rowData) > 0 {
+			tableData.Rows = append(tableData.Rows, rowData)
+		}
+	})
+
+	return tableData
+}
+
+// 判断字符串是否是数字
+func isNumeric(value string) bool {
+	// 正则表达式匹配整数和浮点数
+	re := regexp.MustCompile(`^[+-]?(\d+(\.\d*)?|\.\d+)$`)
+	return re.MatchString(value)
+}
+
+// 只保留汉字
+func extractChinese(text string) string {
+	re := regexp.MustCompile(`[^\p{Han}]`) // 匹配非汉字字符
+	return re.ReplaceAllString(text, "")
+}
+
+// 去除括号中的内容 包含括号 ()
+func removeParentheses(text string) string {
+	re := regexp.MustCompile(`\([^)]*\)`)
+	return re.ReplaceAllString(text, "")
+}
+
+// IsChinese 判断传入的是否是汉字
+func IsChinese(str string) bool {
+	for _, r := range str {
+		if unicode.Is(unicode.Han, r) {
+			return true
+		}
+	}
+	return false
+}
+
+// 判断是否是数字
+func isNumber(str string) bool {
+	_, err := strconv.ParseFloat(str, 64)
+	return err == nil
+}

+ 110 - 0
services/liangyou/processor_factory.go

@@ -0,0 +1,110 @@
+// @Author gmy 2024/8/6 10:48:00
+package liangyou
+
+import (
+	"context"
+	"eta/eta_data_analysis/models"
+	"fmt"
+)
+
+type ReportProcessor interface {
+	Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error)
+}
+
+func GetProcessor(product string, category string) (ReportProcessor, error) {
+	if product == "大豆" {
+		switch category {
+		case "进口成本":
+			return &ImportCostProcessor{}, nil
+		case "加工利润":
+			return &ProcessingProfitProcessor{}, nil
+		case "船运费用":
+			return &ShippingCostProcessor{}, nil
+		case "供需平衡":
+			return &SupplyDemandBalanceProcessor{}, nil
+		case "采购装船":
+			return &PurchaseShippingProcessor{}, nil
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "加工报告":
+			return &ProcessingReportProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "豆粕" {
+		switch category {
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "大豆油" {
+		switch category {
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "价差套利":
+			return &PriceSpreadArbitrageProcessor{}, nil
+		case "每日成交":
+			return &DailyTransactionProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "棕榈油" {
+		switch category {
+		case "国际价格":
+			return &InternationalPriceProcessor{}, nil
+		case "进口成本":
+			return &PalmOilImportCostProcessor{}, nil
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "每日成交":
+			return &DailyTransactionProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "油菜籽" {
+		switch category {
+		case "进口成本":
+			return &ImportCostProcessor{}, nil
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "进口预估":
+			return &ImportEstimateProcessor{}, nil
+		case "加拿大统计局":
+			return &CanadaStatisticsBureauProcessor{}, nil
+		case "进出口分析":
+			return &ImportExportAnalysisProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "菜粕" {
+		switch category {
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "进出口分析":
+			return &ImportExportAnalysisProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "菜籽油" {
+		switch category {
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "进口预估":
+			return &ImportEstimateProcessor{}, nil
+		case "每日成交":
+			return &DailyTransactionProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "葵花粕" {
+		switch category {
+		case "进口预估":
+			return &ImportEstimateProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	}
+	// 可以添加更多的逻辑来处理其他产品和类别
+	return nil, fmt.Errorf("no processor found for product %s and category %s", product, category)
+}

+ 7 - 0
services/task.go

@@ -4,6 +4,7 @@ import (
 	ccfService "eta/eta_data_analysis/services/base_from_ccf"
 	oilchemService "eta/eta_data_analysis/services/base_from_oilchem"
 	"eta/eta_data_analysis/services/fenwei"
+	"eta/eta_data_analysis/services/liangyou"
 	"eta/eta_data_analysis/utils"
 	"fmt"
 	"github.com/beego/beego/v2/task"
@@ -56,6 +57,12 @@ func Task() {
 		task.AddTask("汾渭网络数据处理", fenWeiNetDataDeal)
 	}
 
+	if utils.LY_OPEN == "1" {
+		lyData := task.NewTask("refreshData", "0 0 12 * * *", liangyou.LyDataDeal) // 粮油商务网
+
+		task.AddTask("粮油商务网", lyData)
+	}
+
 	if utils.MtjhOpen == "1" {
 		c := cron.New(cron.WithSeconds())
 		//每2分钟检测一次指标文件是否更新

+ 497 - 0
static/liangyou.json

@@ -0,0 +1,497 @@
+{
+  "大豆": {
+    "进口成本": {
+      "国际大豆进口成本参考价": [
+        "美湾:国际大豆进口成本价:期货收盘:张家港:美分/蒲式耳:日度",
+        "美湾:国际大豆进口成本价:升贴水:张家港:美分/蒲式耳:日度",
+        "美湾:国际大豆进口成本价:FOB价:张家港:美元/吨:日度",
+        "美湾:国际大豆进口成本价:运费:张家港:美元/吨:日度",
+        "美湾:国际大豆进口成本价:CNF升贴水:张家港:美分/蒲式耳:日度",
+        "美湾:国际大豆进口成本价:CNF:张家港:美元/吨:日度",
+        "美湾:国际大豆进口成本价:进口成本:张家港:元/吨:日度",
+        "巴西:国际大豆进口成本价:期货收盘:张家港:美分/蒲式耳:日度",
+        "巴西:国际大豆进口成本价:升贴水:张家港:美分/蒲式耳:日度",
+        "巴西:国际大豆进口成本价:FOB价:张家港:美元/吨:日度",
+        "巴西:国际大豆进口成本价:运费:张家港:美元/吨:日度",
+        "巴西:国际大豆进口成本价:CNF升贴水:张家港:美分/蒲式耳:日度",
+        "巴西:国际大豆进口成本价:CNF:张家港:美元/吨:日度",
+        "巴西:国际大豆进口成本价:进口成本:张家港:元/吨:日度"
+      ]
+    },
+    "加工利润": {
+      "进口大豆盘面榨利及现货榨利表": [
+        "美湾:进口大豆盘面榨利:元/吨:日度",
+        "巴西:进口大豆盘面榨利:元/吨:日度",
+        "美湾:进口大豆现货榨利:元/吨:日度",
+        "巴西:进口大豆现货榨利:元/吨:日度"
+      ]
+    },
+    "船运费用": {
+      "国际谷物船运费报价及走势图": [
+        "巴西桑托斯:中国北方港口:超灵便型船:国际谷物船运费:当日价格:美元:日度",
+        "阿根廷:中国北方港口:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "美湾密西西比河:中国北方港口:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "美西塔科马:中国北方港口:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "美国北太平洋沿岸:中国:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "美国墨西哥湾:中国:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "巴西巴拉那瓜:中国:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "伊特科提亚拉港:中国:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "波罗的海巴拿马型指数(BPI):国际谷物船运费:当日价格:%:日度",
+        "波罗的海超灵便型指数(BSI):国际谷物船运费:当日价格:%:日度",
+        "波罗的海海岬型指数(BCI):国际谷物船运费:当日价格:%:日度",
+        "波罗的海干散货指数(BDI):国际谷物船运费:当日价格:%:日度",
+        "波罗的海灵便型指数(BHSI):国际谷物船运费:当日价格:%:日度"
+      ]
+    },
+    "供需平衡": {
+      "年度中国大豆市场供需报告": [
+        "中国大豆市场供需:期初库存:万吨:月度",
+        "中国大豆市场供需:种植面积:万吨:月度",
+        "中国大豆市场供需:国内产量:万吨:月度",
+        "中国大豆市场供需:进口量:万吨:月度",
+        "中国大豆市场供需:总供应量:万吨:月度",
+        "中国大豆市场供需:压榨用量:万吨:月度",
+        "中国大豆市场供需:其中:国产大豆:万吨:月度",
+        "中国大豆市场供需:进口大豆:万吨:月度",
+        "中国大豆市场供需:出口量:万吨:月度",
+        "中国大豆市场供需:食用量:万吨:月度",
+        "中国大豆市场供需:种用及其他:万吨:月度",
+        "中国大豆市场供需:总需求量:万吨:月度",
+        "中国大豆市场供需:期末库存:万吨:月度"
+      ]
+    },
+    "采购装船": {
+      "中国大豆采购进度周统计": [
+        "中国大豆计划采购量:万吨:周度",
+        "中国大豆已采购量:美国:万吨:周度",
+        "中国大豆已采购量:巴西:万吨:周度",
+        "中国大豆已采购量:阿根廷/乌拉圭:万吨:周度",
+        "中国大豆已采购量:小计:万吨:周度",
+        "中国大豆未采购量:万吨:周度",
+        "中国大豆采购进度:%:周度"
+      ]
+    },
+    "加工报告": {
+      "国内大豆周度加工量调查": [
+        "国内大豆加工量:河南省:万吨:周度",
+        "国内大豆加工量:湖北省:万吨:周度",
+        "国内大豆加工量:湖南省:万吨:周度",
+        "国内大豆加工量:黑龙江:万吨:周度",
+        "国内大豆加工量:吉林省:万吨:周度",
+        "国内大豆加工量:辽宁省:万吨:周度",
+        "国内大豆加工量:内蒙古:万吨:周度",
+        "国内大豆加工量:河北省:万吨:周度",
+        "国内大豆加工量:天津市:万吨:周度",
+        "国内大豆加工量:江西省:万吨:周度",
+        "国内大豆加工量:山东省:万吨:周度",
+        "国内大豆加工量:安徽省:万吨:周度",
+        "国内大豆加工量:江苏省:万吨:周度",
+        "国内大豆加工量:上海市:万吨:周度",
+        "国内大豆加工量:浙江省:万吨:周度",
+        "国内大豆加工量:福建省:万吨:周度",
+        "国内大豆加工量:广东省:万吨:周度",
+        "国内大豆加工量:广西省:万吨:周度",
+        "国内大豆加工量:海南省:万吨:周度",
+        "国内大豆加工量:陕西省:万吨:周度",
+        "国内大豆加工量:四川省:万吨:周度",
+        "国内大豆加工量:重庆市:万吨:周度",
+        "国内大豆加工量:云南省:万吨:周度",
+        "国内大豆加工量:合计:万吨:周度",
+        "国内大豆加工量:其中:国产:万吨:周度",
+        "国内大豆加工量:进口:万吨:周度",
+        "国内大豆开机率:河南省:%:周度",
+        "国内大豆开机率:湖北省:%:周度",
+        "国内大豆开机率:湖南省:%:周度",
+        "国内大豆开机率:黑龙江:%:周度",
+        "国内大豆开机率:吉林省:%:周度",
+        "国内大豆开机率:辽宁省:%:周度",
+        "国内大豆开机率:内蒙古:%:周度",
+        "国内大豆开机率:河北省:%:周度",
+        "国内大豆开机率:天津市:%:周度",
+        "国内大豆开机率:山西省:%:周度",
+        "国内大豆开机率:山东省:%:周度",
+        "国内大豆开机率:安徽省:%:周度",
+        "国内大豆开机率:江苏省:%:周度",
+        "国内大豆开机率:上海市:%:周度",
+        "国内大豆开机率:浙江省:%:周度",
+        "国内大豆开机率:福建省:%:周度",
+        "国内大豆开机率:广东省:%:周度",
+        "国内大豆开机率:广西省:%:周度",
+        "国内大豆开机率:海南省:%:周度",
+        "国内大豆开机率:陕西省:%:周度",
+        "国内大豆开机率:四川省:%:周度",
+        "国内大豆开机率:重庆市:%:周度",
+        "国内大豆开机率:云南省:%:周度",
+        "国内大豆开机率:合计:%:周度",
+        "国内大豆开机率:其中:国产:%:周度",
+        "国内大豆开机率:进口:%:周度"
+      ]
+    },
+    "库存分析": {
+      "全国油厂进口大豆库存量统计周报": [
+        "全国油厂进口大豆库存量(万吨):东北地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):华北地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):华东地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):华南地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):西南地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):其他地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):全国统计:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):其中:沿海库存:万吨:周度"
+      ]
+    }
+  },
+  "豆粕": {
+    "库存分析": {
+      "全国油厂豆粕库存与合同统计周报": [
+        "全国油厂豆粕库存量(万吨):东北地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):华北地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):华东地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):华中地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):华南地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):西南地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):西北地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):全国合计:万吨:周度",
+        "全国油厂豆粕库存量(万吨):其中:沿海库存:万吨:周度",
+        "全国油厂豆粕合同量(万吨):东北地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):华北地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):华东地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):华中地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):华南地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):西南地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):西北地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):全国合计:万吨:周度",
+        "全国油厂豆粕合同量(万吨):其中:沿海库存:万吨:周度"
+      ]
+    }
+  },
+  "大豆油": {
+    "库存分析": {
+      "全国油厂豆油库存与合同统计周报": [
+        "全国油厂豆油库存量(万吨):东北地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):华北地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):华东地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):华中地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):华南地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):西南地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):西北地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):全国合计:万吨:周度",
+        "全国油厂豆油库存量(万吨):其中:沿海库存:万吨:周度",
+        "全国油厂豆油合同量(万吨):东北地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):华北地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):华东地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):华中地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):华南地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):西南地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):西北地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):全国合计:万吨:周度",
+        "全国油厂豆油合同量(万吨):其中:沿海库存:万吨:周度"
+      ]
+    },
+    "价差套利": {
+      "豆棕油期现货价差统计": [
+        "棕榈油主力:大商所:期现货价差:元/吨:日度",
+        "豆油主力:大商所:期现货价差:元/吨:日度",
+        "24度棕榈油:进口液:期现货价差:元/吨:日度",
+        "一级豆油:进口压榨:期现货价差:元/吨:日度"
+      ]
+    },
+    "每日成交": {
+      "豆油成交量及价格统计": [
+        "豆油成交量:东北:吨:日度",
+        "豆油成交量:华北:吨:日度",
+        "豆油成交量:江苏:吨:日度",
+        "豆油成交量:浙江:吨:日度",
+        "豆油成交量:山东:吨:日度",
+        "豆油成交量:广东:吨:日度",
+        "豆油成交量:广西:吨:日度",
+        "豆油成交量:福建:吨:日度",
+        "豆油成交量:其他:吨:日度",
+        "豆油成交量:合计:吨:日度",
+        "豆油周度成交量:东北:吨:周度",
+        "豆油周度成交量:华北:吨:周度",
+        "豆油周度成交量:江苏:吨:周度",
+        "豆油周度成交量:浙江:吨:周度",
+        "豆油周度成交量:山东:吨:周度",
+        "豆油周度成交量:广东:吨:周度",
+        "豆油周度成交量:广西:吨:周度",
+        "豆油周度成交量:福建:吨:周度",
+        "豆油周度成交量:其他:吨:周度",
+        "豆油周度成交量:合计:吨:周度",
+        "主要集团:九三:豆油现货成交量:吨:周度",
+        "主要集团:中粮:豆油现货成交量:吨:周度",
+        "主要集团:中储粮:豆油现货成交量:吨:周度",
+        "豆油成交量:东北:1:吨:日度",
+        "豆油成交量:华北:1:吨:日度",
+        "豆油成交量:江苏:1:吨:日度",
+        "豆油成交量:浙江:1:吨:日度",
+        "豆油成交量:山东:1:吨:日度",
+        "豆油成交量:广东:1:吨:日度",
+        "豆油成交量:广西:1:吨:日度",
+        "豆油成交量:福建:1:吨:日度",
+        "豆油成交量:其他:1:吨:日度",
+        "豆油成交量:合计:1:吨:日度",
+        "豆油周度成交量:东北:1:吨:周度",
+        "豆油周度成交量:华北:1:吨:周度",
+        "豆油周度成交量:江苏:1:吨:周度",
+        "豆油周度成交量:浙江:1:吨:周度",
+        "豆油周度成交量:山东:1:吨:周度",
+        "豆油周度成交量:广东:1:吨:周度",
+        "豆油周度成交量:广西:1:吨:周度",
+        "豆油周度成交量:福建:1:吨:周度",
+        "豆油周度成交量:其他:1:吨:周度",
+        "豆油周度成交量:合计:1:吨:周度",
+        "主要集团:九三:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:中粮:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:中储粮:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:达孚:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:嘉吉:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:金光:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:邦基:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:益海:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:汇福:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:渤海:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:香驰:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:中海:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:其他:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:总计:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:九三:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:中粮:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:中储粮:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:达孚:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:嘉吉:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:金光:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:邦基:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:益海:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:汇福:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:渤海:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:香驰:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:中海:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:其他:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:总计:豆油基差成交量:本周(吨)基差成交:吨:周度"
+      ]
+    }
+  },
+  "棕榈油": {
+    "国际价格": {
+      "国际棕榈油FOB报价及走势": [
+        "马来棕榈液油:FOB价格:美元/吨:日度",
+        "马来棕榈油:FOB价格:美元/吨:日度",
+        "马来棕榈硬脂:FOB价格:美元/吨:日度",
+        "印尼毛棕油:FOB价格:美元/吨:日度"
+      ]
+    },
+    "进口成本": {
+      "马来西亚棕榈油进口成本参考价": [
+        "棕榈液油(24度):运费:张家港:美元/吨:日度",
+        "棕榈液油(24度):CNF:张家港:美元/吨:日度",
+        "棕榈液油(24度):完税价:张家港:美元/吨:日度",
+        "棕榈液油(24度):进口成本:张家港:元/吨:日度"
+      ]
+    },
+    "库存分析": {
+      "全国棕榈油库存与合同统计周报": [
+        "棕榈油24度及以下库存:华北地区:万吨:周度",
+        "棕榈油24度及以下库存:华东地区:万吨:周度",
+        "棕榈油24度及以下库存:华南地区:万吨:周度",
+        "棕榈油24度及以下库存:其他地区:万吨:周度",
+        "棕榈油24度及以下库存:全国合计:万吨:周度",
+        "棕油总库存:华北地区:万吨:周度",
+        "棕油总库存:华东地区:万吨:周度",
+        "棕油总库存:华南地区:万吨:周度",
+        "棕油总库存:其他地区:万吨:周度",
+        "棕油总库存:全国合计:万吨:周度",
+        "棕榈油合同量:华北地区:万吨:周度",
+        "棕榈油合同量:华东地区:万吨:周度",
+        "棕榈油合同量:华南地区:万吨:周度",
+        "棕榈油合同量:其他地区:万吨:周度",
+        "棕榈油合同量:全国合计:万吨:周度"
+      ]
+    },
+    "每日成交": {
+      "棕榈油成交量及价格统计": [
+        "棕榈油成交量:华北:1:吨:日度",
+        "棕榈油成交量:山东:1:吨:日度",
+        "棕榈油成交量:江苏:1:吨:日度",
+        "棕榈油成交量:浙江:1:吨:日度",
+        "棕榈油成交量:福建:1:吨:日度",
+        "棕榈油成交量:广东:1:吨:日度",
+        "棕榈油成交量:广西:1:吨:日度",
+        "棕榈油成交量:其他:1:吨:日度",
+        "棕榈油成交量:合计:1:吨:日度",
+        "棕榈油周度成交量:华北:1:吨:日度",
+        "棕榈油周度成交量:山东:1:吨:日度",
+        "棕榈油周度成交量:江苏:1:吨:日度",
+        "棕榈油周度成交量:浙江:1:吨:日度",
+        "棕榈油周度成交量:福建:1:吨:日度",
+        "棕榈油周度成交量:广东:1:吨:日度",
+        "棕榈油周度成交量:广西:1:吨:日度",
+        "棕榈油周度成交量:其他:1:吨:日度",
+        "棕榈油周度成交量:合计:1:吨:日度",
+        "主要集团:中粮:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:金光:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:益海:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:来宝:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:合益荣:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:其他:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:总计:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:中粮:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:金光:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:益海:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:来宝:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:合益荣:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:其他:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:总计:豆油基差成交量:本周(吨)基差成交:吨:周度"
+      ]
+    }
+  },
+  "油菜籽": {
+    "进口成本": {
+      "加拿大油菜籽理论进口成本": [
+        "加拿大油菜籽:期货收盘:广州港:美分/蒲式耳:日度",
+        "加拿大油菜籽:升贴水:广州港:美元/吨:日度",
+        "加拿大油菜籽:FOB价:广州港:美元/吨:日度",
+        "加拿大油菜籽:运费:广州港:美元/吨:日度",
+        "加拿大油菜籽:CNF升贴水:广州港:美元/吨:日度",
+        "加拿大油菜籽:CNF:广州港:美元/吨:日度",
+        "加拿大油菜籽:进口成本:广州港:元/吨:日度"
+      ]
+    },
+    "库存分析": {
+      "全国油厂进口油菜籽库存量统计周报": [
+        "全国油厂进口油菜籽库存量:广西地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:广东地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:福建地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:江苏地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:辽宁地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:其它地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:全国统计:万吨:周度"
+      ]
+    },
+    "进口预估": {
+      "进口油菜籽月度进口量预估": [
+        "进口油菜籽月度进口量预估:本年进口量:万吨:周度",
+        "进口油菜籽月度进口量预估:本年海关进口量:万吨:周度"
+      ]
+    },
+    "加拿大统计局": {
+      "加拿大双低油菜籽周度商业库存": [
+        "加拿大双低油菜籽:期初库存:万吨:周度",
+        "加拿大双低油菜籽:上市量:万吨:周度",
+        "加拿大双低油菜籽:出口量:万吨:周度",
+        "加拿大双低油菜籽:消费量:万吨:周度",
+        "加拿大双低油菜籽:期末库存:万吨:周度"
+      ]
+    },
+    "进出口分析": {
+      "油菜籽进口数量分析": [
+        "油菜籽进口量:万吨:月度"
+      ],
+      "油菜籽出口数量分析": [
+        "油菜籽出口量:吨:月度"
+      ]
+    }
+  },
+  "菜粕": {
+    "库存分析": {
+      "全国油厂进口压榨菜粕库存与合同统计周报": [
+        "全国油厂进口压榨菜粕库存量:广西地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:广东地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:福建地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:江苏地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:辽宁地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:其他地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:全国合计:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:广西地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:广东地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:福建地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:江苏地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:辽宁地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:其他地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:全国合计:万吨:周度"
+
+      ]
+    },
+    "进出口分析": {
+      "菜粕出口数量分析": [
+        "菜粕出口数量:吨:月度"
+      ],
+      "菜粕进口数量分析": [
+        "菜粕进口数量:万吨:月度"
+      ]
+    }
+  },
+  "菜籽油": {
+    "库存分析": {
+      "全国油厂进口压榨菜油库存与合同统计周报": [
+        "全国油厂进口压榨菜油库存量:广西地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:广东地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:福建地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:江苏地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:辽宁地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:其他地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:全国合计:万吨:周度",
+        "全国油厂进口压榨菜油库存量:其中:非油企库存:万吨:周度",
+        "全国油厂进口压榨菜油库存量:油企库存:万吨:周度",
+        "全国油厂进口压榨菜油合同量:广西地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:广东地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:福建地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:江苏地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:辽宁地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:其他地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:全国合计:万吨:周度",
+        "全国油厂进口压榨菜油合同量:其中:非油企库存:万吨:周度",
+        "全国油厂进口压榨菜油合同量:油企库存:万吨:周度"
+      ]
+    },
+    "进口预估": {
+      "菜籽油月度进口量预估": [
+        "菜籽油月度进口量预估:本年进口量:万吨:周度",
+        "菜籽油月度进口量预估:本年海关进口量:万吨:周度"
+      ]
+    },
+    "每日成交": {
+      "菜籽油成交量及价格统计": [
+        "菜籽油成交量:东北:1:吨:日度",
+        "菜籽油成交量:华东:1:吨:日度",
+        "菜籽油成交量:福建:1:吨:日度",
+        "菜籽油成交量:广东:1:吨:日度",
+        "菜籽油成交量:广西:1:吨:日度",
+        "菜籽油成交量:其它:1:吨:日度",
+        "菜籽油成交量:合计:1:吨:日度",
+        "菜籽油成交量:华南合计:1:吨:日度",
+        "菜籽油成交量:沿海合计:1:吨:日度",
+        "菜籽油周度成交量:东北:1:吨:周度",
+        "菜籽油周度成交量:华东:1:吨:周度",
+        "菜籽油周度成交量:福建:1:吨:周度",
+        "菜籽油周度成交量:广东:1:吨:周度",
+        "菜籽油周度成交量:广西:1:吨:周度",
+        "菜籽油周度成交量:其它:1:吨:周度",
+        "菜籽油周度成交量:合计:1:吨:周度",
+        "菜籽油周度成交量:华南合计:1:吨:周度",
+        "菜籽油周度成交量:沿海合计:1:吨:周度",
+        "主要集团:营口嘉里:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:富之源:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:防城大海:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:防城澳加:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:成都中粮:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:其它:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:总计:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:营口嘉里:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:富之源:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:防城大海:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:防城澳加:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:成都中粮:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:其它:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:总计:菜籽油基差成交量:本周(吨)基差成交:吨:周度"
+      ]
+    }
+  },
+  "葵花粕": {
+    "进口预估": {
+      "进口葵花粕月度进口量预估": [
+        "进口葵花粕月度进口量预估:本年进口量:万吨:周度",
+        "进口葵花粕月度进口量预估:本年海关进口量:万吨:周度"
+      ]
+    }
+  }
+}

+ 20 - 1
utils/common.go

@@ -3,6 +3,7 @@ package utils
 import (
 	"bufio"
 	"crypto/md5"
+	cryRand "crypto/rand"
 	"crypto/sha1"
 	"encoding/base64"
 	"encoding/hex"
@@ -15,6 +16,7 @@ import (
 	"image/png"
 	"io"
 	"math"
+	"math/big"
 	"math/rand"
 	"net"
 	"net/http"
@@ -1310,4 +1312,21 @@ func ContainsEnglishLetter(s string) bool {
 		}
 	}
 	return false
-}
+}
+
+// RangeRand 取区间随机数
+func RangeRand(min, max int64) int64 {
+	if min > max {
+		return max
+	}
+	if min < 0 {
+		f64Min := math.Abs(float64(min))
+		i64Min := int64(f64Min)
+		result, _ := cryRand.Int(cryRand.Reader, big.NewInt(max+1+i64Min))
+
+		return result.Int64() - i64Min
+	} else {
+		result, _ := cryRand.Int(cryRand.Reader, big.NewInt(max-min+1))
+		return min + result.Int64()
+	}
+}

+ 15 - 0
utils/config.go

@@ -72,6 +72,14 @@ var (
 	MtjhOpen     string //是否配置煤炭江湖数据源,1已配置
 )
 
+// 粮油商务网
+var (
+	LY_USERNAME  string
+	LY_PASSWORD  string
+	LY_JSON_PATH string
+	LY_OPEN      string
+)
+
 // CCF化纤信息
 var (
 	CCFOpen           string // 是否配置CCF
@@ -199,6 +207,13 @@ func init() {
 		}
 	}
 
+	{
+		LY_USERNAME = config["ly_username"]
+		LY_PASSWORD = config["ly_password"]
+		LY_JSON_PATH = config["ly_json_path"]
+		LY_OPEN = config["ly_open"]
+	}
+
 	// 隆众数据
 	{
 		OilchemAccount = config["oilchem_account"]

+ 35 - 16
utils/constants.go

@@ -97,6 +97,7 @@ const (
 	DATA_SOURCE_PREDICT_CALCULATE_STANDARD_DEVIATION            //预测标准差->69
 	DATA_SOURCE_PREDICT_CALCULATE_PERCENTILE                    //预测百分位->70
 	DATA_SOURCE_FUBAO                                           //富宝数据->71
+	DATA_SOURCE_LY                                   = 91       // 粮油商务网
 )
 
 // 指标来源的中文展示
@@ -233,20 +234,38 @@ var FrequencyDaysMap = map[string]int{
 
 // edb_index_lib 的接口名称
 const (
-	LIB_ROUTE_YONGYI_HANDLE             = "yongyi/handle/excel_data"   //涌益咨询处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_JSM_HISTORY     = "/coal_mine/jsm/history"     //jsm三省煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_COASTAL_HISTORY = "/coal_mine/coastal/history" //沿海煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_INLAND_HISTORY  = "/coal_mine/inland/history"  //内陆三省煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_JSM             = "/coal_mine/jsm"             //jsm三省煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_COASTAL         = "/coal_mine/coastal"         //沿海煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_INLAND          = "/coal_mine/inland"          //内陆三省煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_FIRM            = "/coal_mine/firm"            //分公司旬度煤炭网数据处理excel数据并入库 数据地址
-	LIB_ROUTE_FENWEI_HANDLE             = "fenwei/handle/excel_data"   // 汾渭煤炭excel数据入库接口地址
-	LIB_ROUTE_FENWEI_CLASSIFY           = "fenwei/classify_tree"       // 汾渭煤炭分类接口地址
-	LIB_ROUTE_FENWEI_INDEX_LIST         = "fenwei/base_index_list"     // 汾渭煤炭指标列表接口地址
-	LIB_ROUTE_COAL_MINE_MTJH            = "/mtjh/data"                 //煤炭江湖数据处理excel数据并入库 数据地址
-	LIB_ROUTE_CCF_EDB_HANDLE            = "ccf/handle/edb_data"        // CCF化纤信息指标入库接口地址
-	LIB_ROUTE_CCF_TABLE_HANDLE          = "ccf/handle/table_data"      // CCF化纤信息装置表格入库接口地址
-	LIB_ROUTE_OILCHEM_TABLE_HANDLE      = "oilchem/handle/edb_data"  // 隆众资讯入库接口地址
-	LIB_ROUTE_FENWEI_NET_DATA_HANDLE    = "/fenwei/net/data/handle"    // 汾渭网页数据处理
+	LIB_ROUTE_YONGYI_HANDLE                           = "yongyi/handle/excel_data"                              //涌益咨询处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_JSM_HISTORY                   = "/coal_mine/jsm/history"                                //jsm三省煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_COASTAL_HISTORY               = "/coal_mine/coastal/history"                            //沿海煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_INLAND_HISTORY                = "/coal_mine/inland/history"                             //内陆三省煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_JSM                           = "/coal_mine/jsm"                                        //jsm三省煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_COASTAL                       = "/coal_mine/coastal"                                    //沿海煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_INLAND                        = "/coal_mine/inland"                                     //内陆三省煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_FIRM                          = "/coal_mine/firm"                                       //分公司旬度煤炭网数据处理excel数据并入库 数据地址
+	LIB_ROUTE_FENWEI_HANDLE                           = "fenwei/handle/excel_data"                              // 汾渭煤炭excel数据入库接口地址
+	LIB_ROUTE_FENWEI_CLASSIFY                         = "fenwei/classify_tree"                                  // 汾渭煤炭分类接口地址
+	LIB_ROUTE_FENWEI_INDEX_LIST                       = "fenwei/base_index_list"                                // 汾渭煤炭指标列表接口地址
+	LIB_ROUTE_COAL_MINE_MTJH                          = "/mtjh/data"                                            //煤炭江湖数据处理excel数据并入库 数据地址
+	LIB_ROUTE_CCF_EDB_HANDLE                          = "ccf/handle/edb_data"                                   // CCF化纤信息指标入库接口地址
+	LIB_ROUTE_CCF_TABLE_HANDLE                        = "ccf/handle/table_data"                                 // CCF化纤信息装置表格入库接口地址
+	LIB_ROUTE_OILCHEM_TABLE_HANDLE                    = "oilchem/handle/edb_data"                               // 隆众资讯入库接口地址
+	LIB_ROUTE_FENWEI_NET_DATA_HANDLE                  = "/fenwei/net/data/handle"                               // 汾渭网页数据处理
+	GET_LY_CLASSIFY_BY_NAME                           = "/ly/get/ly/classify/by/name"                           // 获取分类
+	GET_LY_INDEX_RECORD_BY_URL                        = "/ly/get/ly/index/record/by/url"                        // 根据url获取指标已读取记录
+	ADD_LY_INDEX_RECORD                               = "/ly/add/ly/index/record"                               // 维护指标数据读取进度到数据库
+	ADD_LY_DATA_LIST                                  = "/ly/add/ly/data/list"                                  // 新增指标数据列表
+	ADD_LY_INDEX                                      = "/ly/add/ly/index"                                      // 新增指标
+	GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME             = "/ly/get/ly/data/by/index/id/and/data/time"             // 根据指标id和时间获取指标数据
+	GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME_YM          = "/ly/get/ly/data/by/index/id/and/data/time/ym"          // 根据指标id和年月时间获取指标数据
+	UPDATE_LY_DATA_BY_ID                              = "/ly/update/ly/data/by/id"                              // 更新数据源指标数据
+	UPDATE_LY_EDB_DATA_BY_ID                          = "/ly/update/ly/edb/data/by/id"                          // 更新指标库指标数据
+	GET_LY_EDB_DATA_BY_INDEX_CODE_AND_DATA_TIME       = "/ly/get/ly/edb/data/by/index/code/and/data/time"       // 根据指标编码和模糊日期获取指标库数据
+	GET_LY_EDB_DATA_BY_INDEX_CODE_AND_EXACT_DATA_TIME = "/ly/get/ly/edb/data/by/index/code/and/exact/data/time" //根据指标编码和精确日期获取指标库数据
+	GET_LY_INDEX_BY_CODE                              = "/ly/get/ly/index/by/code"                              // 根据指标编码获取指标
+	GET_EDB_INFO_BY_INDEX_CODE                        = "/ly/get/edb/info/by/index/code"                        // 根据指标code获取指标信息
+	ADD_BATCH_LY_EDB_DATA                             = "/ly/add/batch/ly/edb/data"                             // 批量增加粮油指标库数据
+)
+
+const (
+	APPNAME = "弘则-数据爬虫"
 )

+ 488 - 0
utils/date_util.go

@@ -0,0 +1,488 @@
+// Package utils @Author gmy 2024/8/6 16:06:00
+package utils
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// ParseDateAndWeek parseDateAndWeek 解析日期并计算当前周数 ==> 24年31周
+func ParseDateAndWeek(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(strings.Split(dateText, " ")[0]))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	// 计算年份和周数
+	year, week := reportDate.ISOWeek()
+	// 获取年份的后两位
+	shortYear := year % 100
+	targetWeek := fmt.Sprintf("%02d年第%d周", shortYear, week)
+
+	return targetWeek, nil
+}
+
+// ParseDateAndMonth 解析时间并计算当前月份 和 后两月 1月就是1月F,二月是二月G 规则:F=1月,G=2月,H=3月,J=4月,K=5月,M=6月,N=7月,Q=8月,U=9月,V=10月,X=11月,Z=12月
+func ParseDateAndMonth(dateText string) ([]string, error) {
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(strings.Split(dateText, " ")[0]))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	months := make([]string, 3)
+	monthMap := map[string]string{
+		"01": "1月F",
+		"02": "2月G",
+		"03": "3月H",
+		"04": "4月J",
+		"05": "5月K",
+		"06": "6月M",
+		"07": "7月N",
+		"08": "8月Q",
+		"09": "9月X",
+		"10": "10月X",
+		"11": "11月X",
+		"12": "12月Z",
+	}
+
+	for i := 0; i < 3; i++ {
+		month := reportDate.AddDate(0, i, 0).Format("01")
+		months[i] = monthMap[month]
+	}
+
+	return months, nil
+}
+
+// ParseDateAndMonthColzaOil 油菜籽 进口成本 时间映射
+func ParseDateAndMonthColzaOil(dateText string) ([]string, error) {
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(strings.Split(dateText, " ")[0]))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	months := make([]string, 3)
+	monthMap := map[string]string{
+		"01": "1月F",
+		"02": "2月H",
+		"03": "3月H",
+		"04": "4月K",
+		"05": "5月K",
+		"06": "6月N",
+		"07": "7月N",
+		"08": "8月X",
+		"09": "9月X",
+		"10": "10月X",
+		"11": "11月X",
+		"12": "12月F",
+	}
+
+	for i := 0; i < 3; i++ {
+		month := reportDate.AddDate(0, i, 0).Format("01")
+		months[i] = monthMap[month]
+	}
+
+	return months, nil
+}
+
+// GetCurrentTime 获取当前时间 格式为 2024-08-07 15:29:58
+func GetCurrentTime() string {
+	return time.Now().Format("2006-01-02 15:04:05")
+}
+
+// ConvertTimeFormat 转换时间格式 dateText 格式为 2024-08-03 07:53 --> 2024-08-03
+func ConvertTimeFormat(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02 15:04", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	return reportDate.Format("2006-01-02"), nil
+}
+
+// GetNextThreeMonthsNoYear 获取当前月和后两月 不带年份,转换时间格式 dateText 格式为 2024-08-03 --> 8月,9月,10月
+func GetNextThreeMonthsNoYear(dateText string) ([]string, error) {
+	// 解析日期字符串为时间类型
+	date, err := time.Parse("2006-01-02", dateText)
+	if err != nil {
+		return nil, fmt.Errorf("日期解析错误: %v", err)
+	}
+
+	// 存储结果的切片
+	var result []string
+
+	// 获取本月及后两个月的月份
+	for i := 0; i < 3; i++ {
+		month := int(date.Month())
+
+		// 构建并添加当前年月到结果中
+		result = append(result, fmt.Sprintf("%d月", month))
+
+		// 将日期加一个月
+		date = date.AddDate(0, 1, 0)
+	}
+
+	return result, nil
+}
+
+// GetNextThreeMonthsLastDay 取当前月的最后一天和后两个月的最后一天 时间格式为 2024-08-03 --> 2024-08-31, 2024-09-30, 2024-10-31
+func GetNextThreeMonthsLastDay(dateText string) ([]string, error) {
+	// 解析日期字符串为时间类型
+	date, err := time.Parse("2006-01-02", dateText)
+	if err != nil {
+		return nil, fmt.Errorf("日期解析错误: %v", err)
+	}
+
+	// 存储结果的切片
+	var result []string
+
+	// 获取本月及后两个月的最后一天
+	for i := 0; i < 3; i++ {
+		// 获取下个月的第一天
+		nextMonth := date.AddDate(0, 1, 0)
+
+		// 获取当前月的最后一天
+		lastDay := nextMonth.AddDate(0, 0, -nextMonth.Day())
+
+		// 添加到结果中
+		result = append(result, lastDay.Format("2006-01-02"))
+
+		// 将日期加一个月
+		date = date.AddDate(0, 1, 0)
+	}
+
+	return result, nil
+}
+
+// GetElementInSlice 获取切片中特定的元素,判断传入月份是否在当前切片月份匹配,如果匹配则返回切片中对应的元素 参数格式为 dateTexts month, [2024-08-31, 2024-09-30, 2024-10-31] 08 --> 2024-08-31, 07 --> nil
+func GetElementInSlice(dateTexts []string, month string) (string, error) {
+	for _, dateText := range dateTexts {
+		reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+		if err != nil {
+			return "", fmt.Errorf("failed to parse report date: %v", err)
+		}
+
+		if strings.HasSuffix(reportDate.Format("2006-01"), month) {
+			return dateText, nil
+		}
+	}
+
+	return "", fmt.Errorf("未找到匹配的月份")
+}
+
+// StringToTime string 类型时间转换为 time 类型时间 dateText 格式为 2024-08-03 00:00:00 --> 2024-08-03 00:00:00
+func StringToTime(dateText string) (time.Time, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02 15:04:05", strings.TrimSpace(dateText))
+	if err != nil {
+		return time.Time{}, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	return reportDate, nil
+}
+
+// StringToTimeZero string 类型时间转换为 time dateText 格式为 2024-08-03 --> 2024-08-03 00:00:00
+func StringToTimeZero(dateText string) (time.Time, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return time.Time{}, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	return reportDate, nil
+}
+
+// GetWeekdaysInSameWeek 拿到传入时间本周当前工作日的时间列表,时间格式 dataText 格式为 2024-08-03 --> 8月3日
+func GetWeekdaysInSameWeek(dateStr string) ([]string, error) {
+	// 解析输入日期字符串
+	t, err := time.Parse("2006-01-02", dateStr)
+	if err != nil {
+		return nil, err
+	}
+
+	// 获取星期几
+	weekday := t.Weekday()
+
+	// 计算星期一的日期
+	monday := t.AddDate(0, 0, -int(weekday)+1)
+
+	// 生成这周的工作日列表(周一至周五)
+	var weekdays []string
+	for i := 0; i < 5; i++ {
+		day := monday.AddDate(0, 0, i)
+		weekdays = append(weekdays, fmt.Sprintf("%d月%d日", day.Month(), day.Day()))
+	}
+
+	return weekdays, nil
+}
+
+// ConvertToDate 转换后获取当前传入的时间 时间格式为 7月22日 --> 2024-07-22
+func ConvertToDate(dateText string) (string, error) {
+	// 假设当前年份为 2024
+	currentYear := time.Now().Year()
+
+	// 分割日期字符串
+	parts := strings.Split(dateText, "月")
+	if len(parts) != 2 {
+		return "", fmt.Errorf("日期格式错误")
+	}
+
+	// 获取月和日的部分
+	month, err := strconv.Atoi(parts[0])
+	if err != nil {
+		return "", fmt.Errorf("月份解析错误: %v", err)
+	}
+	day, err := strconv.Atoi(strings.TrimSuffix(parts[1], "日"))
+	if err != nil {
+		return "", fmt.Errorf("日期解析错误: %v", err)
+	}
+
+	// 构建日期
+	date := time.Date(currentYear, time.Month(month), day, 0, 0, 0, 0, time.Local)
+
+	// 格式化为 "2024-07-22"
+	return date.Format("2006-01-02"), nil
+}
+
+// ConvertTimeFormatToYearMonthDay 转换时间格式 dateText 格式为 2024-08-03 --> 24年8月3日
+func ConvertTimeFormatToYearMonthDay(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	// 获取年份的后两位
+	shortYear := reportDate.Year() % 100
+
+	return fmt.Sprintf("%02d年%d月%d日", shortYear, reportDate.Month(), reportDate.Day()), nil
+}
+
+// GetCurrentYearAndLastYear 获取当前年份和前一年的年份 转换时间格式 dateText 格式为 2024-08-03 --> 2024年,2023年
+func GetCurrentYearAndLastYear(dateText string) ([]string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	years := make([]string, 2)
+	year := reportDate.Year()
+
+	// 当前年份
+	years[0] = fmt.Sprintf("%d年", year)
+	// 前一年
+	years[1] = fmt.Sprintf("%d年", year-1)
+
+	return years, nil
+}
+
+// ConvertTimeFormatToYearMonth 转换时间格式 dateText 返回本月 和 后两月 格式为 2024-08-03 --> 2024年8月,2024-10-03 --> 2024年10月
+func ConvertTimeFormatToYearMonth(dateText string) ([]string, error) {
+
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	months := make([]string, 3)
+	for i := 0; i < 3; i++ {
+		month := reportDate.AddDate(0, i, 0).Format("2006年1月")
+		months[i] = month
+	}
+
+	return months, nil
+}
+
+// GetYearMonthNoYear 获取本月和后两月的年月 2024-08-03 --> 24年8月,24年9月,24年10月
+func GetYearMonthNoYear(dateText string) ([]string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	months := make([]string, 3)
+	for i := 0; i < 3; i++ {
+		month := reportDate.AddDate(0, i, 0).Format("06年1月")
+		months[i] = month
+	}
+
+	return months, nil
+}
+
+// GetCurrentYearAndNextYear 获取当时所在得年度和明年得年度列表 2024-08-03 --> 2023/24年度, 2024/25年度
+func GetCurrentYearAndNextYear(dateText string) ([]string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	years := make([]string, 2)
+	year := reportDate.Year()
+
+	// 当前年度
+	years[0] = fmt.Sprintf("%d/%02d年度", year-1, year%100)
+	// 下一年度
+	years[1] = fmt.Sprintf("%d/%02d年度", year, (year+1)%100)
+
+	return years, nil
+}
+
+// IsSameMonth 判断当前传入年月是否是同一月 2024-08-03, 2024-08 --> true, 2024-08-03, 2024-07 --> false
+func IsSameMonth(dateText1, dateText2 string) (bool, error) {
+	// 解析日期
+	date1, err := time.Parse("2006-01-02", strings.TrimSpace(dateText1))
+	if err != nil {
+		return false, fmt.Errorf("failed to parse date1: %v", err)
+	}
+
+	date2, err := time.Parse("2006-01", strings.TrimSpace(dateText2))
+	if err != nil {
+		return false, fmt.Errorf("failed to parse date2: %v", err)
+	}
+
+	return date1.Year() == date2.Year() && date1.Month() == date2.Month(), nil
+}
+
+// GetLastDayOfMonth 获取传入年月的最后一天 dateText 格式为 2024-08 --> 2024-08-31
+func GetLastDayOfMonth(dateText string) (string, error) {
+	// 解析日期
+	date, err := time.Parse("2006-01", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse date: %v", err)
+	}
+
+	// 获取下个月的第一天
+	nextMonth := date.AddDate(0, 1, 0)
+
+	// 获取本月的最后一天
+	lastDay := nextMonth.AddDate(0, 0, -1)
+
+	return lastDay.Format("2006-01-02"), nil
+}
+
+// ConvertMonthToNumber 时间转换 格式 8月 --> 08
+func ConvertMonthToNumber(dateText string) (string, error) {
+	// 去掉字符串中的 "月"
+	trimmed := strings.TrimSuffix(strings.TrimSpace(dateText), "月")
+
+	// 将月份转换为整数
+	month, err := strconv.Atoi(trimmed)
+	if err != nil {
+		return "", fmt.Errorf("failed to parse month: %v", err)
+	}
+
+	return fmt.Sprintf("%02d", month), nil
+}
+
+// 时间转换 格式 1 --> 01
+func ConvertMonthToNumber1(dateText string) (string, error) {
+	// 将月份转换为整数
+	month, err := strconv.Atoi(dateText)
+	if err != nil {
+		return "", fmt.Errorf("failed to parse month: %v", err)
+	}
+
+	return fmt.Sprintf("%02d", month), nil
+}
+
+// GetNextThreeMonths 获取传入时间的本月及后两月的年月 2024-08-03 --> 24年8月
+func GetNextThreeMonths(dateText string) ([]string, error) {
+	// 解析日期字符串为时间类型
+	date, err := time.Parse("2006-01-02", dateText)
+	if err != nil {
+		return nil, fmt.Errorf("日期解析错误: %v", err)
+	}
+
+	// 存储结果的切片
+	var result []string
+
+	// 获取本月及后两个月的年份和月份
+	for i := 0; i < 3; i++ {
+		year := date.Year() % 100
+		month := int(date.Month())
+
+		// 构建并添加当前年月到结果中
+		result = append(result, fmt.Sprintf("%d年%d月", year, month))
+
+		// 将日期加一个月
+		date = date.AddDate(0, 1, 0)
+	}
+
+	return result, nil
+}
+
+// IsCurrentYear 判断是否是当前年度 传入日期格式为 2023/24年度  --> true, 2024/25年度 --> false
+func IsCurrentYear(dateText string) (bool, error) {
+	// 去掉字符串中的 "年度"
+	trimmed := strings.TrimSuffix(strings.TrimSpace(dateText), "年度")
+
+	// 分割年份,例如 "2023/24" -> ["2023", "24"]
+	parts := strings.Split(trimmed, "/")
+	if len(parts) != 2 {
+		return false, fmt.Errorf("invalid date format: %s", dateText)
+	}
+
+	// 将前一年的年份转换为整数
+	startYear, err := strconv.Atoi(parts[0])
+	if err != nil {
+		return false, fmt.Errorf("failed to parse start year: %v", err)
+	}
+
+	// 获取当前年份
+	currentYear := time.Now().Year()
+
+	// 如果当前年份等于 dateText 中的后一年的年份,返回 true
+	if currentYear == startYear+1 {
+		return true, nil
+	}
+
+	return false, nil
+}
+
+// GetNextYearLastDay 获取明年本月份的最后一天 2024-08-03 --> 2025-08-31
+func GetNextYearLastDay(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	// 获取下一年的年份
+	nextYear := reportDate.Year() + 1
+	// 获取本月份的最后一天
+	lastDay := time.Date(nextYear, reportDate.Month()+1, 0, 0, 0, 0, 0, reportDate.Location())
+
+	return lastDay.Format("2006-01-02"), nil
+}
+
+// GetYearMonth 获取年月日 2024-08-03 --> 2024-08
+func GetYearMonth(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	return reportDate.Format("2006-01"), nil
+}
+
+// GetCurrentMonth 获取当前月份 2024-08-03 --> 8月
+func GetCurrentMonth(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	// 计算月份
+	month := reportDate.Month()
+
+	return fmt.Sprintf("%d月", month), nil
+}

+ 60 - 0
utils/http_request.go

@@ -0,0 +1,60 @@
+package utils
+
+import (
+	"encoding/json"
+
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+)
+
+// PostEdbLibRequest 调用指标接口
+func PostEdbLibRequest(param map[string]interface{}, method string) (result []byte, err error) {
+	postUrl := EDB_LIB_URL + method
+	postData, err := json.Marshal(param)
+	if err != nil {
+		return
+	}
+	result, err = HttpPostRequest(postUrl, string(postData), "application/json")
+	if err != nil {
+		return
+	}
+	return
+}
+
+func HttpPostRequest(url, postData string, params ...string) ([]byte, error) {
+	fmt.Println("HttpPost Url:" + url)
+	body := ioutil.NopCloser(strings.NewReader(postData))
+	client := &http.Client{}
+	req, err := http.NewRequest("POST", url, body)
+	if err != nil {
+		return nil, err
+	}
+	contentType := "application/x-www-form-urlencoded;charset=utf-8"
+	if len(params) > 0 && params[0] != "" {
+		contentType = params[0]
+	}
+	req.Header.Set("Content-Type", contentType)
+	req.Header.Set("authorization", MD5(APP_EDB_LIB_NAME_EN+EDB_LIB_Md5_KEY))
+	resp, err := client.Do(req)
+	if err != nil {
+		fmt.Println("client.Do err:" + err.Error())
+		return nil, err
+	}
+	defer resp.Body.Close()
+	b, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println("HttpPost:" + string(b))
+	}
+	return b, err
+}
+
+func HttpGetRequest(url string) ([]byte, error) {
+	res, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+	return ioutil.ReadAll(res.Body)
+}

+ 88 - 0
utils/index_code_util.go

@@ -0,0 +1,88 @@
+// @Author gmy 2024/8/7 10:41:00
+package utils
+
+import (
+	"fmt"
+	"github.com/mozillazg/go-pinyin"
+	"strings"
+	"unicode"
+)
+
+// GenerateIndexCode 指标编码规则:粮油商务网拼音首字母+指标名称拼音首字母,数字、字母保留,特殊字符拿掉
+// 例:美湾:9月U:国际大豆进口成本价:期货收盘:张家港 -----> lyswwmw9yUgjddjkcbjqhspzjg
+func GenerateIndexCode(sourceName string, indexName string) string {
+
+	// 获取汉字的拼音首字母,保留数字和大写字母
+	firstLetters := getFirstLetters(indexName)
+
+	// 组合 sourceName 和处理后的拼音首字母
+	indexCode := fmt.Sprintf("%s%s", sourceName, firstLetters)
+
+	return indexCode
+}
+
+// getFirstLetters 获取汉字的拼音首字母,并保留数字和大写字母
+func getFirstLetters(input string) string {
+	// 设置拼音转换选项,只获取首字母
+	args := pinyin.NewArgs()
+	args.Style = pinyin.FirstLetter
+
+	// 定义用于存储结果的字符串
+	var result strings.Builder
+
+	// 遍历输入字符串中的每个字符
+	for _, r := range input {
+		if unicode.IsDigit(r) || unicode.IsUpper(r) {
+			// 保留数字和大写字母
+			result.WriteRune(r)
+		} else if unicode.Is(unicode.Han, r) {
+			// 如果是汉字,则获取其拼音首字母
+			py := pinyin.Pinyin(string(r), args)
+			if len(py) > 0 && len(py[0]) > 0 {
+				result.WriteString(py[0][0])
+			}
+		}
+		// 对于其他字符,忽略处理
+	}
+
+	return result.String()
+}
+
+/*func GenerateIndexCode(sourceName string, indexName string) string {
+	// 获取拼音首字母
+	indexInitials := getFirstLetter(indexName)
+
+	// 保留源名称中的字母和数字
+	sourceNameFiltered := filterAlphanumeric(indexName)
+
+	// 拼接源名称和首字母
+	indexCode := sourceName + sourceNameFiltered + indexInitials
+
+	// 保留字母和数字,去掉其他特殊字符
+	re := regexp.MustCompile(`[^a-zA-Z0-9]`)
+	indexCode = re.ReplaceAllString(indexCode, "")
+
+	// 转换为小写
+	indexCode = strings.ToLower(indexCode)
+
+	return indexCode
+}
+
+// 获取字符串中的拼音首字母
+func getFirstLetter(s string) string {
+	a := pinyin.NewArgs()
+	p := pinyin.Pinyin(s, a)
+	firstLetters := ""
+	for _, syllables := range p {
+		if len(syllables) > 0 {
+			firstLetters += strings.ToUpper(syllables[0][:1])
+		}
+	}
+	return firstLetters
+}
+
+// 过滤只保留字母和数字
+func filterAlphanumeric(s string) string {
+	re := regexp.MustCompile(`[^a-zA-Z0-9]`)
+	return re.ReplaceAllString(s, "")
+}*/