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
+125 -25
View File
@@ -4,6 +4,14 @@ export type ApiEnvelope<T> = { code: number; message: string; data?: T }
const TOKEN_KEY = 'yuxingu_admin_token'
/** Under /psy/admin use /psy/api; local vite uses /api. */
function adminAPIBase(): string {
if (typeof location !== 'undefined' && location.pathname.startsWith('/psy/admin')) {
return '/psy/api/v1/admin'
}
return '/api/v1/admin'
}
export function getToken(): string | null {
return localStorage.getItem(TOKEN_KEY)
}
@@ -18,7 +26,7 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
if (body !== undefined) headers['Content-Type'] = 'application/json'
const token = getToken()
if (token) headers.Authorization = `Bearer ${token}`
const res = await fetch(`/api/v1/admin${path}`, {
const res = await fetch(`${adminAPIBase()}${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
@@ -30,6 +38,62 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
return env.data as T
}
export type DashboardStats = {
users_total: number
membership_active: number
orders_today: number
paid_cents_today: number
ask_replies_today: number
profiles_total: number
reports_total: number
series?: Array<{
day: string
new_users: number
orders: number
paid_cents: number
ask_replies: number
}>
reports_by_type?: Array<{ type: string; count: number }>
}
export type UserListItem = {
id: string
status: string
phone?: string | null
nickname?: string | null
ask_paid_quota_left: number
profile_count: number
membership_active: boolean
membership_plan?: string | null
created_at: string
}
export type UserDetail = {
id: string
status: string
phone?: string | null
nickname?: string | null
ask_paid_quota_left: number
created_at: string
profiles: Array<{ id: string; relation: string; display_name: string; birth_date?: string }>
reports: Array<{ id: string; type: string; created_at: string }>
membership: {
active: boolean
plan?: string
status: string
expires_at?: string | null
ask_quota_left?: number
}
recent_orders: Array<{
id: string
kind: string
plan?: string
status: string
amount_cents: number
created_at: string
}>
}
export const adminApi = {
login: (username: string, password: string) =>
request<{ token: string; admin: { id: string; username: string } }>('POST', '/auth/login', {
@@ -38,14 +102,16 @@ export const adminApi = {
}),
logout: () => request<{ ok: boolean }>('POST', '/auth/logout'),
me: () => request<{ id: string; username: string }>('GET', '/me'),
stats: () => request<DashboardStats>('GET', '/stats'),
users: (q = '') =>
request<{ items: Array<{ id: string; status: string; created_at: string }> }>(
'GET',
`/users?q=${encodeURIComponent(q)}`,
),
request<{ items: UserListItem[] }>('GET', `/users?q=${encodeURIComponent(q)}`),
user: (id: string) => request<UserDetail>('GET', `/users/${id}`),
grant: (id: string, plan: string) =>
request<{ ok: boolean }>('POST', `/users/${id}/membership/grant`, { plan }),
grantAskQuota: (id: string, delta: number) =>
request<{ ok: boolean; ask_paid_quota_left: number }>('POST', `/users/${id}/ask-quota/grant`, {
delta,
}),
orders: () =>
request<{
items: Array<{
@@ -70,26 +136,60 @@ export const adminApi = {
created_at: string
}>
}>('GET', '/audit-logs'),
analyticsOverview: (from: string, to: string) =>
request<AnalyticsOverview>('GET', `/analytics/overview?from=${from}&to=${to}`),
analyticsPages: (from: string, to: string) =>
request<{ items: AnalyticsPageRow[] }>('GET', `/analytics/pages?from=${from}&to=${to}`),
analyticsExits: (from: string, to: string) =>
request<{ items: AnalyticsExitRow[] }>('GET', `/analytics/exits?from=${from}&to=${to}`),
analyticsClicks: (from: string, to: string) =>
request<{ items: AnalyticsClickRow[] }>('GET', `/analytics/clicks?from=${from}&to=${to}`),
analyticsFunnel: (from: string, to: string) =>
request<{ steps: AnalyticsFunnelStep[] }>('GET', `/analytics/funnel?from=${from}&to=${to}`),
homeTools: () => request<{ items: HomeToolAdmin[] }>('GET', '/home/tools'),
saveHomeTools: (items: HomeToolAdmin[]) =>
request<{ ok: boolean }>('PUT', '/home/tools', { items }),
scales: () => request<{ items: ScaleAdmin[] }>('GET', '/scales'),
patchScale: (id: string, status: 'published' | 'draft') =>
request<{ ok: boolean }>('PATCH', `/scales/${id}`, { status }),
}
export type UserDetail = {
id: string
status: string
created_at: string
profiles: Array<{ id: string; relation: string; display_name: string }>
membership: {
active: boolean
plan?: string
status: string
expires_at?: string
ask_quota_left?: number
}
recent_orders: Array<{
id: string
kind: string
plan?: string
status: string
amount_cents: number
created_at: string
}>
export type HomeToolAdmin = {
id?: string
row_index: number
sort_order: number
path: string
icon: string
label: string
badge?: string | null
badge_tone?: string | null
enabled: boolean
}
export type ScaleAdmin = {
id: string
slug: string
title: string
description: string
status: string
}
export type AnalyticsOverview = {
dau: number
new_users: number
sessions: number
avg_session_ms: number
series?: Array<{ day: string; dau: number; sessions: number }>
}
export type AnalyticsPageRow = {
page_path: string
pv: number
uv: number
avg_dwell_ms: number
exit_count: number
}
export type AnalyticsExitRow = { exit_page: string; count: number }
export type AnalyticsClickRow = { element_id: string; count: number }
export type AnalyticsFunnelStep = { name: string; count: number }