本地 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

+60
View File
@@ -11,8 +11,11 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"opsxapi/server/internal/mocksrv"
_ "modernc.org/sqlite"
)
@@ -81,6 +84,19 @@ CREATE TABLE IF NOT EXISTS workspaces (
);
CREATE INDEX IF NOT EXISTS idx_tokens_user ON tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_members_user ON team_members(user_id);
CREATE TABLE IF NOT EXISTS mock_endpoints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
method TEXT NOT NULL,
path TEXT NOT NULL,
status INTEGER NOT NULL DEFAULT 200,
content_type TEXT NOT NULL DEFAULT 'application/json',
body TEXT NOT NULL DEFAULT '',
delay_ms INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL,
UNIQUE(workspace_id, method, path)
);
CREATE INDEX IF NOT EXISTS idx_mock_ws ON mock_endpoints(workspace_id);
CREATE INDEX IF NOT EXISTS idx_workspaces_owner ON workspaces(owner_id);
`)
return err
@@ -333,6 +349,50 @@ func (s *Store) WorkspaceByID(wsID int64) (*Workspace, error) {
return w, err
}
/* ---------------- Mock 接口 ---------------- */
// ReplaceMockEndpoints 全量替换某空间的 Mock 接口(发布即同步)。
func (s *Store) ReplaceMockEndpoints(workspaceID int64, list []mocksrv.Endpoint) error {
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(`DELETE FROM mock_endpoints WHERE workspace_id = ?`, workspaceID); err != nil {
return err
}
now := time.Now().Unix()
stmt, err := tx.Prepare(`INSERT OR REPLACE INTO mock_endpoints(workspace_id, method, path, status, content_type, body, delay_ms, updated_at) VALUES(?,?,?,?,?,?,?,?)`)
if err != nil {
return err
}
defer stmt.Close()
for _, e := range list {
if e.Path == "" {
continue
}
if _, err := stmt.Exec(workspaceID, strings.ToUpper(e.Method), e.Path, e.Status, e.ContentType, e.Body, e.DelayMs, now); err != nil {
return err
}
}
return tx.Commit()
}
func (s *Store) FindMockEndpoint(workspaceID int64, method, path string) (*mocksrv.Endpoint, bool, error) {
e := &mocksrv.Endpoint{}
err := s.db.QueryRow(
`SELECT method, path, status, content_type, body, delay_ms FROM mock_endpoints WHERE workspace_id = ? AND method = ? AND path = ?`,
workspaceID, strings.ToUpper(method), path,
).Scan(&e.Method, &e.Path, &e.Status, &e.ContentType, &e.Body, &e.DelayMs)
if errors.Is(err, sql.ErrNoRows) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
return e, true, nil
}
func (s *Store) SaveWorkspace(wsID int64, data string) (int64, error) {
now := time.Now().Unix()
_, err := s.db.Exec(`UPDATE workspaces SET data = ?, updated_at = ? WHERE id = ?`, data, now, wsID)