feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
ci / h5 (push) Canceled after 0s
ci / api (push) Canceled after 0s
ci / ess-docs (push) Canceled after 0s

落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-07 02:26:16 +08:00
co-authored by Cursor
parent 7e9023f0a8
commit 7ab9add5dd
132 changed files with 8276 additions and 491 deletions
+81 -5
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
@@ -60,6 +61,9 @@ func doJSON(t *testing.T, r http.Handler, method, path string, body any, deviceK
if deviceKey != "" {
req.Header.Set("X-Device-Key", deviceKey)
}
if testBearer != "" {
req.Header.Set("Authorization", "Bearer "+testBearer)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code >= 500 {
@@ -79,6 +83,25 @@ func doJSON(t *testing.T, r http.Handler, method, path string, body any, deviceK
return env, key
}
var testBearer string
func mustRegister(t *testing.T, r http.Handler) string {
t.Helper()
testBearer = ""
phone := fmt.Sprintf("1%010d", time.Now().UnixNano()%10_000_000_000)
env, key := doJSON(t, r, http.MethodPost, "/api/v1/auth/register", map[string]any{
"phone": phone, "password": "secret12", "nickname": "测",
}, "")
sess := decodeData[map[string]any](t, env.Data)
tok, _ := sess["token"].(string)
if tok == "" {
t.Fatal("missing token from register")
}
testBearer = tok
t.Cleanup(func() { testBearer = "" })
return key
}
func decodeData[T any](t *testing.T, raw json.RawMessage) T {
t.Helper()
var v T
@@ -91,7 +114,7 @@ func decodeData[T any](t *testing.T, raw json.RawMessage) T {
// Flow 1: create profile → portrait → deep_access mock → detail visible
func TestFlowPortraitDeepAccess(t *testing.T) {
r, _ := setupAPI(t)
var key string
key := mustRegister(t, r)
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "self", "birth_date": "1990-05-12", "display_name": "我",
@@ -136,7 +159,7 @@ func TestFlowPortraitDeepAccess(t *testing.T) {
// Flow 2: two profiles → relation insight → deep_access → tips visible
func TestFlowRelationDeepAccess(t *testing.T) {
r, _ := setupAPI(t)
var key string
key := mustRegister(t, r)
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "self", "birth_date": "1988-03-01", "display_name": "我",
@@ -189,7 +212,7 @@ func TestFlowRelationDeepAccess(t *testing.T) {
// Flow 3: membership mock → entitlement → portrait detail without per-report deep_access
func TestFlowMembershipUnlock(t *testing.T) {
r, _ := setupAPI(t)
var key string
key := mustRegister(t, r)
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "self", "birth_date": "1995-11-07", "display_name": "我",
@@ -232,7 +255,7 @@ func TestFlowMembershipUnlock(t *testing.T) {
// Flow 5: profile update + soft
func TestFlowProfileUpdateDelete(t *testing.T) {
r, _ := setupAPI(t)
var key string
key := mustRegister(t, r)
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "other", "birth_date": "1993-04-04", "display_name": "旧名", "relation_type": "friend",
@@ -262,7 +285,7 @@ func TestFlowProfileUpdateDelete(t *testing.T) {
// Flow 4: profile → ask thread → message → assistant reply + quota
func TestFlowAskThread(t *testing.T) {
r, _ := setupAPI(t)
var key string
key := mustRegister(t, r)
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "self", "birth_date": "1991-02-14", "display_name": "我",
@@ -308,3 +331,56 @@ func TestFlowAskThread(t *testing.T) {
t.Fatalf("expected user+assistant history, got %d", len(items))
}
}
// Flow 4b: exhaust free ask quota → buy ask_pack → can ask again
func TestFlowAskPackPurchase(t *testing.T) {
r, _ := setupAPI(t)
key := mustRegister(t, r)
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "self", "birth_date": "1991-02-14", "display_name": "我",
}, key)
profileID := decodeData[map[string]any](t, env.Data)["id"].(string)
env, key = doJSON(t, r, http.MethodPost, "/api/v1/ask/threads", map[string]any{
"profile_id": profileID, "scene": "self",
}, key)
threadID := decodeData[map[string]any](t, env.Data)["id"].(string)
for i := 0; i < 3; i++ {
_, key = doJSON(t, r, http.MethodPost, "/api/v1/ask/threads/"+threadID+"/messages", map[string]any{
"content": "第几问",
}, key)
}
env, key, status := doJSONExpect(t, r, http.MethodPost, "/api/v1/ask/threads/"+threadID+"/messages", map[string]any{
"content": "应该没额度了",
}, key, 40210)
if status != http.StatusPaymentRequired {
t.Fatalf("expected 402 after free exhaust, got %d %#v", status, env)
}
env, key = doJSON(t, r, http.MethodPost, "/api/v1/orders", map[string]any{
"kind": "ask_pack", "plan": "pack10",
}, key)
orderID := decodeData[map[string]any](t, env.Data)["order_id"].(string)
_, key = doJSON(t, r, http.MethodPost, "/api/v1/orders/"+orderID+"/pay-mock", nil, key)
env, key = doJSON(t, r, http.MethodGet, "/api/v1/ask/quota", nil, key)
q := decodeData[map[string]any](t, env.Data)
rem, _ := q["remaining"].(float64)
paid, _ := q["paid_left"].(float64)
if rem < 10 || paid < 10 {
t.Fatalf("expected paid pack quota, got %#v", q)
}
env, _ = doJSON(t, r, http.MethodPost, "/api/v1/ask/threads/"+threadID+"/messages", map[string]any{
"content": "买完额度继续问",
}, key)
out := decodeData[map[string]any](t, env.Data)
q2, _ := out["quota"].(map[string]any)
rem2, _ := q2["remaining"].(float64)
if rem2 != rem-1 {
t.Fatalf("paid quota should decrease: before=%v after=%v", rem, rem2)
}
}