From ea3a589f0bc65071c01eb4344642edadd0485892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=8B=A5?= Date: Tue, 14 Apr 2026 18:44:44 +0800 Subject: [PATCH] chore: sync docs, gitea wiki assets, templates Made-with: Cursor --- .gitea/ISSUE_TEMPLATE/bug_report.md | 29 +++ .gitea/ISSUE_TEMPLATE/feature_request.md | 27 +++ .gitea/ISSUE_TEMPLATE/task.md | 19 ++ .gitea/pull_request_template.md | 15 ++ .gitea/workflows/ci.yml | 17 ++ app/admin/journey/page.tsx | 126 ++++++++++ app/admin/layout.tsx | 6 + app/admin/rfm-rules/page.tsx | 227 ++++++++++++++++++ app/admin/user-value/page.tsx | 76 ++++++ app/api/admin/journey-events/route.ts | 67 ++++++ app/api/admin/rfm-rules/[id]/route.ts | 82 +++++++ app/api/admin/rfm-rules/route.ts | 83 +++++++ app/api/admin/user-value-summary/route.ts | 105 ++++++++ app/api/analytics/journey-events/route.ts | 115 +++++++++ app/api/streamer/audience-insights/route.ts | 170 +++++++++++++ app/live/[id]/page.tsx | 127 +++++++++- app/live/page.tsx | 57 ++++- app/mall/points/[game]/client.tsx | 18 +- app/streamer/audience/page.tsx | 117 +++++++++ app/streamer/studio/page.tsx | 11 + lib/analytics/journey-client.ts | 39 +++ lib/analytics/journey-session.ts | 43 ++++ lib/analytics/journey-track.ts | 29 +++ lib/api/streamer-studio.ts | 30 +++ lib/db/mongo/collections.ts | 21 ++ .../OKR任务拆解与排期_王明政_许永平_4-5月.md | 126 ++++++++++ 玩值开发文档/10、项目管理/开发进度总表.md | 7 +- 玩值开发文档/2、架构/03_玩值电竞_数据架构.md | 2 +- .../2、架构/用户旅程埋点字典_WM-O1-KR1-01.md | 62 +++++ .../8、部署/Gitea_API_一键填充说明.md | 64 +++++ 玩值开发文档/8、部署/Gitea同步说明.md | 8 + 玩值开发文档/8、部署/gitea-wiki/Home.md | 38 +++ 玩值开发文档/8、部署/gitea-wiki/_Sidebar.md | 12 + .../8、部署/gitea-wiki/工单与合并请求规范.md | 35 +++ .../8、部署/gitea-wiki/本地开发与运行.md | 37 +++ .../8、部署/gitea-wiki/架构与技术栈.md | 33 +++ .../8、部署/gitea-wiki/版本发布与标签约定.md | 26 ++ .../8、部署/gitea-wiki/直播与主播工作台.md | 30 +++ .../8、部署/gitea-wiki/软件包与制品说明.md | 28 +++ 玩值开发文档/8、部署/gitea-wiki/部署与运维.md | 24 ++ .../8、部署/gitea-wiki/项目概述与范围.md | 23 ++ .../8、部署/gitea-wiki/项目看板使用说明.md | 24 ++ 玩值开发文档/8、部署/sync_gitea_wiki.sh | 60 +++++ 43 files changed, 2289 insertions(+), 6 deletions(-) create mode 100644 .gitea/ISSUE_TEMPLATE/bug_report.md create mode 100644 .gitea/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitea/ISSUE_TEMPLATE/task.md create mode 100644 .gitea/pull_request_template.md create mode 100644 .gitea/workflows/ci.yml create mode 100644 app/admin/journey/page.tsx create mode 100644 app/admin/rfm-rules/page.tsx create mode 100644 app/admin/user-value/page.tsx create mode 100644 app/api/admin/journey-events/route.ts create mode 100644 app/api/admin/rfm-rules/[id]/route.ts create mode 100644 app/api/admin/rfm-rules/route.ts create mode 100644 app/api/admin/user-value-summary/route.ts create mode 100644 app/api/analytics/journey-events/route.ts create mode 100644 app/api/streamer/audience-insights/route.ts create mode 100644 app/streamer/audience/page.tsx create mode 100644 lib/analytics/journey-client.ts create mode 100644 lib/analytics/journey-session.ts create mode 100644 lib/analytics/journey-track.ts create mode 100644 玩值开发文档/10、项目管理/OKR任务拆解与排期_王明政_许永平_4-5月.md create mode 100644 玩值开发文档/2、架构/用户旅程埋点字典_WM-O1-KR1-01.md create mode 100644 玩值开发文档/8、部署/Gitea_API_一键填充说明.md create mode 100644 玩值开发文档/8、部署/gitea-wiki/Home.md create mode 100644 玩值开发文档/8、部署/gitea-wiki/_Sidebar.md create mode 100644 玩值开发文档/8、部署/gitea-wiki/工单与合并请求规范.md create mode 100644 玩值开发文档/8、部署/gitea-wiki/本地开发与运行.md create mode 100644 玩值开发文档/8、部署/gitea-wiki/架构与技术栈.md create mode 100644 玩值开发文档/8、部署/gitea-wiki/版本发布与标签约定.md create mode 100644 玩值开发文档/8、部署/gitea-wiki/直播与主播工作台.md create mode 100644 玩值开发文档/8、部署/gitea-wiki/软件包与制品说明.md create mode 100644 玩值开发文档/8、部署/gitea-wiki/部署与运维.md create mode 100644 玩值开发文档/8、部署/gitea-wiki/项目概述与范围.md create mode 100644 玩值开发文档/8、部署/gitea-wiki/项目看板使用说明.md create mode 100755 玩值开发文档/8、部署/sync_gitea_wiki.sh diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8c90ca9 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug 反馈 +about: 缺陷与异常行为 +title: "[Bug] " +labels: bug +--- + +## 复现步骤 + +1. +2. + +## 期望行为 + + +## 实际行为 + + +## 环境 + +- 分支 / commit: +- 开发 / 生产: +- 浏览器(若相关): + +## 日志或截图(脱敏) + +``` +粘贴关键日志或接口返回 +``` diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.md b/.gitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..03c3fc2 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,27 @@ +--- +name: 功能建议 +about: 新功能或改进 +title: "[Feature] " +labels: enhancement +--- + +## 用户故事 + +作为 … 我希望 … 以便 … + +## 验收标准 + +- [ ] +- [ ] + +## 影响范围 + +- [ ] 直播 / 推流 +- [ ] 支付 / 订单 +- [ ] 管理后台 +- [ ] 其它: + +## 参考文档 + +- Wiki: +- 玩值开发文档路径: diff --git a/.gitea/ISSUE_TEMPLATE/task.md b/.gitea/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..4b5b12d --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/task.md @@ -0,0 +1,19 @@ +--- +name: 任务报备 +about: 运维、迁移、文档等非功能任务 +title: "[Task] " +labels: task +--- + +## 背景 + + +## 要做的事 + +- [ ] +- [ ] + +## 完成定义(DoD) + + +## 风险 / 回滚 diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 0000000..91026b1 --- /dev/null +++ b/.gitea/pull_request_template.md @@ -0,0 +1,15 @@ +## 变更说明 + + +## 关联工单 + +Closes # + +## 自检 + +- [ ] `pnpm lint`(或说明未跑原因) +- [ ] 无 `.env` / 密钥入库 +- [ ] 已更新「玩值开发文档」或 Gitea 百科(若行为或接口有变) + +## 截图 / 演示(可选) + diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..262752f --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,17 @@ +name: CI +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + notice: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: https://gitea.com/actions/checkout@v4 + - name: Placeholder + run: | + echo "Gitea Actions 已接入。若 Runner 已安装 Node/pnpm,可改为:corepack enable && pnpm install && pnpm lint && pnpm build" + ls -la diff --git a/app/admin/journey/page.tsx b/app/admin/journey/page.tsx new file mode 100644 index 0000000..e4e5007 --- /dev/null +++ b/app/admin/journey/page.tsx @@ -0,0 +1,126 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" + +type Row = { + id: string + eventName: unknown + source: unknown + sessionId: unknown + properties: unknown + occurredAt: unknown + receivedAt: unknown +} + +export default function AdminJourneyPage() { + const [userId, setUserId] = useState("") + const [from, setFrom] = useState("") + const [to, setTo] = useState("") + const [loading, setLoading] = useState(false) + const [rows, setRows] = useState([]) + const [error, setError] = useState(null) + + const load = async () => { + setError(null) + if (!userId.trim()) { + setError("请填写用户 MongoDB ObjectId") + return + } + setLoading(true) + try { + const q = new URLSearchParams({ userId: userId.trim(), limit: "200" }) + if (from.trim()) q.set("from", from.trim()) + if (to.trim()) q.set("to", to.trim()) + const res = await fetch(`/api/admin/journey-events?${q}`) + const json = await res.json() + if (!json.success) { + setError(json.message || "加载失败") + setRows([]) + return + } + setRows(json.data || []) + } catch { + setError("网络错误") + setRows([]) + } finally { + setLoading(false) + } + } + + return ( +
+
+

