diff --git a/go.work b/go.work index d7c43ca..b9c7c2f 100644 --- a/go.work +++ b/go.work @@ -1,8 +1,8 @@ -go 1.23 +go 1.24 use ( - ./sundynix-shared - ./sundynix-gateway ./sundynix-dispatcher + ./sundynix-gateway ./sundynix-mcp-go + ./sundynix-shared ) diff --git a/go.work.sum b/go.work.sum index 0b01978..34bfde8 100644 --- a/go.work.sum +++ b/go.work.sum @@ -116,11 +116,13 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= diff --git a/sundynix-gateway/cmd/server/main.go b/sundynix-gateway/cmd/server/main.go index fb2583e..4e4adc6 100644 --- a/sundynix-gateway/cmd/server/main.go +++ b/sundynix-gateway/cmd/server/main.go @@ -12,9 +12,13 @@ import ( func main() { natsURL := envOr("NATS_URL", "nats://localhost:4222") + pgDSN := envOr("POSTGRES_DSN", "postgres://sundynix:sundynix@localhost:5432/sundynix?sslmode=disable") + redisAddr := envOr("REDIS_ADDR", "localhost:6379") - db := store.MustOpenPostgres() // MainDB: Users / Billing / DSL - cache := store.MustOpenRedis() // CacheDB: Session / Rate Limit + db := store.OpenPostgres(pgDSN) // MainDB: Users / Billing / DSL(连不上则降级) + defer db.Close() + cache := store.OpenRedis(redisAddr) // CacheDB: Session / Rate Limit(连不上则降级) + defer cache.Close() bus := nats.MustConnect(natsURL) // 接入 NATS 零拷贝骨干网 + 声明任务流 defer bus.Close() diff --git a/sundynix-gateway/go.mod b/sundynix-gateway/go.mod index 7087b97..193df55 100644 --- a/sundynix-gateway/go.mod +++ b/sundynix-gateway/go.mod @@ -1,10 +1,13 @@ module github.com/sundynix/sundynix-gateway -go 1.23 +go 1.24 require ( github.com/gin-gonic/gin v1.10.0 + github.com/redis/go-redis/v9 v9.20.0 github.com/sundynix/sundynix-shared v0.0.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.1 ) replace github.com/sundynix/sundynix-shared => ../sundynix-shared @@ -12,6 +15,7 @@ replace github.com/sundynix/sundynix-shared => ../sundynix-shared require ( github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect @@ -20,10 +24,16 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.9 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/kr/pretty v0.3.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -32,14 +42,16 @@ require ( github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/rogpeppe/go-internal v1.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/atomic v1.11.0 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.27.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/sundynix-gateway/go.sum b/sundynix-gateway/go.sum index ae0b10e..1d47b1b 100644 --- a/sundynix-gateway/go.sum +++ b/sundynix-gateway/go.sum @@ -1,7 +1,13 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= @@ -29,16 +35,27 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -70,6 +87,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0= +github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -88,19 +107,24 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -115,5 +139,9 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/sundynix-gateway/internal/handler/task_handler.go b/sundynix-gateway/internal/handler/task_handler.go index a8edbdc..ae9ec0b 100644 --- a/sundynix-gateway/internal/handler/task_handler.go +++ b/sundynix-gateway/internal/handler/task_handler.go @@ -4,6 +4,7 @@ package handler import ( "encoding/json" "io" + "log" "net/http" "github.com/gin-gonic/gin" @@ -35,6 +36,10 @@ func (h *Handler) SubmitTask(c *gin.Context) { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) return } + // 持久化任务提交(best-effort:降级模式下静默跳过,不阻断发布)。 + if err := h.db.SaveTask(c.Request.Context(), task.ID, string(task.Graph)); err != nil { + log.Printf("[gateway] save task %s failed: %v", task.ID, err) + } if err := h.bus.PublishTask(c.Request.Context(), task); err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return @@ -82,5 +87,11 @@ func (h *Handler) StreamTask(c *gin.Context) { } func (h *Handler) Billing(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok"}) // TODO: 商业化与计费模块 + // TODO: 商业化与计费模块;暂以已提交任务计数演示真实读库。 + n, err := h.db.CountTasks(c.Request.Context()) + if err != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"status": "ok", "tasks_submitted": n, "persisted": h.db.Enabled()}) } diff --git a/sundynix-gateway/internal/middleware/guardrail.go b/sundynix-gateway/internal/middleware/guardrail.go index 1615a83..fb32a24 100644 --- a/sundynix-gateway/internal/middleware/guardrail.go +++ b/sundynix-gateway/internal/middleware/guardrail.go @@ -2,6 +2,9 @@ package middleware import ( + "net/http" + "time" + "github.com/gin-gonic/gin" "github.com/sundynix/sundynix-gateway/internal/store" @@ -16,10 +19,16 @@ func Guardrail() gin.HandlerFunc { } } -// RateLimit 基于 Redis 的会话级限流。 +// RateLimit 基于 Redis 的会话级限流(按客户端 IP,每分钟上限)。 +// Redis 降级时 Allow 始终放行,不阻断业务。 func RateLimit(cache *store.Redis) gin.HandlerFunc { + const perMinute = 120 return func(c *gin.Context) { - // TODO: 令牌桶 / 滑动窗口 + ok, _ := cache.Allow(c.Request.Context(), c.ClientIP(), perMinute, time.Minute) + if !ok { + c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"}) + return + } c.Next() } } diff --git a/sundynix-gateway/internal/store/models.go b/sundynix-gateway/internal/store/models.go new file mode 100644 index 0000000..c31dbc4 --- /dev/null +++ b/sundynix-gateway/internal/store/models.go @@ -0,0 +1,22 @@ +package store + +import "time" + +// 数据库映射模型。表名经 GORM NamingStrategy 统一加 sundynix_ 前缀 + 单数: +// User → sundynix_user,Task → sundynix_task。 +// 建表/改表一律走 AutoMigrate,不手写 DDL。 + +// User 是平台用户(Users)。 +type User struct { + ID uint `gorm:"primaryKey"` + Email string `gorm:"uniqueIndex;size:255"` + CreatedAt time.Time +} + +// Task 是一次提交的 Agent 编排任务(DSL)。 +type Task struct { + ID string `gorm:"primaryKey;size:64"` // task_xxx + Graph string `gorm:"type:jsonb"` // React Flow 导出的 DSL 原文 + Status string `gorm:"size:32"` // submitted / done / failed + CreatedAt time.Time +} diff --git a/sundynix-gateway/internal/store/pgsql.go b/sundynix-gateway/internal/store/pgsql.go index ca92fb2..c95a155 100644 --- a/sundynix-gateway/internal/store/pgsql.go +++ b/sundynix-gateway/internal/store/pgsql.go @@ -1,14 +1,70 @@ // Package store 封装 MainDB(PgSQL) 与 CacheDB(Redis) 的访问。 package store -import "log" +import ( + "context" + "log" -// Postgres 持有 MainDB 连接池(Users / Billing / DSL)。 -type Postgres struct{ /* *pgxpool.Pool */ } + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) -// MustOpenPostgres 建立 Postgres 连接,失败即退出。 -func MustOpenPostgres() *Postgres { - // TODO: pgxpool.New(ctx, dsn) - log.Println("[store] postgres connected (stub)") - return &Postgres{} +// Postgres 持有 MainDB 连接(Users / Billing / DSL)。 +// db 为 nil 表示降级模式(连接失败时仍允许网关启动)。 +type Postgres struct { + db *gorm.DB +} + +// OpenPostgres 用 GORM 连接 MainDB 并自动迁移表结构。 +// 表名统一 sundynix_ 前缀 + 单数(User→sundynix_user, Task→sundynix_task)。 +// 连接失败不 fatal:返回降级实例,网关仍可启动(无 Docker 跑 demo 时即此路径)。 +func OpenPostgres(dsn string) *Postgres { + db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}), &gorm.Config{ + NamingStrategy: schema.NamingStrategy{ + TablePrefix: "sundynix_", // 所有表加前缀 + SingularTable: true, // 单数表名 + }, + }) + if err != nil { + log.Printf("[store] postgres 不可用,降级运行(不持久化): %v", err) + return &Postgres{} + } + if err := db.AutoMigrate(&User{}, &Task{}); err != nil { + log.Printf("[store] postgres AutoMigrate 失败,降级运行: %v", err) + return &Postgres{} + } + log.Println("[store] postgres connected, migrated sundynix_user / sundynix_task") + return &Postgres{db: db} +} + +// Enabled 报告是否处于真实持久化模式。 +func (p *Postgres) Enabled() bool { return p.db != nil } + +// SaveTask 持久化一次任务提交(best-effort:降级模式下静默跳过)。 +func (p *Postgres) SaveTask(ctx context.Context, id, graph string) error { + if p.db == nil { + return nil + } + return p.db.WithContext(ctx).Create(&Task{ID: id, Graph: graph, Status: "submitted"}).Error +} + +// CountTasks 返回已提交任务数(降级模式返回 0)。 +func (p *Postgres) CountTasks(ctx context.Context) (int64, error) { + if p.db == nil { + return 0, nil + } + var n int64 + err := p.db.WithContext(ctx).Model(&Task{}).Count(&n).Error + return n, err +} + +// Close 释放底层连接。 +func (p *Postgres) Close() { + if p.db == nil { + return + } + if sqlDB, err := p.db.DB(); err == nil { + _ = sqlDB.Close() + } } diff --git a/sundynix-gateway/internal/store/redis.go b/sundynix-gateway/internal/store/redis.go index 572b78e..798f427 100644 --- a/sundynix-gateway/internal/store/redis.go +++ b/sundynix-gateway/internal/store/redis.go @@ -1,13 +1,56 @@ package store -import "log" +import ( + "context" + "log" + "time" + + "github.com/redis/go-redis/v9" +) // Redis 持有 CacheDB 连接(Session / Rate Limit)。 -type Redis struct{ /* *redis.Client */ } - -// MustOpenRedis 建立 Redis 连接,失败即退出。 -func MustOpenRedis() *Redis { - // TODO: redis.NewClient(opts) - log.Println("[store] redis connected (stub)") - return &Redis{} +// rdb 为 nil 表示降级模式(连接失败时放行,不限流)。 +type Redis struct { + rdb *redis.Client +} + +// OpenRedis 连接 CacheDB。连接失败不 fatal:返回降级实例(限流放行)。 +func OpenRedis(addr string) *Redis { + rdb := redis.NewClient(&redis.Options{Addr: addr}) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := rdb.Ping(ctx).Err(); err != nil { + log.Printf("[store] redis 不可用,降级运行(不限流): %v", err) + _ = rdb.Close() + return &Redis{} + } + log.Println("[store] redis connected") + return &Redis{rdb: rdb} +} + +// Enabled 报告是否处于真实限流模式。 +func (r *Redis) Enabled() bool { return r.rdb != nil } + +// Allow 滑动窗口计数限流:在 window 内对 key 累加,超过 limit 即拒绝。 +// 降级模式(rdb==nil)始终放行。 +func (r *Redis) Allow(ctx context.Context, key string, limit int64, window time.Duration) (bool, error) { + if r.rdb == nil { + return true, nil + } + rk := "sundynix:ratelimit:" + key + n, err := r.rdb.Incr(ctx, rk).Result() + if err != nil { + return true, err // 限流后端故障时放行,不阻断业务 + } + if n == 1 { + _ = r.rdb.Expire(ctx, rk, window).Err() // 首次计数设置窗口过期 + } + return n <= limit, nil +} + +// Close 释放底层连接。 +func (r *Redis) Close() { + if r.rdb != nil { + _ = r.rdb.Close() + } }