From f50090132d9eacfdd72e2f5ec98b6acb56c6580a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Thu, 30 Apr 2026 16:53:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0wz-app=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=EF=BC=8C=E8=B0=83=E6=95=B4=E6=96=87=E6=A1=A3=E4=BB=A5=E6=98=8E?= =?UTF-8?q?=E7=A1=AE=E7=8E=A9=E5=80=BC=E7=A7=BB=E5=8A=A8=E7=AB=AF=EF=BC=88?= =?UTF-8?q?`new/wz-app`=EF=BC=89=E7=9A=84=E7=A7=B0=E8=B0=93=EF=BC=8C?= =?UTF-8?q?=E7=A6=81=E6=AD=A2=E4=BD=BF=E7=94=A8=E3=80=8C=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E3=80=8D=E7=9A=84=E8=A1=A8=E8=BF=B0?= =?UTF-8?q?=EF=BC=8C=E5=90=8C=E6=97=B6=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=8F=AF=E8=A7=81=E6=96=87=E6=A1=88=E8=A7=84=E8=8C=83=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E7=94=A8=E6=88=B7=E7=95=8C=E9=9D=A2=E4=B8=8D?= =?UTF-8?q?=E6=9A=B4=E9=9C=B2=E6=8A=80=E6=9C=AF=E7=BB=86=E8=8A=82=EF=BC=8C?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E4=B8=8E?= =?UTF-8?q?=E5=90=88=E8=A7=84=E6=80=A7=E3=80=82=E6=AD=A4=E5=A4=96=EF=BC=8C?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=86=E7=9B=B8=E5=85=B3API=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=92=8C=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E4=BA=86=E7=B3=BB=E7=BB=9F=E7=9A=84=E7=A8=B3=E5=AE=9A=E6=80=A7?= =?UTF-8?q?=E4=B8=8E=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/agent/团队/evolution/2026-04-30.md | 3 + .../agent/开发助理/evolution/2026-04-30.md | 17 + .cursor/agent/开发助理/经验清单.md | 3 +- .cursor/agent/开发助理/项目索引/产品.md | 3 +- .cursor/agent/开发助理/项目索引/后端.md | 3 +- .cursor/agent/开发助理/项目索引/小程序.md | 3 +- .cursor/agent/开发助理/项目索引/测试.md | 3 +- .cursor/agent/开发助理/项目索引/玩值.md | 3 +- .cursor/agent/开发助理/项目索引/管理端.md | 3 +- .cursor/agent/智能体-技能-经验总索引.md | 2 +- .cursor/agent/玩值移动端工程师/README.md | 8 +- .../2026-04-30-wz-app目录对接与用户可见文案.md | 48 ++ .../agent/玩值移动端工程师/evolution/索引.md | 1 + .cursor/config/paths.py | 2 + .../2026-04-30_Soul三端文档与接口对齐.md | 107 ++++ .cursor/meeting/README.md | 1 + .cursor/rules/soul-project-boundary.mdc | 11 +- .cursor/rules/wanzhi-project-boundary.mdc | 10 +- .cursor/rules/老板分身-索引.mdc | 8 +- .cursor/skills/assistant-doc-sync/sync-log.md | 6 + .cursor/skills/change-checklist/SKILL.md | 4 +- .cursor/skills/wz-app-dev/SKILL.md | 29 +- new/wz-admin/src/api/wanzhiAdmin.ts | 5 +- new/wz-admin/src/pages/UserValuePage.tsx | 7 +- new/wz-api/.env.production | 4 +- new/wz-api/.gitignore | 5 +- new/wz-api/__pycache__/master.cpython-311.pyc | Bin 22410 -> 22400 bytes .../internal/handler/playhub_aggregate.go | 12 + .../internal/handler/playhub_wz_admin.go | 99 +-- .../handler/playhub_wz_app_catalog.go | 574 ++++++++++++++++++ .../internal/handler/playhub_wz_app_wallet.go | 277 +++++++++ .../internal/handler/playhub_wz_live.go | 11 + .../internal/handler/playhub_wz_register.go | 18 + new/wz-api/internal/store/mongo/auth.go | 67 ++ new/wz-api/master.py | 9 +- new/wz-api/scripts/MONGO_宝塔同步.md | 110 ++++ .../mongo_push_baota_api.cpython-311.pyc | Bin 0 -> 17213 bytes .../mongo_sync_baota.cpython-311.pyc | Bin 0 -> 15774 bytes new/wz-api/scripts/mongo_push_baota_api.py | 286 +++++++++ new/wz-api/scripts/mongo_sync_baota.py | 252 ++++++++ new/wz-app/README.md | 6 +- new/wz-app/dist/index.html | 4 +- new/wz-app/package-lock.json | 7 + new/wz-app/package.json | 1 + new/wz-app/src/api/catalog.ts | 191 ++++++ new/wz-app/src/api/games.ts | 17 + new/wz-app/src/api/live.ts | 7 +- new/wz-app/src/api/orders.ts | 39 ++ .../components/live/LivePlaybackSurface.tsx | 152 +++++ .../src/components/providers/app-provider.tsx | 144 +++-- new/wz-app/src/pages/CoachDetailPage.tsx | 247 ++++---- new/wz-app/src/pages/CoachListPage.tsx | 154 +++-- new/wz-app/src/pages/CourseDetailPage.tsx | 377 +++++------- new/wz-app/src/pages/HomePage.tsx | 164 ++--- new/wz-app/src/pages/HotelPage.tsx | 112 +++- new/wz-app/src/pages/LivePage.tsx | 20 +- new/wz-app/src/pages/MallPage.tsx | 6 +- new/wz-app/src/pages/MallPointsPage.tsx | 9 +- new/wz-app/src/pages/MessagesPage.tsx | 4 +- new/wz-app/src/pages/MomentsPage.tsx | 36 +- new/wz-app/src/pages/PartyDetailPage.tsx | 89 +-- new/wz-app/src/pages/PlanetPage.tsx | 303 +++------ new/wz-app/src/pages/RechargePage.tsx | 8 +- new/wz-app/src/pages/ScreenPage.tsx | 10 +- new/wz-app/src/pages/StarDetailPage.tsx | 182 +++--- new/wz-app/src/pages/VipPage.tsx | 4 +- new/wz-app/src/pages/ZoneDetailPage.tsx | 189 +++--- .../src/pages/guild/GuildDetailPage.tsx | 171 ++++-- new/wz-app/src/pages/guild/GuildListPage.tsx | 150 +++-- .../src/pages/mall/MallCompanionPage.tsx | 321 +++++----- new/wz-app/src/pages/mall/MallCoursesPage.tsx | 105 ++-- new/wz-app/src/pages/mall/MallGearPage.tsx | 122 ++-- .../src/pages/mall/MallPointsGamePage.tsx | 4 +- .../src/pages/mall/MallPointsItemPage.tsx | 3 +- .../pages/services/AccountServicesPage.tsx | 197 +++--- .../src/pages/services/LevelingPage.tsx | 121 ++-- new/wz-app/src/pages/stack/LiveListPage.tsx | 2 +- .../pages/streamer/StreamerAudiencePage.tsx | 10 +- .../src/pages/streamer/StreamerStudioPage.tsx | 10 +- 开发文档/Soul/README.md | 9 + 开发文档/Soul/三端路由与联调说明.md | 78 +++ 开发文档/玩值/2026-04-30-日志.md | 5 + 开发文档/玩值/三端对齐项目清单.md | 2 +- 开发文档/玩值/迁移-API对照表.md | 9 +- 开发文档/玩值/进度-看板.md | 8 +- 85 files changed, 4151 insertions(+), 1672 deletions(-) create mode 100644 .cursor/agent/团队/evolution/2026-04-30.md create mode 100644 .cursor/agent/开发助理/evolution/2026-04-30.md create mode 100644 .cursor/agent/玩值移动端工程师/evolution/2026-04-30-wz-app目录对接与用户可见文案.md create mode 100644 .cursor/meeting/2026-04-30_Soul三端文档与接口对齐.md create mode 100644 new/wz-api/internal/handler/playhub_wz_app_catalog.go create mode 100644 new/wz-api/internal/handler/playhub_wz_app_wallet.go create mode 100644 new/wz-api/scripts/MONGO_宝塔同步.md create mode 100644 new/wz-api/scripts/__pycache__/mongo_push_baota_api.cpython-311.pyc create mode 100644 new/wz-api/scripts/__pycache__/mongo_sync_baota.cpython-311.pyc create mode 100644 new/wz-api/scripts/mongo_push_baota_api.py create mode 100644 new/wz-api/scripts/mongo_sync_baota.py create mode 100644 new/wz-app/src/api/catalog.ts create mode 100644 new/wz-app/src/api/games.ts create mode 100644 new/wz-app/src/api/orders.ts create mode 100644 new/wz-app/src/components/live/LivePlaybackSurface.tsx create mode 100644 开发文档/Soul/README.md create mode 100644 开发文档/Soul/三端路由与联调说明.md create mode 100644 开发文档/玩值/2026-04-30-日志.md diff --git a/.cursor/agent/团队/evolution/2026-04-30.md b/.cursor/agent/团队/evolution/2026-04-30.md new file mode 100644 index 0000000..165d85f --- /dev/null +++ b/.cursor/agent/团队/evolution/2026-04-30.md @@ -0,0 +1,3 @@ +# 团队共享经验 - 2026-04-30 + +Soul 三端契约与文档落点:**miniprogram 仅用 `/api/miniprogram/*`,管理端仅用 `/api/admin/*` 与 `/api/db/*`**;文档统一放在 `开发文档/Soul/`。玩值 **`/api/app/*`** 与前缀分离,禁止混用。未检出 Soul 源码时先做环境与 smoke,代码级对接在检出后执行。 diff --git a/.cursor/agent/开发助理/evolution/2026-04-30.md b/.cursor/agent/开发助理/evolution/2026-04-30.md new file mode 100644 index 0000000..c7911b0 --- /dev/null +++ b/.cursor/agent/开发助理/evolution/2026-04-30.md @@ -0,0 +1,17 @@ +# 开发助理 - 会议与文档归档 - 2026-04-30 + +- 执笔 Soul 专题会议纪要;新建 `开发文档/Soul/` 联调文档;同步 meeting README、sync-log、多角色项目索引。**说明**:本会话工作区不含 `miniprogram`/`soul-admin`/`soul-api` 目录,Soul 接口对接以检出后按文档执行为准;玩值三端仍以 `开发文档/玩值/`、`new/wz-*` 为交付真源。 + +--- + +## 同日补充(术语) + +- **玩值 `new/wz-app`** 条线统一称 **移动端 / H5+C 端**,**不称「小程序智能体」「微信小程序」**;「小程序」保留给 Soul `miniprogram/`。已改 soul/wanzhi boundary、wz-app Skill、paths 注释、`玩值移动端工程师/README`、change-checklist 引言等。 + +--- + +## 同日补充(wz-app 去 mock + C 端文案) + +- **目录数据**:多块页面改接 `catalog.ts`与既有 `mall`/`games`/`streamers` 等,减少硬编码列表;无后端能力时用用户向空态,不写「无 C 口」「GET /api/…」。 +- **界面文案红线**:用户在 App 内**不得**看到接口路径、`Mongo`、`wz-api`、`ext_id`、`埋点` 等开发与库表术语;详见 **`玩值移动端工程师/evolution/2026-04-30-wz-app目录对接与用户可见文案.md`** 与 **`wz-app-dev` Skill**(用户可见文案专节)。 +- **可选后续**:请求层对用户可见 **`Error.message`** 做温和映射,避免遗留技术句。 diff --git a/.cursor/agent/开发助理/经验清单.md b/.cursor/agent/开发助理/经验清单.md index 69a7a2e..d82994f 100644 --- a/.cursor/agent/开发助理/经验清单.md +++ b/.cursor/agent/开发助理/经验清单.md @@ -22,6 +22,7 @@ | 日期 | 角色 | 类型 | 升级 Skill | 摘要 | |------|------|------|------------|------| +| 2026-04-30 | 玩值移动端、开发助理 | 规范/迁移 | **wz-app-dev §7**、**wanzhi boundary** | wz-app 目录页去 mock 接 `/api/app`;用户界面禁止暴露接口路径/Mongo/wz-api 等技术话术;详见玩值移动端 evolution 2026-04-30 | | 2026-04-24 | 玩值三端、开发助理 | 收官实施 | wz-api-dev 可参考 | 纪要好四问;build-info gitCommit;点卡订单管理页与真源表同步 | | 2026-04-24 | 玩值三端、产品、测试、团队、开发助理 | 团队会议 | — | 玩值全项目进度与三端缺口:P0/P1/P2、纪要、看板/三端注记、evolution 与项目索引同步 | | 2026-04-24 | 玩值移动端、开发助理 | 迁移/联调/文档 | wz-app-dev SKILL §5 | wz-app 占位清零:大屏只读、主播/观众 API、公会旅程埋点、动态评论本地态、删 Stub;对照表/看板/MIGRATION 同步 | @@ -69,4 +70,4 @@ --- -**最后更新**:2026-04-22 +**最后更新**:2026-04-30 diff --git a/.cursor/agent/开发助理/项目索引/产品.md b/.cursor/agent/开发助理/项目索引/产品.md index bb40de1..08ebecb 100644 --- a/.cursor/agent/开发助理/项目索引/产品.md +++ b/.cursor/agent/开发助理/项目索引/产品.md @@ -34,9 +34,10 @@ Soul 创业派对产品定位:面向创业者的社区/工具型小程序。 | 2026-04-13 | 会议:按功能同步开发文档;功能完成=实现+文档/索引可追溯;待办见纪要问题区 | 进行中 | | 2026-04-14 | 吸收沉淀:新建 `开发文档/1、需求/需求汇总.md` 与 `开发文档/10、项目管理/运营与变更.md`;需求清单 SYNC/LOOP/MAINT/BACKEND 行 | 进行中 | | 2026-04-24 | 玩值全项目会议:三端「打通」验收定义、P0 域锁定、评论/次线抬主线等待问题区作答;纪见 `.cursor/meeting/2026-04-24_玩值全项目进度与三端缺口.md` | 进行中 | +| 2026-04-30 | 会议:Soul 三端联调文档与小程序→管理端逆推核对;纪元 `.cursor/meeting/2026-04-30_Soul三端文档与接口对齐.md` | 进行中 | > **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置 --- -**最后更新**:2026-04-24 +**最后更新**:2026-04-30 diff --git a/.cursor/agent/开发助理/项目索引/后端.md b/.cursor/agent/开发助理/项目索引/后端.md index e8a8903..90b4ca1 100644 --- a/.cursor/agent/开发助理/项目索引/后端.md +++ b/.cursor/agent/开发助理/项目索引/后端.md @@ -46,9 +46,10 @@ soul-api(Go + Gin + GORM + MySQL)提供三组路由:`/api/miniprogram/*` | 2026-04-13 | 会议:按功能同步开发文档;接口/迁移变更须标路由组与 P0/P1;文档待补齐 | 进行中 | | 2026-04-14 | 吸收沉淀:`需求汇总.md` 增加 BACKEND-P0 行;接口说明与迁移落文档 | 进行中 | | 2026-04-15 | 业务补齐:`mentor_consultation` 支付计价/回调/权益;`CKBJoin` investor 显式失败;`getStandardPrice` 增加 buyerUserID | 已完成 | +| 2026-04-30 | 会议:Soul 三端路由组隔离与玩值 `/api/app` 区分写入 `开发文档/Soul/`;soul-api 检出后补充路由矩阵与 smoke | 进行中 | > **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置 --- -**最后更新**:2026-04-15 +**最后更新**:2026-04-30 diff --git a/.cursor/agent/开发助理/项目索引/小程序.md b/.cursor/agent/开发助理/项目索引/小程序.md index 2fc93a8..34d154f 100644 --- a/.cursor/agent/开发助理/项目索引/小程序.md +++ b/.cursor/agent/开发助理/项目索引/小程序.md @@ -52,9 +52,10 @@ | 2026-04-13 | 会议:按功能同步开发文档;页面级记 miniprogram 接口与分享行为;无接口变更亦标注 | 进行中 | | 2026-04-14 | 吸收沉淀:逆推闭环自检写入 evolution;LOOP 需求行 | 进行中 | | 2026-04-15 | 业务补齐:`app.js` 挂 `err.response`;导师详情微信支付;找伙伴 join 失败展示后端文案 | 已完成 | +| 2026-04-30 | 会议:Soul 三端文档与路由契约;baseUrl/`/api/miniprogram/*`/合法域名对齐见 `开发文档/Soul/三端路由与联调说明.md`;Soul 源码需单独检出后方可改接口 | 进行中 | > **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置 --- -**最后更新**:2026-04-15 +**最后更新**:2026-04-30 diff --git a/.cursor/agent/开发助理/项目索引/测试.md b/.cursor/agent/开发助理/项目索引/测试.md index 0099f1c..3239180 100644 --- a/.cursor/agent/开发助理/项目索引/测试.md +++ b/.cursor/agent/开发助理/项目索引/测试.md @@ -40,7 +40,8 @@ | 2026-04-14 | 吸收沉淀:LOOP 需求与逆推清单纳入回归裁剪依据 | 进行中 | | 2026-04-24 | 玩值全项目会议:P0 联调+回归(点卡/订单/直播/认证)、环境检查(wz-api 端口与新构建);纪见 `.cursor/meeting/2026-04-24_玩值全项目进度与三端缺口.md` | 进行中 | | 2026-04-24 | 玩值 P0 可重复步骤:`scripts/test/wz-playhub-p0.md`(点卡/直播/管理端核对 + `go build ./cmd/server`) | 进行中 | +| 2026-04-30 | 会议:Soul 三端 smoke(路径前缀负例、`/api/miniprogram` 与 admin/db 分离);文档 `开发文档/Soul/三端路由与联调说明.md` | 进行中 | --- -**最后更新**:2026-04-24 +**最后更新**:2026-04-30 diff --git a/.cursor/agent/开发助理/项目索引/玩值.md b/.cursor/agent/开发助理/项目索引/玩值.md index d1f5bfc..26fbaa1 100644 --- a/.cursor/agent/开发助理/项目索引/玩值.md +++ b/.cursor/agent/开发助理/项目索引/玩值.md @@ -22,5 +22,6 @@ | 2026-04-27 | 三端迁移完成度会议 | 纪要 `meeting/2026-04-27_三端迁移完成度与文档同步.md`;结论「主迁移完成、全量对齐 ongoing」;看板/三端清单/日志/团队与玩值 evolution 已同步 | | 2026-04-28 | wz-admin 大屏 API 补齐 | `ScreenViewPage` 接 `GET /api/screen/data/dashboard`(失败回退);`wanzhiAdmin.ts` 增 `fetchScreenDashboardStats`;wz-admin build 通过;看板与 2026-04-28 日志同步 | | 2026-04-28 | wz-admin 实时区补齐 | `ScreenViewPage` 接 `GET /api/screen/streamers`;新增营收 TOP5 与城市分布 Top5;`wanzhiAdmin.ts` 增 `fetchScreenStreamers`;build 通过并同步看板 | +| 2026-04-30 | **new/wz-app(C 端)** | 多块页面去 mock、接 catalog/真实 `/api/app/*`;**用户可见文案**去除接口路径/Mongo/wz-api 等;Skill `wz-app-dev` §7、`wanzhi-project-boundary` 补充;经验 `玩值移动端工程师/evolution/2026-04-30-wz-app目录对接与用户可见文案.md` | -最后更新:2026-04-28 +最后更新:2026-04-30 diff --git a/.cursor/agent/开发助理/项目索引/管理端.md b/.cursor/agent/开发助理/项目索引/管理端.md index 921a69a..e0eb941 100644 --- a/.cursor/agent/开发助理/项目索引/管理端.md +++ b/.cursor/agent/开发助理/项目索引/管理端.md @@ -50,11 +50,12 @@ | 2026-04-02 | 会议:工作进度与需求同步;UsersPage 待接口字段就绪后联调;deploy 文档基址与后端对齐 | 进行中 | | 2026-04-13 | 会议:按功能同步开发文档;页面粒度记 admin/db 依赖;类型债排期入文档 | 进行中 | | 2026-04-14 | 吸收沉淀:MAINT-ADMIN-TS 入需求表;配置入口与运营与变更联动 | 进行中 | +| 2026-04-30 | 会议:Soul 文档《逆推表》核对小程序模块与管理端入口;`VITE_API_BASE_URL` 与 `/api/admin` `/api/db` 约定见 `开发文档/Soul/三端路由与联调说明.md` | 进行中 | > **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置 --- -**最后更新**:2026-04-14 +**最后更新**:2026-04-30 > 注:soul-admin 构建仍有 DistributionPage Order.description 类型错误(与本次迁移无关),待修复。 diff --git a/.cursor/agent/智能体-技能-经验总索引.md b/.cursor/agent/智能体-技能-经验总索引.md index e56b4da..106764e 100644 --- a/.cursor/agent/智能体-技能-经验总索引.md +++ b/.cursor/agent/智能体-技能-经验总索引.md @@ -20,7 +20,7 @@ - **玩值 API**:`skills/wz-api-dev/SKILL.md` — `new/wz-api`、玩值后端 - **玩值管理端**:`skills/wz-admin-dev/SKILL.md` — `new/wz-admin` -- **玩值 C 端 / 移动端(Vite+React in `new/wz-app`)**:`skills/wz-app-dev/SKILL.md` — **`new/wz-app` 为交付主线**;`old/app` 仅对照 +- **玩值移动端 / C 端(Vite+React in `new/wz-app`)**:`skills/wz-app-dev/SKILL.md` — **`new/wz-app` 为交付主线**;**不叫「微信小程序」**,`old/app` 仅对照 - Soul 小程序(归档期仍维护 `miniprogram/` 时):`skills/miniprogram-dev/SKILL.md` - Soul 管理端(`soul-admin/`):`skills/admin-dev/SKILL.md` - Soul 后端(`soul-api/`):`skills/api-dev/SKILL.md` diff --git a/.cursor/agent/玩值移动端工程师/README.md b/.cursor/agent/玩值移动端工程师/README.md index 56ed3e0..b7c5164 100644 --- a/.cursor/agent/玩值移动端工程师/README.md +++ b/.cursor/agent/玩值移动端工程师/README.md @@ -1,7 +1,9 @@ -# 玩值移动端工程师(玩值 C 端,React / Next) +# 玩值移动端工程师(`new/wz-app`,Vite + React) -- **绑定目录(主线)**:`old/app/`、`old/components/` 等玩值 **C 端**代码 +- **称谓**:本角色负责 **玩值用户侧移动端 / H5 C 端**,**不叫「微信小程序」或「Soul 小程序智能体」**;后者指归档内的 **Soul `miniprogram/`**。 +- **绑定目录(主线)**:**`new/wz-app/`**(唯一新功能交付) +- **对照/考古**:`old/app/`、`old/components/` — 仅核对漏页、漏接口,非真源 - **主 Skill**:`.cursor/skills/wz-app-dev/SKILL.md` - **API 前缀**:`/api/app/*`(见 `开发文档/玩值/迁移-API前缀决议.md`) -负责 Next+React 页面、与 wz-api 的 C 端契约;不维护微信原生 `miniprogram/`;**`new/wz-app`(uni)已归档,勿作为新功能主落点**(见 `new/wz-app/README.md`)。 +不维护 Soul 微信原生 `miniprogram/`(另有「小程序开发工程师」归档角色)。 diff --git a/.cursor/agent/玩值移动端工程师/evolution/2026-04-30-wz-app目录对接与用户可见文案.md b/.cursor/agent/玩值移动端工程师/evolution/2026-04-30-wz-app目录对接与用户可见文案.md new file mode 100644 index 0000000..82613d9 --- /dev/null +++ b/.cursor/agent/玩值移动端工程师/evolution/2026-04-30-wz-app目录对接与用户可见文案.md @@ -0,0 +1,48 @@ +# 2026-04-30 — wz-app 目录接口去 mock 与用户可见文案规范 + +## 场景 + +玩值 C 端 `new/wz-app` 从硬编码/mock 清单迁到 **`/api/app/*`** 真实数据(与 `wz-api` Mongo 目录只读口径一致);随后发现 **空态/副标题/Tost** 中写了 `GET /api/...`、Mongo 集合名、`wz-api` 等,**会直接出现在用户界面**,体验与合规不达标。 + +## 结论 / 做法 + +### 1. 数据来源与封装 + +- **目录只读**:统一走 **`src/api/catalog.ts`**(`skipAuth: true` 的列表/详情:`guilds`、`venues`、`products`、`content/items`、`stars`、`game-accounts`、`leveling/products`、`party/rooms` 等),与后端 `playhub_*_wz_app_*` 注册路由对齐。 +- **已有专项封装**:主播列表 `streamers.ts`;游戏分区 `games.ts`;点卡 `mall.ts`。**新 C 口子先查对照表再在 `catalog` 或独立模块补齐**,避免页面内手写 `fetch`。 + +### 2. 移除 mock 的边界 + +- **能接库的页**:guild/venue/mall/companion/coach/course/star(部分)/planet 派对 Tab/派对详情/专区/星球匹配等,优先 **加载态 + 空态**,不要保留假卡片当「演示」。 +- **尚无 C 口的域**:车队列表、部分社交流等用 **简短用户向说明**(如「暂未开放」「敬请期待」),**不写**「无 C 端列表接口」一类开发话术。 +- **主播工作台 / 大屏 `/screen`**:仍可能偏运营,但也要避免把 **路径字符串**、**栈名** 摆在标题或正文里。 + +### 3. 用户可见文案(强制) + +以下 **禁止** 出现在用户在 `new/wz-app` 能看到的任何 UI(含 Toast `description`、页面脚注、大屏说明)——接口说明应写在 **`开发文档/玩值/迁移-API对照表.md`** 或代码注释: + +- HTTP 方法与路径字面量:`GET /api/app/...`、`/api/admin/...`、反引号路径 +- 内部实现词:`Mongo`、`collection`、`wz-api`、`contentItems`、`ext_id`、`埋点`、`streamerId`(除类型安全的代码字段名外)、`Vite 代理`、`SKU`/`福禄` 等供应链内部词 +- 空态:**用产品语言**,例如「暂时没有可展示的点卡,请稍后再来」,**不要** 「暂无数据 · 接口 xxx」 + +**允许**:`npm run build`、PR 描述、`.md` 文档、**仅开发者可见**的注释。 + +### 4. 全局报错 + +- `request.ts` 等在 **502/非 JSON** 时抛给用户看的 Error 文案仍可能偏技术;后续若产品上要求「一律温和提示」,再在请求层统一映射(本次未改层,规则先记在 Skill)。 + +### 5. Gotchas + +- **TS**:空态重置 `messages` 等状态时,若有 `system`/`user` 联合类型,`useState` 需显式 **`ChatMsg[]`**,否则会收窄类型报错。 +- **地图/列表**:传给弹层的参数用 **VenueRow** 或 `{ name, lat, lng }` 分清,不要将「城市默认定位」伪造成 `VenueRow`。 +- **索引号**:`guild.id` 等对用户可称「公会编号」,避免写 **Mongo ext_id**。 + +## 升级 Skill / 规则 + +- `.cursor/skills/wz-app-dev/SKILL.md`:新增 **「用户可见文案」**专节。 +- `.cursor/rules/wanzhi-project-boundary.mdc`:**玩值 C 端**界面禁止暴露接口路径与技术栈内幕(与 Skill 呼应)。 + +## 待续 + +- 剩余页面若有 mock(装扮、路演、活动等)按需接 API 或统一空壳。 +- 可选:`request.ts` 对用户可见错误做一次 **文案脱敏映射**。 diff --git a/.cursor/agent/玩值移动端工程师/evolution/索引.md b/.cursor/agent/玩值移动端工程师/evolution/索引.md index 13efe12..8d3d863 100644 --- a/.cursor/agent/玩值移动端工程师/evolution/索引.md +++ b/.cursor/agent/玩值移动端工程师/evolution/索引.md @@ -2,6 +2,7 @@ | 日期 | 主题 | |------|------| +| 2026-04-30 | [wz-app 目录对接与用户可见文案规范](2026-04-30-wz-app目录对接与用户可见文案.md) | | 2026-04-27 | [三端迁移完成度会议](2026-04-27-三端迁移完成度会议.md) | | 2026-04-24 | [全项目会议与主次线](2026-04-24-全项目会议与主次线.md) | | 2026-04-24 | [wz-app 占位补齐与 wz-api 对接](2026-04-24-wz-app占位补齐与wz-api对接.md) | diff --git a/.cursor/config/paths.py b/.cursor/config/paths.py index e9e15dd..496963c 100644 --- a/.cursor/config/paths.py +++ b/.cursor/config/paths.py @@ -66,6 +66,8 @@ LOG_EVOLUTION = SCRIPTS / "进化日志.md" TEMPLATE_EXPERIENCE = SCRIPTS / "经验模板.md" # ========== 角色 → agent 目录名映射 ========== +# 玩值用户侧:**玩值移动端工程师** + `new/wz-app`。勿把该角色称为「微信小程序」; +# 「小程序」「小程序开发工程师」映射指 Soul `miniprogram/`(归档目录)。 ROLE_TO_AGENT = { # 管理层 "老板分身": "老板分身", diff --git a/.cursor/meeting/2026-04-30_Soul三端文档与接口对齐.md b/.cursor/meeting/2026-04-30_Soul三端文档与接口对齐.md new file mode 100644 index 0000000..159902b --- /dev/null +++ b/.cursor/meeting/2026-04-30_Soul三端文档与接口对齐.md @@ -0,0 +1,107 @@ +# 会议纪要 - 2026-04-30 | Soul 三端文档同步与小程序—管理端—API 对齐 + +> 本文件由**助理橙子**在会议结束后整理。 + +--- + +## 基本信息 + +- **时间**:2026-04-30 12:00 +- **议题**:根据现有功能更新开发文档;按小程序能力推测管理端是否齐全合理;小程序、管理端与 API 对接 +- **触发方式**:开个会 / 团队会议 +- **参与角色**:乘风(主持)、产品经理、后端开发、管理端开发工程师、小程序开发工程师、测试人员、助理橙子(书记) + +--- + +## 各角色发言 + +### 【产品经理】 + +验收仍以 **C 端真实路径 + 数据来源可配置** 为准。从小程序已有的登录、阅读、VIP、分销、提现、代付、找伙伴/留资等板块看,运营侧应对应 **用户可查、订单可核、内容与 VIP 可调、提现可审、链接与推广可配**;若仅有 C 端接口无后台入口,须在需求表标「不可运营」并降级优先级。 + +### 【后端开发】 + +Soul 必须坚持 **miniprogram / admin / db** 三组分离;接口变更要落文档(路径、方法、关键字段、错误码)。当前 **部分工作区未检出 soul-api**,无法在本会话中提交路由级补丁;检出后应以 `router` 注册表与现有 handler 为准做一次 **路径清单与调用方矩阵**。 + +### 【管理端开发工程师】 + +管理端功能在历史稳定版中对 **用户、订单、提现、VIP、章节内容、分销、代付、推广设置、链接/CKB** 等覆盖面较完整。风险提示:**小程序新加的只读展示**若依赖新字段,管理端列表/表单未同步则运营不可见——属于「不全」而非「不合理」。需在内容页、用户详情、订单/分销页保持字段与后端一致。 + +### 【小程序开发工程师】 + +所有请求前缀 **`/api/miniprogram/*`**;`app.request` 的 baseUrl 必须与微信合法域名、soul-api 部署一致。**分享、scene、代付跳转、singlePage** 等需在联调文档或可验收清单中写明,避免仅接接口未报验。 + +### 【测试人员】 + +三端联调以 **文档 + 网络面板** 为真源:小程序网络里不得出现 `/api/admin`;管理端不得出现 `/api/miniprogram`。部署变更后优先跑 **登录—列表—一条写—后台核对** 的 smoke;融合与回归范围以项目索引与 `开发文档/Soul/` 为准。 + +--- + +## 讨论过程 + +- **【管理端开发工程师】回复【产品经理】**:「齐全」定义为 **每个 C 端资金/权益相关动作** 在后台可追溯或可配置;纯展示型页面可只需只读接口,不强制加管理页。 +- **【后端开发】回复【小程序开发工程师】**:预览内容、提现错误码、代付受益人等规则以后端为准,文档中注明 **errorCode / needBindWechat** 等分支,避免前端猜状态。 +- **【乘风】**:本仓库当前焦点在 **玩值 `new/wz-*`** 时,Soul 三端需 **单独检出** 才能改代码;文档层先统一 **契约与自检清单**,避免与玩值 `/api/app` 混淆。 + +--- + +## 会议决议 + +1. **新增/更新开发文档**:在仓库 `开发文档/Soul/` 下建立 **README** 与 **《三端路由与联调说明》**,写清路由组、环境变量、小程序↔管理端能力对照表、联调 smoke、与玩值 API 前缀区别。 +2. **管理端齐全性**:在 Soul 历史能力范围内 **整体合理且较齐全**;缺口模式统一记为 **「C 端已接、后台无入口/无字段」**,由产品决定是否补管理端。 +3. **对接动作**:Soul 栈检出后,分别配置 **小程序 baseUrl**、**soul-admin 的 `VITE_API_BASE_URL`**、**soul-api 对外域名与白名单**;本仓库内 **玩值三端** 继续以 `new/wz-api` 路由与 `VITE_API_BASE_URL` 为准,**不与 Soul 混前缀**。 +4. **待确认项**:各环境实际 API 域名、是否合并工作区、router/ossConfig 等历史待办仍按后端索引跟进。 + +--- + +## 待办事项 + +| 责任角色 | 任务 | 优先级 | 截止建议 | +|---------|------|--------|---------| +| 全员 | 检出或挂载 `miniprogram`、`soul-admin`、`soul-api` 后按《三端路由与联调说明》做 smoke | 高 | 下次发版前 | +| 后端开发 | soul-api 路由与调用方矩阵文档化(可选:导出 OpenAPI 或维护 md 表) | 中 | 2026-05-15 | +| 管理端开发工程师 | 对照《逆推表》核对近期小程序新功能是否缺列表/配置页 | 中 | 2026-05-15 | +| 小程序开发工程师 | 核对 `app.request` baseUrl 与各环境合法域名 | 高 | 持续 | +| 测试人员 | 增加「跨路径违规」负例到回归清单 | 中 | 2026-05-15 | + +--- + +## 问题与作答区 + +| # | 问题 | 责任角色 | 作答 | +|---|------|---------|------| +| 1 | 当前团队使用的 soul-api 生产/预发基址与证书域名是否已在三端配置统一? | 后端开发 + 运维 | (待补充) | +| 2 | Soul 三端是否计划与玩值仓合并为 monorepo,还是保持独立仓库? | 产品经理 | (待补充) | +| 3 | 小程序最近一次发版新增页面是否已全部在《以界面定需求》或索引中登记? | 产品经理 + 小程序开发工程师 | (待补充) | + +--- + +## 各角色经验与业务理解更新 + +### 产品经理 + +- 用 **小程序模块 → 管理端配套** 表格做缺口扫描,比纯凭记忆列功能更稳。 + +### 后端开发 + +- 文档中强调 **路由组** 与 **玩值 `/api/app` 隔离**,减少联调串环境。 + +### 管理端开发工程师 + +- 「齐全」= 运营可追溯 + 字段对齐;不等于每个 C 端页都对应一个独立后台页。 + +### 小程序开发工程师 + +- baseUrl、合法域名、`err.response` 与后端 errorCode 分支需与联调说明一致。 + +### 测试人员 + +- **路径前缀负例** 与 smoke 可快速发现配置错误。 + +### 团队共享 + +- 文档位置:`开发文档/Soul/`;未检出 Soul 代码时以 **契约 + 环境** 推进,代码级对接在检出后执行。 + +--- + +*会议纪要由助理橙子生成 | 会议收尾已更新 sync-log 与项目索引* diff --git a/.cursor/meeting/README.md b/.cursor/meeting/README.md index ed5c907..ac4be18 100644 --- a/.cursor/meeting/README.md +++ b/.cursor/meeting/README.md @@ -58,6 +58,7 @@ YYYY-MM-DD_会议主题.md | 日期 | 主题 | 参与角色 | 文件 | |------|------|---------|------| +| 2026-04-30 | Soul 三端文档与接口对齐(小程序↔管理端↔API 契约、检出与玩值区分) | 产品、后端、管理端、小程序、测试、橙子 | [2026-04-30_Soul三端文档与接口对齐.md](2026-04-30_Soul三端文档与接口对齐.md) | | 2026-04-27 | 玩值三端迁移完成度与开发文档同步(主迁移完成 / 全量对齐 ongoing) | 产品、玩值 API、管理端、移动端、测试、橙子 | [2026-04-27_三端迁移完成度与文档同步.md](2026-04-27_三端迁移完成度与文档同步.md) | | 2026-04-24 | 玩值全项目进度、三端缺口与收官排期(乘风主持,P0/P1/P2) | 产品、玩值 API、管理端、移动端、测试、橙子 | [2026-04-24_玩值全项目进度与三端缺口.md](2026-04-24_玩值全项目进度与三端缺口.md) | | 2026-04-23 | 玩值三端:直播 + 点卡 接口与功能打通 | 玩值 API、管理端、移动端、产品 | [2026-04-23_玩值三端直播点卡打通.md](2026-04-23_玩值三端直播点卡打通.md) | diff --git a/.cursor/rules/soul-project-boundary.mdc b/.cursor/rules/soul-project-boundary.mdc index 6495b6a..805e8b7 100644 --- a/.cursor/rules/soul-project-boundary.mdc +++ b/.cursor/rules/soul-project-boundary.mdc @@ -21,7 +21,7 @@ alwaysApply: true | **新版管理端** | **new-soul/soul-admin/** | 新版参考实现,迁移时对照 | soul-api | | **玩值 API** | **new/wz-api/** | 玩值 Go + Gin + GORM + MySQL | - | | **玩值管理端** | **new/wz-admin/** | 玩值 React + Vite 管理后台 | wz-api | -| **玩值 C 端(主线)** | **`new/wz-app/`** | Vite + **React**(玩值用户侧 **唯一新功能交付**) | wz-api `/api/app/*`;见 `new/wz-app/README.md` | +| **玩值移动端(主线)** | **`new/wz-app/`** | Vite + **React**:玩值用户侧 **不叫「微信小程序」**,见 `README.md` | wz-api `/api/app/*` | | **玩值 old C 端(对照)** | **`old/app/` 等** | Next.js 历史实现:**仅对照** 是否漏页/漏接口,**不** 作为新需求真源 | wz-api `/api/app/*`(双轨期可读) | | **old 非 App** | **old/** 脚本/Mongo 工具等 | 种子与对照,与 wz-api 双轨期按需 | 非 wz 管理端调用的 C 口 | @@ -29,7 +29,8 @@ alwaysApply: true ## 核心原则 -- 小程序只调 `/api/miniprogram/*`;管理端只调 `/api/admin/*`、`/api/db/*`;禁止混用。 +- **术语**:**玩值用户侧(`new/wz-app`)称「移动端 / C 端」**,**不称「小程序」**。**「微信小程序 / 小程序」** 在本仓库默认指 Soul **`miniprogram/`**(微信原生),另有路由与角色,勿与玩值移动端混称。 +- Soul **微信原生小程序**只调 `/api/miniprogram/*`;Soul 管理端只调 `/api/admin/*`、`/api/db/*`;**禁止混用**。玩值 C 端只调 **`/api/app/*`**(见 `wanzhi-project-boundary.mdc`)。 - 变更完成必过 soul-change-checklist.mdc;聊天中说「变更完成」「检查一下」「准备提交」时主动触发检查。 - **需求即执行 + 零提问**:收到需求后**禁止**列出分析表格再问用户选哪个执行,**禁止**「是否帮你执行」类确认。正确做法:内部定方案 → **直接改代码/跑命令** → 回复结果。缺信息:先查仓库与配置推断;**仅**在验证码、缺失密钥、不可逆删除等无法代劳时极简说明。 - **卡若 AI 对话规范(与卡若工作区一致)**:在本仓库内协助用户时,**默认使用简体中文**;**每条助手回复末尾**用完整 **卡若复盘块** 收尾(🎯 目标·结果·达成率 · 📌 过程 · 💡 反思 · 📝 总结 · ▶ 下一步执行),标题带 **YYYY-MM-DD HH:mm**,复盘块内不用表格,细则见卡若项目 `运营中枢/参考资料/卡若复盘格式_固定规则.md`(多根工作区时请 Read 该文件)。Mongo 同步、飞书 webhook 等以卡若 `.cursor/rules/karuo-ai.mdc` 为准(本仓库 Agent 在能执行脚本时同样执行对话留存脚本)。 @@ -53,7 +54,7 @@ alwaysApply: true | soul-api/ | 后端开发 | `.cursor/skills/api-dev/SKILL.md` | | new/wz-api/ | 玩值 API 工程师 | `.cursor/skills/wz-api-dev/SKILL.md` | | new/wz-admin/ | 玩值管理端工程师 | `.cursor/skills/wz-admin-dev/SKILL.md` | -| **new/wz-app/**(Vite+React,玩值 C 端主线) | 玩值移动端工程师 | `.cursor/skills/wz-app-dev/SKILL.md` | +| **`new/wz-app/`**(玩值移动端,Vite+React) | 玩值移动端工程师 | `.cursor/skills/wz-app-dev/SKILL.md` | | old/app/、old/components/(**对照**,非新功能真源) | 玩值移动端工程师 | `.cursor/skills/wz-app-dev/SKILL.md` | | 开发文档/玩值/ | 助理橙子 | `.cursor/skills/assistant-doc-sync/SKILL.md` | | 开发文档/1、需求/、临时需求池/ | 产品经理 | `.cursor/skills/product-manager/SKILL.md` | @@ -69,8 +70,8 @@ alwaysApply: true | wz-api、玩值 API、new/wz-api | 玩值 API 工程师 | `.cursor/skills/wz-api-dev/SKILL.md` | | 管理端、soul-admin、React、后台管理 | 管理端开发工程师 | `.cursor/skills/admin-dev/SKILL.md` | | wz-admin、玩值管理端、new/wz-admin | 玩值管理端工程师 | `.cursor/skills/wz-admin-dev/SKILL.md` | -| 小程序、miniprogram、C 端、微信小程序 | 小程序开发工程师 | `.cursor/skills/miniprogram-dev/SKILL.md` | -| 玩值移动端、玩值 C 端、**new/wz-app**、Vite、(对照用 **old/app**) | 玩值移动端工程师 | `.cursor/skills/wz-app-dev/SKILL.md` | +| **Soul** 微信小程序、**Soul** 小程序、miniprogram、WXML/WXSS、微信原生(**非**玩值) | 小程序开发工程师 | `.cursor/skills/miniprogram-dev/SKILL.md` | +| **玩值**移动端、玩值 C 端、**`new/wz-app`**、Vite+H5、`wz-app`、(对照 **`old/app`**) | 玩值移动端工程师 | `.cursor/skills/wz-app-dev/SKILL.md` | | 产品、需求、验收、排期、需求文档 | 产品经理 | `.cursor/skills/product-manager/SKILL.md` | | 测试、测试用例、回归测试、功能测试、QA | 测试人员 | `.cursor/skills/testing/SKILL.md` | diff --git a/.cursor/rules/wanzhi-project-boundary.mdc b/.cursor/rules/wanzhi-project-boundary.mdc index 2bdebc6..c9abcc5 100644 --- a/.cursor/rules/wanzhi-project-boundary.mdc +++ b/.cursor/rules/wanzhi-project-boundary.mdc @@ -16,16 +16,18 @@ alwaysApply: false |--------|------|------|----------| | API | `new/wz-api/` | Go + Gin + GORM + MySQL | - | | 管理端 | `new/wz-admin/` | React + Vite 管理后台 | wz-api `/api/admin/*`、`/api/db/*` | -| **玩值 C 端(主线)** | **`new/wz-app/`** | **Vite + React**:玩值用户侧 H5/壳 **唯一交付目录**;**非** uni-app(无根 `pages.json`),勿用 HBuilderX uni 编译;见 `README.md` | wz-api **`/api/app/*`**(禁止直连 admin 前缀;Vite 代理见该目录配置) | +| **玩值移动端(主线)** | **`new/wz-app/`** | **Vite + React**:玩值用户侧 **不叫「微信小程序/小程序」**;**唯一新功能交付**;见 `README.md` | wz-api **`/api/app/*`**(禁止直连 admin 前缀;Vite 代理见该目录配置) | | 对照/考古(不扩新功能) | **`old/app/`** 等 | **Next.js** 历史 C 端:仅用于 **与 `new/wz-app` 对照** 是否漏页、漏接口;新需求 **不写** 于此 | 可读 wz-api;**不得**作为新功能真源 | | 原始/对照 | 其它 `old/**` 脚本与 Mongo 工具等 | 种子与迁移脚本对照 | 与 wz-api 双轨迁移期按需 | ## 核心原则 -- **玩值 C 端**只调 `/api/app/*`(及文档规定的 `/api/public/*`、`/api/screen/*` 若适用);**管理端**只调 `/api/admin/*`、`/api/db/*`;**禁止混用**。 +- **称谓**:编辑 **`new/wz-app`** 时角色为 **玩值移动端工程师**;对外描述用 **移动端 / H5+C 端**,**不叫「微信小程序」或「小程序」**(二者在本仓库留给 Soul **`miniprogram/`**)。 +- **玩值 C 端(`new/wz-app`)**只调 `/api/app/*`(及文档规定的 `/api/public/*`、`/api/screen/*` 若适用);**管理端**只调 `/api/admin/*`、`/api/db/*`;**禁止混用**。 - 契约变更必须同步 **`开发文档/玩值/迁移-API对照表.md`** 与 **`进度-看板.md`**。 - 变更完成走 **`.cursor/skills/change-checklist/SKILL.md`**(玩值三端:wz-api、wz-admin、**`new/wz-app` C 端**;`old/` 仅对照)。 - **需求即执行 + 零提问**:缺信息先查 **`new/`** 与开发文档,**`old/`** 仅作漏项核对;仅验证码、密钥、不可逆删除等不可代劳时极简说明。 +- **玩值 C 端用户可见文案**:`new/wz-app` 的页面/Tost 等面向用户的文字 **禁止** 暴露 **`/api/...` 路径**、**`Mongo`/库表/collection**、**`wz-api`** 及同类开发运维术语(详细清单见 **`.cursor/skills/wz-app-dev/SKILL.md` §7**);契约与排障说明写在 **`开发文档/玩值/迁移-API对照表.md`** 或注释。 ## 角色推断与 Skill 加载(必须 Read 完整 SKILL) @@ -35,7 +37,7 @@ alwaysApply: false |----------|----------|----------------------| | `new/wz-api/` | 玩值 API 工程师 | `.cursor/skills/wz-api-dev/SKILL.md` | | `new/wz-admin/` | 玩值管理端工程师 | `.cursor/skills/wz-admin-dev/SKILL.md` | -| **`new/wz-app/`** | **玩值 C 端主线**(Vite+React);**非** uni、**勿** 用 HBuilderX uni 编译 | `.cursor/skills/wz-app-dev/SKILL.md` | +| **`new/wz-app/`** | **玩值移动端主线**(Vite+React;**不叫微信小程序**);**非** uni、**勿** HBuilderX uni 编译 | `.cursor/skills/wz-app-dev/SKILL.md` | | `old/app/`、`old/components/` | **对照/考古**;新功能 **不** 在此实现 | `.cursor/skills/wz-app-dev/SKILL.md`(读 Skill 知边界;优先改 `new/wz-app`) | | `开发文档/玩值/` | 助理橙子(文档) | `.cursor/skills/assistant-doc-sync/SKILL.md` | @@ -45,6 +47,6 @@ alwaysApply: false |--------|-----------| | wz-api、玩值 API、new/wz-api、Go、Gin、玩值后端 | `.cursor/skills/wz-api-dev/SKILL.md` | | wz-admin、玩值管理端、new/wz-admin | `.cursor/skills/wz-admin-dev/SKILL.md` | -| 玩值移动端、玩值 C 端、Next / Vite、**`new/wz-app`**、`old/app`、**wz-app 角色** | `.cursor/skills/wz-app-dev/SKILL.md` | +| **玩值**移动端、`new/wz-app`、wz-app、玩值 C 端(Vite/H5)、Next(**仅** **`old/app`** 对照) | `.cursor/skills/wz-app-dev/SKILL.md` | | 跨端、迁移闭环、垂直切片 | `.cursor/skills/role-flow-control/SKILL.md` | | old 对照、Next 迁移、路由矩阵 | `开发文档/玩值/迁移-API对照表.md` + `.cursor/skills/next-split/SKILL.md` | diff --git a/.cursor/rules/老板分身-索引.mdc b/.cursor/rules/老板分身-索引.mdc index 4ad10fc..1254f20 100644 --- a/.cursor/rules/老板分身-索引.mdc +++ b/.cursor/rules/老板分身-索引.mdc @@ -34,7 +34,7 @@ alwaysApply: true 1. 从对话中提取:问题描述、解决过程、关键决策、可提炼的规则方向 2. **推断目标角色**(可多选): - - **`new/wz-app` 玩值 C 端(Vite+React,主线)**→**玩值移动端工程师**;`old/app` 仅对照/考古,经验可写但须注明**非交付真源**;`miniprogram/` 微信原生→**小程序开发工程师**(经验归档目录) + - **`new/wz-app` 玩值移动端(Vite+React,主线)**→**玩值移动端工程师**(**不叫「小程序智能体」**;玩值移动端 **≠** 微信原生小程序);`old/app` 仅对照/考古,经验可写但须注明**非交付真源**;Soul **`miniprogram/`** 微信原生→**小程序开发工程师**(经验归档目录) - `new/wz-admin`→**玩值管理端工程师**;`soul-admin/`→**管理端开发工程师**(归档) - `new/wz-api`→**玩值 API 工程师**;`soul-api/`→**后端工程师**(归档) - 产品/需求/config→**开发助理**(新经验默认)或按需读归档「产品经理」 @@ -50,9 +50,9 @@ alwaysApply: true | 推断场景 | target_roles | |----------|--------------| -| **`new/wz-app` 玩值 C 端、Vite/React(主线)** | `["玩值移动端工程师"]` | -| `old/app` 玩值 C 端(**对照**,非新功能真源) | `["玩值移动端工程师"]` | -| 小程序/WXML/微信(Soul) | `["小程序开发工程师"]` | +| **`new/wz-app` 玩值移动端、Vite/React(主线);口语「C 端用户侧」** | `["玩值移动端工程师"]` | +| `old/app` 玩值对照(**非新功能真源**) | `["玩值移动端工程师"]` | +| Soul 微信原生、WXML/**`miniprogram/`**(**非 `new/wz-app`**) | `["小程序开发工程师"]` | | `new/wz-admin` | `["玩值管理端工程师"]` | | 管理端/React/admin(Soul) | `["管理端开发工程师"]` | | `new/wz-api` | `["玩值API工程师"]` | diff --git a/.cursor/skills/assistant-doc-sync/sync-log.md b/.cursor/skills/assistant-doc-sync/sync-log.md index baf711d..cf0e2a4 100644 --- a/.cursor/skills/assistant-doc-sync/sync-log.md +++ b/.cursor/skills/assistant-doc-sync/sync-log.md @@ -6,6 +6,12 @@ --- +## 2026-04-30 + +- 会议收尾:Soul 三端文档与接口对齐;新增 `开发文档/Soul/README.md`、`三端路由与联调说明.md`;纪要 `meeting/2026-04-30_Soul三端文档与接口对齐.md`;团队+开发助理 evolution;后端/管理端/小程序/测试索引各一行(本行) +- **术语**:玩值 `new/wz-app` 称「移动端」不称「小程序」;已同步 soul-project-boundary、wanzhi boundary、wz-app Skill、老板分身、`玩值移动端工程师/README`、paths 注释、change-checklist、智能体总索引、`agent/开发助理/evolution/2026-04-30.md` +- **吸收经验**:玩值移动端 + 开发助理 — wz-app 目录对接 `/api/app`、去 mock、**C 端 UI 禁止技术向文案**(`玩值移动端工程师/evolution/2026-04-30-wz-app目录对接与用户可见文案.md`,`wz-app-dev` §7,`wanzhi-project-boundary` 一条,经验清单/项目索引玩值/sync-log) + ## 2026-04-28 - 玩值继续落地:wz-admin `ScreenViewPage` 接 `GET /api/screen/data/dashboard`(失败回退展示值),`wanzhiAdmin.ts` 新增 `fetchScreenDashboardStats`;`开发文档/玩值/进度-看板.md`、`2026-04-28-日志.md`、项目索引玩值 已同步 diff --git a/.cursor/skills/change-checklist/SKILL.md b/.cursor/skills/change-checklist/SKILL.md index 7c2929a..ef020e1 100644 --- a/.cursor/skills/change-checklist/SKILL.md +++ b/.cursor/skills/change-checklist/SKILL.md @@ -1,12 +1,12 @@ --- name: soul-change-check -description: Soul/玩值变更关联检查。miniprogram/soul-admin/soul-api 或 new/wz-api、new/wz-admin、new/wz-app 玩值 C 端 任一变更后必过。三端关联、防漏改、字段/接口同步。Use when 变更完成, 改完检查, 关联层, 防漏改. +description: Soul/玩值变更关联检查。miniprogram/soul-admin/soul-api 或玩值 new/wz-api、new/wz-admin、new/wz-app(移动端)任一变更后必过。「小程序」= Soul miniprogram;玩值移动不叫小程序。 --- # Soul 创业派对 - 变更关联检查 Skill 当你在 **miniprogram**、**soul-admin**、**soul-api**,或 **玩值** 的 **`new/wz-api`**、**`new/wz-admin`**、**玩值 C 端 `old/app` 等** 中做**功能新增、字段/接口修改、优化**时,使用本 Skill 做「关联层检查」,减少漏改。 -玩值三端前缀:**管理端** `/api/admin/*`、`/api/db/*`;**C 端(`new/wz-app`)** `/api/app/*`(禁止混用)。**`old/app`** 仅对照、非新功能真源。每处接口变更须同步 **`开发文档/玩值/迁移-API对照表.md`** 与 **`进度-看板.md`**(若该功能已登记)。 +玩值三端前缀:**管理端** `/api/admin/*`、`/api/db/*`;**移动端(`new/wz-app`)** 使用 **`/api/app/*`**(本条线用户侧**不叫「微信小程序」**)。Soul **微信小程序**仍为 **`miniprogram/`** + **`/api/miniprogram/*`**。**`old/app`** 仅对照、非新功能真源。每处接口变更须同步 **`开发文档/玩值/迁移-API对照表.md`** 与 **`进度-看板.md`**(若该功能已登记)。 --- diff --git a/.cursor/skills/wz-app-dev/SKILL.md b/.cursor/skills/wz-app-dev/SKILL.md index c7d804c..2b9f3bd 100644 --- a/.cursor/skills/wz-app-dev/SKILL.md +++ b/.cursor/skills/wz-app-dev/SKILL.md @@ -1,9 +1,9 @@ --- name: wz-app-dev -description: Trigger when 编辑 new/wz-app 玩值 C 端、或对照 old/app 时必读。新功能只在 new/wz-app + wz-api;old 仅核对遗漏。仅调 wz-api /api/app/*。勿用 HBuilderX 将 new/wz-app 当 uni 编译。 +description: Trigger when 编辑 new/wz-app(玩值移动端 / H5+Vite React)或对照 old/app 时必读。本角色条线称「移动端」,不称「微信小程序/小程序」。新功能只在 new/wz-app + wz-api;仅调 /api/app/*。勿用 HBuilderX 当 uni。 --- -# 玩值 - 移动端 / C 端开发 Skill(`new/wz-app` 主线 + `old/` 对照) +# 玩值移动端开发 Skill(`new/wz-app`,不称小程序) **交付真源**:玩值用户侧功能 **一律在 `new/wz-app/`(Vite + React)** 与 **`new/wz-api`** 上实现;**`old/app/`、`old/components/`** 为 **Next 历史 C 端**,**仅用于对照**「是否漏页面、漏接口、漏交互」,**不在此写新业务能力**(紧急修复线上等例外由负责人定)。 @@ -48,7 +48,8 @@ description: Trigger when 编辑 new/wz-app 玩值 C 端、或对照 old/app 时 ## 5. `new/wz-app` 能力清单(持续与 wz-api 对齐) -- **点卡/商城**:`src/api/mall.ts` — `GET /api/app/mall/point-cards`、`POST /api/app/orders/point-card` 等;页面如 `MallPointsPage`。 +- **Mongo 目录只读(anonymous OK)**:`src/api/catalog.ts` — guilds / venues / products / content/items / stars / game-accounts / leveling / party rooms 等与 wz-api 注册的 **`/api/app/*`** 只读一览;列表页优先封装在此,避免页面内散落硬编码数组。 +- **点卡/商城**:`src/api/mall.ts` — `GET /api/app/mall/point-cards`、`POST /api/app/orders/point-card`;**订单/钱包**:`src/api/orders.ts` — `GET /api/app/orders/me`、`POST /api/app/wallet/recharge`、`POST /api/app/wallet/consume`(`AppProvider.pay` 已登录走 consume);页面如 `MallPointsPage`、`OrdersPage`、`RechargePage`。 - **直播**:`src/api/live.ts` — `live/context`、`comments`、`gift`;`LivePage` 等。 - **大屏**:如 **`/screen`** — `GET /api/screen/data/dashboard`;可并联 **`GET /api/app/health`**、**`GET /api/public/build-info`**。 - **旅程/埋点**:**`POST /api/app/analytics/journey-events`**(`src/lib/journey.ts` 等)。 @@ -62,3 +63,25 @@ description: Trigger when 编辑 new/wz-app 玩值 C 端、或对照 old/app 时 - **漏项核对**:新需求在 `new/wz-app` 落地后,可用 **`old/app` 同路由** 扫一眼是否少入口、少字段、少错误态。 - **禁止长期双轨**:同一业务不要在 `old` 与 `new/wz-app` 各写一套;契约以 **wz-api + 文档** 为真源,**只维护 `new/wz-app` 调用侧**。 + +--- + +## 7. 用户可见文案(红线 · 产品与合规) + +适用于 **`new/wz-app`** 所有用户能看见的文案:**页面正文、标题、空态、按钮说明、Toast 标题与描述** 等。「日志 / PR / `.md` / 代码注释」可保留技术表述。 + +### 禁止出现在 UI 的字面内容 + +- **接口路径与方法**:如 `GET /api/app/...`、`/api/admin/...`、`/api/screen/...`,以及反引号包裹的路径。 +- **内部实现称呼**:如 `Mongo`、`wz-api`、`collection`、`contentItems`、`ext_id`、`SKU`、供应链专有名词、`埋点`、`streamerId`(面向用户的句子中)、`Vite`、代理、`HBuilderX` 等开发与运维术语。 +- **「开发向空态」**:例如「暂无数据 · 接口 xxx」「请在 Mongo xxx 维护」——应改为用户语言,如「暂时没有可展示的点卡,请稍后再来」「敬请期待」。 + +### 应当怎么做 + +- 接口说明写在 **`开发文档/玩值/迁移-API对照表.md`** 或代码注释,**不写进** C 端界面。 +- 无数据的模块:**加载态 + 简短业务说明**,可附带站内跳转(如「前往点卡馆」);与后端能力相关的排障留给客服/后台,不向终端暴露路径。 +- 若需串联 **多条 API** 或对账说明,用词保持 **订单/上架/客服** 等产品词,不出现库表名。 + +### 经验与案例 + +详见 **`.cursor/agent/玩值移动端工程师/evolution/2026-04-30-wz-app目录对接与用户可见文案.md`**(内含目录 mock Removal、catalog 封装、Toast/大屏注意点)。 diff --git a/new/wz-admin/src/api/wanzhiAdmin.ts b/new/wz-admin/src/api/wanzhiAdmin.ts index e2570cd..19ed3cb 100644 --- a/new/wz-admin/src/api/wanzhiAdmin.ts +++ b/new/wz-admin/src/api/wanzhiAdmin.ts @@ -164,8 +164,9 @@ export async function fetchFinance(): Promise { return r.success && r.data ? r.data : null } -export async function fetchUserValueSummary(): Promise | null> { - const r = await get>>(`${WZ}/user-value-summary`) +export async function fetchUserValueSummary(userId?: string): Promise | null> { + const q = userId ? `?userId=${encodeURIComponent(userId)}` : '' + const r = await get>>(`${WZ}/user-value-summary${q}`) return r.success && r.data ? r.data : null } diff --git a/new/wz-admin/src/pages/UserValuePage.tsx b/new/wz-admin/src/pages/UserValuePage.tsx index ef88362..65f66c6 100644 --- a/new/wz-admin/src/pages/UserValuePage.tsx +++ b/new/wz-admin/src/pages/UserValuePage.tsx @@ -20,12 +20,12 @@ export default function UserValuePage() { setLoading(true) try { if (adminUseApi()) { - const json = await fetchUserValueSummary() + const json = await fetchUserValueSummary(userId.trim()) if (!json) { setErr('接口返回为空') return } - setData({ ...json, queriedUserId: userId.trim() }) + setData(json) } else { setData({ ...MOCK_USER_VALUE_SUMMARY, userId: userId.trim() }) } @@ -41,7 +41,8 @@ export default function UserValuePage() {

