feat: Gateway store 桩换真实 GORM/Postgres + go-redis (含自动迁移与优雅降级)

第 2 层网关持久层落地,遵守 sundynix_ 表名前缀 + AutoMigrate 约定。

- store: GORM(NamingStrategy 前缀 sundynix_/单数) → User=sundynix_user, Task=sundynix_task
  启动 AutoMigrate;go-redis/v9 滑动窗口限流(Incr+Expire,按 IP)
- 优雅降级:连不上库则 warn 继续(不 fatal),保证无 Docker 的 make demo 仍跑通
- handler: SubmitTask 持久化任务(best-effort),Billing 真实读库返回 tasks_submitted
- main: OpenPostgres/OpenRedis 读 POSTGRES_DSN/REDIS_ADDR 环境变量
- 验证: 4 模块 build ✓;e2e 3 测试 PASS;live 双路径(真实库持久化 + 坏DSN降级)实测通过

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-10 11:43:53 +08:00
parent adc521f94d
commit e5fa0ae36c
10 changed files with 228 additions and 41 deletions
@@ -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()})
}