游牧中国 NomadCNA
NomadCNA 是面向数字游民的生活操作系统,用一个站点承载城市决策、会员加入、线上/线下活动、视频会议、Web 群聊、内容资源、任务赏金、支付和媒体存储等能力。
生产站点:
- 主站:https://nomadro.cn
- API:https://api.nomadro.cn
- PocketBase:https://pocketbase.nomadro.cn
- MinIO:https://minio.nomadro.cn
- MiroTalk P2P 视频房:https://mirotalk.nomadro.cn
- Web 群聊:https://lounge.nomadro.cn
- IRC 服务名:https://ergo.nomadro.cn
业务介绍
NomadCNA 的目标用户是正在中国或亚洲范围内长期旅居、远程工作、寻找社群和机会的数字游民。业务上分为以下几条主线:
| 模块 | 说明 |
|---|---|
| 城市探索 | 展示中国数字游民城市数据,包括生活成本、网络、温度、社群密度、安全、文化等指标。 |
| 下一站决策 | 根据预算、网络、气候、标签偏好生成城市推荐。 |
| 线上/线下活动 | 支持公开、登录成员、VIP、主持人等权限层级;线上和混合活动可进入 MiroTalk 视频房。 |
| Web 群聊 | 使用 The Lounge + Ergo IRC Server 提供数字游民 Web 群组聊天,活动参与人可进入群聊。 |
| 会员加入与支付 | 支持加入表单、支付订单、支付状态轮询、VIP/会员写入。 |
| Digital 子站 | /digital 是内容专题站,包含数字游民指南、课程、电子书、资源、工作机会、个人资料等。 |
| 媒体与内容 | 支持图片/视频上传、远程媒体代理缓存、内容投稿、博客、电子书、视频等内容页面。 |
| 社区与增长 | 包含讨论、交友匹配、任务赏金、反馈、通知、站内消息、服务线索。 |
技术栈
| 层级 | 技术 |
|---|---|
| Web 前端 | Next.js 16 App Router、React 19、TypeScript、Tailwind CSS 4 |
| API | Next.js Route Handlers、FastAPI、PocketBase SDK/HTTP |
| 数据 | PocketBase、静态数据、MinIO 对象存储 |
| 支付 | ZPay / XorPay 适配层 |
| 视频会议 | MiroTalk P2P,Nginx HTTPS 反代 |
| Web 群聊 | The Lounge 4.5.0 + Ergo IRC Server 2.18.0 |
| 部署 | systemd、Nginx、Certbot、Docker |
| 包管理 | pnpm 10.25.0 |
生产服务器当前 Node.js 版本为 v22.13.1。
源码解析
顶层结构
.
├── app/ # Next.js App Router 主目录
│ ├── [locale]/ # 中文/英文路由,默认 zh
│ ├── api/ # Next.js Route Handlers
│ ├── components/ # 主站通用组件
│ ├── context/ # Locale、Theme 等上下文
│ ├── data/ # 城市、地图、mock 数据
│ ├── digital/ # Digital 子站组件、配置、数据和工具
│ └── lib/ # API、支付、媒体、活动、PocketBase 等工具
├── backend/ # FastAPI 后端
├── config/ # 主站和 Digital 子站配置
├── i18n/ # 路由和国际化导航封装
├── messages/ # zh/en 文案
├── public/ # 静态资源
├── cnomadcna.sh # 生产 Next.js 启动脚本
├── next.config.ts # Next.js 配置与 API rewrites
├── package.json # 前端依赖与脚本
└── tsconfig.json # TypeScript 配置
Next.js 页面结构
主站所有面向用户的页面位于 app/[locale],路由强制带语言前缀:
/zh、/en:首页与城市探索/zh/dashboard:下一站决策仪表盘/zh/meetups:线上/线下活动,含 MiroTalk 视频房和 Web 群聊入口/zh/meetups/host:活动发布页/zh/dating:社区交友/匹配/zh/discuss:讨论/zh/gigs:任务赏金/zh/tools:工具箱/zh/report、/zh/report/data:统计与年报/zh/digital:Digital 子站入口
通用布局与组件:
app/layout.tsx:根布局app/[locale]/layout.tsx:语言布局app/components/Navbar.tsx、Footer.tsx、Sidebar.tsx:主站导航与外壳app/context/LocaleContext.tsx:运行时文案读取app/context/ThemeContext.tsx:亮色/暗色主题
Digital 子站
Digital 子站代码集中在 app/digital 与 app/[locale]/digital:
app/[locale]/digital/page.tsx:专题首页app/[locale]/digital/course:课程页app/[locale]/digital/course/[moduleIndex]/[lessonIndex]:课程章节app/[locale]/digital/ebook:电子书app/[locale]/digital/jobs:远程工作app/[locale]/digital/join:加入/申请app/[locale]/digital/profile:用户资料
配置在 config/digital:
site.config.ts:专题主题、功能开关、课程/电子书/加入等能力services.config.ts:PocketBase、支付、MinIO 配置读取domain.config.ts:域名和服务地址推导course.ts、lessons.ts:课程数据
API 分层
本项目有两层 API:
- Next.js 内置 API:
app/api/* - FastAPI 后端:
backend/main.py
next.config.ts 将大多数 /api/* 转发到 FastAPI:
source: "/api/:path((?!(?:digital|media)(?:$|/.*)).*)"
destination: "http://127.0.0.1:8000/api/:path*"
因此:
/api/digital/*由 Next.js Route Handlers 处理/api/media/*由 Next.js Route Handlers 处理- 其他
/api/*默认转发到 FastAPI
Next.js API
| 路径 | 作用 |
|---|---|
app/api/digital/auth/* |
Digital 子站登录、退出、当前用户、注册、跨站同步 |
app/api/digital/join |
Digital 加入/申请表单写入 PocketBase |
app/api/digital/pay/* |
Digital 支付创建、确认、状态查询 |
app/api/digital/upload-media |
媒体上传到 MinIO |
app/api/digital/vip/check |
VIP 状态检查 |
app/api/media/proxy |
远程媒体抓取、校验、缓存到 MinIO 后跳转公开 URL |
FastAPI 后端
FastAPI 入口为 backend/main.py,配置读取在 backend/settings.py。核心能力:
| API | 作用 |
|---|---|
/api/health |
健康检查 |
/api/auth/email、/api/auth/google、/api/auth/me |
主站认证 |
/api/join、/api/join/status |
加入表单与状态 |
/api/upload-media |
后端媒体上传 |
/api/pay、/api/pay/status |
支付创建与查询 |
/cnomadcna/*_notify |
支付网关回调 |
/api/cities、/api/cities/{slug} |
城市数据 |
/api/content/* |
首页内容、内容投稿、内容详情 |
/api/notifications/* |
通知、回执、偏好 |
/api/recommendations/next-stop |
下一站推荐 |
/api/meetups |
活动列表与活动创建 |
/api/discussions |
讨论列表与创建 |
/api/gigs |
任务赏金列表、创建、申请 |
/api/profiles、/api/matches/swipes |
资料与匹配 |
/api/feedback |
用户反馈 |
/api/stats/* |
成员地图、概览、城市排行 |
/api/services、/api/services/leads |
增值服务与线索 |
/api/ai/assistant |
AI 助手占位接口 |
后端辅助模块:
backend/pb_client.py:PocketBase HTTP 客户端backend/payment.py:ZPay / XorPay 支付适配backend/minio_storage.py:MinIO 上传backend/init_db.py、seed_data.py、enhanced_seed.py:初始化与种子数据backend/settings.py:环境变量读取
活动、视频与群聊源码
活动相关逻辑主要在:
app/[locale]/meetups/page.tsxapp/[locale]/meetups/host/page.tsxapp/lib/meetups.ts
app/lib/meetups.ts 负责:
- MiroTalk 默认地址:
https://mirotalk.nomadro.cn - MiroTalk 房间名规范化
- MiroTalk 入会链接生成
- Lounge 默认地址:
https://lounge.nomadro.cn - Web 群聊服务标签:
The Lounge + Ergo IRC
线上/混合活动详情页会展示:
- MiroTalk P2P 视频入口
- 页面内 iframe 打开视频房
- The Lounge Web 群聊入口
- 信令/中继服务器和群聊入口域名
配置与国际化
| 文件 | 作用 |
|---|---|
config/site.config.ts |
主站 SEO 与站点基础信息 |
config/nav.config.ts |
顶部导航 |
config/footer.config.ts |
页脚 |
config/services.config.ts |
主站支付配置 |
config/runtime-env.ts |
运行环境判断 |
config/digital/* |
Digital 子站主题、服务、课程、域名配置 |
messages/zh.json、messages/en.json |
中英文文案 |
i18n/routing.ts |
语言列表与默认语言 |
i18n/navigation.ts |
语言路由 Link / Router 封装 |
本地开发
环境要求
- Node.js 20+,生产当前使用 Node.js 22.13.1
- pnpm 10+
- Python 3.12+
- PocketBase
- 可选:MinIO、ZPay/XorPay 测试配置
安装依赖
pnpm install
前端开发
pnpm dev
默认启动在:
http://localhost:3001
FastAPI 后端开发
python3 -m venv backend/.venv
source backend/.venv/bin/activate
pip install -r backend/requirements.txt
uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload
构建与启动
pnpm build
pnpm start
常用检查
pnpm lint
pnpm exec tsc --noEmit --pretty false
APP_ENV=production NEXT_PUBLIC_APP_ENV=production NODE_ENV=production pnpm build
环境变量
不要把真实密钥提交到 Git。.env.example 只放示例值,生产值放 .env.local、backend/.env 或 systemd Environment。
前端公开变量
| 变量 | 说明 |
|---|---|
APP_ENV |
development / production |
NEXT_PUBLIC_APP_ENV |
浏览器可见运行环境 |
NEXT_PUBLIC_SITE_URL |
站点根地址 |
NEXT_PUBLIC_SITE_ID |
站点 ID,用于会员/VIP 隔离 |
NEXT_PUBLIC_API_BASE_URL |
API 基础地址,生产通常为 /api |
NEXT_PUBLIC_POCKETBASE_URL |
浏览器可访问 PocketBase 地址 |
NEXT_PUBLIC_MIROTALK_URL |
MiroTalk P2P 服务地址 |
NEXT_PUBLIC_LOUNGE_URL |
The Lounge Web 群聊地址 |
NEXT_PUBLIC_MINIO_PUBLIC_URL |
MinIO 文件公开地址 |
后端和服务端变量
| 变量 | 说明 |
|---|---|
POCKETBASE_URL |
服务端访问 PocketBase 的内网地址 |
POCKETBASE_EMAIL |
PocketBase 管理员邮箱 |
POCKETBASE_PASSWORD |
PocketBase 管理员密码 |
PAYMENT_API_URL |
支付 API 地址 |
PAYMENT_PROVIDER |
zpay 或 xorpay |
PAYMENT_DEFAULT_AMOUNT |
默认支付金额 |
PAYMENT_JOIN_AMOUNT |
加入场景金额 |
PAYMENT_JOIN_TYPE |
加入场景订单类型 |
ZPAY_PID、ZPAY_KEY |
ZPay 商户配置 |
XORPAY_AID、XORPAY_SECRET |
XorPay 商户配置 |
MINIO_ENDPOINT |
MinIO 内网端点 |
MINIO_PORT |
MinIO 端口 |
MINIO_USE_SSL |
是否对 MinIO 内网连接启用 SSL |
MINIO_BUCKET |
存储桶 |
MINIO_ACCESS_KEY |
MinIO Access Key |
MINIO_SECRET_KEY |
MinIO Secret Key |
MINIO_REGION |
MinIO 区域 |
MINIO_UPLOAD_PREFIX |
上传路径前缀 |
FRONTEND_ORIGINS |
FastAPI CORS 白名单 |
UPLOAD_DIR |
后端本地上传目录 |
生产服务器支持
生产代码目录:
/home/ubuntu/gitlab-instance-0a899031_cnomadcna
域名与服务映射
| 域名 | 上游 | 说明 |
|---|---|---|
nomadro.cn / www.nomadro.cn |
127.0.0.1:4002 |
Next.js 主站,cnomadcna.service |
api.nomadro.cn |
127.0.0.1:8000 |
FastAPI |
pocketbase.nomadro.cn |
127.0.0.1:8090 |
PocketBase |
minio.nomadro.cn |
127.0.0.1:9100 / 9101 |
MinIO API / Console |
mirotalk.nomadro.cn |
127.0.0.1:9300 |
MiroTalk P2P |
lounge.nomadro.cn |
127.0.0.1:9000 |
The Lounge Web IRC |
ergo.nomadro.cn |
HTTPS redirect | 跳转到 lounge.nomadro.cn;IRC 后端只内网监听 |
ntfy.nomadro.cn |
127.0.0.1:9180 |
ntfy 通知服务 |
systemd 服务
| 服务 | 作用 | 当前策略 |
|---|---|---|
cnomadcna.service |
Next.js 生产服务 | enabled,监听 0.0.0.0:4002 |
mirotalk-p2p.service |
MiroTalk P2P | enabled,监听 127.0.0.1:9300 |
ergo.service |
Ergo IRC Server | enabled,监听 127.0.0.1:6667 和 [::1]:6667 |
thelounge.service |
The Lounge Web IRC | enabled,监听 127.0.0.1:9000 |
常用命令:
systemctl status cnomadcna.service
systemctl status mirotalk-p2p.service
systemctl status ergo.service
systemctl status thelounge.service
journalctl -u cnomadcna.service -n 100 --no-pager
journalctl -u mirotalk-p2p.service -n 100 --no-pager
journalctl -u ergo.service -n 100 --no-pager
journalctl -u thelounge.service -n 100 --no-pager
Docker 服务
当前生产容器:
| 容器 | 镜像 | 端口 |
|---|---|---|
nomadro-minio |
minio/minio:latest |
127.0.0.1:9100->9000,127.0.0.1:9101->9001 |
nomadro-ntfy |
binwiederhier/ntfy:latest |
127.0.0.1:9180->80 |
检查:
docker ps
docker logs nomadro-minio --tail 100
docker logs nomadro-ntfy --tail 100
Nginx
Nginx vhost 目录:
/www/server/panel/vhost/nginx
关键配置:
www.nomadro.cn.conf:主站api.nomadro.cn.conf:FastAPImirotalk.nomadro.cn.conf:MiroTalklounge.nomadro.cn.conf:The Lounge + Ergo redirectminio.nomadro.cn.conf:MinIOpocketbase.nomadro.cn.conf:PocketBase
常用命令:
nginx -t
nginx -s reload
HTTPS 证书
证书由 Certbot 管理。lounge.nomadro.cn 证书包含两个域名:
lounge.nomadro.cn
ergo.nomadro.cn
当前证书路径:
/etc/letsencrypt/live/lounge.nomadro.cn/fullchain.pem
/etc/letsencrypt/live/lounge.nomadro.cn/privkey.pem
检查:
certbot certificates
MiroTalk P2P
安装目录:
/opt/nomadro-services/mirotalk
运行方式:
- systemd:
mirotalk-p2p.service - 内网端口:
127.0.0.1:9300 - 公网入口:
https://mirotalk.nomadro.cn - 活动页自动生成链接:
https://mirotalk.nomadro.cn/join?room=...&name=...
健康检查:
curl -I https://mirotalk.nomadro.cn/join?room=nomadcna-healthcheck
Ergo IRC Server
安装目录:
/opt/ergo
配置:
/etc/ergo/ircd.yaml
/etc/ergo/ergo.motd
/var/lib/ergo/ircd.db
当前策略:
- 网络名:
NomadCNA - 服务名:
ergo.nomadro.cn - 只监听本机
127.0.0.1:6667和[::1]:6667 - 不开放公网 IRC TLS
6697 ergo.nomadro.cn的 HTTPS 请求跳转到 The Lounge
本机 IRC 握手检查:
{ printf 'NICK health\r\nUSER health 0 * :NomadCNA Health\r\nJOIN #nomadcna\r\nQUIT :healthcheck\r\n'; sleep 1; } \
| timeout 5s nc 127.0.0.1 6667
The Lounge Web 群聊
安装目录:
/opt/nomadro-services/thelounge
配置:
/var/lib/thelounge/config.js
当前策略:
- public mode
- 只监听
127.0.0.1:9000 - Nginx 通过
https://lounge.nomadro.cn反代 - 默认网络锁定到本机 Ergo:
127.0.0.1:6667 - 默认加入频道:
#nomadcna,#online-events - 文件上传关闭
- The Lounge 自身消息持久化关闭,Ergo 保留内存历史
健康检查:
curl -I https://lounge.nomadro.cn/
curl 'https://lounge.nomadro.cn/socket.io/?EIO=4&transport=polling&t=health'
上线流程
前端上线
cd /home/ubuntu/gitlab-instance-0a899031_cnomadcna
APP_ENV=production NEXT_PUBLIC_APP_ENV=production NODE_ENV=production pnpm build
systemctl restart cnomadcna.service
systemctl status cnomadcna.service --no-pager
后端上线
FastAPI 当前运行在 127.0.0.1:8000。更新后需重启对应 uvicorn 进程或其 systemd 服务。如果后续为 FastAPI 增加 systemd 单元,建议命名为:
cnomadcna-api.service
Nginx 配置变更
nginx -t
nginx -s reload
服务健康检查
curl -I https://nomadro.cn/zh/meetups
curl -I https://api.nomadro.cn/api/health
curl -I https://mirotalk.nomadro.cn/join?room=nomadcna-healthcheck
curl -I https://lounge.nomadro.cn/
curl -I https://ergo.nomadro.cn/
数据与备份
建议纳入备份的内容:
| 路径/服务 | 说明 |
|---|---|
.env.local、backend/.env |
生产环境变量和密钥 |
| PocketBase 数据目录 | 用户、会员、订单、内容、活动等业务数据 |
| MinIO 数据卷 | 上传媒体、远程媒体缓存 |
/var/lib/ergo |
Ergo IRC 数据库 |
/var/lib/thelounge |
The Lounge 配置、VAPID key、包配置 |
/opt/nomadro-services/mirotalk |
MiroTalk 源码部署目录 |
/www/server/panel/vhost/nginx/*.conf |
Nginx vhost 配置 |
/etc/systemd/system/*.service |
systemd 单元 |
安全注意事项
- README 不应包含真实密钥、管理员密码、支付密钥或 MinIO Secret。
.env.local和backend/.env不应提交到 Git。- The Lounge 当前为 public mode,适合活动公开群聊;如需长期身份和历史,可切换 private mode 并创建用户账号。
- Ergo 原生 IRC 公网端口当前不开放,这是有意设计;需要原生 IRC 客户端时再开放
6697并配置 TLS。 - 内部服务优先绑定
127.0.0.1,统一通过 Nginx HTTPS 暴露。 - 修改 Nginx 前必须先
nginx -t。 - 更新证书后确认 SAN 包含对应域名。
开发约定
- 新页面优先放入
app/[locale],并补充messages/zh.json、messages/en.json。 - 需要语言感知链接时使用
i18n/navigation.ts导出的Link。 - 调用后端 API 使用
app/lib/api-client.ts。 - 主站通用配置放
config/*,Digital 子站配置放config/digital/*。 - 媒体上传和远程媒体代理优先走 MinIO。
- 支付相关逻辑优先复用
app/lib/payment、backend/payment.py和config/*/services.config.ts。 - 线上活动相关逻辑优先复用
app/lib/meetups.ts。
常见问题
为什么 ergo.nomadro.cn 不直接打开 IRC?
ergo.nomadro.cn 是 IRC 服务名,但原生 IRC 端口没有对公网开放。用户从浏览器进入 lounge.nomadro.cn,The Lounge 在服务器本机连接 Ergo。这样能减少公网攻击面,同时满足 Web 群聊需求。
为什么 /api/digital/* 没有转发到 FastAPI?
next.config.ts 明确排除了 digital 和 media 两类 API,它们由 Next.js Route Handlers 直接处理。其他 /api/* 才会转发到 FastAPI。
活动视频房如何生成?
app/lib/meetups.ts 会根据活动城市、日期、时间和 ID 生成 MiroTalk room slug。如果活动有自定义 meetingUrl,则优先使用自定义链接。
群聊频道如何进入?
The Lounge public mode 的默认网络已锁定到 Ergo,并默认加入 #nomadcna 和 #online-events。活动详情页的“进入群聊”按钮指向 https://lounge.nomadro.cn。
维护命令速查
# 构建并重启主站
APP_ENV=production NEXT_PUBLIC_APP_ENV=production NODE_ENV=production pnpm build
systemctl restart cnomadcna.service
# 查看服务
systemctl status cnomadcna.service --no-pager
systemctl status mirotalk-p2p.service --no-pager
systemctl status ergo.service --no-pager
systemctl status thelounge.service --no-pager
# 查看日志
journalctl -u cnomadcna.service -n 100 --no-pager
journalctl -u mirotalk-p2p.service -n 100 --no-pager
journalctl -u ergo.service -n 100 --no-pager
journalctl -u thelounge.service -n 100 --no-pager
# Nginx
nginx -t
nginx -s reload
# 端口
ss -ltnp | rg ':4002|:8000|:8090|:9000|:9300|:9100|:9101|:6667|:6697'
# Docker
docker ps
# HTTPS
certbot certificates