用户估值摘要

- 后端当前为占位汇总 GET /api/admin/wz/user-value-summary;仅记录查询 ID 供后续接入。 + GET /api/admin/wz/user-value-summary:无参数为平台计数;填写用户 ID 后带上 userId{' '} + 可查单用户摘要(订单、绑定、旅程采样)。

diff --git a/new/wz-api/.env.production b/new/wz-api/.env.production index 30bb8e9..d1177e7 100644 --- a/new/wz-api/.env.production +++ b/new/wz-api/.env.production @@ -12,10 +12,12 @@ APP_VERSION=0.0.0 SKIP_PROD_SECRET_CHECK=staging # 必填:MongoDB(唯一业务库) -MONGODB_URI=mongodb://wzdj:NR6aLyyEaZKWZXky@43.139.27.93:27017/wzdj?authSource=wzdj +MONGODB_URI=mongodb://wzdj:NR6aLyyEaZKWZXky@127.0.0.1:27017/wzdj?authSource=wzdj # 已废弃(Mongo-only):原 MySQL,可留空或删除 # DB_DSN= + + # 玩值 API 公网根地址(无尾斜杠;回调与 H5 依赖) API_BASE_URL=https://wzapi.quwanzhi.com diff --git a/new/wz-api/.gitignore b/new/wz-api/.gitignore index 2aca771..a86ac06 100644 --- a/new/wz-api/.gitignore +++ b/new/wz-api/.gitignore @@ -3,4 +3,7 @@ log/ wz-api server.exe wz-api.exe -deploy/ \ No newline at end of file +deploy/ + +# mongodump 持久目录(可能含业务数据,勿提交) +build/mongo_dump_wz/ \ No newline at end of file diff --git a/new/wz-api/__pycache__/master.cpython-311.pyc b/new/wz-api/__pycache__/master.cpython-311.pyc index 3eafce18ff6fe92a4c33afe3692bc89b84670cfa..94bd1eb3ec6462b7231b8668bbcdbff3dc9283fb 100644 GIT binary patch delta 198 zcmeBL&)Be@k#{*SFBbz4gmHh)wAjddiiMfAylV1emT8QxldD)A*bIPj#XBb-U|q^_ z!2F_|_Z2zs&3SB*%#61tpWs}_Xt%kXYdeUe&v%M3a`P{~ee8@mn|F$(vof}AR+M37 z0!pdNy3=+5FxxoSpIOW_Qc& uOpLsn|63cfGm30Bbl%O%sK5EGM-&I6^Jb608_cZyj4U6NHmioFG64W)LP2~0 delta 228 zcmZoz&)Bt|k#{*SFBbz4*c|$pxpyP)DHa~S@~V_9|ALat{Ji4Hw^*hzrcN$obzsv4 zDk|PJc{}S;&I``L7v(~($c1c9VT)vD{55$W=Q>8~&AD9LK^#@SQ;cDoKk)5iXUyEZ zUM!uJv3aw&3@Z~*N>(nHv1fC)oCk<=MWKap_U1@MMpnk|BgM@q7l5_+m91c6LD!e;yN%ueUkcxt*0!XY*r^C=N!4&31t| Rm|1xkSw6^YmJUs20sv0YNSXiu diff --git a/new/wz-api/internal/handler/playhub_aggregate.go b/new/wz-api/internal/handler/playhub_aggregate.go index 6c1bc34..103a2e5 100644 --- a/new/wz-api/internal/handler/playhub_aggregate.go +++ b/new/wz-api/internal/handler/playhub_aggregate.go @@ -304,6 +304,18 @@ func playhubRecentOrdersSlice(orders []playhubUnifiedOrder) []gin.H { return out } +// playhubDashboardEmpty 与前端 DashboardData 同形:Mongo 不可读或未接入时使用,不向管理端伪造业务规模。 +func playhubDashboardEmpty() gin.H { + return gin.H{ + "stats": gin.H{ + "totalUsers": 0, "totalStreamers": 0, "totalOrders": 0, "totalRevenue": 0.0, + "todayNewUsers": 0, "todayOrders": 0, "activeStreamers": 0, "pendingPawns": 0, + }, + "recentUsers": []gin.H{}, + "recentOrders": []gin.H{}, + } +} + func playhubScreenDashboardStats() gin.H { u, _ := wzstore.List(wzstore.CollUsers, 5000) s, _ := wzstore.List(wzstore.CollStreamers, 2000) diff --git a/new/wz-api/internal/handler/playhub_wz_admin.go b/new/wz-api/internal/handler/playhub_wz_admin.go index c505355..b78b7f6 100644 --- a/new/wz-api/internal/handler/playhub_wz_admin.go +++ b/new/wz-api/internal/handler/playhub_wz_admin.go @@ -53,7 +53,7 @@ func WzAdminUsers(c *gin.Context) { return } if len(rows) == 0 { - c.JSON(http.StatusOK, gin.H{"success": true, "data": playhubMockUsers()}) + c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}}) return } var out []interface{} @@ -63,12 +63,6 @@ func WzAdminUsers(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "data": out}) } -func playhubMockUsers() []gin.H { - return []gin.H{ - {"id": "1", "name": "张伟", "phone": "13812345678", "status": "active", "isVip": true, "balance": 1280, "createdAt": time.Now().UTC().Format(time.RFC3339)}, - } -} - // WzAdminUserGet GET /api/admin/wz/users/:id func WzAdminUserGet(c *gin.Context) { wzAdminJSONGet(c, wzstore.CollUsers, c.Param("id")) } @@ -147,8 +141,12 @@ func WzAdminStreamers(c *gin.Context) { return } rows, err := wzstore.List(wzstore.CollStreamers, 500) - if err != nil || len(rows) == 0 { - c.JSON(http.StatusOK, gin.H{"success": true, "data": playhubLytiaoSeed()}) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "data": []interface{}{}, "message": err.Error()}) + return + } + if len(rows) == 0 { + c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}}) return } out := make([]interface{}, 0, len(rows)) @@ -171,17 +169,6 @@ func WzAdminStreamers(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "data": out}) } -func playhubLytiaoSeed() []gin.H { - now := time.Now().UTC().Format(time.RFC3339) - return []gin.H{{ - "id": "lytiao-0-demo", "name": "示例导师", "title": "王者荣耀", "intro": "占位种子", - "game": "王者荣耀", "gameId": 1, "tags": []string{"电竞"}, "avatar": "", "coverImage": "", - "level": 20, "fans": 100000, "hotValue": 0, "isLive": false, "totalGifts": 0, "totalIncome": 0, - "commissionRate": 0.7, "status": "approved", "source": "lytiao", "city": "深圳", - "bindingCount": 0, "guildName": nil, "createdAt": now, "updatedAt": now, - }} -} - // WzAdminStreamerGet GET func WzAdminStreamerGet(c *gin.Context) { id := c.Param("id") @@ -251,26 +238,15 @@ func WzAdminSeedScreen(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "message": "seed-screen 占位:可在数据库写入 streamers 后重试"}) } -// WzAdminDashboard GET +// WzAdminDashboard GET — 仅从 Mongo 聚合;库不可用时返回「全零」结构,不注入演示数据。 func WzAdminDashboard(c *gin.Context) { if _, err := wzstore.List(wzstore.CollUsers, 1); err != nil { - c.JSON(http.StatusOK, gin.H{"success": true, "data": playhubDashboardMock()}) + c.JSON(http.StatusOK, gin.H{"success": true, "data": playhubDashboardEmpty()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": playhubDashboardPayload()}) } -func playhubDashboardMock() gin.H { - return gin.H{ - "stats": gin.H{ - "totalUsers": 2847, "totalStreamers": 36, "totalOrders": 12568, "totalRevenue": 386520, - "todayNewUsers": 23, "todayOrders": 156, "activeStreamers": 12, "pendingPawns": 5, - }, - "recentUsers": []gin.H{{"id": "1", "name": "张伟", "phone": "138****5678", "createdAt": time.Now().UTC().Format(time.RFC3339)}}, - "recentOrders": []gin.H{{"orderNo": "WZ001", "itemName": "示例订单", "amount": 99, "createdAt": time.Now().UTC().Format(time.RFC3339)}}, - } -} - // WzAdminFinance GET func WzAdminFinance(c *gin.Context) { tx, _ := wzstore.List(wzstore.CollTransactions, 300) @@ -335,9 +311,62 @@ func WzAdminOrderStatus(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true}) } -// WzAdminUserValueSummary GET +// WzAdminUserValueSummary GET ?userId= — 无 userId 时返回平台量级;有 userId 时返回该用户文档 + 聚合统计(基于统一订单与用户旅程采样)。 func WzAdminUserValueSummary(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"message": "占位汇总:接入业务指标后可替换", "stats": gin.H{}}}) + uid := strings.TrimSpace(c.Query("userId")) + if uid == "" { + nu, _ := wzstore.Count(wzstore.CollUsers) + ns, _ := wzstore.Count(wzstore.CollStreamers) + nOrders := len(playhubCollectUnifiedOrders()) + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{ + "scope": "platform", + "stats": gin.H{ + "registeredUsers": nu, "streamers": ns, "unifiedOrderRows": int64(nOrders), + }, + "hint": "附带查询参数 userId= 可查看单用户在库摘要", + }}) + return + } + doc, err := wzstore.Get(wzstore.CollUsers, uid) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "用户不存在"}) + return + } + orders := playhubCollectUnifiedOrders() + var nUserOrders int + var paidLikeSum float64 + for _, o := range orders { + if o.UserID != uid { + continue + } + nUserOrders++ + switch strings.ToLower(o.Status) { + case "paid", "completed", "delivered": + paidLikeSum += o.Amount + } + } + bindCnt, _ := wzstore.CountActiveBindingsForUser(uid) + jrows, _ := wzstore.List(wzstore.CollUserJourneyEvents, 800) + jn := 0 + for _, r := range jrows { + m, e := wzstore.BodyMap(&r) + if e != nil { + continue + } + if playhubStr(m["userId"]) == uid { + jn++ + } + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{ + "queriedUserId": uid, + "user": wzstore.ToResponse(doc), + "stats": gin.H{ + "unifiedOrdersForUser": nUserOrders, + "amountSumPaidCompletedLike": paidLikeSum, + "activeStreamerBindings": bindCnt, + "journeyEventsInRecentSample": jn, + }, + }}) } // WzAdminMallImport POST diff --git a/new/wz-api/internal/handler/playhub_wz_app_catalog.go b/new/wz-api/internal/handler/playhub_wz_app_catalog.go new file mode 100644 index 0000000..5080c9e --- /dev/null +++ b/new/wz-api/internal/handler/playhub_wz_app_catalog.go @@ -0,0 +1,574 @@ +package handler + +import ( + "net/http" + "sort" + "strconv" + "strings" + + "wz-api/internal/wzstore" + + "github.com/gin-gonic/gin" +) + +func playhubListingOkForApp(m map[string]interface{}) bool { + st := strings.ToLower(strings.TrimSpace(playhubStr(m["status"]))) + if st == "" { + return true + } + switch st { + case "active", "approved", "listed", "selling", "completed", "open", "available": + return true + case "inactive", "suspended", "dissolved", "deleted", "banned", "off_shelf": + return false + default: + return true + } +} + +func playhubVenueMatchesType(m map[string]interface{}, filter string) bool { + filter = strings.ToLower(strings.TrimSpace(filter)) + if filter == "" { + return true + } + t := strings.ToLower(playhubStr(m["type"])) + switch filter { + case "cafe": + return strings.Contains(t, "cafe") || strings.Contains(t, "网咖") || strings.Contains(t, "电竞馆") + case "hotel": + return strings.Contains(t, "hotel") || strings.Contains(t, "酒店") + default: + return t == filter || strings.Contains(t, filter) + } +} + +func playhubTruthy(v interface{}) bool { + if v == nil { + return false + } + switch x := v.(type) { + case bool: + return x + default: + return strings.ToLower(playhubStr(v)) == "true" || playhubStr(v) == "1" + } +} + +func playhubFansShortLabel(f float64) string { + if f >= 10000 { + return strconv.FormatFloat(f/10000, 'f', 1, 64) + "万" + } + if f <= 0 { + return "0" + } + return strconv.FormatFloat(f, 'f', 0, 64) +} + +// WzAppGuildsList GET /api/app/guilds +func WzAppGuildsList(c *gin.Context) { + rows, err := wzstore.List(wzstore.CollGuilds, 200) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "服务暂不可用"}) + return + } + var data []gin.H + for _, r := range rows { + m, e := wzstore.BodyMap(&r) + if e != nil { + continue + } + if strings.ToLower(playhubStr(m["status"])) == "dissolved" { + continue + } + if !playhubListingOkForApp(m) && strings.TrimSpace(playhubStr(m["status"])) != "" { + continue + } + data = append(data, gin.H{ + "id": r.ExtID, + "name": playhubStr(m["name"]), + "logo": playhubStr(m["logo"]), + "coverImage": playhubStr(m["coverImage"]), + "description": playhubStr(m["description"]), + "memberCount": playhubFloat(m["memberCount"]), + "maxMembers": playhubFloat(m["maxMembers"]), + "level": playhubFloat(m["level"]), + "totalIncome": playhubFloat(m["totalIncome"]), + "games": m["games"], + "tags": m["tags"], + "status": playhubStr(m["status"]), + }) + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": data}) +} + +// WzAppGuildByID GET /api/app/guilds/:id +func WzAppGuildByID(c *gin.Context) { + id := c.Param("id") + if !playhubIsHexID(id) { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效 id"}) + return + } + d, err := wzstore.Get(wzstore.CollGuilds, id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) + return + } + m, err := wzstore.BodyMap(d) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) + return + } + o := gin.H{ + "id": d.ExtID, + "name": playhubStr(m["name"]), + "logo": playhubStr(m["logo"]), + "coverImage": playhubStr(m["coverImage"]), + "description": playhubStr(m["description"]), + "ownerId": playhubStr(m["ownerId"]), + "memberCount": playhubFloat(m["memberCount"]), + "maxMembers": playhubFloat(m["maxMembers"]), + "level": playhubFloat(m["level"]), + "games": m["games"], + "tags": m["tags"], + "benefits": m["benefits"], + "requirements": playhubStr(m["requirements"]), + "totalIncome": playhubFloat(m["totalIncome"]), + "commissionRate": playhubFloat(m["commissionRate"]), + "status": playhubStr(m["status"]), + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": o}) +} + +// WzAppVenuesList GET /api/app/venues?type=cafe|hotel(宽松匹配 Mongo type 字段) +func WzAppVenuesList(c *gin.Context) { + typeFilter := c.Query("type") + rows, err := wzstore.List(wzstore.CollEsportsVenues, 300) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "服务暂不可用"}) + return + } + var data []gin.H + for _, r := range rows { + m, e := wzstore.BodyMap(&r) + if e != nil { + continue + } + if !playhubListingOkForApp(m) && playhubStr(m["status"]) != "" { + continue + } + if !playhubVenueMatchesType(m, typeFilter) { + continue + } + data = append(data, gin.H{ + "id": r.ExtID, + "name": playhubStr(m["name"]), + "type": playhubStr(m["type"]), + "city": playhubStr(m["city"]), + "address": playhubStr(m["address"]), + "image": playhubStr(m["coverImage"]), + "coverImage": playhubStr(m["coverImage"]), + "priceNote": playhubStr(m["priceNote"]), + "discount": playhubFloat(m["discount"]), + "status": playhubStr(m["status"]), + "lat": playhubFloat(m["lat"]), + "lng": playhubFloat(m["lng"]), + }) + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": data}) +} + +// WzAppVenueByID GET /api/app/venues/:id +func WzAppVenueByID(c *gin.Context) { + id := c.Param("id") + if !playhubIsHexID(id) { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效 id"}) + return + } + d, err := wzstore.Get(wzstore.CollEsportsVenues, id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": wzstore.ToResponse(d)}) +} + +// WzAppProductsList GET /api/app/products?category= +func WzAppProductsList(c *gin.Context) { + cat := strings.ToLower(strings.TrimSpace(c.Query("category"))) + rows, err := wzstore.List(wzstore.CollProducts, 500) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "服务暂不可用"}) + return + } + var data []gin.H + for _, r := range rows { + m, e := wzstore.BodyMap(&r) + if e != nil { + continue + } + if !playhubListingOkForApp(m) && playhubStr(m["status"]) != "" { + continue + } + pc := strings.ToLower(playhubStr(m["category"])) + pg := strings.ToLower(playhubStr(m["game"])) + if cat != "" && !strings.Contains(pc, cat) && !strings.Contains(pg, cat) { + continue + } + data = append(data, gin.H{ + "id": r.ExtID, + "name": playhubStr(m["name"]), + "category": playhubStr(m["category"]), + "game": playhubStr(m["game"]), + "price": playhubFloat(m["price"]), + "coverImage": playhubStr(m["coverImage"]), + "image": coalesceImg(playhubStr(m["coverImage"]), playhubStr(m["image"])), + "stock": playhubFloat(m["stock"]), + "status": playhubStr(m["status"]), + }) + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": data}) +} + +func coalesceImg(a, b string) string { + if strings.TrimSpace(a) != "" { + return a + } + return b +} + +// WzAppProductByID GET /api/app/products/:id +func WzAppProductByID(c *gin.Context) { + id := c.Param("id") + if !playhubIsHexID(id) { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效 id"}) + return + } + d, err := wzstore.Get(wzstore.CollProducts, id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) + return + } + m, err := wzstore.BodyMap(d) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) + return + } + o := gin.H{ + "id": d.ExtID, + "name": playhubStr(m["name"]), + "description": playhubStr(m["description"]), + "category": playhubStr(m["category"]), + "game": playhubStr(m["game"]), + "price": playhubFloat(m["price"]), + "coverImage": playhubStr(m["coverImage"]), + "image": coalesceImg(playhubStr(m["coverImage"]), playhubStr(m["image"])), + "detailImages": m["detailImages"], + "stock": playhubFloat(m["stock"]), + "status": playhubStr(m["status"]), + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": o}) +} + +// WzAppContentItemsList GET /api/app/content/items (课程等内容) +func WzAppContentItemsList(c *gin.Context) { + itemType := strings.TrimSpace(strings.ToLower(c.Query("type"))) + rows, err := wzstore.List(wzstore.CollContentItems, 400) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "服务暂不可用"}) + return + } + var data []gin.H + for _, r := range rows { + m, e := wzstore.BodyMap(&r) + if e != nil { + continue + } + if !playhubListingOkForApp(m) && playhubStr(m["status"]) != "" { + continue + } + t := strings.ToLower(playhubStr(m["type"])) + if itemType != "" && t != "" && !strings.Contains(t, itemType) { + continue + } + data = append(data, gin.H{ + "id": r.ExtID, + "title": playhubStr(m["title"]), + "name": playhubStr(m["title"]), + "type": playhubStr(m["type"]), + "author": playhubStr(m["author"]), + "price": playhubFloat(m["price"]), + "coverImage": coalesceImg(playhubStr(m["coverImage"]), playhubStr(m["image"])), + "image": coalesceImg(playhubStr(m["coverImage"]), playhubStr(m["image"])), + "students": playhubFloat(m["students"]), + "game": playhubStr(m["game"]), + "status": playhubStr(m["status"]), + }) + } + sort.Slice(data, func(i, j int) bool { + return playhubFloat(data[i]["students"]) > playhubFloat(data[j]["students"]) + }) + c.JSON(http.StatusOK, gin.H{"success": true, "data": data}) +} + +// WzAppContentItemByID GET /api/app/content/items/:id +func WzAppContentItemByID(c *gin.Context) { + id := c.Param("id") + if !playhubIsHexID(id) { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效 id"}) + return + } + d, err := wzstore.Get(wzstore.CollContentItems, id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) + return + } + m, err := wzstore.BodyMap(d) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) + return + } + img := coalesceImg(playhubStr(m["coverImage"]), playhubStr(m["image"])) + o := gin.H{ + "id": d.ExtID, + "title": playhubStr(m["title"]), + "type": playhubStr(m["type"]), + "author": playhubStr(m["author"]), + "price": playhubFloat(m["price"]), + "coverImage": img, + "cover": img, + "description": playhubStr(m["description"]), + "sessions": m["sessions"], + "chapters": m["chapters"], + "game": playhubStr(m["game"]), + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": o}) +} + +// WzAppStarsList GET /api/app/stars(大神/Coach 数据源) +func WzAppStarsList(c *gin.Context) { + rows, err := wzstore.List(wzstore.CollStars, 300) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "服务暂不可用"}) + return + } + var data []gin.H + for _, r := range rows { + m, e := wzstore.BodyMap(&r) + if e != nil { + continue + } + if strings.ToLower(playhubStr(m["status"])) == "suspended" { + continue + } + if !playhubListingOkForApp(m) && playhubStr(m["status"]) != "" { + continue + } + data = append(data, gin.H{ + "id": r.ExtID, + "name": playhubStr(m["name"]), + "title": playhubStr(m["title"]), + "game": playhubStr(m["game"]), + "avatar": playhubStr(m["avatar"]), + "coverImage": playhubStr(m["coverImage"]), + "tags": m["tags"], + "fans": playhubFloat(m["fans"]), + "hotValue": playhubFloat(m["hotValue"]), + "rating": playhubFloat(m["rating"]), + "price": playhubFloat(m["hourlyRate"]), + "hourlyRate": playhubFloat(m["hourlyRate"]), + "isOnline": playhubTruthy(m["isOnline"]), + "status": playhubStr(m["status"]), + }) + } + sort.Slice(data, func(i, j int) bool { + return playhubFloat(data[i]["hotValue"]) > playhubFloat(data[j]["hotValue"]) + }) + c.JSON(http.StatusOK, gin.H{"success": true, "data": data}) +} + +// WzAppStarByID GET /api/app/stars/:id +func WzAppStarByID(c *gin.Context) { + id := c.Param("id") + if !playhubIsHexID(id) { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效 id"}) + return + } + d, err := wzstore.Get(wzstore.CollStars, id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) + return + } + m, err := wzstore.BodyMap(d) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) + return + } + partyID := playhubStr(m["partyRoomId"]) + img := coalesceImg(playhubStr(m["coverImage"]), playhubStr(m["avatar"])) + o := gin.H{ + "id": d.ExtID, + "name": playhubStr(m["name"]), + "title": playhubStr(m["title"]), + "intro": playhubStr(m["intro"]), + "game": playhubStr(m["game"]), + "games": m["games"], + "avatar": playhubStr(m["avatar"]), + "cover": img, + "coverImage": playhubStr(m["coverImage"]), + "rating": playhubFloat(m["rating"]), + "fans": playhubFansShortLabel(playhubFloat(m["fans"])), + "followers": playhubFloat(m["fans"]), + "tags": m["tags"], + "achievements": m["achievements"], + "hourlyRate": playhubFloat(m["hourlyRate"]), + "price": playhubFloat(m["hourlyRate"]), + "isOnline": playhubTruthy(m["isOnline"]), + "partyRoomId": partyID, + "socialLinks": m["socialLinks"], + } + services := m["services"] + if services != nil { + o["services"] = services + } else { + rate := playhubFloat(m["hourlyRate"]) + o["services"] = []gin.H{ + {"id": "default-1", "name": "单局陪玩", "price": rate, "desc": "接单后商议时间"}, + } + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": o}) +} + +// WzAppGameAccountsList GET /api/app/game-accounts?game= +func WzAppGameAccountsList(c *gin.Context) { + gameQ := strings.ToLower(strings.TrimSpace(c.Query("game"))) + rows, err := wzstore.List(wzstore.CollGameAccounts, 400) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "服务暂不可用"}) + return + } + var data []gin.H + for _, r := range rows { + m, e := wzstore.BodyMap(&r) + if e != nil { + continue + } + if !playhubListingOkForApp(m) && playhubStr(m["status"]) != "" { + continue + } + g := strings.ToLower(playhubStr(m["game"])) + if gameQ != "" && !strings.Contains(g, gameQ) { + continue + } + data = append(data, gin.H{ + "id": r.ExtID, + "title": playhubStr(m["title"]), + "name": playhubStr(m["characterName"]), + "characterName": playhubStr(m["characterName"]), + "game": playhubStr(m["game"]), + "server": playhubStr(m["server"]), + "rank": playhubStr(m["rank"]), + "price": playhubFloat(m["price"]), + "level": playhubFloat(m["level"]), + "heroCount": playhubFloat(m["heroCount"]), + "skins": playhubFloat(m["skins"]), + "image": coalesceImg(playhubStr(m["coverImage"]), playhubStr(m["image"])), + "sellerRating": playhubFloat(m["sellerRating"]), + "verified": playhubTruthy(m["verified"]), + "status": playhubStr(m["status"]), + }) + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": data}) +} + +// WzAppLevelingProductsList GET /api/app/leveling/products +func WzAppLevelingProductsList(c *gin.Context) { + rows, err := wzstore.List(wzstore.CollLevelingProducts, 300) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "服务暂不可用"}) + return + } + var data []gin.H + for _, r := range rows { + m, e := wzstore.BodyMap(&r) + if e != nil { + continue + } + if !playhubListingOkForApp(m) && playhubStr(m["status"]) != "" { + continue + } + priceNote := strings.TrimSpace(playhubStr(m["priceNote"])) + if priceNote == "" { + priceNote = playhubStr(m["priceHint"]) + } + data = append(data, gin.H{ + "id": r.ExtID, + "name": playhubStr(m["name"]), + "type": playhubStr(m["type"]), + "game": playhubStr(m["game"]), + "subtitle": playhubStr(m["subtitle"]), + "priceNote": priceNote, + "delivery": playhubStr(m["delivery"]), + "icon": playhubStr(m["icon"]), + "image": coalesceImg(playhubStr(m["image"]), playhubStr(m["coverImage"])), + }) + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": data}) +} + +// WzAppPartyRoomsList GET /api/app/party/rooms +func WzAppPartyRoomsList(c *gin.Context) { + rows, err := wzstore.List(wzstore.CollStarPartyRooms, 200) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "服务暂不可用"}) + return + } + var data []gin.H + for _, r := range rows { + m, e := wzstore.BodyMap(&r) + if e != nil { + continue + } + if v, ok := m["isActive"].(bool); ok && !v { + continue + } + data = append(data, gin.H{ + "id": r.ExtID, + "name": playhubStr(m["name"]), + "host": playhubStr(m["hostName"]), + "avatar": playhubStr(m["coverImage"]), + "game": playhubStr(m["game"]), + "background": coalesceImg(playhubStr(m["background"]), playhubStr(m["coverImage"])), + "memberCount": playhubFloat(m["memberCount"]), + }) + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": data}) +} + +// WzAppPartyRoomByID GET /api/app/party/rooms/:id +func WzAppPartyRoomByID(c *gin.Context) { + id := c.Param("id") + if !playhubIsHexID(id) { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效 id"}) + return + } + d, err := wzstore.Get(wzstore.CollStarPartyRooms, id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) + return + } + m, err := wzstore.BodyMap(d) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "未找到"}) + return + } + bg := coalesceImg(playhubStr(m["background"]), playhubStr(m["coverImage"])) + o := gin.H{ + "id": d.ExtID, + "name": playhubStr(m["name"]), + "host": coalesceImg(playhubStr(m["hostName"]), playhubStr(m["ownerName"])), + "avatar": coalesceImg(playhubStr(m["avatar"]), playhubStr(m["coverImage"])), + "game": playhubStr(m["game"]), + "background": bg, + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": o}) +} \ No newline at end of file diff --git a/new/wz-api/internal/handler/playhub_wz_app_wallet.go b/new/wz-api/internal/handler/playhub_wz_app_wallet.go new file mode 100644 index 0000000..6346227 --- /dev/null +++ b/new/wz-api/internal/handler/playhub_wz_app_wallet.go @@ -0,0 +1,277 @@ +package handler + +import ( + "fmt" + "net/http" + "sort" + "strconv" + "strings" + "time" + + mongostore "wz-api/internal/store/mongo" + "wz-api/internal/wzstore" + + "github.com/gin-gonic/gin" +) + +type appOrderSort struct { + t time.Time + row gin.H +} + +func normalizeAppConsumeKind(raw string) string { + s := strings.ToLower(strings.TrimSpace(raw)) + switch s { + case "service", "product", "recharge", "hotel", "cafe", "course", "pawn": + return s + default: + return "product" + } +} + +func normalizeAppConsumerOrderStatus(raw string) string { + s := strings.ToLower(strings.TrimSpace(raw)) + switch s { + case "completed", "paid", "success", "done", "delivered": + return "completed" + case "cancelled", "canceled", "closed": + return "cancelled" + case "refunded": + return "refunded" + case "pending", "pending_pay", "processing": + return "pending" + default: + return "pending" + } +} + +func parseAppOrderTime(v interface{}) time.Time { + s := strings.TrimSpace(playhubStr(v)) + if s == "" { + return time.Time{} + } + layouts := []string{time.RFC3339, time.RFC3339Nano, "2006-01-02 15:04:05"} + for _, l := range layouts { + if t, e := time.Parse(l, s); e == nil { + return t + } + } + if len(s) >= 10 { + if t, e := time.Parse("2006-01-02", s[:10]); e == nil { + return t + } + } + return time.Time{} +} + +func formatAppOrderDate(t time.Time) string { + if t.IsZero() { + return time.Now().UTC().Format("2006-01-02") + } + return t.UTC().Format("2006-01-02") +} + +// WzAppOrdersMeGET GET /api/app/orders/me — 当前用户点卡订单 + rechargeOrders(与统一订单源一致)。 +func WzAppOrdersMeGET(c *gin.Context) { + uid := playhubAppUserID(c) + if uid == "" { + c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) + return + } + var acc []appOrderSort + + pointRows, err := wzstore.List(wzstore.CollPointCardOrders, 500) + if err == nil { + for i := range pointRows { + m, e := wzstore.BodyMap(&pointRows[i]) + if e != nil { + continue + } + if playhubStr(m["userId"]) != uid { + continue + } + t := parseAppOrderTime(m["createdAt"]) + title := strings.TrimSpace(playhubStr(m["skuName"])) + if title == "" { + title = strings.TrimSpace(playhubStr(m["game"])) + if title == "" { + title = "游戏点卡" + } else { + title = title + " 点卡" + } + } + acc = append(acc, appOrderSort{t: t, row: gin.H{ + "id": pointRows[i].ExtID, + "orderNo": playhubStr(m["orderNo"]), + "type": "product", + "title": title, + "price": playhubFloat(m["totalAmount"]), + "status": normalizeAppConsumerOrderStatus(playhubStr(m["status"])), + "date": formatAppOrderDate(t), + "image": playhubStr(m["gameLogo"]), + }}) + } + } + + reRows, err2 := wzstore.List(wzstore.CollRechargeOrders, 400) + if err2 == nil { + for i := range reRows { + m, e := wzstore.BodyMap(&reRows[i]) + if e != nil { + continue + } + if playhubStr(m["userId"]) != uid { + continue + } + t := parseAppOrderTime(m["createdAt"]) + amt := playhubFloat(m["amount"]) + if amt == 0 { + amt = playhubFloat(m["totalAmount"]) + } + acc = append(acc, appOrderSort{t: t, row: gin.H{ + "id": reRows[i].ExtID, + "orderNo": playhubStr(m["orderNo"]), + "type": "recharge", + "title": "充值 " + strconv.FormatFloat(amt, 'f', -1, 64) + " 玩值币", + "price": amt, + "status": normalizeAppConsumerOrderStatus(playhubStr(m["status"])), + "date": formatAppOrderDate(t), + }}) + } + } + + prodRows, err3 := wzstore.List(wzstore.CollProductOrders, 500) + if err3 == nil { + for i := range prodRows { + m, e := wzstore.BodyMap(&prodRows[i]) + if e != nil { + continue + } + if cb, ok := m["wzAppConsume"].(bool); !ok || !cb { + continue + } + if playhubStr(m["userId"]) != uid { + continue + } + t := parseAppOrderTime(m["createdAt"]) + title := strings.TrimSpace(playhubStr(m["title"])) + if title == "" { + title = "商城消费" + } + kind := normalizeAppConsumeKind(playhubStr(m["wzConsumeKind"])) + amt := playhubFloat(m["finalAmount"]) + if amt == 0 { + amt = playhubFloat(m["totalAmount"]) + } + acc = append(acc, appOrderSort{t: t, row: gin.H{ + "id": prodRows[i].ExtID, + "orderNo": playhubStr(m["orderNo"]), + "type": kind, + "title": title, + "price": amt, + "status": normalizeAppConsumerOrderStatus(playhubStr(m["status"])), + "date": formatAppOrderDate(t), + "image": playhubStr(m["image"]), + }}) + } + } + + sort.Slice(acc, func(i, j int) bool { + ti, tj := acc[i].t, acc[j].t + if ti.Equal(tj) { + return playhubStr(acc[i].row["orderNo"]) > playhubStr(acc[j].row["orderNo"]) + } + return ti.After(tj) + }) + out := make([]gin.H, len(acc)) + for i := range acc { + out[i] = acc[i].row + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": out}) +} + +// WzAppWalletRechargePOST POST /api/app/wallet/recharge — body: {"amount"} 递增 C 端「币」(与移动端充值档位一致);落 rechargeOrders。 +func WzAppWalletRechargePOST(c *gin.Context) { + uid := playhubAppUserID(c) + if uid == "" { + c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) + return + } + var body struct { + Amount float64 `json:"amount"` + } + if err := c.ShouldBindJSON(&body); err != nil || body.Amount <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的金额"}) + return + } + if body.Amount > 99999999 { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "单笔金额过大"}) + return + } + bal, err := mongostore.AddAppUserBalanceDelta(uid, body.Amount) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "余额更新失败"}) + return + } + orderNo := fmt.Sprintf("WZRC%s%06d", time.Now().UTC().Format("20060102150405"), time.Now().Nanosecond()%1000000) + _, _ = wzstore.Insert(wzstore.CollRechargeOrders, map[string]interface{}{ + "orderNo": orderNo, + "userId": uid, + "amount": body.Amount, + "totalAmount": body.Amount, + "status": "completed", + "createdAt": time.Now().UTC().Format(time.RFC3339), + }) + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"balance": bal, "orderNo": orderNo}}) +} + +// WzAppWalletConsumePOST POST /api/app/wallet/consume — 扣 Mongo 余额并写入 productOrders(带 wzAppConsume,仅供 C 端玩值币虚拟消费)。 +func WzAppWalletConsumePOST(c *gin.Context) { + uid := playhubAppUserID(c) + if uid == "" { + c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"}) + return + } + var body struct { + Amount float64 `json:"amount"` + Title string `json:"title"` + Kind string `json:"kind"` + Image string `json:"image"` + } + if err := c.ShouldBindJSON(&body); err != nil || body.Amount <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的金额"}) + return + } + if body.Amount > 99999999 { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "单笔金额过大"}) + return + } + title := strings.TrimSpace(body.Title) + if title == "" { + title = "玩值币消费" + } + bal, ok, err := mongostore.TryDecrementAppUserBalance(uid, body.Amount) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "扣款失败"}) + return + } + if !ok { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "余额不足"}) + return + } + orderNo := fmt.Sprintf("WZVO%s%06d", time.Now().UTC().Format("20060102150405"), time.Now().Nanosecond()%1000000) + kind := normalizeAppConsumeKind(body.Kind) + _, _ = wzstore.Insert(wzstore.CollProductOrders, map[string]interface{}{ + "orderNo": orderNo, + "userId": uid, + "title": title, + "totalAmount": body.Amount, + "finalAmount": body.Amount, + "status": "completed", + "wzConsumeKind": kind, + "wzAppConsume": true, + "image": strings.TrimSpace(body.Image), + "createdAt": time.Now().UTC().Format(time.RFC3339), + }) + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"balance": bal, "orderNo": orderNo}}) +} diff --git a/new/wz-api/internal/handler/playhub_wz_live.go b/new/wz-api/internal/handler/playhub_wz_live.go index 3a3cb5a..b65af80 100644 --- a/new/wz-api/internal/handler/playhub_wz_live.go +++ b/new/wz-api/internal/handler/playhub_wz_live.go @@ -219,6 +219,15 @@ func WzAppLiveGift(c *gin.Context) { if price <= 0 { price = 1 } + newBal, ok, decErr := mongostore.TryDecrementAppUserBalance(uid, price) + if decErr != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "扣款失败"}) + return + } + if !ok { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "玩值币余额不足"}) + return + } gdoc := map[string]interface{}{ "streamerId": sid, "userId": uid, @@ -228,6 +237,7 @@ func WzAppLiveGift(c *gin.Context) { "createdAt": time.Now().UTC().Format(time.RFC3339), } if _, err := wzstore.Insert(wzstore.CollLiveGifts, gdoc); err != nil { + _, _ = mongostore.AddAppUserBalanceDelta(uid, price) c.JSON(http.StatusOK, gin.H{"success": false, "message": "送礼失败"}) return } @@ -243,6 +253,7 @@ func WzAppLiveGift(c *gin.Context) { "data": gin.H{ "orderNo": orderNo, "deducted": price, + "balance": newBal, "giftId": "new", "split": gin.H{"platformCoins": price * 0.2, "guildCoins": price * 0.3, "streamerCoins": price * 0.5}, }, diff --git a/new/wz-api/internal/handler/playhub_wz_register.go b/new/wz-api/internal/handler/playhub_wz_register.go index 5f612f1..dbde953 100644 --- a/new/wz-api/internal/handler/playhub_wz_register.go +++ b/new/wz-api/internal/handler/playhub_wz_register.go @@ -77,10 +77,28 @@ func RegisterPlayhubAppPlayhubRoutes(app *gin.RouterGroup) { app.GET("/mall/point-cards", WzAppMallPointCardsList) app.GET("/mall/point-cards/:cardId", WzAppMallPointCardByID) app.POST("/orders/point-card", WzAppOrderPointCardPOST) + app.GET("/orders/me", WzAppOrdersMeGET) + app.POST("/wallet/recharge", WzAppWalletRechargePOST) + app.POST("/wallet/consume", WzAppWalletConsumePOST) app.GET("/streamers", WzAppStreamersList) app.GET("/streamers/:id", WzAppStreamerByID) app.GET("/games", WzAppGamesList) + + app.GET("/guilds", WzAppGuildsList) + app.GET("/guilds/:id", WzAppGuildByID) + app.GET("/venues", WzAppVenuesList) + app.GET("/venues/:id", WzAppVenueByID) + app.GET("/products", WzAppProductsList) + app.GET("/products/:id", WzAppProductByID) + app.GET("/content/items", WzAppContentItemsList) + app.GET("/content/items/:id", WzAppContentItemByID) + app.GET("/stars", WzAppStarsList) + app.GET("/stars/:id", WzAppStarByID) + app.GET("/game-accounts", WzAppGameAccountsList) + app.GET("/leveling/products", WzAppLevelingProductsList) + app.GET("/party/rooms", WzAppPartyRoomsList) + app.GET("/party/rooms/:id", WzAppPartyRoomByID) app.POST("/analytics/journey-events", WzAppAnalyticsJourney) app.POST("/customer", WzAppCustomerPOST) diff --git a/new/wz-api/internal/store/mongo/auth.go b/new/wz-api/internal/store/mongo/auth.go index a2daa61..f8424b8 100644 --- a/new/wz-api/internal/store/mongo/auth.go +++ b/new/wz-api/internal/store/mongo/auth.go @@ -4,6 +4,7 @@ package mongostore import ( "context" + "errors" "time" "wz-api/internal/database" @@ -11,6 +12,7 @@ import ( "go.mongodb.org/mongo-driver/bson" mongodriver "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) const ( @@ -229,6 +231,71 @@ func decodeAppUser(raw bson.M) (*model.User, error) { return u, nil } +// AddAppUserBalanceDelta 在用户余额上递增 delta(Mongo user_balances),文档不存在则 upsert。 +func AddAppUserBalanceDelta(userID string, delta float64) (float64, error) { + if userID == "" || delta == 0 { + return GetAppUserBalance(userID), nil + } + ctx, cancel := opCtx() + defer cancel() + now := time.Now().UTC() + _, err := coll(CollUserBalances).UpdateOne(ctx, + bson.M{"user_id": userID}, + bson.M{ + "$inc": bson.M{"balance": delta}, + "$set": bson.M{"updated_at": now}, + "$setOnInsert": bson.M{ + "user_id": userID, + "created_at": now, + }, + }, + options.Update().SetUpsert(true), + ) + if err != nil { + return 0, err + } + return GetAppUserBalance(userID), nil +} + +// TryDecrementAppUserBalance 若余额足够则原子扣减;余额不足或未建档则 ok=false。 +func TryDecrementAppUserBalance(userID string, amount float64) (newBalance float64, ok bool, err error) { + if userID == "" || amount <= 0 { + return GetAppUserBalance(userID), false, nil + } + ctx, cancel := opCtx() + defer cancel() + now := time.Now().UTC() + res := coll(CollUserBalances).FindOneAndUpdate(ctx, + bson.M{ + "user_id": userID, + "balance": bson.M{"$gte": amount}, + }, + bson.M{ + "$inc": bson.M{"balance": -amount}, + "$set": bson.M{"updated_at": now}, + }, + options.FindOneAndUpdate().SetReturnDocument(options.After), + ) + var raw bson.M + de := res.Decode(&raw) + if de != nil { + if errors.Is(de, mongodriver.ErrNoDocuments) { + return GetAppUserBalance(userID), false, nil + } + return 0, false, de + } + switch x := raw["balance"].(type) { + case float64: + return x, true, nil + case int32: + return float64(x), true, nil + case int64: + return float64(x), true, nil + default: + return GetAppUserBalance(userID), true, nil + } +} + // GetAppUserBalance 读取 user_balances.balance。 func GetAppUserBalance(userID string) float64 { ctx, cancel := opCtx() diff --git a/new/wz-api/master.py b/new/wz-api/master.py index d37b7c8..b641c2b 100644 --- a/new/wz-api/master.py +++ b/new/wz-api/master.py @@ -12,7 +12,10 @@ from __future__ import print_function python master.py --skip-bt-ping 环境变量: DEPLOY_HOST、BT_PANEL_URL、BT_API_KEY、DEPLOY_PROJECT_PATH、 - BT_GO_PROJECT_NAME(默认 wzdjOptionsApi,与面板一致)、BT_GO_SITE_ID、DEPLOY_API_PORT(默认 4040) + BT_GO_PROJECT_NAME(默认 wzApi,与面板一致)、BT_GO_SITE_ID、DEPLOY_API_PORT(默认 4040) + +Mongo 数据上云(本地 → 宝塔 wzdj,推荐走面板 API 上传+服务端 mongorestore)见 scripts/mongo_push_baota_api.py; +亦可用手动 mongorestore 见 scripts/MONGO_宝塔同步.md 与 scripts/mongo_sync_baota.py(非本脚本职责)。 依赖: pip install requests """ @@ -51,8 +54,8 @@ def get_cfg(): "project_path": os.environ.get("DEPLOY_PROJECT_PATH", DEPLOY_PROJECT_PATH), "bt_panel_url": bt_url, "bt_api_key": (os.environ.get("BT_API_KEY") or BT_API_KEY_DEFAULT).strip(), - # 与宝塔「Go 项目」名称一致;若启动失败可改环境变量 BT_GO_PROJECT_NAME(常见: wzApi / wzdjOptionsApi) - "bt_go_project_name": os.environ.get("BT_GO_PROJECT_NAME", "wzdjOptionsApi"), + # 与宝塔「Go 项目」名称一致;若启动失败可改环境变量 BT_GO_PROJECT_NAME(常见: wzApi / wzApi) + "bt_go_project_name": os.environ.get("BT_GO_PROJECT_NAME", "wzApi"), } diff --git a/new/wz-api/scripts/MONGO_宝塔同步.md b/new/wz-api/scripts/MONGO_宝塔同步.md new file mode 100644 index 0000000..91fc023 --- /dev/null +++ b/new/wz-api/scripts/MONGO_宝塔同步.md @@ -0,0 +1,110 @@ +# 本地 Mongo → 宝塔 Mongo(`wzdj` 库)同步说明 + +## 你需要什么 + +1. 本机安装 **MongoDB Database Tools**(含 `mongodump`、`mongorestore`),且已在 PATH 里能直接运行。 +2. 本机能 **访问** 宝塔服务器上的 Mongo 端口(默认 `27017`;若只对内网开放,需 VPN/SSH 隧道)。 +3. 线上库与面板一致:**数据库名 `wzdj`**,用户 **`wzdj`**,`authSource` 一般为 **`wzdj`**(与连接串一致即可)。 + +## 一键脚本(推荐) + +在仓库 `new/wz-api` 目录下: + +```bash +# 生产连接串请用宝塔/面板里复制的 URI(主机不要用 127.0.0.1,应是服务器 IP 或域名) +set REMOTE_MONGODB_URI=mongodb://wzdj:密码@43.139.27.93:27017/wzdj?authSource=wzdj +python scripts/mongo_sync_baota.py +``` + +可选环境变量: + +| 变量 | 含义 | 默认 | +|------|------|------| +| `LOCAL_MONGODB_URI` | 本机源库 | `mongodb://127.0.0.1:27017/wz` | +| `LOCAL_DB_NAME` | 源逻辑库名(与 URI 路径一致) | 从 URI 解析,失败则为 `wz` | +| `REMOTE_MONGODB_URI` | 宝塔目标库 | 未设时尝试读 `.env.production` 的 `MONGODB_URI` | +| `REMOTE_DB_NAME` | 目标库名 | `wzdj` | +| `MONGO_DUMP_OUT` | `mongodump --out` 持久目录(不设则用临时目录) | 空 | +| `DEPLOY_HOST` | 本机执行 `mongorestore` 时,若线上 `.env` 里 Mongo 主机为 `127.0.0.1`/`localhost`,可自动替换为该主机(通常与 `master.py` 部署 IP 一致) | 未设时为 `43.139.27.93` | + +未显式传远端 URI 时,脚本默认从 `new/wz-api/.env.development` 读本机、从 `.env.production` 读远端(键名均为 `MONGODB_URI`)。 + +## 宝塔 API 一键覆盖线上(推荐) + +在 **`new/wz-api`** 下执行(需与本仓库 `master.py` 相同的 `BT_PANEL_URL`、`BT_API_KEY`、`DEPLOY_PROJECT_PATH`;本机 PATH 有 `mongodump` 与 `tar`): + +```bash +python scripts/mongo_push_baota_api.py +``` + +流程:本机 `mongodump` 库 `wz` → 打 `tar.gz` → 宝塔「文件」接口上传 → 远程解压到「Go 项目目录下的 `wz_mongo_bt_push/`」→ 调用面板 **`/files?action=ExecShell`** 在服务器本机执行 `mongorestore`(`MONGODB_URI` 从 **`/www/.../api/.env`** 读取,不写进请求体)。使用 **`--drop -d wzdj`** 指向解压后的 `wz/` 目录,覆盖 **`wzdj`** 中对应集合。 + +仅当面板 API 已开启、当前出口 IP 在白名单、且允许 `ExecShell` 时可用。若 ExecShell 被拒绝,仍可改用 `mongo_sync_baota.py`(隧道)或面板「数据库」手动导入。 + +```bash +python scripts/mongo_push_baota_api.py --skip-dump # 使用已有 build/mongo_dump_wz +``` + +仅生成导出目录、不推线上: + +```bash +python scripts/mongo_sync_baota.py --dump-only --dump-out ./build/mongo_dump_wz +``` + +若不想自动把远端 URI 里的本机地址换成 `DEPLOY_HOST`,可加:`--no-fix-remote-host`。 + +**用本地数据覆盖线上 `wzdj`(先删集合再导入)**:在确认无误后加 `--drop`: + +```bash +python scripts/mongo_sync_baota.py --drop +``` + +**已有 dump、只执行恢复**(例如隧道建立后重试,免再 dump): + +```bash +python scripts/mongo_sync_baota.py --from-dump ./build/mongo_dump_wz --drop --no-fix-remote-host +``` + +(配合隧道时,请先把 `REMOTE_MONGODB_URI` 指到 `127.0.0.1:隧道本地端口`,**必须**加 `--no-fix-remote-host`,否则 `127.0.0.1` 会被改成公网 IP 导致仍连不上。) + +## 公网直连 `43.x.x.x:27017` 被拒绝(常见) + +现象:`connectex: actively refused` / `context deadline exceeded`。多半是 **Mongo 只监听 `127.0.0.1`** 或 **云安全组/防火墙未放行 27017**(生产环境一般会拒绝公网直连数据库)。 + +**做法 A:SSH 本地转发(推荐)** + +1. 修正本机 `~/.ssh/config`:OpenSSH **不支持** `Password` 这类非法选项;有的话删掉或改成密钥登录。 +2. 在一个终端保持运行: + +```bash +ssh -N -L 27018:127.0.0.1:27017 root@你的服务器IP +``` + +3. 另开终端(`new/wz-api` 下),用与线上一致的账号,**仅把主机与端口改成隧道**: + +```bash +set REMOTE_MONGODB_URI=mongodb://wzdj:线上密码@127.0.0.1:27018/wzdj?authSource=wzdj +python scripts/mongo_sync_baota.py --from-dump ./build/mongo_dump_wz --drop --no-fix-remote-host +``` + +**做法 B:把 `build/mongo_dump_wz` 拷到服务器**,SSH 登录后装 Database Tools,对 `mongodb://127.0.0.1:27017/...` 执行与脚本等价的 `mongorestore --nsFrom=wz.* --nsTo=wzdj.* --drop`。 + +## 宝塔面板「导入」在干什么 + +你备份里出现的 **`xxx.bson` + `xxx.metadata.json`** 是 `mongodump` 的标准产物。面板里的 **导入** 一般是把这类目录/压缩包恢复到**当前选中的库(如 `wzdj`)**。 + +若你只在本地执行脚本推送到线上,**不必**再点面板导入;两种方式二选一即可,避免重复覆盖。 + +## 手动命令(等价于脚本) + +```bash +mongodump --uri="mongodb://127.0.0.1:27017/wz" -o ./dump_wz_export +mongorestore --uri="mongodb://wzdj:密码@服务器IP:27017/wzdj?authSource=wzdj" --nsFrom="wz.*" --nsTo="wzdj.*" ./dump_wz_export +``` + +**注意**:本地开发常用库名 **`wz`**,宝塔侧是 **`wzdj`**,必须用 `--nsFrom` / `--nsTo` 改名,否则集合会进错库。 + +## 与安全 + +- 不要把含密码的 URI 提交到 Git;用环境变量或本机临时导出。 +- `master.py` 只管 **Go 二进制部署**,**不包含** Mongo 同步;数据库请用本脚本或面板导入。 diff --git a/new/wz-api/scripts/__pycache__/mongo_push_baota_api.cpython-311.pyc b/new/wz-api/scripts/__pycache__/mongo_push_baota_api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..308895ea1d4c0a484e8e19c5cdf51e6c398afe08 GIT binary patch literal 17213 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 TF<)lH{B(2UJmcq8G^GCz@!B(X literal 0 HcmV?d00001 diff --git a/new/wz-api/scripts/__pycache__/mongo_sync_baota.cpython-311.pyc b/new/wz-api/scripts/__pycache__/mongo_sync_baota.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e84ebe802691ab67f4bd30f5b1a65a4f08cc31f GIT binary patch literal 15774 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 literal 0 HcmV?d00001 diff --git a/new/wz-api/scripts/mongo_push_baota_api.py b/new/wz-api/scripts/mongo_push_baota_api.py new file mode 100644 index 0000000..78dd96e --- /dev/null +++ b/new/wz-api/scripts/mongo_push_baota_api.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +本地 Mongo(库 wz)→ 打包 → 宝塔 API 上传/解压 → 面板 API 在服务器本机执行 mongorestore 覆盖 wzdj。 + +依赖:本机 mongodump、tar(Windows 10+ 自带 bsdtar 或需装 Git tar);pip: requests。 +配置:与 master.py 相同(BT_PANEL_URL、BT_API_KEY、DEPLOY_PROJECT_PATH 等),另读 .env.development / .env.production 仅用于本机 dump。 + +服务器侧:使用「网站/API 目录」里的 .env(部署后名为 .env)中 MONGODB_URI 连接 127.0.0.1 Mongo, +故无需把数据库密码塞进 API 请求体。 + +用法(在 new/wz-api 下): + python scripts/mongo_push_baota_api.py + python scripts/mongo_push_baota_api.py --skip-dump # 已有 build/mongo_dump_wz +""" +from __future__ import print_function + +import argparse +import hashlib +import os +import shutil +import subprocess +import sys +import tempfile +import time + +try: + import requests + + try: + import urllib3 + + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + except Exception: + pass +except ImportError: + requests = None + + +def _bt_signed_post(base_url, key, path, extra_data, timeout=120): + req_time = int(time.time()) + sk_md5 = hashlib.md5(key.encode()).hexdigest() + req_token = hashlib.md5(("%s%s" % (req_time, sk_md5)).encode()).hexdigest() + data = {"request_time": req_time, "request_token": req_token} + data.update(extra_data or {}) + return requests.post(base_url + path, data=data, timeout=timeout, verify=False) + + +def _bt_parse_json(r): + if r is None or r.status_code != 200: + return None + try: + return r.json() + except Exception: + t = (r.text or "").lstrip() + if t.startswith("{"): + try: + return __import__("json").loads(r.text) + except Exception: + return None + return None + + +def _read_uri_from_env_file(path, key="MONGODB_URI"): + if not os.path.isfile(path): + return "" + try: + with open(path, "r", encoding="utf-8", errors="replace") as f: + for line in f: + s = line.strip() + if s.startswith("#") or "=" not in s: + continue + k, v = s.split("=", 1) + if k.strip() == key: + return v.strip().strip('"').strip("'") + except Exception: + pass + return "" + + +def _import_master(root): + sys.path.insert(0, root) + import importlib.util + + p = os.path.join(root, "master.py") + spec = importlib.util.spec_from_file_location("wz_api_master", p) + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + return m + + +def _which(cmd): + return shutil.which(cmd) is not None + + +def _run_local(cmd, cwd=None): + print("[local]", " ".join(cmd)) + r = subprocess.run(cmd, cwd=cwd, shell=False) + return r.returncode == 0 + + +def _make_tgz_from_dump(dump_parent, local_db): + """dump_parent 下须有子目录 local_db(如 wz)。""" + src = os.path.join(dump_parent, local_db) + if not os.path.isdir(src): + return None + fd, tgz = tempfile.mkstemp(prefix="wz_mongo_", suffix=".tar.gz") + os.close(fd) + try: + # GNU tar: tar czf tgz -C dump_parent local_db + if not _run_local(["tar", "-czf", tgz, "-C", dump_parent, local_db]): + return None + return tgz + except Exception: + try: + os.remove(tgz) + except Exception: + pass + return None + + +def bt_exec_shell(cfg, shell, cwd_path): + url = (cfg["bt_panel_url"] or "").rstrip("/") + key = cfg["bt_api_key"] + j = _bt_parse_json( + _bt_signed_post( + url, + key, + "/files?action=ExecShell", + {"path": cwd_path, "shell": shell}, + timeout=60, + ) + ) + if not isinstance(j, dict) or j.get("status") is not True: + print("[失败] ExecShell 未接受: %s" % (j,)) + return False + return True + + +def bt_get_exec_msg(cfg): + url = (cfg["bt_panel_url"] or "").rstrip("/") + key = cfg["bt_api_key"] + return _bt_parse_json(_bt_signed_post(url, key, "/files?action=GetExecShellMsg", {}, timeout=60)) + + +def wait_exec_shell(cfg, timeout_sec=300, poll=2): + """轮询 GetExecShellMsg,直到任务结束或超时。""" + deadline = time.time() + timeout_sec + last_msg = None + while time.time() < deadline: + j = bt_get_exec_msg(cfg) + last_msg = j + if isinstance(j, dict): + st = j.get("status") + if st is True: + print("[信息] 远程命令已结束(面板返回 status=True)") + if j.get("msg"): + print(str(j.get("msg"))[-4000:]) + return True + time.sleep(poll) + print("[失败] 等待远程 Shell 超时(%ss)。最后一次面板返回: %s" % (timeout_sec, last_msg)) + return False + + +def main(): + parser = argparse.ArgumentParser(description="Mongo 覆盖到线上(宝塔 API + 服务端 mongorestore)") + parser.add_argument("--skip-dump", action="store_true", help="跳过本机 mongodump,使用已有目录") + parser.add_argument( + "--dump-dir", + default="", + help="mongodump 输出父目录(内含 wz 子目录),默认 build/mongo_dump_wz", + ) + parser.add_argument("--local-db", default=os.environ.get("LOCAL_DB_NAME", "wz")) + parser.add_argument("--remote-db", default=os.environ.get("REMOTE_DB_NAME", "wzdj")) + parser.add_argument("--skip-bt-ping", action="store_true") + parser.add_argument("--exec-timeout", type=int, default=int(os.environ.get("BT_MONGO_EXEC_TIMEOUT", "300") or "300")) + args = parser.parse_args() + + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + m = _import_master(root) + cfg = m.get_cfg() + + if not requests: + print("[失败] pip install requests") + return 1 + if not args.skip_bt_ping and not m.bt_panel_ping(cfg): + print("[失败] 宝塔 API 探活失败") + return 1 + + local_db = (args.local_db or "wz").strip() + remote_db = (args.remote_db or "wzdj").strip() + dump_dir = (args.dump_dir or "").strip() or os.path.join(root, "build", "mongo_dump_wz") + + if not args.skip_dump: + if not _which("mongodump"): + print("[失败] 未找到 mongodump") + return 1 + os.makedirs(dump_dir, exist_ok=True) + env_dev = os.path.join(root, ".env.development") + local_uri = _read_uri_from_env_file(env_dev) or "mongodb://127.0.0.1:27017/wz" + if not _run_local(["mongodump", "--uri=%s" % local_uri, "--db", local_db, "--out", dump_dir]): + print("[失败] mongodump 失败") + return 1 + else: + print("[信息] --skip-dump,使用已有: %s" % dump_dir) + + if not os.path.isdir(os.path.join(dump_dir, local_db)): + print("[失败] 缺少子目录 %s/%s" % (dump_dir, local_db)) + return 1 + + tgz = _make_tgz_from_dump(dump_dir, local_db) + if not tgz: + print("[失败] 打包 tar.gz 失败(需要系统 tar 命令)") + return 1 + + project_path = cfg["project_path"].rstrip("/") + remote_work = project_path + "/wz_mongo_bt_push" + remote_tgz = remote_work + "/mongo_wz_dump.tar.gz" + # 解压后布局:remote_work//*.bson;用 -d 指向目标库(避免 nsFrom/nsTo 与目录扫描兼容问题) + remote_dump_parent = remote_work + remote_db_dir = remote_work + "/" + local_db + + try: + print("[1/4] 上传 dump 压缩包 ...") + if not m.bt_upload_file_resumable(cfg, tgz, remote_work, "mongo_wz_dump.tar.gz"): + print("[失败] 上传失败") + return 1 + + print("[2/4] 远程解压 ...") + if not m.bt_unzip_remote(cfg, remote_tgz, remote_work + "/"): + print("[失败] 远程解压失败") + return 1 + + if m.bt_delete_file(cfg, remote_tgz): + print("[信息] 已删除远程压缩包,避免 mongorestore 误扫 tar.gz") + else: + print("[警告] 删除远程 tar 失败,mongorestore 可能跳过集合;请在面板手动删: %s" % remote_tgz) + + pp = project_path.replace("'", "'\\''") + rd = remote_dump_parent.replace("'", "'\\''") + rdbdir = remote_db_dir.replace("'", "'\\''") + shell = ( + "set -e;" + "MRS=$(command -v mongorestore 2>/dev/null || true);" + "if [ -z \"$MRS\" ] && [ -x /www/server/mongodb/bin/mongorestore ]; then MRS=/www/server/mongodb/bin/mongorestore; fi;" + 'if [ -z "$MRS" ]; then echo "mongorestore not found"; exit 127; fi;' + + "ENVF='" + + pp + + "/.env';" + + 'if [ ! -f "$ENVF" ]; then echo "missing $ENVF"; exit 1; fi;' + + "URI=$(grep '^MONGODB_URI=' \"$ENVF\" | head -1 | cut -d= -f2- | tr -d '\\r');" + + "URI=$(printf '%s' \"$URI\" | sed \"s/^\\\"//;s/\\\"$//\");" + + "rm -f '" + + rd + + "/mongo_wz_dump.tar.gz' || true;" + + "if [ ! -d '" + + rdbdir + + "' ]; then echo missing_dump; ls -la '" + + rd + + "'; exit 2; fi;" + + '"$MRS" --uri="$URI" --drop -d ' + + remote_db + + " '" + + rdbdir + + "'" + ) + + print("[3/4] 请求面板在服务器执行 mongorestore(读取线上 .env 的 MONGODB_URI)...") + if not bt_exec_shell(cfg, shell, "/tmp"): + return 1 + + print("[4/4] 等待执行结果(最多 %ss)..." % args.exec_timeout) + ok = wait_exec_shell(cfg, timeout_sec=args.exec_timeout) + if ok: + print("[成功] 远程 mongorestore 已完成(请核对上方的 bson 日志)。工作目录: %s" % remote_work) + return 0 + return 1 + finally: + try: + os.remove(tgz) + except Exception: + pass + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/new/wz-api/scripts/mongo_sync_baota.py b/new/wz-api/scripts/mongo_sync_baota.py new file mode 100644 index 0000000..d382b01 --- /dev/null +++ b/new/wz-api/scripts/mongo_sync_baota.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +本地 MongoDB → 宝塔 Mongo(默认目标库 wzdj)一遍式同步。 + +依赖:mongodump、mongorestore(MongoDB Database Tools),且在 PATH 中。 + +示例(PowerShell): + $env:REMOTE_MONGODB_URI = "mongodb://wzdj:你的密码@服务器IP:27017/wzdj?authSource=wzdj" + python scripts/mongo_sync_baota.py + +仅导出、不恢复(并保留目录便于宝塔「导入」打包): + python scripts/mongo_sync_baota.py --dump-only --dump-out ./build/mongo_dump_wz + +本机推送时若 .env.production 里 Mongo 写的是 127.0.0.1(进程在服务器本机连库),脚本会读取环境变量 +DEPLOY_HOST(未设置则 43.139.27.93,与 master.py 默认一致)自动替换远端主机再 mongorestore; +可用 --no-fix-remote-host 关闭。 +""" + +from __future__ import print_function + +import argparse +import os +import shutil +import subprocess +import sys +import tempfile +from urllib.parse import urlparse, urlunparse, unquote + +# 与 master.py 默认 DEPLOY_HOST 一致:用于本机 push 时把 .env.production 里的 127.0.0.1 换成可访问的服务器 +DEFAULT_DEPLOY_HOST = "43.139.27.93" + + +def _replace_uri_hostname(uri, new_host): + """仅替换 hostname,保留 userinfo、port、path、query。""" + try: + u = urlparse(uri.replace("mongodb+srv://", "mongodb://", 1)) + if not u.scheme: + return uri + netloc = "" + if u.username: + netloc = u.username + if u.password: + netloc += ":" + u.password + netloc += "@" + netloc += new_host + if u.port: + netloc += ":%s" % u.port + return urlunparse((u.scheme, netloc, u.path, u.params, u.query, u.fragment)) + except Exception: + return uri + + +def _db_name_from_uri(uri): + """从 mongodb://host:port/dbname 取 dbname(忽略 query)。""" + try: + u = urlparse(uri.replace("mongodb+srv://", "mongodb://", 1)) + path = (u.path or "").lstrip("/") + if not path: + return "" + return path.split("/")[0] if "/" in path else path.split("?")[0] + except Exception: + return "" + + +def _mask_uri(uri): + if not uri: + return uri + try: + u = urlparse(uri) + if u.username: + host = u.hostname or "" + port = ":%s" % u.port if u.port else "" + path = u.path or "" + q = "?%s" % u.query if u.query else "" + return "%s://%s:***@%s%s%s%s" % (u.scheme, u.username, host, port, path, q) + except Exception: + pass + return "***" + + +def _which(cmd): + return shutil.which(cmd) is not None + + +def _run(cmd, cwd=None): + disp = [] + for x in cmd: + if x.startswith("--uri="): + disp.append("--uri=%s" % _mask_uri(x[6:])) + else: + disp.append(x) + print("[exec]", " ".join(disp)) + r = subprocess.run(cmd, cwd=cwd, shell=False) + return r.returncode == 0 + + +def _read_uri_from_env_file(path, key="MONGODB_URI"): + if not os.path.isfile(path): + return "" + try: + with open(path, "r", encoding="utf-8", errors="replace") as f: + for line in f: + s = line.strip() + if s.startswith("#") or "=" not in s: + continue + k, v = s.split("=", 1) + if k.strip() == key: + return v.strip().strip('"').strip("'") + except Exception: + pass + return "" + + +def main(): + parser = argparse.ArgumentParser(description="mongodump local -> mongorestore Baota wzdj") + parser.add_argument("--dump-only", action="store_true", help="只 dump 到临时目录并打印路径,不 restore") + parser.add_argument("--local-uri", default=os.environ.get("LOCAL_MONGODB_URI", "")) + parser.add_argument("--remote-uri", default=os.environ.get("REMOTE_MONGODB_URI", "")) + parser.add_argument("--local-db", default=os.environ.get("LOCAL_DB_NAME", "")) + parser.add_argument("--remote-db", default=os.environ.get("REMOTE_DB_NAME", "wzdj")) + parser.add_argument("--env-local", default="", help="从该文件读 LOCAL MONGODB_URI(key MONGODB_URI)") + parser.add_argument("--env-remote", default="", help="从该文件读 REMOTE MONGODB_URI(key MONGODB_URI)") + parser.add_argument( + "--dump-out", + default=os.environ.get("MONGO_DUMP_OUT", ""), + help="mongodump --out 目录(将生成 子目录/%s);设置后不删目录。空则使用临时目录" % "库名", + ) + parser.add_argument( + "--no-fix-remote-host", + action="store_true", + help="不要用 DEPLOY_HOST 替换远端 URI 中的 127.0.0.1/localhost", + ) + parser.add_argument( + "--drop", + action="store_true", + help="mongorestore 在导入每个集合前先 drop(实现覆盖线上同库数据,慎用)", + ) + parser.add_argument( + "--from-dump", + default="", + help="跳过 mongodump,直接使用已有 dump 目录(mongodump --out 的父目录,内含子目录=源库名)", + ) + args = parser.parse_args() + + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + local_uri = (args.local_uri or "").strip() + if not local_uri: + env_path = (args.env_local or os.path.join(root, ".env.development")).strip() + local_uri = _read_uri_from_env_file(env_path) + if not local_uri: + local_uri = "mongodb://127.0.0.1:27017/wz" + + remote_uri = (args.remote_uri or "").strip() + if not remote_uri: + env_path = (args.env_remote or os.path.join(root, ".env.production")).strip() + remote_uri = _read_uri_from_env_file(env_path) + if remote_uri and not args.no_fix_remote_host: + try: + u = urlparse(remote_uri.replace("mongodb+srv://", "mongodb://", 1)) + h = (u.hostname or "").lower() + if h in ("127.0.0.1", "localhost"): + deploy_host = (os.environ.get("DEPLOY_HOST") or "").strip() or DEFAULT_DEPLOY_HOST + remote_uri = _replace_uri_hostname(remote_uri, deploy_host) + print("[信息] 远端 URI 原为主机本机地址,已改为 DEPLOY_HOST=%s 便于本机 mongorestore" % deploy_host) + except Exception: + pass + elif "127.0.0.1" in remote_uri or "localhost" in remote_uri: + print("[警告] REMOTE_MONGODB_URI 指向本机。可调 REMOTE_MONGODB_URI 或使用默认自动替换(勿加 --no-fix-remote-host)。") + + local_db = (args.local_db or "").strip() or _db_name_from_uri(local_uri) or "wz" + remote_db = (args.remote_db or "wzdj").strip() + + from_dump = (args.from_dump or "").strip() + if from_dump and args.dump_only: + print("[失败] 不能同时使用 --from-dump 与 --dump-only") + return 1 + + if not _which("mongorestore"): + print("[失败] 未找到 mongorestore,请先安装 MongoDB Database Tools 并加入 PATH。") + return 1 + if not from_dump and not _which("mongodump"): + print("[失败] 未找到 mongodump,请先安装 MongoDB Database Tools 并加入 PATH。") + return 1 + + persistent = (args.dump_out or "").strip() + tmp = None + if from_dump: + dump_parent = os.path.abspath(from_dump) + elif persistent: + os.makedirs(persistent, exist_ok=True) + dump_parent = os.path.abspath(persistent) + else: + tmp = tempfile.mkdtemp(prefix="wz_mongo_dump_") + dump_parent = os.path.join(tmp, "out") + os.makedirs(dump_parent, exist_ok=True) + + try: + if not from_dump: + # mongodump --out dump_parent 会在 dump_parent// 下生成 .bson + dump_cmd = ["mongodump", "--uri=%s" % local_uri, "--db", local_db, "--out", dump_parent] + if not _run(dump_cmd): + print("[失败] mongodump 退出非 0") + return 1 + else: + print("[信息] 使用已有 dump(--from-dump),跳过 mongodump") + + src_dir = os.path.join(dump_parent, local_db) + if not os.path.isdir(src_dir): + print("[失败] 未找到 dump 子目录: %s" % src_dir) + return 1 + + if not from_dump: + print("[信息] 源库: %s (%s)" % (_mask_uri(local_uri), local_db)) + print("[信息] dump 目录(含子库 %s): %s" % (local_db, dump_parent)) + + if args.dump_only: + print("[信息] --dump-only:跳过后续 mongorestore。宝塔导入可压缩「%s」整目录或按面板要求选择。" % local_db) + return 0 + + if not remote_uri.strip(): + print("[失败] 未设置远端 URI。请设置环境变量 REMOTE_MONGODB_URI 或 --remote-uri(或 --env-remote 指向含线上 MONGODB_URI 的文件)。") + return 1 + + ns_from = "%s.*" % local_db + ns_to = "%s.*" % remote_db + restore_cmd = [ + "mongorestore", + "--uri=%s" % remote_uri, + "--nsFrom=%s" % ns_from, + "--nsTo=%s" % ns_to, + ] + if args.drop: + restore_cmd.append("--drop") + restore_cmd.append(dump_parent) + print("[信息] 目标库: %s (%s)" % (_mask_uri(remote_uri), remote_db)) + if not _run(restore_cmd): + print("[失败] mongorestore 退出非 0。") + print(" 若报错为 connection refused / timeout:线上 Mongo 常仅监听 127.0.0.1,公网连不到。") + print(" 做法:本机开 SSH 隧道 ssh -N -L 27018:127.0.0.1:27017 你的用户@服务器IP") + print(" 再设 REMOTE_MONGODB_URI 为 mongodb://wzdj:***@127.0.0.1:27018/wzdj?authSource=wzdj 并加 --no-fix-remote-host") + print(" 或把 dump 拷到服务器后对 127.0.0.1 执行 mongorestore;见 scripts/MONGO_宝塔同步.md") + return 1 + print("[成功] 已从 %s 恢复到 %s" % (local_db, remote_db)) + return 0 + finally: + if tmp: + shutil.rmtree(tmp, ignore_errors=True) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/new/wz-app/README.md b/new/wz-app/README.md index b769d69..71b7f06 100644 --- a/new/wz-app/README.md +++ b/new/wz-app/README.md @@ -1,4 +1,6 @@ -# wz-app(玩值 C 端 · Vite + React) +# wz-app(玩值移动端 · Vite + React) + +> **不叫「微信小程序」**:本仓库中「小程序/微信原生」指 Soul 目录 **`miniprogram/`**。本目录是 **玩值用户侧移动端(H5 等)** 的唯一新功能交付,智能体角色为 **玩值移动端工程师**。 ## 不要用 HBuilderX 的「uni-app / 运行到浏览器」编译本目录 @@ -36,7 +38,7 @@ | 项 | 说明 | |----|------| | `new/wz-api` | 玩值 Go 后端,C 端 `/api/app/*` | -| **`new/wz-app/`(本目录)** | **玩值 C 端唯一新功能交付**(Vite + React) | +| **`new/wz-app/`(本目录)** | **玩值移动端唯一新功能交付**(Vite + React) | | `old/app/` | Next 历史 C 端,**仅对照** 是否漏页/漏接口,不在此扩新功能 | ## 技术栈 diff --git a/new/wz-app/dist/index.html b/new/wz-app/dist/index.html index 6011320..9dfae1c 100644 --- a/new/wz-app/dist/index.html +++ b/new/wz-app/dist/index.html @@ -11,8 +11,8 @@ rel="stylesheet" /> 玩值电竞 - WanZhi Esports - - + +
diff --git a/new/wz-app/package-lock.json b/new/wz-app/package-lock.json index daec873..eb0e92a 100644 --- a/new/wz-app/package-lock.json +++ b/new/wz-app/package-lock.json @@ -16,6 +16,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.23.24", + "hls.js": "^1.6.16", "lucide-react": "^1.8.0", "react": "^19.2.5", "react-dom": "^19.2.5", @@ -2905,6 +2906,12 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hls.js": { + "version": "1.6.16", + "resolved": "https://registry.npmmirror.com/hls.js/-/hls.js-1.6.16.tgz", + "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", + "license": "Apache-2.0" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", diff --git a/new/wz-app/package.json b/new/wz-app/package.json index 5f16834..de69391 100644 --- a/new/wz-app/package.json +++ b/new/wz-app/package.json @@ -18,6 +18,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.23.24", + "hls.js": "^1.6.16", "lucide-react": "^1.8.0", "react": "^19.2.5", "react-dom": "^19.2.5", diff --git a/new/wz-app/src/api/catalog.ts b/new/wz-app/src/api/catalog.ts new file mode 100644 index 0000000..2b35d82 --- /dev/null +++ b/new/wz-app/src/api/catalog.ts @@ -0,0 +1,191 @@ +/** + * wz-api 玩值 Mongo 目录只读接口(anonymous OK),与 `/api/admin/wz/*` 同源数据、字段已由后端裁剪。 + */ +import { request } from "@/lib/request" + +export type AppGuildDTO = { + id: string + name?: string + logo?: string + coverImage?: string + description?: string + memberCount?: number + maxMembers?: number + level?: number + totalIncome?: number + games?: unknown + tags?: unknown + benefits?: unknown + requirements?: string + status?: string +} + +export async function appGuilds(): Promise { + const r = await request("/api/app/guilds", { method: "GET", skipAuth: true }) + return Array.isArray(r.data) ? r.data : [] +} + +export async function appGuild(id: string) { + return request(`/api/app/guilds/${encodeURIComponent(id)}`, { method: "GET", skipAuth: true }) +} + +export type AppVenueDTO = { + id: string + name?: string + type?: string + city?: string + address?: string + image?: string + coverImage?: string + priceNote?: string + discount?: number + lat?: number + lng?: number +} + +export async function appVenues(type?: string): Promise { + const q = type?.trim() ? `?type=${encodeURIComponent(type)}` : "" + const r = await request(`/api/app/venues${q}`, { method: "GET", skipAuth: true }) + return Array.isArray(r.data) ? r.data : [] +} + +export type AppProductDTO = { + id: string + name?: string + category?: string + game?: string + price?: number + image?: string + coverImage?: string +} + +export async function appProducts(category?: string): Promise { + const q = category?.trim() ? `?category=${encodeURIComponent(category)}` : "" + const r = await request(`/api/app/products${q}`, { method: "GET", skipAuth: true }) + return Array.isArray(r.data) ? r.data : [] +} + +export type AppContentItemDTO = { + id: string + title?: string + name?: string + author?: string + price?: number + image?: string + coverImage?: string + students?: number + game?: string +} + +export async function appContentItems(opts?: { type?: string }) { + const q = opts?.type ? `?type=${encodeURIComponent(opts.type)}` : "" + const r = await request(`/api/app/content/items${q}`, { + method: "GET", + skipAuth: true, + }) + return Array.isArray(r.data) ? r.data : [] +} + +export async function appContentItem(id: string) { + return request( + `/api/app/content/items/${encodeURIComponent(id)}`, + { method: "GET", skipAuth: true }, + ) +} + +export type AppStarDTO = { + id: string + name?: string + title?: string + game?: string + avatar?: string + coverImage?: string + tags?: string[] + fans?: number + rating?: number + price?: number + hourlyRate?: number + isOnline?: boolean +} + +export async function appStars(): Promise { + const r = await request("/api/app/stars", { method: "GET", skipAuth: true }) + return Array.isArray(r.data) ? r.data : [] +} + +export async function appStar(id: string) { + return request< + AppStarDTO & { + intro?: string + cover?: string + partyRoomId?: string + services?: { id: string; name: string; price: number; desc?: string; courseId?: string; hot?: boolean }[] + achievements?: unknown + } + >(`/api/app/stars/${encodeURIComponent(id)}`, { method: "GET", skipAuth: true }) +} + +export type AppGameAccountDTO = { + id: string + title?: string + name?: string + characterName?: string + game?: string + server?: string + rank?: string + price?: number + heroCount?: number + skins?: number + verified?: boolean + image?: string +} + +export async function appGameAccounts(game?: string): Promise { + const q = game?.trim() ? `?game=${encodeURIComponent(game)}` : "" + const r = await request(`/api/app/game-accounts${q}`, { + method: "GET", + skipAuth: true, + }) + return Array.isArray(r.data) ? r.data : [] +} + +export type AppLevelProductDTO = { + id: string + name?: string + type?: string + game?: string + subtitle?: string + priceNote?: string + delivery?: string + icon?: string + image?: string +} + +export async function appLevelingProducts(): Promise { + const r = await request("/api/app/leveling/products", { + method: "GET", + skipAuth: true, + }) + return Array.isArray(r.data) ? r.data : [] +} + +export type AppPartyRoomDTO = { + id: string + name?: string + host?: string + avatar?: string + game?: string + background?: string +} + +export async function appPartyRooms(): Promise { + const r = await request("/api/app/party/rooms", { method: "GET", skipAuth: true }) + return Array.isArray(r.data) ? r.data : [] +} + +export async function appPartyRoom(id: string) { + return request(`/api/app/party/rooms/${encodeURIComponent(id)}`, { + method: "GET", + skipAuth: true, + }) +} diff --git a/new/wz-app/src/api/games.ts b/new/wz-app/src/api/games.ts new file mode 100644 index 0000000..8a1fe1d --- /dev/null +++ b/new/wz-app/src/api/games.ts @@ -0,0 +1,17 @@ +import { request } from "@/lib/request" + +export type AppGame = { + id?: string + name?: string + shortName?: string + category?: string + status?: string + sortOrder?: number +} + +/** GET /api/app/games — Mongo `games` 集合,前端用于分区入口 */ +export async function getAppGames(): Promise { + const r = await request("/api/app/games", { method: "GET", skipAuth: true }) + if (!Array.isArray(r.data)) return [] + return r.data as AppGame[] +} diff --git a/new/wz-app/src/api/live.ts b/new/wz-app/src/api/live.ts index 9f6f932..650419f 100644 --- a/new/wz-app/src/api/live.ts +++ b/new/wz-app/src/api/live.ts @@ -56,14 +56,15 @@ export async function postLiveComment( export async function postLiveGift(streamerId: string, body: { giftId: string; quantity: number; amount: number }): Promise<{ orderNo: string deducted: number + balance?: number }> { - const r = await request<{ orderNo: string; deducted: number }>("/api/app/live/gift", { + const r = await request<{ orderNo: string; deducted: number; balance?: number }>("/api/app/live/gift", { method: "POST", body: JSON.stringify({ streamerId, ...body }), }) if (!r.data || typeof r.data !== "object") { return { orderNo: "", deducted: 0 } } - const d = r.data as { orderNo?: string; deducted?: number } - return { orderNo: d.orderNo ?? "", deducted: d.deducted ?? 0 } + const d = r.data as { orderNo?: string; deducted?: number; balance?: number } + return { orderNo: d.orderNo ?? "", deducted: d.deducted ?? 0, balance: d.balance } } diff --git a/new/wz-app/src/api/orders.ts b/new/wz-app/src/api/orders.ts new file mode 100644 index 0000000..3384b46 --- /dev/null +++ b/new/wz-app/src/api/orders.ts @@ -0,0 +1,39 @@ +/** + * C 端订单与钱包:`/api/app/orders/me`、`/api/app/wallet/recharge` + */ +import { request } from "@/lib/request" + +export type AppOrderDTO = { + id: string + orderNo?: string + type: string + title: string + price: number + status: string + date: string + image?: string +} + +export async function appOrdersMe() { + return request("/api/app/orders/me", { method: "GET" }) +} + +export async function appWalletRecharge(amount: number) { + return request<{ balance?: number; orderNo?: string }>("/api/app/wallet/recharge", { + method: "POST", + body: JSON.stringify({ amount }), + }) +} + +/** 虚拟消费:扣 Mongo 余额并写入带 wzAppConsume 的商品订单占位 */ +export async function appWalletConsume(payload: { + amount: number + title: string + kind: string + image?: string +}) { + return request<{ balance?: number; orderNo?: string }>("/api/app/wallet/consume", { + method: "POST", + body: JSON.stringify(payload), + }) +} diff --git a/new/wz-app/src/components/live/LivePlaybackSurface.tsx b/new/wz-app/src/components/live/LivePlaybackSurface.tsx new file mode 100644 index 0000000..157926f --- /dev/null +++ b/new/wz-app/src/components/live/LivePlaybackSurface.tsx @@ -0,0 +1,152 @@ +import { useEffect, useRef, useState } from "react" +import Hls from "hls.js" +import { ExternalLink } from "lucide-react" +import { FillImage } from "@/components/media/FillImage" +import { Button } from "@/components/ui/button" + +export type PlaybackMode = + | { kind: "iframe"; src: string } + | { kind: "video"; src: string; useHls: boolean } + | { kind: "openExternal"; url: string } + +/** 将主播填写的拉流/第三方地址分类为 iframe、HLS/直链 video、或外链打开 */ +export function classifyPlaybackUrl(raw: string): PlaybackMode | null { + const s = raw.trim() + if (!s) return null + let u: URL + try { + u = new URL(s) + } catch { + return null + } + if (u.protocol !== "http:" && u.protocol !== "https:") return null + const host = u.hostname.toLowerCase() + const pathname = u.pathname + + if (host === "youtu.be") { + const id = pathname.replace(/^\//, "").split("/")[0] ?? "" + if (id && /^[\w-]{6,}$/.test(id)) { + return { kind: "iframe", src: `https://www.youtube.com/embed/${encodeURIComponent(id)}` } + } + } + if (host.includes("youtube.com")) { + let id = u.searchParams.get("v") ?? "" + if (!id && pathname.toLowerCase().startsWith("/embed/")) { + id = pathname.slice("/embed/".length).split("/")[0] ?? "" + } + if (!id && pathname.toLowerCase().startsWith("/shorts/")) { + id = pathname.slice("/shorts/".length).split("/")[0] ?? "" + } + if (id && /^[\w-]{6,}$/.test(id)) { + return { kind: "iframe", src: `https://www.youtube.com/embed/${encodeURIComponent(id)}` } + } + } + + const bvMatch = s.match(/(BV[\w]+)/i) + if (bvMatch) { + const bvid = bvMatch[1]! + return { + kind: "iframe", + src: `https://player.bilibili.com/player.html?bvid=${encodeURIComponent(bvid)}&page=1&high_quality=1&danmaku=0`, + } + } + + const pathAndQuery = (pathname + u.search).toLowerCase() + if (pathAndQuery.includes(".m3u8") || s.toLowerCase().includes(".m3u8")) { + return { kind: "video", src: s, useHls: true } + } + if (/\.(mp4|webm|ogg)(\?|#|$)/i.test(pathAndQuery)) { + return { kind: "video", src: s, useHls: false } + } + + if (host.includes("twitch.tv") || host.includes("douyin.com") || host.includes("tiktok.com")) { + return { kind: "openExternal", url: s } + } + + return { kind: "openExternal", url: s } +} + +type Props = { + url: string + coverUrl?: string + /** 直播中但无法在本页解码时兜底文案 */ + isLive?: boolean +} + +export function LivePlaybackSurface({ url, coverUrl, isLive }: Props) { + const mode = classifyPlaybackUrl(url) + const videoRef = useRef(null) + const [hlsUnsupported, setHlsUnsupported] = useState(false) + + useEffect(() => { + setHlsUnsupported(false) + if (!mode || mode.kind !== "video") return + const el = videoRef.current + if (!el) return + if (!mode.useHls) { + el.src = mode.src + return + } + if (Hls.isSupported()) { + const hls = new Hls({ enableWorker: true, lowLatencyMode: true }) + hls.loadSource(mode.src) + hls.attachMedia(el) + return () => { + hls.destroy() + } + } + if (el.canPlayType("application/vnd.apple.mpegurl")) { + el.src = mode.src + return + } + setHlsUnsupported(true) + }, [mode]) + + if (!mode) return null + + if (mode.kind === "iframe") { + return ( +