本地 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:
1 parent
522e5d6300
commit
d9ccccd280
17 files changed
+1070
-6
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user