本地 Mock 服务 /mock + 限流 20QPS/60QPM + 服务条款

- 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);浏览器发布+试一试通过
This commit is contained in:
logan committed 2026-09-18 12:29:49 +08:00
1 parent 522e5d6300
commit d9ccccd280
17 files changed
+1070 -6

No files matched your search

+30
View File
@@ -9,6 +9,7 @@ import (
"github.com/gin-gonic/gin"
"opsxapi/server/internal/mocksrv"
"opsxapi/server/internal/store"
)
@@ -24,6 +25,9 @@ func New(path string) (*API, error) {
return &API{store: s}, nil
}
// Store 暴露底层存储(供 Mock 服务等复用同一 SQLite)。
func (a *API) Store() *store.Store { return a.store }
func (a *API) Register(r *gin.Engine) {
g := r.Group("/api")
g.POST("/auth/register", a.handleRegister)
@@ -34,6 +38,7 @@ func (a *API) Register(r *gin.Engine) {
g.POST("/teams/:id/members", a.auth(a.handleAddMember))
g.GET("/workspaces/:id", a.auth(a.handleGetWorkspace))
g.PUT("/workspaces/:id", a.auth(a.handlePutWorkspace))
g.PUT("/workspaces/:id/mock", a.auth(a.handlePublishMock))
}
/* ---------------- 中间件 ---------------- */
@@ -184,6 +189,31 @@ func (a *API) handleAddMember(c *gin.Context, user *store.User) {
c.JSON(http.StatusOK, gin.H{"ok": true, "member": member})
}
/* ---------------- Mock 发布 ---------------- */
func (a *API) handlePublishMock(c *gin.Context, user *store.User) {
ws := a.workspaceOf(c, user)
if ws == nil {
return
}
var req struct {
Endpoints []mocksrv.Endpoint `json:"endpoints" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 endpoints"})
return
}
if len(req.Endpoints) > 200 {
c.JSON(http.StatusBadRequest, gin.H{"error": "单空间最多发布 200 个 Mock 接口"})
return
}
if err := a.store.ReplaceMockEndpoints(ws.ID, req.Endpoints); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "发布失败:" + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "count": len(req.Endpoints), "base": "/mock/" + strconv.FormatInt(ws.ID, 10)})
}
/* ---------------- 工作区同步 ---------------- */
func (a *API) workspaceOf(c *gin.Context, user *store.User) *store.Workspace {