- Go Mock 引擎(与前端同语法 @cname/@integer/... 每次请求随机生成,JSON 叶子保持类型) - mock_endpoints 表 + 发布 API(PUT /api/workspaces/:id/mock,全量替换,单空间 200 个) - 路由 /mock/:wsid/*path:状态码/延迟(≤5s)/?_method 覆盖;未发布 404 - 限流(IP+空间 滑动窗口):20 QPS / 60 QPM → 429 + Retry-After + 条款提示 - 官网 /terms 服务条款(七节:含禁止滥用——未经授权或滥用有权采取措施、技术手段规避限流属违规) - UI:HTTP 编辑器「Mock 链接」弹窗(发布/状态码/延迟/模板预览/复制/试一试); 发布自动烘焙智能 Mock 表达式;集合端点去重 + INSERT OR REPLACE - docs:14-mock 增本地 Mock 服务与限流章节;页脚/429 响应均指向 /terms - 单测:mocksrv 引擎/限流/路径 5 用例;curl E2E(随机×2、201+延迟、404、连发 30 次触发 429);浏览器发布+试一试通过
81 lines
2.0 KiB
Go
81 lines
2.0 KiB
Go
// Package site 在 /home(及 /)提供产品官网单页(SSR,资源嵌入二进制)。
|
|
package site
|
|
|
|
import (
|
|
"embed"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
//go:embed template.html terms.html assets
|
|
var files embed.FS
|
|
|
|
// Handler 官网服务
|
|
type Handler struct {
|
|
html string
|
|
terms string
|
|
}
|
|
|
|
func New() *Handler {
|
|
raw, err := files.ReadFile("template.html")
|
|
if err != nil {
|
|
panic("site: template.html 缺失: " + err.Error())
|
|
}
|
|
terms, err := files.ReadFile("terms.html")
|
|
if err != nil {
|
|
panic("site: terms.html 缺失: " + err.Error())
|
|
}
|
|
return &Handler{html: string(raw), terms: string(terms)}
|
|
}
|
|
|
|
// Register 注册路由:/ 与 /home 均渲染官网
|
|
func (h *Handler) Register(r *gin.Engine) {
|
|
r.GET("/", h.Page)
|
|
r.GET("/home", h.Page)
|
|
r.GET("/index.html", h.Page)
|
|
r.GET("/site/assets/*file", h.Asset)
|
|
r.GET("/terms", h.Terms)
|
|
}
|
|
|
|
// Terms 渲染服务条款
|
|
func (h *Handler) Terms(c *gin.Context) {
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
c.Header("Cache-Control", "no-cache")
|
|
_, _ = c.Writer.WriteString(h.terms)
|
|
}
|
|
|
|
// Page 渲染官网
|
|
func (h *Handler) Page(c *gin.Context) {
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
c.Header("Cache-Control", "no-cache")
|
|
_, _ = c.Writer.WriteString(h.html)
|
|
}
|
|
|
|
// Asset 静态资源
|
|
func (h *Handler) Asset(c *gin.Context) {
|
|
name := strings.TrimPrefix(c.Param("file"), "/")
|
|
if strings.Contains(name, "..") {
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
data, err := files.ReadFile("assets/" + name)
|
|
if err != nil {
|
|
c.Status(http.StatusNotFound)
|
|
return
|
|
}
|
|
switch {
|
|
case strings.HasSuffix(name, ".css"):
|
|
c.Header("Content-Type", "text/css; charset=utf-8")
|
|
case strings.HasSuffix(name, ".js"):
|
|
c.Header("Content-Type", "application/javascript; charset=utf-8")
|
|
case strings.HasSuffix(name, ".svg"):
|
|
c.Header("Content-Type", "image/svg+xml")
|
|
default:
|
|
c.Header("Content-Type", "application/octet-stream")
|
|
}
|
|
c.Header("Cache-Control", "public, max-age=3600")
|
|
c.Data(http.StatusOK, "", data)
|
|
}
|