feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+125
-25
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user