diff --git a/.env.example b/.env.example index 4a3bf06..2b50497 100644 --- a/.env.example +++ b/.env.example @@ -84,3 +84,13 @@ ALERTMANAGER_PORT=19093 GRAFANA_PORT=13000 NODE_EXPORTER_PORT=19100 GRAFANA_URL="http://127.0.0.1:13000/" + +# ===== 极光离线推送(OpenIM push)===== +# AppKey 可以进配置;Master Secret 禁止写在本文件、Git、日志或客户端。 +# 公网部署时由 scripts/load-jpush-secret.sh 从 +# /root/.config/jinxunda/jpush_master_secret 注入 JPUSH_MASTER_SECRET。 +OPENIM_PUSH_ENABLE= +JPUSH_APPKEY=95fc2352648a7d4568ac8d72 +JPUSH_PUSH_URL=https://api.jpush.cn/v3/push +JPUSH_PUSH_INTENT=intent:#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;component=cn.solsum.jinxunda/cn.solsum.jinxunda.MainActivity;end + diff --git a/.gitignore b/.gitignore index 8f97012..3f3e4c0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /components/ .selftest/ data/ +**/agconnect-services.json + diff --git a/README.md b/README.md index 6e15253..6866da6 100644 --- a/README.md +++ b/README.md @@ -77,11 +77,13 @@ docker compose up -d # 再启动 ## 客户端接入参数(自测通过后) -- OpenIM API: `http://<服务器IP>:10002` -- OpenIM WebSocket: `ws://<服务器IP>:10001` +- OpenIM API: `http://<服务器IP>:10002`(公网测试见 `deploy/public/README.md`:`https://jxd.jinniu.ink`) +- OpenIM WebSocket: `ws://<服务器IP>:10001`(公网:`wss://jxd.jinniu.ink/msg_gateway`) - LiveKit: `ws://<服务器IP>:17880`(17880 为默认信令端口;改端口时必须 `.env` 的 `LIVEKIT_PORT`、`config/livekit.yaml` 的 `port`、`docker-compose.yaml` 的容器侧端口三者一起改),API Key/Secret 见 `.env` 的 `LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET` - 测试账号:`test001` / `test002`(由自测脚本注册);语音通话时两名用户以各自 identity 进同一房间即可 +公网测试部署(不改本机内网步骤):见 `deploy/public/README.md`。施工员不执行该部署。 + ## 常见问题 - **文件/语音能发但打不开**:`MINIO_EXTERNAL_ADDRESS` 没写成客户端能访问的 IP。改 `.env` 后 `docker compose up -d` 重建 openim-server。 diff --git a/config/livekit.public.yaml b/config/livekit.public.yaml new file mode 100644 index 0000000..973e504 --- /dev/null +++ b/config/livekit.public.yaml @@ -0,0 +1,14 @@ +# LiveKit 公网测试配置(jxd.jinniu.ink) +# 仅由 deploy/public/docker-compose.public.yaml 挂载;内网仍用 livekit.yaml。 +# +# 媒体候选地址走公网探测;信令经 Nginx 443 反代,UDP/TCP 媒体端口仍需对公网开放。 +port: 17880 +rtc: + tcp_port: 17881 + udp_port: 17882 + use_external_ip: true +room: + departure_timeout: 300 + empty_timeout: 300 +logging: + level: info diff --git a/deploy/public/.env.public.example b/deploy/public/.env.public.example new file mode 100644 index 0000000..dd142a5 --- /dev/null +++ b/deploy/public/.env.public.example @@ -0,0 +1,16 @@ +# 复制到仓库根目录的 .env 后按机填写。禁止把 Master Secret 写进本文件或 Git。 +# 公网测试域名已解析到测试机;客户端只认域名,不认 IP。 + +# 本机公网 IP(LiveKit 媒体候选用)。在服务器上用 `curl -4 -s ifconfig.me` 查看,不要写进客户端。 +SERVER_IP= + +MINIO_EXTERNAL_ADDRESS="https://jxd.jinniu.ink/files" + +OPENIM_PUSH_ENABLE=jpush +JPUSH_APPKEY=95fc2352648a7d4568ac8d72 +JPUSH_PUSH_URL=https://api.jpush.cn/v3/push +JPUSH_PUSH_INTENT=intent:#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;component=cn.solsum.jinxunda/cn.solsum.jinxunda.MainActivity;end + +# Master Secret 由 scripts/load-jpush-secret.sh 从 +# /root/.config/jinxunda/jpush_master_secret 注入为环境变量 JPUSH_MASTER_SECRET。 +# JPUSH_MASTER_SECRET_FILE=/root/.config/jinxunda/jpush_master_secret diff --git a/deploy/public/README.md b/deploy/public/README.md new file mode 100644 index 0000000..b57e6c9 --- /dev/null +++ b/deploy/public/README.md @@ -0,0 +1,66 @@ +# 公网测试部署说明(jxd.jinniu.ink) + +施工员本卡**不执行**本节命令、不 SSH 改测试机。由总工审核通过后再部署。 + +目标机:已解析到 `jxd.jinniu.ink` 的香港测试服务器。数据目录仍是仓库下的 `components/` 与 `data/`,回滚不删数据。 + +## 对外暴露与对内绑定 + +| 入口 | 协议 | 后端 | 说明 | +| --- | --- | --- | --- | +| `https://jxd.jinniu.ink/` | HTTPS | OpenIM API `:10002` | 客户端 `apiAddr` | +| `wss://jxd.jinniu.ink/msg_gateway` | WSS | OpenIM 网关 `:10001` | 客户端 `wsAddr` | +| `https://jxd.jinniu.ink/account/` | HTTPS | 账号服务 `:10010` | 客户端 `authApiBase` | +| `https://jxd.jinniu.ink/files/` | HTTPS | MinIO `:10005` | `MINIO_EXTERNAL_ADDRESS` | +| `wss://jxd.jinniu.ink/livekit/` | WSS | LiveKit 信令 `:17880` | 客户端 `livekitUrl` | +| `:17881/tcp` `:17882/udp` | 媒体 | LiveKit RTC | 必须对公网开放,不能只走 443 | + +内部端口(10001/10002/10005/10010/17880/etcd)在 overlay 里绑 `127.0.0.1`。防火墙建议只放行 80/tcp、443/tcp、17881/tcp、17882/udp。 + +## 证书 + +1. 先把 `deploy/public/nginx.bootstrap.conf` 挂到 edge(只听 80)。 +2. `certbot certonly --webroot -w deploy/public/acme -d jxd.jinniu.ink` +3. 确认 `/etc/letsencrypt/live/jxd.jinniu.ink/` 有 `fullchain.pem` 与 `privkey.pem`。 +4. 改回 `nginx.conf` 后重载 edge。 + +私钥不得复制进 Git。 + +## 极光 Master Secret + +文件在测试机 `/root/.config/jinxunda/jpush_master_secret`(权限 600)。部署前: + +```bash +source ./scripts/load-jpush-secret.sh # 只导出环境变量,不打印内容 +``` + +不要 `cat` 该文件,不要写进 `.env` 或 compose 明文。 + +## 启动(总工) + +```bash +cd /path/to/tongxunruanjian +cp .env.example .env # 若还没有 +# 按 deploy/public/.env.public.example 写入 SERVER_IP、MINIO_EXTERNAL_ADDRESS、JPUSH_*(不含 Master Secret) +source ./scripts/load-jpush-secret.sh +./scripts/deploy-public.sh +``` + +`SERVER_IP` 填本机公网 IP(LiveKit 媒体候选),不要写进手机/电脑客户端代码。 + +## 回滚 + +```bash +./scripts/rollback-public.sh +``` + +效果:停掉 nginx edge,按原来的 `docker-compose.yaml` 拉起;`components/` 与 `data/account` 不动。 + +## 客户端地址(已写进代码默认值) + +- 手机:`mobile/lib/config.dart`,默认 `jxd.jinniu.ink` + TLS。内网回切:`--dart-define=SERVER_HOST=192.168.200.11 --dart-define=USE_TLS=false` +- 电脑:改 `pc-client/.env` 的 `VITE_*` 为 `https://` / `wss://` 路径,见该目录 `.env.example` + +## 华为通道缺口 + +见 `mobile/android/app/README-agconnect.md`。缺 `agconnect-services.json` 时华为真机厂商通道不通,其余施工已完成。 diff --git a/deploy/public/acme/.gitkeep b/deploy/public/acme/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/public/docker-compose.public.yaml b/deploy/public/docker-compose.public.yaml new file mode 100644 index 0000000..80282bc --- /dev/null +++ b/deploy/public/docker-compose.public.yaml @@ -0,0 +1,56 @@ +# 公网测试 overlay。用法(在仓库根目录,由总工执行,本卡不跑): +# set -a && . ./.env && ./scripts/load-jpush-secret.sh && set +a +# docker compose -f docker-compose.yaml -f deploy/public/docker-compose.public.yaml up -d +# +# 原则:内部端口只绑 127.0.0.1;对外只开 80/443 + LiveKit 媒体 17881/tcp、17882/udp。 +# 回滚:去掉本 overlay,改回 docker compose -f docker-compose.yaml up -d,数据目录 components/ 与 data/ 不动。 + +services: + openim-server: + ports: + - "127.0.0.1:${OPENIM_MSG_GATEWAY_PORT}:10001" + - "127.0.0.1:${OPENIM_API_PORT}:10002" + environment: + - IMENV_OPENIM_PUSH_ENABLE=${OPENIM_PUSH_ENABLE:-jpush} + - IMENV_OPENIM_PUSH_JPUSH_APPKEY=${JPUSH_APPKEY} + - IMENV_OPENIM_PUSH_JPUSH_MASTERSECRET=${JPUSH_MASTER_SECRET} + - IMENV_OPENIM_PUSH_JPUSH_PUSHURL=${JPUSH_PUSH_URL:-https://api.jpush.cn/v3/push} + - IMENV_OPENIM_PUSH_JPUSH_PUSHINTENT=${JPUSH_PUSH_INTENT} + + minio: + ports: + - "127.0.0.1:${MINIO_PORT}:9000" + - "127.0.0.1:${MINIO_CONSOLE_PORT}:9090" + + etcd: + ports: + - "127.0.0.1:12379:2379" + - "127.0.0.1:12380:2380" + + livekit: + command: --config /etc/livekit.yaml --node-ip ${SERVER_IP} + ports: + - "127.0.0.1:${LIVEKIT_PORT:-17880}:17880" + - "${LIVEKIT_RTC_TCP_PORT:-17881}:17881" + - "${LIVEKIT_RTC_UDP_PORT:-17882}:17882/udp" + volumes: + - ./config/livekit.public.yaml:/etc/livekit.yaml + + account-server: + ports: + - "127.0.0.1:${ACCOUNT_PORT:-10010}:10010" + + edge: + image: nginx:1.27-alpine + container_name: jxd-edge + restart: always + network_mode: host + volumes: + - ./deploy/public/nginx.conf:/etc/nginx/nginx.conf:ro + - ./deploy/public/acme:/var/www/acme:ro + - /etc/letsencrypt:/etc/letsencrypt:ro + depends_on: + - openim-server + - account-server + - minio + - livekit diff --git a/deploy/public/nginx.bootstrap.conf b/deploy/public/nginx.bootstrap.conf new file mode 100644 index 0000000..df7f3e8 --- /dev/null +++ b/deploy/public/nginx.bootstrap.conf @@ -0,0 +1,30 @@ +# 仅 80 端口:申请 Let's Encrypt 证书用。证书拿到后换成 nginx.conf。 +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + server_tokens off; + + server { + listen 80; + listen [::]:80; + server_name jxd.jinniu.ink; + + location /.well-known/acme-challenge/ { + root /var/www/acme; + } + + location / { + return 200 'jxd.jinniu.ink bootstrap\n'; + add_header Content-Type text/plain; + } + } +} diff --git a/deploy/public/nginx.conf b/deploy/public/nginx.conf new file mode 100644 index 0000000..1e63c37 --- /dev/null +++ b/deploy/public/nginx.conf @@ -0,0 +1,120 @@ +# 公网测试:jxd.jinniu.ink 终止 TLS,反代到本机回环上的 OpenIM / 账号 / 对象存储 / LiveKit。 +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 4096; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + server_tokens off; + client_max_body_size 100m; + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + upstream openim_api { + server 127.0.0.1:10002; + } + upstream openim_ws { + server 127.0.0.1:10001; + } + upstream account_api { + server 127.0.0.1:10010; + } + upstream minio_s3 { + server 127.0.0.1:10005; + } + upstream livekit_signal { + server 127.0.0.1:17880; + } + + server { + listen 80; + listen [::]:80; + server_name jxd.jinniu.ink; + + location /.well-known/acme-challenge/ { + root /var/www/acme; + } + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name jxd.jinniu.ink; + + ssl_certificate /etc/letsencrypt/live/jxd.jinniu.ink/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/jxd.jinniu.ink/privkey.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + + # OpenIM 消息长连接 + location /msg_gateway { + proxy_pass http://openim_ws/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # 公司账号服务(登录、管理页、rtc_token) + location /account/ { + proxy_pass http://account_api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # MinIO 对象(MINIO_EXTERNAL_ADDRESS=https://jxd.jinniu.ink/files) + location /files/ { + proxy_pass http://minio_s3/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_request_buffering off; + } + + # LiveKit 信令(客户端 wss://jxd.jinniu.ink/livekit → 容器 /rtc 等) + location /livekit/ { + proxy_pass http://livekit_signal/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # OpenIM REST API(catch-all,须放最后) + location / { + proxy_pass http://openim_api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 13c4e0f..25d96d2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -358,6 +358,13 @@ services: - IMENV_LOG_ISSTDOUT=${LOG_IS_STDOUT} - IMENV_LOG_REMAINLOGLEVEL=${LOG_LEVEL} - IMENV_OPENIM_API_PROMETHEUS_GRAFANAURL=${GRAFANA_URL} + # 离线推送(极光)。未启用时保持空,OpenIM 走 dummy pusher。 + # Master Secret 只从环境变量注入,禁止写进仓库。 + - IMENV_OPENIM_PUSH_ENABLE=${OPENIM_PUSH_ENABLE:-} + - IMENV_OPENIM_PUSH_JPUSH_APPKEY=${JPUSH_APPKEY:-} + - IMENV_OPENIM_PUSH_JPUSH_MASTERSECRET=${JPUSH_MASTER_SECRET:-} + - IMENV_OPENIM_PUSH_JPUSH_PUSHURL=${JPUSH_PUSH_URL:-https://api.jpush.cn/v3/push} + - IMENV_OPENIM_PUSH_JPUSH_PUSHINTENT=${JPUSH_PUSH_INTENT:-} restart: always depends_on: - mongo diff --git a/docs/README.md b/docs/README.md index 605f3d0..f44957c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,7 @@ - `pc-client/`:电脑端,使用 Electron,Windows 是当前优先支持的平台。 - `account-service/`:公司账号服务,负责工号密码登录、管理员导入和管理名单。 - `docker-compose.yaml`、`config/`、`scripts/`:OpenIM、语音服务和内网环境的配置及维护脚本。 +- `deploy/public/`:香港测试机 `jxd.jinniu.ink` 的公网反代、证书和回滚说明(由总工部署)。 - `docs/`:本手册和以前留下的验收、回归、使用说明。 ## 更新规矩 diff --git a/docs/任务清单.md b/docs/任务清单.md index 8d2a7ee..1db4e92 100644 --- a/docs/任务清单.md +++ b/docs/任务清单.md @@ -1,12 +1,10 @@ # 任务清单 -更新时间:2026-08-23。任务状态以当前仓库和项目约束为准;没有证据的事项不写成“已完成”。 +更新时间:2026-08-30。任务状态以当前仓库和项目约束为准;没有证据的事项不写成“已完成”。 ## 正在做 -目前没有已确认正在施工的业务代码任务。 - -本次已完成仓库交接手册整理,后续所有任务都应按 `docs/README.md` 的更新规矩同步维护本清单。 +公网第一阶段:客户端极光/华为接入与 `jxd.jinniu.ink` 部署配置已提交专用分支,等待总工审核并部署测试环境。 ## 已做完 @@ -16,11 +14,15 @@ - 完成一轮手机与电脑的跨端回归记录;其中已通过的项目见旧档 `phase4-regression-2026-08-16.md`。 - 将 LiveKit 的信令/媒体端口统一为 `17880`、`17881`、`17882`,并提供可重复执行的修复脚本。 - 建立本目录四份交接手册,并为三份过时旧说明加上明确的留档提示。 +- 手机端应用标识统一为 `cn.solsum.jinxunda`。 +- 安卓极光 SDK 初始化、通知权限、alias 回传、点击处理,以及 OpenIM 离线推送所需的服务端配置入口(Master Secret 从服务器文件注入)。 +- 公网反向代理 / HTTPS / WSS / 防火墙与回滚说明(配置进仓库,未实际部署)。 ## 还没安排 -- 组织真实手机和 Windows 电脑的回归:重点确认文件传输和一对一语音通话。 +- 总工在测试机按 `deploy/public/README.md` 部署并验证证书与推送。 +- 向构建机放入华为 `agconnect-services.json` 后重打安卓包(见 `mobile/android/app/README-agconnect.md`)。 +- 组织真实手机和 Windows 电脑的回归:重点确认文件传输、一对一语音通话和杀进程后的离线通知。 - 明确手机正式签名的负责人和安全保管方式;不能把签名密钥提交进仓库。 - 获得负责人同意后,再导入真实员工名单并验证账号管理流程。 -- 等负责人对三个公网问题拍板后,才安排公网 IP/域名、备案、反向代理和 HTTPS 的具体施工;当前禁止擅自开始。 -- 公网方案获批后,重新评估电脑端 Electron、登录失败限制、令牌本地保存和跨域设置的安全风险。 \ No newline at end of file +- 不接入小米、OPPO、vivo、荣耀、魅族、FCM 或苹果推送(本阶段明确不做)。 diff --git a/docs/最新进度.md b/docs/最新进度.md index 0848a72..9a251c5 100644 --- a/docs/最新进度.md +++ b/docs/最新进度.md @@ -1,16 +1,17 @@ # 最新进度 -更新时间:2026-08-23。以下内容依据当前仓库代码和最新提交 `fa8584a` 整理。 +更新时间:2026-08-30。以下内容依据当前仓库代码整理。 ## 现在做到哪里 -项目已经具备可继续测试的手机端、Windows 电脑端、账号服务和内网部署配置: +项目已经具备可继续测试的手机端、Windows 电脑端、账号服务和内网部署配置,并完成本阶段公网接入的**客户端与部署配置**(未在测试机上实际部署): -- 手机端在 `mobile/`,使用 OpenIM Flutter SDK `3.8.3+hotfix.12`,版本 `1.0.6+7`。 -- 电脑端在 `pc-client/`,使用 Electron,显示版本 `v1.0.2`。 -- 员工用工号和密码通过 `account-service/` 登录;管理员导入和管理账号的页面、接口已在仓库中。 -- 手机和电脑端已接入聊天、通讯录、同事申请、语音消息、文件/图片消息和一对一语音通话相关代码。 -- 语音服务 LiveKit 的信令和媒体端口已统一为 `17880`、`17881`、`17882`;仓库提供 `scripts/fix-livekit-ports.sh` 用于服务器已部署环境的端口修复。 +- 手机端在 `mobile/`,使用 OpenIM Flutter SDK `3.8.3+hotfix.12`,版本 `1.0.7+8`。 +- 安卓/苹果应用标识为 `cn.solsum.jinxunda`。 +- 手机端已接入极光官方 Flutter 插件(AppKey 在客户端配置),登录后把 OpenIM userID 设为极光 alias;华为厂商通道依赖已打开,缺华为后台下载的 `agconnect-services.json`。 +- 手机默认服务地址为 `jxd.jinniu.ink` 的 HTTPS/WSS;内网可用 `--dart-define=SERVER_HOST=... --dart-define=USE_TLS=false` 覆盖。 +- 电脑端 `.env.example` 已改为同一套公网路径。 +- 公网部署 overlay、Nginx、证书步骤、Master Secret 注入脚本和回滚脚本在 `deploy/public/` 与 `scripts/deploy-public.sh`;由总工部署,施工员不改测试机。 ## 已有验证记录 @@ -20,12 +21,10 @@ ## 当前停在哪 -公网接入施工暂停,等待负责人对三个公网相关问题拍板后再恢复。现阶段没有把内网服务发布到公网的授权。 - -这意味着:代码可继续维护和内网验证,但公网 IP/域名、备案、反向代理和 HTTPS 等工作不能擅自启动。 +客户端与部署配置已提交。测试机上的 Nginx/证书/compose 公网拉起、以及华为 `agconnect-services.json` 放入构建机,等总工审核后处理。本阶段未在测试机执行部署。 ## 已知风险和待验证事实 -- 真机上的文件传输和一对一语音通话仍需实际设备验证,不能只根据模拟器结果宣称完成。 -- 手机正式签名、真实员工名单导入和试用前的管理员安全设置,都需要负责人明确确认并由有权限的人执行。 -- 电脑端使用较旧的 Electron,且历史记录提到内网场景的安全取舍;若未来获准公网接入,必须先重新评估 HTTPS、浏览器安全设置、登录限流和跨域策略。 \ No newline at end of file +- 真机上的文件传输、一对一语音通话、杀进程后的离线推送(尤其华为通道)仍需实际设备验证。 +- 本施工环境无 Flutter / JDK / Android SDK,未能在本机执行 `flutter analyze` 与 `flutter build apk`。 +- 电脑端使用较旧的 Electron;公网接入后仍需评估 HTTPS、登录限流和跨域策略。 diff --git a/docs/项目需求.md b/docs/项目需求.md index 40276cd..9ef12f7 100644 --- a/docs/项目需求.md +++ b/docs/项目需求.md @@ -18,14 +18,14 @@ ## 支持范围 -- 手机端:Android 和 iPhone 工程都在仓库中;当前手机端版本号为 `1.0.6+7`。 +- 手机端:Android 和 iPhone 工程都在仓库中;当前手机端版本号为 `1.0.7+8`。应用标识 `cn.solsum.jinxunda`。 - 电脑端:Windows 优先;当前电脑端显示版本为 `v1.0.2`。 -- 服务端:以 Docker Compose(把一组服务一起启动的工具)编排 OpenIM、账号服务和语音服务。 +- 服务端:以 Docker Compose(把一组服务一起启动的工具)编排 OpenIM、账号服务和语音服务。公网测试域名为 `jxd.jinniu.ink`(HTTPS/WSS),部署配置在 `deploy/public/`,实际部署由总工执行。 ## 当前边界 - 只做集团内部通讯,不改造成公开社交产品。 - 保留 OpenIM 的现有标识,禁止替换品牌。 -- 本项目当前只记录和维护内网环境;公网接入尚未获准施工。 -- 不把员工名单、密码、管理员口令、密钥或服务器私密配置写入仓库和文档。 +- 公网第一阶段:安卓极光 + 华为厂商通道(华为配置文件仍缺,见 `mobile/android/app/README-agconnect.md`);不接小米/OPPO/vivo/荣耀/魅族/FCM/苹果推送。 +- 不把员工名单、密码、管理员口令、极光 Master Secret、华为 Secret、私钥或服务器私密配置写入仓库和文档。 - 本仓库的日常任务如无明确授权,不做发布、部署或改动生产环境。 \ No newline at end of file diff --git a/mobile/.gitignore b/mobile/.gitignore index f93e2a0..596cfb6 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -44,6 +44,9 @@ app.*.map.json /android/app/profile /android/app/release +# 华为 AppGallery Connect 客户端配置(含 client_secret,禁止入库) +/android/app/agconnect-services.json + # 本机路径与构建缓存(不入库) **/android/local.properties **/android/.gradle/ diff --git a/mobile/README.md b/mobile/README.md index dbc0976..dba24de 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -16,20 +16,32 @@ iOS 需要一台 Mac 搭配 Xcode 与开发者证书,另按苹果流程打包 已声明的平台权限(勿删): -- `android/app/src/main/AndroidManifest.xml`:`INTERNET`、`RECORD_AUDIO`(语音消息/通话)、`MODIFY_AUDIO_SETTINGS`、存储/相册读取(发文件、发图片);并开启了 `usesCleartextTraffic`(内网服务器暂用 http/ws 明文,开通外网改 HTTPS 后应移除)。 +- `android/app/src/main/AndroidManifest.xml`:`INTERNET`、`RECORD_AUDIO`(语音消息/通话)、`MODIFY_AUDIO_SETTINGS`、存储/相册读取(发文件、发图片)、`POST_NOTIFICATIONS`(极光离线通知)。公网默认 HTTPS;内网联调 `--dart-define=USE_TLS=false` 时仍允许明文,故暂保留 `usesCleartextTraffic`。 - `ios/Runner/Info.plist`:`NSMicrophoneUsageDescription`(语音消息与通话)、`NSPhotoLibraryUsageDescription`(发图片)、`NSAppTransportSecurity/NSAllowsArbitraryLoads`(同上,内网明文)。 -- `android/build.gradle.kts` / `settings.gradle.kts` 把阿里云 Maven 镜像放在最前(本网络访问 maven.google.com 不稳定)。 +- `android/build.gradle.kts` / `settings.gradle.kts` 把阿里云 Maven 镜像放在最前(本网络访问 maven.google.com 不稳定),并增加华为 Maven(厂商通道)。 ## 修改服务器地址 -所有服务器地址集中在 `lib/config.dart`,改 `serverHost`(内网 IP/域名)和对应端口常量即可: +所有服务器地址集中在 `lib/config.dart`。默认公网:`jxd.jinniu.ink` + HTTPS/WSS。 -| 常量 | 用途 | 默认值 | +内网联调不要改文件,打包时覆盖: + +```bash +flutter build apk --release \ + --dart-define=SERVER_HOST=192.168.200.11 \ + --dart-define=USE_TLS=false +``` + +仅覆盖登录服务:`--dart-define=AUTH_HOST=127.0.0.1` + +| 常量 | 用途 | 公网默认 | | --- | --- | --- | -| `apiAddr` | OpenIM API | `http://192.168.200.11:10002` | -| `wsAddr` | OpenIM WebSocket | `ws://192.168.200.11:10001` | -| `livekitUrl` | LiveKit 语音通话 | `ws://192.168.200.11:17880`(注意已重映射,非默认 7880) | -| `authApiBase` | 公司账号登录后端 | `http://192.168.200.11:10010` | +| `apiAddr` | OpenIM API | `https://jxd.jinniu.ink` | +| `wsAddr` | OpenIM WebSocket | `wss://jxd.jinniu.ink/msg_gateway` | +| `livekitUrl` | LiveKit 语音通话 | `wss://jxd.jinniu.ink/livekit` | +| `authApiBase` | 公司账号登录后端 | `https://jxd.jinniu.ink/account` | + +华为厂商通道还缺 `android/app/agconnect-services.json`,取得方式见同目录 `README-agconnect.md`。不要把该文件提交进 Git。 ## 登录接口契约(后端:任务 B-60 account-service,已上线) diff --git a/mobile/android/app/README-agconnect.md b/mobile/android/app/README-agconnect.md new file mode 100644 index 0000000..88a597a --- /dev/null +++ b/mobile/android/app/README-agconnect.md @@ -0,0 +1,21 @@ +# 华为厂商通道配置文件(缺口) + +华为通道客户端其余依赖已接入。**唯一还缺的文件**是: + +`mobile/android/app/agconnect-services.json` + +## 从哪里取得 + +1. 打开华为 [AppGallery Connect](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html) +2. 选择与包名 `cn.solsum.jinxunda` 对应的应用 +3. 项目设置 → 应用 → 下载 `agconnect-services.json` +4. 把文件放到本目录(与 `build.gradle.kts` 同级) +5. **不要提交进 Git**(文件含华为 client_secret)。仓库已忽略该路径。 + +没有这份文件时: + +- 安卓包仍可构建(AGConnect 插件按文件是否存在条件启用) +- 极光自己的通道仍可初始化 +- **华为真机的厂商通道无法接通**,杀进程后的到达率会明显下降 + +本阶段不伪造、不提交该文件。 diff --git a/mobile/android/app/build.gradle.kts b/mobile/android/app/build.gradle.kts index 01f264f..13c3e6f 100644 --- a/mobile/android/app/build.gradle.kts +++ b/mobile/android/app/build.gradle.kts @@ -4,6 +4,13 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } +// 华为 agconnect-services.json 到位后才启用 AGConnect 插件,避免缺文件时整包编不过。 +// 文件从华为 AppGallery Connect 下载,放到本目录,不要提交进 Git。 +val agconnectServices = file("agconnect-services.json") +if (agconnectServices.exists()) { + apply(plugin = "com.huawei.agconnect") +} + android { namespace = "cn.solsum.jinxunda" compileSdk = flutter.compileSdkVersion @@ -15,14 +22,15 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "cn.solsum.jinxunda" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion + // 极光厂商通道插件要求 minSdk 24 + minSdk = 24 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + manifestPlaceholders["JPUSH_PKGNAME"] = "cn.solsum.jinxunda" + manifestPlaceholders["JPUSH_APPKEY"] = "95fc2352648a7d4568ac8d72" + manifestPlaceholders["JPUSH_CHANNEL"] = "developer-default" } // 压缩打包 .so(默认未压缩存储,OpenIM/WebRTC 的 native 库让包多占近一倍体积); diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 77186a8..7672f77 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,9 @@ + + + @@ -33,6 +36,10 @@ android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> + + diff --git a/mobile/android/app/src/main/kotlin/cn/solsum/jinxunda/MainActivity.kt b/mobile/android/app/src/main/kotlin/cn/solsum/jinxunda/MainActivity.kt index 6ab5972..40e0569 100644 --- a/mobile/android/app/src/main/kotlin/cn/solsum/jinxunda/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/cn/solsum/jinxunda/MainActivity.kt @@ -2,4 +2,8 @@ package cn.solsum.jinxunda import io.flutter.embedding.android.FlutterActivity -class MainActivity : FlutterActivity() +class MainActivity : FlutterActivity() { + // Flutter 3.29+ 默认把启动 Intent 当深链接解析。华为等厂商通道点击拉起时 + // intent 带 n_extra,路由表没有对应页会白屏。本应用不用官方深链接。 + override fun shouldHandleDeeplinking(): Boolean = false +} diff --git a/mobile/android/build.gradle.kts b/mobile/android/build.gradle.kts index f332b8e..a290246 100644 --- a/mobile/android/build.gradle.kts +++ b/mobile/android/build.gradle.kts @@ -4,6 +4,7 @@ allprojects { maven("https://maven.aliyun.com/repository/google") maven("https://maven.aliyun.com/repository/central") maven("https://maven.aliyun.com/repository/public") + maven("https://developer.huawei.com/repo/") google() mavenCentral() } diff --git a/mobile/android/settings.gradle.kts b/mobile/android/settings.gradle.kts index c78db2e..d2c3964 100644 --- a/mobile/android/settings.gradle.kts +++ b/mobile/android/settings.gradle.kts @@ -15,6 +15,7 @@ pluginManagement { maven("https://maven.aliyun.com/repository/google") maven("https://maven.aliyun.com/repository/central") maven("https://maven.aliyun.com/repository/gradle-plugin") + maven("https://developer.huawei.com/repo/") google() mavenCentral() gradlePluginPortal() @@ -25,6 +26,7 @@ plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "9.0.1" apply false id("org.jetbrains.kotlin.android") version "2.3.20" apply false + id("com.huawei.agconnect") version "1.9.1.301" apply false } include(":app") diff --git a/mobile/lib/config.dart b/mobile/lib/config.dart index ad0889c..b0dae3e 100644 --- a/mobile/lib/config.dart +++ b/mobile/lib/config.dart @@ -1,14 +1,24 @@ /// 服务器地址集中配置。 /// -/// 服务器换了地址只需要改这一个文件: -/// 1. 改下面的 [serverHost] 为新的服务器内网 IP(或域名); -/// 2. 如果端口也被重新映射,改对应端口常量即可。 +/// 默认指向公网测试域名 `jxd.jinniu.ink`(HTTPS / WSS)。 +/// 内网联调不要改本文件,打包时覆盖: +/// `--dart-define=SERVER_HOST=192.168.200.11 --dart-define=USE_TLS=false` +/// 仅覆盖登录服务:`--dart-define=AUTH_HOST=127.0.0.1` library; -/// 服务器内网地址(不带协议、不带端口) -const String serverHost = '192.168.200.11'; +/// 公网测试域名(不带协议、不带端口) +const String defaultPublicHost = 'jxd.jinniu.ink'; -/// OpenIM API 端口(服务端 docker 映射,默认 10002) +/// 服务器地址(不带协议、不带端口) +const String serverHost = String.fromEnvironment( + 'SERVER_HOST', + defaultValue: defaultPublicHost, +); + +/// 是否走 HTTPS/WSS(公网默认 true;内网联调传 false) +const bool useTls = bool.fromEnvironment('USE_TLS', defaultValue: true); + +/// OpenIM API 端口(服务端 docker 映射,默认 10002;TLS 时由反向代理走 443) const int apiPort = 10002; /// OpenIM WebSocket 端口(默认 10001) @@ -20,17 +30,30 @@ const int livekitPort = 17880; /// 公司账号登录后端端口(B-60 account-server,docker 默认映射 10010) const int authApiPort = 10010; -/// OpenIM API 地址,例:http://192.168.200.11:10002 -const String apiAddr = 'http://$serverHost:$apiPort'; +const String _authHostOverride = String.fromEnvironment('AUTH_HOST', defaultValue: ''); -/// OpenIM 消息长连接地址,例:ws://192.168.200.11:10001 -const String wsAddr = 'ws://$serverHost:$wsPort'; +/// OpenIM API 地址 +String get apiAddr => useTls ? 'https://$serverHost' : 'http://$serverHost:$apiPort'; -/// LiveKit 连接地址,例:ws://192.168.200.11:17880 -const String livekitUrl = 'ws://$serverHost:$livekitPort'; +/// OpenIM 消息长连接地址 +String get wsAddr => useTls ? 'wss://$serverHost/msg_gateway' : 'ws://$serverHost:$wsPort'; -/// 公司账号登录后端地址,例:http://192.168.200.11:10010 +/// LiveKit 连接地址 +String get livekitUrl => useTls ? 'wss://$serverHost/livekit' : 'ws://$serverHost:$livekitPort'; + +/// 公司账号登录后端地址 /// /// 仅本地联调用:`--dart-define=AUTH_HOST=127.0.0.1` 可覆盖登录服务地址(OpenIM/LiveKit /// 仍走 [serverHost]),正式打包不带该参数则与 [serverHost] 一致。 -const String authApiBase = 'http://${String.fromEnvironment('AUTH_HOST', defaultValue: serverHost)}:$authApiPort'; +String get authApiBase { + if (_authHostOverride.isNotEmpty) { + return 'http://$_authHostOverride:$authApiPort'; + } + return useTls ? 'https://$serverHost/account' : 'http://$serverHost:$authApiPort'; +} + +/// 极光 AppKey(可进客户端;Master Secret 禁止出现在客户端或仓库) +const String jpushAppKey = '95fc2352648a7d4568ac8d72'; + +/// 登录页自检:探测消息长连接入口(TLS 走反向代理路径,明文走宿主端口) +String get wsProbeUrl => useTls ? 'https://$serverHost/msg_gateway' : 'http://$serverHost:$wsPort/'; diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 8e4481a..3d0fbbf 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -7,10 +7,12 @@ import 'screens/home_screen.dart'; import 'screens/login_screen.dart'; import 'services/call_service.dart'; import 'services/im_service.dart'; +import 'services/push_service.dart'; import 'theme.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); + PushService.instance.init(); runApp(const ChanglianApp()); } @@ -27,6 +29,7 @@ class _ChanglianAppState extends State { super.initState(); // 被踢下线 / token 失效:清凭证、回登录页 IMService.instance.onForceLogout = () async { + await PushService.instance.unbindUser(); await LoginScreen.clearCredential(); navigatorKey.currentState?.pushAndRemoveUntil( MaterialPageRoute(builder: (_) => const LoginScreen()), @@ -90,6 +93,7 @@ class _SplashGateState extends State { await im.init().timeout(const Duration(seconds: 20)); await im.login(userID: credential.$1, token: credential.$2).timeout(const Duration(seconds: 20)); CallService.instance.start(); + await PushService.instance.bindUser(credential.$1); if (!mounted) return; Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen())); return; diff --git a/mobile/lib/screens/login_screen.dart b/mobile/lib/screens/login_screen.dart index 50687c2..e6ef13d 100644 --- a/mobile/lib/screens/login_screen.dart +++ b/mobile/lib/screens/login_screen.dart @@ -6,6 +6,7 @@ import '../config.dart'; import '../services/auth_api.dart'; import '../services/call_service.dart'; import '../services/im_service.dart'; +import '../services/push_service.dart'; import '../theme.dart'; import 'home_screen.dart'; @@ -92,7 +93,9 @@ class _LoginScreenState extends State { ); // 3. 启动通话信令监听 CallService.instance.start(); - // 4. 存凭证用于下次自动登录 + // 4. 极光 alias = OpenIM userID,供服务端离线推送 + await PushService.instance.bindUser(result.userID); + // 5. 存凭证用于下次自动登录 await LoginScreen.saveCredential(result.userID, result.token, result.nickname); if (!mounted) return; Navigator.of(context).pushReplacement( @@ -150,7 +153,7 @@ class _LoginScreenState extends State { final results = await Future.wait([ probe('账号服务', '$authApiBase/api/health'), probe('消息接口', '$apiAddr/'), - probe('消息长连接', 'http://$serverHost:$wsPort/'), + probe('消息长连接', wsProbeUrl), ]); return '自检:${results.join(',')}'; } diff --git a/mobile/lib/screens/mine_screen.dart b/mobile/lib/screens/mine_screen.dart index 0da9f85..7cc19ec 100644 --- a/mobile/lib/screens/mine_screen.dart +++ b/mobile/lib/screens/mine_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; import '../services/im_service.dart'; +import '../services/push_service.dart'; import '../theme.dart'; import '../widgets/avatar.dart'; import 'login_screen.dart'; @@ -166,6 +167,7 @@ class _MineScreenState extends State { onPressed: () async { Navigator.of(ctx).pop(); await LoginScreen.clearCredential(); + await PushService.instance.unbindUser(); await IMService.instance.logout(); if (!mounted) return; Navigator.of(context).pushAndRemoveUntil( diff --git a/mobile/lib/services/message_send_service.dart b/mobile/lib/services/message_send_service.dart index 74d53c0..f7f8169 100644 --- a/mobile/lib/services/message_send_service.dart +++ b/mobile/lib/services/message_send_service.dart @@ -50,7 +50,12 @@ class MessageSendService { message: message, userID: isGroup ? null : peer, groupID: isGroup ? groupID : null, - offlinePushInfo: OfflinePushInfo(), + offlinePushInfo: OfflinePushInfo( + title: '畅联', + desc: '你收到一条新消息', + iOSBadgeCount: true, + iOSPushSound: '+1', + ), ); return sent; } catch (e) { diff --git a/mobile/lib/services/push_service.dart b/mobile/lib/services/push_service.dart new file mode 100644 index 0000000..1121b44 --- /dev/null +++ b/mobile/lib/services/push_service.dart @@ -0,0 +1,103 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:jpush_flutter/jpush_flutter.dart'; +import 'package:jpush_flutter/jpush_interface.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../config.dart'; +import '../nav.dart'; +import '../screens/home_screen.dart'; +import 'im_service.dart'; + +/// 极光推送封装:初始化、通知权限、Registration ID、把 OpenIM userID 设为 alias。 +/// +/// OpenIM 服务端 JPUSH 离线推送按 alias=userID 下发,因此登录后必须 setAlias。 +/// 本阶段只接安卓;iOS 不调用 setup。 +class PushService { + PushService._(); + + static final PushService instance = PushService._(); + + final JPushFlutterInterface _jpush = JPush.newJPush(); + bool _ready = false; + String? _boundAlias; + + Future init() async { + if (_ready) return; + if (kIsWeb || !Platform.isAndroid) return; + try { + _jpush.addEventHandler( + onReceiveNotification: (message) async {}, + onOpenNotification: (message) async { + _onNotificationClick(); + }, + onReceiveMessage: (message) async {}, + onReceiveNotificationAuthorization: (message) async {}, + onConnected: (message) async {}, + onCommandResult: (message) async {}, + ); + _jpush.setAuth(enable: true); + _jpush.setup( + appKey: jpushAppKey, + channel: 'developer-default', + production: true, + debug: false, + ); + _jpush.applyPushAuthority( + const NotificationSettingsIOS(sound: true, alert: true, badge: true), + ); + await _requestAndroidNotificationPermission(); + _ready = true; + await _jpush.getRegistrationID(); + } catch (_) { + // 推送失败不能挡住登录和聊天 + } + } + + Future bindUser(String userID) async { + if (userID.isEmpty) return; + await init(); + if (!_ready) return; + try { + await _jpush.setAlias(userID); + _boundAlias = userID; + } catch (_) { + // alias 失败时下次登录再试 + } + } + + Future unbindUser() async { + if (!_ready) return; + try { + await _jpush.deleteAlias(); + } catch (_) { + // 本地照常清理 + } + _boundAlias = null; + } + + String? get boundAlias => _boundAlias; + + Future _requestAndroidNotificationPermission() async { + try { + final status = await Permission.notification.status; + if (!status.isGranted) { + await Permission.notification.request(); + } + } catch (_) { + // 权限申请失败不阻断 + } + } + + void _onNotificationClick() { + final nav = navigatorKey.currentState; + if (nav == null) return; + if (!IMService.instance.loggedIn) return; + nav.pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const HomeScreen()), + (route) => false, + ); + } +} diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index e2aae6b..ef04a36 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -1,7 +1,7 @@ name: changlian description: 畅联 —— 公司内部通讯 App(手机端) publish_to: "none" -version: 1.0.6+7 +version: 1.0.7+8 environment: sdk: ">=3.6.0 <4.0.0" @@ -37,6 +37,16 @@ dependencies: open_filex: 4.7.0 # 通话房间号 uuid: 4.5.2 + # 极光推送官方 Flutter 插件(MIT);厂商通道另见 jpush_flutter_android + jpush_flutter: 3.5.6 + jpush_flutter_android: 1.0.2 + +# 极光安卓厂商通道:本阶段只开华为。AppKey 可进客户端;其余厂商密钥不要填。 +jpush_android: + app_key: 95fc2352648a7d4568ac8d72 + channel: developer-default + huawei: + enable: true dev_dependencies: flutter_test: diff --git a/mobile/test/config_test.dart b/mobile/test/config_test.dart new file mode 100644 index 0000000..86e623b --- /dev/null +++ b/mobile/test/config_test.dart @@ -0,0 +1,16 @@ +import 'package:changlian/config.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('默认公网地址走 HTTPS/WSS 且不含测试 IP', () { + expect(serverHost, 'jxd.jinniu.ink'); + expect(useTls, isTrue); + expect(apiAddr, 'https://jxd.jinniu.ink'); + expect(wsAddr, 'wss://jxd.jinniu.ink/msg_gateway'); + expect(livekitUrl, 'wss://jxd.jinniu.ink/livekit'); + expect(authApiBase, 'https://jxd.jinniu.ink/account'); + expect(apiAddr, isNot(contains('154.37'))); + expect(wsAddr, isNot(contains('154.37'))); + expect(jpushAppKey, '95fc2352648a7d4568ac8d72'); + }); +} diff --git a/pc-client/.env.example b/pc-client/.env.example index 8bed870..87f60a9 100644 --- a/pc-client/.env.example +++ b/pc-client/.env.example @@ -1,7 +1,7 @@ -# 内网测试环境(192.168.200.11,见仓库根目录 README 端口表) -VITE_WS_URL=ws://192.168.200.11:10001 -VITE_API_URL=http://192.168.200.11:10002 -# 自研公司账号服务(登录 + 通话令牌)。本地联调可建 .env.local 覆盖为 mock:http://127.0.0.1:11010 -VITE_ACCOUNT_URL=http://192.168.200.11:10010 -# LiveKit 语音通话(宿主端口已改映射为 17880) -VITE_LIVEKIT_URL=ws://192.168.200.11:17880 +# 公网测试环境(jxd.jinniu.ink)。内网联调请建 .env.local 覆盖为 http/ws + 内网 IP。 +VITE_WS_URL=wss://jxd.jinniu.ink/msg_gateway +VITE_API_URL=https://jxd.jinniu.ink +# 自研公司账号服务(登录 + 通话令牌)。本地联调可覆盖为 mock:http://127.0.0.1:11010 +VITE_ACCOUNT_URL=https://jxd.jinniu.ink/account +# LiveKit 语音通话信令(媒体走服务器 17881/17882,不经 443) +VITE_LIVEKIT_URL=wss://jxd.jinniu.ink/livekit diff --git a/scripts/deploy-public.sh b/scripts/deploy-public.sh new file mode 100755 index 0000000..d66f7ba --- /dev/null +++ b/scripts/deploy-public.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# 香港测试机公网部署入口(jxd.jinniu.ink)。由总工执行;施工员本卡不跑本脚本,也不 SSH 改现网。 +# 用法: ./scripts/deploy-public.sh +# 回滚: ./scripts/rollback-public.sh +set -euo pipefail +cd "$(dirname "$0")/.." + +if [ ! -f .env ]; then + echo "没有 .env。先从 .env.example 复制,再按 deploy/public/.env.public.example 补公网项。" >&2 + exit 1 +fi + +# 注入 Master Secret(失败则停,避免空密钥启动) +# shellcheck disable=SC1091 +source ./scripts/load-jpush-secret.sh +if [ -z "${JPUSH_MASTER_SECRET:-}" ]; then + echo "Master Secret 未注入,拒绝启动" >&2 + exit 1 +fi + +mkdir -p deploy/public/acme components/redis/config components/mnt/data components/mongodb components/etcd components/kafka +touch components/redis/config/redis.conf + +docker compose \ + -f docker-compose.yaml \ + -f deploy/public/docker-compose.public.yaml \ + pull + +docker compose \ + -f docker-compose.yaml \ + -f deploy/public/docker-compose.public.yaml \ + up -d + +echo "已提交 compose 启动。检查: docker compose -f docker-compose.yaml -f deploy/public/docker-compose.public.yaml ps" +echo "证书未就绪时先用 nginx.bootstrap.conf 申请证书,见 deploy/public/README.md" diff --git a/scripts/load-jpush-secret.sh b/scripts/load-jpush-secret.sh new file mode 100755 index 0000000..b3c0888 --- /dev/null +++ b/scripts/load-jpush-secret.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# 从服务器安全文件注入极光 Master Secret 到环境变量。 +# 不打印文件内容、不写入仓库。供总工部署时 source,本卡不执行。 +set -euo pipefail + +SECRET_FILE="${JPUSH_MASTER_SECRET_FILE:-/root/.config/jinxunda/jpush_master_secret}" + +if [ ! -f "$SECRET_FILE" ]; then + echo "缺少极光 Master Secret 文件(已配置路径,不打印内容)" >&2 + exit 1 +fi + +perm=$(stat -c '%a' "$SECRET_FILE" 2>/dev/null || stat -f '%OLp' "$SECRET_FILE") +if [ "$perm" != "600" ] && [ "$perm" != "400" ]; then + echo "Master Secret 文件权限应为 600 或 400(当前 $perm)" >&2 + exit 1 +fi + +len=$(wc -c < "$SECRET_FILE" | tr -d ' ') +if [ "$len" -lt 16 ] || [ "$len" -gt 64 ]; then + echo "Master Secret 文件长度异常(不打印内容)" >&2 + exit 1 +fi + +JPUSH_MASTER_SECRET="$(tr -d '\n\r' < "$SECRET_FILE")" +export JPUSH_MASTER_SECRET +# 有意不 echo 该变量 diff --git a/scripts/rollback-public.sh b/scripts/rollback-public.sh new file mode 100755 index 0000000..5d30a00 --- /dev/null +++ b/scripts/rollback-public.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# 公网 overlay 回滚到内网 compose:停掉 edge,按原 docker-compose.yaml 拉起。 +# 不删除 components/ 与 data/,不改现网数据文件。 +set -euo pipefail +cd "$(dirname "$0")/.." + +docker compose -f docker-compose.yaml -f deploy/public/docker-compose.public.yaml stop edge || true +docker compose -f docker-compose.yaml -f deploy/public/docker-compose.public.yaml rm -f edge || true + +docker compose -f docker-compose.yaml up -d + +echo "已回到内网 compose。数据目录未动。检查: docker compose ps"