用户旅程查询

+

WM-O1-KR1-03 · 按用户 ID 与时间筛选关键事件时间线

+
+ + + + 筛选 + + +
+ + setUserId(e.target.value)} placeholder="users._id" /> +
+
+ + setFrom(e.target.value)} /> +
+
+ + setTo(e.target.value)} /> +
+ +
+
+ + {error &&

{error}

} + + + + 时间线(新 → 旧) + + + {rows.length === 0 && !loading ? ( +

暂无数据

+ ) : ( +
+ + + + + + + + + + + + {rows.map((r) => ( + + + + + + + + ))} + +
发生时间事件会话属性
{String(r.occurredAt ?? "")}{String(r.eventName ?? "")}{String(r.source ?? "")} + {String(r.sessionId ?? "")} + + {JSON.stringify(r.properties ?? {})} +
+
+ )} +
+
+
+ ) +} diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index 91be2e1..eabcacf 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -25,6 +25,9 @@ import { LayoutDashboard, Trophy, Monitor, + History, + PieChart, + CircleDollarSign, } from "lucide-react" import { Input } from "@/components/ui/input" import { Avatar, AvatarFallback } from "@/components/ui/avatar" @@ -107,6 +110,9 @@ const menuGroups: { title: string; items: { id: string; label: string; icon: typ title: "用户与创作者", items: [ { id: "users", label: "用户管理", icon: Users, path: "/admin/users" }, + { id: "journey", label: "用户旅程", icon: History, path: "/admin/journey" }, + { id: "rfm-rules", label: "RFM规则", icon: PieChart, path: "/admin/rfm-rules" }, + { id: "user-value", label: "用户估值", icon: CircleDollarSign, path: "/admin/user-value" }, { id: "streamers", label: "主播管理", icon: Radio, path: "/admin/streamers" }, { id: "guilds", label: "公会管理", icon: Building2, path: "/admin/guilds" }, { id: "live", label: "直播管理", icon: Gamepad2, path: "/admin/live" }, diff --git a/app/admin/rfm-rules/page.tsx b/app/admin/rfm-rules/page.tsx new file mode 100644 index 0000000..0f0f324 --- /dev/null +++ b/app/admin/rfm-rules/page.tsx @@ -0,0 +1,227 @@ +"use client" + +import { useEffect, useState } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" + +const DEFAULT_DIMENSIONS = [ + { + code: "R" as const, + label: "近因(观看/活跃)", + metricKey: "watch_minutes_30d", + thresholds: [0, 30, 120, 600], + binLabels: ["D", "C", "B", "A"], + }, + { + code: "F" as const, + label: "频次(下单/打赏次数)", + metricKey: "order_count_90d", + thresholds: [0, 1, 3, 10], + binLabels: ["D", "C", "B", "A"], + }, + { + code: "M" as const, + label: "金额(消费额)", + metricKey: "gmv_cents_90d", + thresholds: [0, 10000, 50000, 200000], + binLabels: ["D", "C", "B", "A"], + }, +] + +type RuleRow = Record & { id?: string; _id?: { toString(): string } } + +export default function AdminRfmRulesPage() { + const [list, setList] = useState([]) + const [loading, setLoading] = useState(true) + const [msg, setMsg] = useState(null) + const [editId, setEditId] = useState(null) + const [editJson, setEditJson] = useState("") + + const rowId = (r: RuleRow) => String(r.id ?? r._id?.toString() ?? "") + + const refresh = async () => { + setLoading(true) + setMsg(null) + try { + const res = await fetch("/api/admin/rfm-rules") + const json = await res.json() + if (json.success) setList(json.data || []) + else setMsg(json.message || "加载失败") + } catch { + setMsg("网络错误") + } finally { + setLoading(false) + } + } + + useEffect(() => { + void refresh() + }, []) + + const seedDefault = async () => { + setMsg(null) + try { + const res = await fetch("/api/admin/rfm-rules", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: "default", + status: "draft", + dimensions: DEFAULT_DIMENSIONS, + }), + }) + const json = await res.json() + if (!json.success) { + setMsg(json.message || "创建失败") + return + } + setMsg(`已创建草稿版本 v${json.data?.version ?? ""}`) + await refresh() + } catch { + setMsg("网络错误") + } + } + + const saveDraft = async () => { + if (!editId) return + setMsg(null) + let dimensions: unknown + try { + dimensions = JSON.parse(editJson) + } catch { + setMsg("JSON 格式错误") + return + } + try { + const res = await fetch(`/api/admin/rfm-rules/${editId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ dimensions }), + }) + const json = await res.json() + if (!json.success) { + setMsg(json.message || "保存失败") + return + } + setMsg("已保存草稿维度") + setEditId(null) + setEditJson("") + await refresh() + } catch { + setMsg("网络错误") + } + } + + const activate = async (id: string) => { + setMsg(null) + try { + const res = await fetch(`/api/admin/rfm-rules/${id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "activate" }), + }) + const json = await res.json() + if (!json.success) { + setMsg(json.message || "激活失败") + return + } + setMsg("已激活该版本(同名将其它 active 归档)") + setEditId(null) + setEditJson("") + await refresh() + } catch { + setMsg("网络错误") + } + } + + return ( +
+
+
+

RFM 规则集

+

+ WM-O1-KR2-02:草稿可编辑 JSON 维度;激活后写入 status=active(权限与后台账号体系后续对齐 RBAC) +

+
+ +
+ + {msg &&

{msg}

} + + {editId && ( + + + 编辑草稿 {editId} + + +