diff --git a/.env.example b/.env.example index 0c8d122..02710b4 100644 --- a/.env.example +++ b/.env.example @@ -1,50 +1,56 @@ -# ========== NomadCNA 环境变量示例(勿提交真实密钥) ========== +# ========== NomadCNA 环境变量示例 ========== +# 复制为 .env.local 后按环境填写。 +# +# Windows 本地开发连线上服务:见 .env.local 已配好的模板,并运行: +# python scripts/dev-tunnel.py # MinIO SSH 隧道 +# pnpm dev # Next.js +# backend/.venv/Scripts/uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload +# 或一键:powershell -ExecutionPolicy Bypass -File scripts/dev-windows.ps1 + APP_ENV=development NEXT_PUBLIC_APP_ENV=development +ROOT_DOMAIN=nomadro.cn +NEXT_PUBLIC_ROOT_DOMAIN=nomadro.cn -# PocketBase(服务端) -POCKETBASE_URL=http://127.0.0.1:8090 -POCKETBASE_EMAIL=admin@example.com -POCKETBASE_PASSWORD=changeme -NEXT_PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090 -POCKETBASE_JOIN_COLLECTION=member_applications - -# 支付 -PAYMENT_API_URL=http://127.0.0.1:8000 -PAYMENT_PROVIDER=zpay -PAYMENT_DEFAULT_AMOUNT=80 -PAYMENT_DEFAULT_TYPE=meetup -PAYMENT_JOIN_AMOUNT=80 -PAYMENT_JOIN_TYPE=meetup -ZPAY_PID=your_zpay_pid -ZPAY_KEY=your_zpay_key -ZPAY_SUBMIT_URL=https://zpayz.cn/submit.php -XORPAY_AID=your_xorpay_aid -XORPAY_SECRET=your_xorpay_secret - -# MinIO 存储 -MINIO_ENDPOINT=127.0.0.1 -MINIO_PORT=9000 -MINIO_USE_SSL=false -MINIO_BUCKET=nomadcna -MINIO_ACCESS_KEY=minioadmin -MINIO_SECRET_KEY=minioadmin -MINIO_REGION=china -MINIO_UPLOAD_PREFIX=uploads -NEXT_PUBLIC_MINIO_PUBLIC_URL=http://127.0.0.1:9000/nomadcna - -# 站点 +# 本地前端 NEXT_PUBLIC_SITE_URL=http://localhost:3001 -NEXT_PUBLIC_SITE_ID=nomadcna +NEXT_PUBLIC_SITE_ID=meetup NEXT_PUBLIC_API_BASE_URL= - -# 视频会议与群聊 -NEXT_PUBLIC_MIROTALK_URL=https://mirotalk.nomadro.cn -NEXT_PUBLIC_LOUNGE_URL=https://lounge.nomadro.cn - -# FastAPI 代理(本地开发默认 127.0.0.1:8000) FASTAPI_PROXY_TARGET=http://127.0.0.1:8000 FRONTEND_ORIGINS=http://localhost:3001,http://127.0.0.1:3001 -# 生产环境跨站 SSO(本地不设置) -# AUTH_COOKIE_DOMAIN=.nomadro.cn +# SSH 隧道(Windows 访问线上 MinIO S3 API) +DEV_SSH_HOST=82.157.112.245 +DEV_SSH_USER=ubuntu +DEV_SSH_PASSWORD=your_ssh_password +DEV_TUNNEL_MINIO_PORT=9100 +DEV_TUNNEL_POCKETBASE=0 + +# PocketBase(Windows 用公网域名;Linux 线上同机可用 http://127.0.0.1:8090) +POCKETBASE_URL=https://pocketbase.nomadro.cn +NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.nomadro.cn +POCKETBASE_EMAIL=admin@example.com +POCKETBASE_PASSWORD=changeme + +# MinIO(Windows:先开隧道,再连 127.0.0.1:9100;公开读 URL 用线上域名) +MINIO_ENDPOINT=127.0.0.1 +MINIO_PORT=9100 +MINIO_USE_SSL=false +MINIO_BUCKET=hackrobot +MINIO_PUBLIC_URL=https://minio.nomadro.cn/hackrobot +NEXT_PUBLIC_MINIO_PUBLIC_URL=https://minio.nomadro.cn/hackrobot +MINIO_UPLOAD_PREFIX=nomadcna +MINIO_ACCESS_KEY=your_minio_access_key +MINIO_SECRET_KEY=your_minio_secret_key +MINIO_REGION=china + +# 支付 +PAYMENT_PROVIDER=zpay +PAYMENT_DEFAULT_AMOUNT=100 +PAYMENT_JOIN_AMOUNT=100 +PAYMENT_API_URL=http://127.0.0.1:8000 +DEV_AUTO_PAY=true + +# 视频会议 / 群聊 +NEXT_PUBLIC_MIROTALK_URL=https://mirotalk.nomadro.cn +NEXT_PUBLIC_LOUNGE_URL=https://lounge.nomadro.cn diff --git a/.env.local b/.env.local index 26a9f62..459e423 100644 --- a/.env.local +++ b/.env.local @@ -1,21 +1,30 @@ -# Linux 生产环境 - nomadro.cn -APP_ENV=production -NEXT_PUBLIC_APP_ENV=production +# ========== Windows 本地开发 · 连线上 nomadro.cn 服务 ========== +APP_ENV=development +NEXT_PUBLIC_APP_ENV=development ROOT_DOMAIN=nomadro.cn NEXT_PUBLIC_ROOT_DOMAIN=nomadro.cn -NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.nomadro.cn -POCKETBASE_URL=http://127.0.0.1:8090 -PAYMENT_API_URL=https://api.nomadro.cn -PAYMENT_PROVIDER=zpay -PAYMENT_DEFAULT_AMOUNT=100 -PAYMENT_JOIN_AMOUNT=100 -NEXT_PUBLIC_SITE_ID=meetup -NEXT_PUBLIC_SITE_URL=https://nomadro.cn -NEXT_PUBLIC_API_BASE_URL=/api -NEXT_PUBLIC_MIROTALK_URL=https://mirotalk.nomadro.cn -NEXT_PUBLIC_LOUNGE_URL=https://lounge.nomadro.cn -# MinIO +# 本地前端 +NEXT_PUBLIC_SITE_URL=http://localhost:3001 +NEXT_PUBLIC_SITE_ID=meetup +NEXT_PUBLIC_API_BASE_URL= +FASTAPI_PROXY_TARGET=http://127.0.0.1:8000 +FRONTEND_ORIGINS=http://localhost:3001,http://127.0.0.1:3001 + +# SSH 隧道(scripts/dev-tunnel.py 用) +DEV_SSH_HOST=82.157.112.245 +DEV_SSH_USER=ubuntu +DEV_SSH_PASSWORD=Xiao4669805@ +DEV_TUNNEL_MINIO_PORT=9100 +DEV_TUNNEL_POCKETBASE=0 + +# PocketBase:服务端走公网域名(Windows 无法直连服务器 127.0.0.1:8090) +POCKETBASE_URL=https://pocketbase.nomadro.cn +NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.nomadro.cn +POCKETBASE_EMAIL=xiaoshuang.eric@gmail.com +POCKETBASE_PASSWORD=Xiao4669805@ + +# MinIO:上传走 SSH 隧道映射的本地 9100;公开读 URL 走线上域名 MINIO_ENDPOINT=127.0.0.1 MINIO_PORT=9100 MINIO_USE_SSL=false @@ -27,6 +36,13 @@ MINIO_ACCESS_KEY=nomadro MINIO_SECRET_KEY=Xiao4669805@ MINIO_REGION=china -# PocketBase 管理员(check-user、ensure-user、vip 需要) -POCKETBASE_EMAIL=xiaoshuang.eric@gmail.com -POCKETBASE_PASSWORD=Xiao4669805@ +# 支付 / 视频会议 / 群聊(线上服务) +PAYMENT_PROVIDER=zpay +PAYMENT_DEFAULT_AMOUNT=100 +PAYMENT_JOIN_AMOUNT=100 +PAYMENT_API_URL=http://127.0.0.1:8000 +NEXT_PUBLIC_MIROTALK_URL=https://mirotalk.nomadro.cn +NEXT_PUBLIC_LOUNGE_URL=https://lounge.nomadro.cn + +# 本地调试可自动完成支付(无需真实回调) +DEV_AUTO_PAY=true diff --git a/README.md b/README.md index 7dff968..1961556 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,10 @@ NomadCNA 的目标用户是正在中国或亚洲范围内长期旅居、远程 | --- | --- | | 城市探索 | 展示中国数字游民城市数据,包括生活成本、网络、温度、社群密度、安全、文化等指标。 | | 下一站决策 | 根据预算、网络、气候、标签偏好生成城市推荐。 | -| 线上/线下活动 | 支持公开、登录成员、VIP、主持人等权限层级;线上和混合活动可进入 MiroTalk 视频房。 | -| Web 群聊 | 使用 The Lounge + Ergo IRC Server 提供数字游民 Web 群组聊天,活动参与人可进入群聊。 | +| 线上/线下活动 | 支持公开、登录成员、VIP、主持人等权限层级;线上/混合活动进入 **统一活动间**(MiroTalk 视频 + The Lounge 群聊分屏),账户昵称自动带入视频房。 | +| Web 群聊 | The Lounge + Ergo IRC;每活动可配置 `loungeChannel`(如 `#nomadcna-20260608`),默认频道 `#nomadcna,#online-events`。 | +| 访谈视频 | `/videos` 博主访谈列表,`/videos/[slug]` 单条详情;首页「访谈视频」链到列表页。 | +| 社区匹配 | `/dating` 滑动匹配:意图 Tab、城市筛选、互赞、每日额度、高级筛选、撤销滑动。 | | 会员加入与支付 | 支持加入表单、支付订单、支付状态轮询、VIP/会员写入。 | | Digital 子站 | `/digital` 是内容专题站,包含数字游民指南、课程、电子书、资源、工作机会、个人资料等。 | | 媒体与内容 | 支持图片/视频上传、远程媒体代理缓存、内容投稿、博客、电子书、视频等内容页面。 | @@ -58,6 +60,8 @@ NomadCNA 的目标用户是正在中国或亚洲范围内长期旅居、远程 │ └── lib/ # API、支付、媒体、活动、PocketBase 等工具 ├── backend/ # FastAPI 后端 ├── config/ # 主站和 Digital 子站配置 +│ └── remote-services.ts # 线上服务地址、Windows 远程开发模式 +├── scripts/ # dev-tunnel.py、dev-windows.ps1 ├── i18n/ # 路由和国际化导航封装 ├── messages/ # zh/en 文案 ├── public/ # 静态资源 @@ -73,9 +77,13 @@ NomadCNA 的目标用户是正在中国或亚洲范围内长期旅居、远程 - `/zh`、`/en`:首页与城市探索 - `/zh/dashboard`:下一站决策仪表盘 -- `/zh/meetups`:线上/线下活动,含 MiroTalk 视频房和 Web 群聊入口 +- `/zh/meetups`:线上/线下活动列表与详情弹窗 - `/zh/meetups/host`:活动发布页 -- `/zh/dating`:社区交友/匹配 +- `/zh/meetups/[id]/live`:**统一活动间**(内嵌 MiroTalk + The Lounge,需权限) +- `/zh/videos`:博主访谈视频列表 +- `/zh/videos/[slug]`:单条访谈详情 +- `/zh/dating`:社区滑动匹配(意图、城市、互赞、额度) +- `/zh/join`:成员加入(城市、寻找意图、资料) - `/zh/discuss`:讨论 - `/zh/gigs`:任务赏金 - `/zh/tools`:工具箱 @@ -154,13 +162,17 @@ FastAPI 入口为 `backend/main.py`,配置读取在 `backend/settings.py`。 | `/cnomadcna/*_notify` | 支付网关回调 | | `/api/cities`、`/api/cities/{slug}` | 城市数据 | | `/api/content/*` | 首页内容、内容投稿、内容详情 | +| `/api/videos` | 访谈视频列表(`type=video` 的 content_items) | | `/api/notifications/*` | 通知、回执、偏好 | | `/api/recommendations/next-stop` | 下一站推荐 | -| `/api/meetups` | 活动列表与活动创建 | +| `/api/meetups` | 活动列表与创建 | +| `/api/meetups/{id}` | 单个活动详情 | +| `/api/meetups/{id}/session` | 活动间会话:权限校验、视频/群聊 URL、社区昵称 | +| `/api/matches/*` | 匹配候选、滑动、互赞、额度、撤销、我的资料 | | `/api/discussions` | 讨论列表与创建 | | `/api/gigs` | 任务赏金列表、创建、申请 | -| `/api/profiles`、`/api/matches/swipes` | 资料与匹配 | -| `/api/feedback` | 用户反馈 | +| `/api/profiles` | 资料列表 | +| `/api/feedback` | 用户反馈(支持 `?type=report` 预填) | | `/api/stats/*` | 成员地图、概览、城市排行 | | `/api/services`、`/api/services/leads` | 增值服务与线索 | | `/api/ai/assistant` | AI 助手占位接口 | @@ -170,31 +182,68 @@ FastAPI 入口为 `backend/main.py`,配置读取在 `backend/settings.py`。 - `backend/pb_client.py`:PocketBase HTTP 客户端 - `backend/payment.py`:ZPay / XorPay 支付适配 - `backend/minio_storage.py`:MinIO 上传 +- `backend/meetup_live.py`:活动间 URL 生成、权限判断(MiroTalk / Lounge) +- `backend/avatar_assets.py`:与前端同步的头像池(seed 用) - `backend/init_db.py`、`seed_data.py`、`enhanced_seed.py`:初始化与种子数据 -- `backend/settings.py`:环境变量读取 +- `backend/settings.py`:环境变量读取(含 `MIROTALK_URL`、`LOUNGE_URL`) -### 活动、视频与群聊源码 +### 活动、视频与群聊(深度融合) -活动相关逻辑主要在: +相关文件: -- `app/[locale]/meetups/page.tsx` -- `app/[locale]/meetups/host/page.tsx` -- `app/lib/meetups.ts` +| 文件 | 作用 | +| --- | --- | +| `app/[locale]/meetups/page.tsx` | 活动列表、详情弹窗、「进入活动间」入口 | +| `app/[locale]/meetups/[id]/live/page.tsx` | 统一活动间页面 | +| `app/components/meetups/EventLiveRoom.tsx` | 分屏 UI:视频 iframe + 群聊 iframe | +| `app/[locale]/meetups/host/page.tsx` | 发起活动 | +| `app/lib/meetups.ts` | MiroTalk/Lounge URL、房间 slug、`loungeChannel`、权限工具 | +| `backend/meetup_live.py` | 后端同名逻辑,供 `/api/meetups/{id}/session` 使用 | `app/lib/meetups.ts` 负责: -- MiroTalk 默认地址:`https://mirotalk.nomadro.cn` -- MiroTalk 房间名规范化 -- MiroTalk 入会链接生成 -- Lounge 默认地址:`https://lounge.nomadro.cn` -- Web 群聊服务标签:`The Lounge + Ergo IRC` +- MiroTalk 默认地址:`NEXT_PUBLIC_MIROTALK_URL`(生产 `https://mirotalk.nomadro.cn`) +- MiroTalk 房间名规范化、`buildMiroTalkJoinUrl(meetup, displayName)` +- Lounge 默认地址:`NEXT_PUBLIC_LOUNGE_URL`(生产 `https://lounge.nomadro.cn`) +- 活动专属 IRC 频道:`loungeChannel` 字段,或从 `mirotalkRoom` 推导 +- 群聊 deep link:`buildMeetupChatUrl(meetup)` → `lounge.nomadro.cn/#/chan-#频道` +- 活动间路径:`buildMeetupLivePath(id)` → `/meetups/{id}/live` -线上/混合活动详情页会展示: +**账户打通**:`GET /api/meetups/{id}/session` 读取 PocketBase 登录态,校验 `public/members/vip/hosts`,将社区昵称写入 MiroTalk `name=` 参数。 -- MiroTalk P2P 视频入口 -- 页面内 iframe 打开视频房 -- The Lounge Web 群聊入口 -- 信令/中继服务器和群聊入口域名 +PocketBase `meetups` 集合字段(线上活动相关): + +- `mode`:`offline` / `online` / `hybrid` +- `accessLevel`:`public` / `members` / `vip` / `hosts` +- `meetingProvider`:如 `mirotalk` +- `mirotalkRoom`:视频房间 slug +- `loungeChannel`:IRC 频道,如 `#nomadcna-20260608` +- `features`:JSON,如 `group_video`、`screen_share` + +### 访谈视频 + +- 列表:`app/[locale]/videos/page.tsx`,数据来自 `GET /api/videos` +- 详情:`app/[locale]/videos/[slug]/page.tsx` +- 首页右侧「访谈视频」卡片链到 `/videos`(非单条 slug) + +### 社区匹配(Dating) + +- 页面:`app/[locale]/dating/page.tsx` +- 组件:`app/[locale]/dating/components/`(`IntentTabs`、`CityFilter`、`SwipeStack`、`LikesPanel`、`MatchModal`、`AdvancedFilters`、`SwipeQuota` 等) +- 工具:`app/[locale]/dating/utils.ts`、`constants.ts`、`types.ts` +- 头像池:`app/data/avatars.ts`(Unsplash 人像,与 `backend/avatar_assets.py` 同步) + +匹配 API(均需登录,部分需 VIP): + +- `GET /api/matches/candidates` — 候选列表(意图、城市、性别筛选) +- `POST /api/matches/swipes` — 左滑/右滑/超级赞 +- `POST /api/matches/swipes/undo` — 撤销上一次滑动 +- `GET /api/matches/likes`、`/mutual`、`/quota`、`/profile/me` + +### 媒体代理 + +- `app/lib/media.ts`:封面 URL 规范化(Unsplash 直连、MinIO 相对路径补全) +- `app/api/media/proxy/route.ts`:远程媒体抓取;MinIO 失败时直返图片 ### 配置与国际化 @@ -205,6 +254,7 @@ FastAPI 入口为 `backend/main.py`,配置读取在 `backend/settings.py`。 | `config/footer.config.ts` | 页脚 | | `config/services.config.ts` | 主站支付配置 | | `config/runtime-env.ts` | 运行环境判断 | +| `config/remote-services.ts` | 线上域名、MinIO/PocketBase 解析、远程开发模式 | | `config/digital/*` | Digital 子站主题、服务、课程、域名配置 | | `messages/zh.json`、`messages/en.json` | 中英文文案 | | `i18n/routing.ts` | 语言列表与默认语言 | @@ -226,6 +276,41 @@ FastAPI 入口为 `backend/main.py`,配置读取在 `backend/settings.py`。 pnpm install ``` +### Windows 本地连线上服务 + +典型配置见 `.env.example` / `.env.local`: + +1. PocketBase 用公网 `https://pocketbase.nomadro.cn` +2. MinIO S3 API 用 SSH 隧道映射到本机 `9100`: + +```powershell +python scripts/dev-tunnel.py +``` + +3. 本地跑 Next.js + FastAPI: + +```powershell +pnpm dev +# 另开终端 +backend\.venv\Scripts\uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload +``` + +或一键脚本: + +```powershell +powershell -ExecutionPolicy Bypass -File scripts/dev-windows.ps1 +``` + +服务地址推导见 `config/remote-services.ts`。 + +### 初始化数据库 + +```bash +python -m backend.init_db +``` + +会创建/补全 PocketBase 集合字段,并 upsert 城市、活动、内容、资料等种子数据。 + ### 前端开发 ```bash @@ -275,6 +360,7 @@ APP_ENV=production NEXT_PUBLIC_APP_ENV=production NODE_ENV=production pnpm build | `NEXT_PUBLIC_SITE_URL` | 站点根地址 | | `NEXT_PUBLIC_SITE_ID` | 站点 ID,用于会员/VIP 隔离 | | `NEXT_PUBLIC_API_BASE_URL` | API 基础地址,生产通常为 `/api` | +| `FASTAPI_PROXY_TARGET` | Next.js rewrite 转发 FastAPI 的目标(本地 `http://127.0.0.1:8000`) | | `NEXT_PUBLIC_POCKETBASE_URL` | 浏览器可访问 PocketBase 地址 | | `NEXT_PUBLIC_MIROTALK_URL` | MiroTalk P2P 服务地址 | | `NEXT_PUBLIC_LOUNGE_URL` | The Lounge Web 群聊地址 | @@ -304,6 +390,8 @@ APP_ENV=production NEXT_PUBLIC_APP_ENV=production NODE_ENV=production pnpm build | `MINIO_UPLOAD_PREFIX` | 上传路径前缀 | | `FRONTEND_ORIGINS` | FastAPI CORS 白名单 | | `UPLOAD_DIR` | 后端本地上传目录 | +| `MIROTALK_URL` / `LOUNGE_URL` | 后端生成活动间 URL(默认同公网域名) | +| `DEV_SSH_HOST` / `DEV_SSH_PASSWORD` | Windows SSH 隧道(仅本地,勿提交) | ## 生产服务器支持 @@ -484,7 +572,7 @@ curl -I https://mirotalk.nomadro.cn/join?room=nomadcna-healthcheck - 只监听 `127.0.0.1:9000` - Nginx 通过 `https://lounge.nomadro.cn` 反代 - 默认网络锁定到本机 Ergo:`127.0.0.1:6667` -- 默认加入频道:`#nomadcna,#online-events` +- 默认加入频道:`#nomadcna,#online-events`(可按活动追加,如 `#nomadcna-20260608`) - 文件上传关闭 - The Lounge 自身消息持久化关闭,Ergo 保留内存历史 @@ -578,11 +666,17 @@ curl -I https://ergo.nomadro.cn/ ### 活动视频房如何生成? -`app/lib/meetups.ts` 会根据活动城市、日期、时间和 ID 生成 MiroTalk room slug。如果活动有自定义 `meetingUrl`,则优先使用自定义链接。 +`app/lib/meetups.ts` 会根据活动 `mirotalkRoom`、或城市/日期/时间/ID 生成 room slug。登录用户通过 `/api/meetups/{id}/session` 获取带 `name=` 的入会 URL。 + +### 如何进入统一活动间? + +活动详情弹窗点 **「进入活动间」** → `/meetups/{id}/live`。页面内分屏嵌入 MiroTalk 与 The Lounge;也可新窗口单独打开。 ### 群聊频道如何进入? -The Lounge public mode 的默认网络已锁定到 Ergo,并默认加入 `#nomadcna` 和 `#online-events`。活动详情页的“进入群聊”按钮指向 `https://lounge.nomadro.cn`。 +- 全局:`https://lounge.nomadro.cn`(默认 `#nomadcna`、`#online-events`) +- 单活动:`buildMeetupChatUrl(meetup)` → `lounge.nomadro.cn/#/chan-#频道` +- PocketBase 活动记录可设 `loungeChannel` 字段 ## 维护命令速查 diff --git a/app/[locale]/dating/components/AdvancedFilters.tsx b/app/[locale]/dating/components/AdvancedFilters.tsx new file mode 100644 index 0000000..277abb8 --- /dev/null +++ b/app/[locale]/dating/components/AdvancedFilters.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { Link } from "@/i18n/navigation"; + +export interface AdvancedFilterState { + gender: string; + single: string; +} + +interface AdvancedFiltersProps { + value: AdvancedFilterState; + onChange: (next: AdvancedFilterState) => void; + isVip: boolean; + t: (key: string) => string; +} + +const GENDER_OPTIONS = ["all", "男", "女"]; +const SINGLE_OPTIONS = ["all", "单身", "恋爱中", "已婚", "不限"]; + +export default function AdvancedFilters({ value, onChange, isVip, t }: AdvancedFiltersProps) { + return ( +
{t("advancedFilters")}
+ {!isVip ? ( + + {t("upgradeVip")} + + ) : null} +{t("vipFilterHint")}
+ ) : null} +{t("likedYou")}
+{t("mutualSubtitle")}
+{t("matchCelebration")}
+{t("matchHint")}
+📍 {profile.location}
+📍 {profile.location}
+ {(profile.gender || profile.single) && ( ++ {[profile.gender, profile.single].filter(Boolean).join(" · ")} +
+ )} +{t("matchTags")}:
+{profile.bio}
+ ++ {t("quotaLow")}{" "} + + {t("upgradeVip")} + +
+ ) : null} +{t("heroSubtitle")}
+