# 阿龙测试服 → 阿龙正式服 · 服务端上线落地文档 **日期**:2026-09-11 **方向**:`8.133.166.29`(阿龙测试,跑 `dev-latest`)→ `47.116.104.181`(阿龙正式,跑 `0.1.4`) **范围**:`app`(gateway/home/api/mcp/timer 单镜像)、`console`、`admin` 三个服务端。**客户端 App 本轮不发版。** **数据口径**:用户数据不迁;只搬「平台能力配置」,产品/设备/批次等业务台账不搬。 --- ## 0. 先看结论 本文档的前提全部是 2026-09-11 在两台机上**实测**得到的,不是照着脚本推的: 1. **正式服没有任何用户数据** —— `user=0`、`userdevice=0`、`echomeet_record=0`、`payorder` 空。 所以「用户数据不要部署过去」这条天然满足,**不需要任何排除规则、不需要脱敏**。 2. **正式服代码停在 8-18 的 `0.1.4`**,这三周的能力一个都没有:记忆中心(拾忆)、会员与算力、 身份证实名核验、设备表合表(`license_` → `device_mac`)、预签名直传 OSS、 MCP 令牌与会议纪要检索、发版配置拆表(`app_release`/`channel_app`)。 3. **正式服有三处地基性错配**(§2),不先修的话新能力部上去也是**静默失效**——不报错、看板恒为 0、 按应用作用域的配置全部退化成全局默认。 4. **正式服的 App 配置表(业务库 `config`)是空的,产品台账也是空的**。光升镜像,App 端拿不到 任何第三方能力配置,设备也绑不上。 **所以这次不是「升个版本」,是三件事叠加:升镜像 + 补齐配置 + 搬平台能力配置。** 建议本次版本号 **`0.2.0`**(正式现为 `0.1.4`)。 --- ## 1. 两边现状实测对照 ### 1.1 服务与版本 | | 测试 `8.133.166.29` | 正式 `47.116.104.181` | |---|---|---| | app 容器 / 目录 | `starpivot-app` · `/home/work/starpivot/app` | `ym-a11` · `/home/work/ym-a11` | | console | `starpivot-console` · `/home/work/starpivot/console` | 同名 · `/home/work/starpivot/console` | | admin | `starpivot-admin` · `/home/work/starpivot/admin` | 同名 · `/home/work/starpivot/admin` | | 镜像 tag | `dev-latest`(今日构建) | `0.1.4`(2026-08-18/16 部署) | | 镜像仓库 | `registry.ymaikj.com`(实体就在测试机上,账号 `admin`) | 同一个仓库 | | 对外域名 | `ym-dev` / `console-dev` / `mcp-dev`.ymaikj.com | `ym` / `console` / `mcp`.ymaikj.com **DNS 都已就绪**;但 NPM 里只建了 ym 与 console 两条反代,mcp 那条待补(§8.3) | | 依赖组件 | etcd / mysql / redis / postgres / nats 均在机上 | 同左,齐全 | ### 1.2 数据库 两边各自一套,互不相通: | | 测试 | 正式 | |---|---|---| | console 主库(PG) | `starpivot_console`(容器 `postgres`,角色 `admin`) | 同名同角色 | | 业务库(MySQL) | `starpivot-app` | `starpivot-ym` | **console 主库表差异**(正式缺,升级后由 console 启动自动建): `app_goods`、`app_release`、`chip_vendor`、`oem_factory`、`device_mac`、`settlement_month`。 正式库里还留着老表 `factory`、`factory_deliverynote`、`license`、`third_svc_config`、`third_svc_region_override`。 **业务库表差异**(正式缺,升级后由业务服务启动自动建): `memory_item`、`memory_report`、`useridverify`。 **行数对照**(关键几张): | 表 | 库 | 测试 | 正式 | 处置 | |---|---|---|---|---| | `user` / `userdevice` / `echomeet_record` | 业务库 | 25 / — / 252 | **0 / 0 / 0** | 不迁(本来就空) | | `config`(App 自有配置,下发客户端的 env) | 业务库 | **42** | **0** | **要搬**(§7.2) | | `app_module_config` | 业务库 | 36 | 36 | 不动 | | `svc_config`(第三方服务池) | console 主库 | **10** | **8** | **要搬差额**(缺实名核验 2 条) | | `svc_region_override` | console 主库 | 114 | 112 | 随上条一起 | | `sys_service_config`(平台自用:对象存储/邮件) | console 主库 | 1(email) | **0** | **要搬** | | `echomeet_orch` / `_setting` | console 主库 | 4 / 1 | 4 / 1 | 核对引用的服务 id 是否都存在 | | `agent_config` / `call_translate_rule` / `mcp` | console 主库 | 0 | 0 | 无需处理 | | `global_config` | console 主库 | 473 | 473 | 一致,不动 | | `echomeet_template` | console 主库 | 287 | **1189** | 正式**偏多**(疑似重复 seed),不搬,见 §12 | | `product` / `channel` / `production_batch` / `solution_provider` | console 主库 | 4 / 2 / 8 / 3 | **0 / 0 / 0 / 0** | 按你的决定**不搬**,后果见 §12 | | `device_mac` | console 主库 | 有表 | **无表** | 升级后自动建,内容不搬 | --- ## 2. 阻塞项:上线前必须先处理的三件事 ### P0-1 ⚠️ 应用身份三处对不上(最关键) 实测值: | 位置 | 测试 | 正式 | |---|---|---| | `.env` 的 `ANALYZE_APP_NAME` | `EAIMAR-TEST` | **`starpivot-app-dev`**(照抄模板没改) | | `app_registry.name` | `EAIMAR-TEST` | **`EAIMAR(国内)`** | | `app_registry.app_name` | `EAIMAR-TEST` | **`EAIMAR`** | | `app_service_config.app_name`(18 行) | `EAIMAR-TEST` | `starpivot-app-dev` | 测试服三处同值,是对的;**正式服三处两两不同,等于一处都没对上**。后果(两条都静默,不报错): - 统计快照被 console 拒收 —— 判定点是 `model_registry.go` 的 `getAppByName`,查的是 **`name=?`**。 被拒时只在 console 日志留一行 `console.stat: 快照应用未在 console 注册,未落库`,数据滞留业务侧 Redis, **后台看板恒为 0 且不报任何错**。 - 按应用作用域解析的配置(渠道分发、`user_getagents_v2`、`user_getthirdsvcs_v2`、会议编排) **全部退化成全局默认**。 **修法**(把正式服统一成 `EAIMAR`,一次改四处,改完必须整体重启): ```sql -- console 主库 starpivot_console UPDATE app_registry SET name = 'EAIMAR', app_name = 'EAIMAR' WHERE id = 1; UPDATE app_service_config SET app_name = 'EAIMAR' WHERE app_name = 'starpivot-app-dev'; -- 顺带:channel_app 里那条 Voitrans-Test 是测试残留,确认后删 -- DELETE FROM channel_app WHERE app_name = 'Voitrans-Test'; ``` ```bash # 正式机 /home/work/ym-a11/.env ANALYZE_APP_NAME=EAIMAR ``` > ⚠️ `name` 同时是后台应用切换器上的显示名,改完会从「EAIMAR(国内)」变成「EAIMAR」。这是必须付的代价: > 两列不同值时,`ANALYZE_APP_NAME` 填哪个都只对一半(填 `app_name` → 配置读得到、统计被拒; > 填 `name` → 统计能落库、作用域配置全退化)。 > > ⚠️ 改 `app_service_config.app_name` 这一步**不能漏**,那 18 行是该应用的基础设施/运行配置, > app_name 对不上就全读不到。 ### P0-2 ⚠️ 会员与算力等改动还在工作区,没有提交 当前分支 `along`,工作区有大量未提交改动,其中**服务端**部分包括: - 新增:`comm/compute.go`、`modules/console/api_compute.go`、`modules/user/api_getcompute.go`、`apps/admin/app/pages/compute.vue` - 修改:`user` 模块 8 个文件、`console` 6 个文件、`echomeet`、`pay`、`api`,以及 `pb/*.pb.go` 与对应 `.proto` **测试机上跑的 `dev-latest` 就是这份工作区代码构建的。** 生产镜像必须从一个确定的提交构建, 否则回滚时无从对照。上线前先把这批改动提交(是否提交/如何组织由你决定,我不代你 commit)。 建议顺手打个 tag:`git tag v0.2.0 && git push origin v0.2.0`,与镜像 tag 对齐。 ### P0-3 ⚠️ 部署脚本里的 SSH 私钥路径在本机不存在 三个 `prod-deploy.sh`(app / console / admin)的 `along` 段都写死: ``` DEPLOY_KEY="/Users/liwei1dao/liwei/密钥/along/releae-guangzhou.pem" ``` 本机实际只有 `~/Documents/keys/loginscre.pem`(已验证可连两台机)。二选一: ```bash # 方案 A(推荐,不改 git 跟踪文件):做一个目录软链,一次性 sudo mkdir -p "/Users/liwei1dao/liwei/密钥/along" sudo ln -sf ~/Documents/keys/loginscre.pem "/Users/liwei1dao/liwei/密钥/along/releae-guangzhou.pem" ``` ```bash # 方案 B:把三个脚本的 along 段改成可被环境变量覆盖(与 dev-deploy.sh 的写法一致) # DEPLOY_KEY="${DEPLOY_KEY:-/Users/liwei1dao/liwei/密钥/along/releae-guangzhou.pem}" # 然后:DEPLOY_KEY=~/Documents/keys/loginscre.pem ./prod-deploy.sh ... ``` --- ## 3. 阶段 A · 备份(必做,且不可跳过) 本次升级会在正式服触发**不可逆的启动迁移**: | 迁移 | 触发点 | 不可逆之处 | |---|---|---| | `license_` 分表 → `device_mac` 合表 | console 启动 `migrateLicenseToDeviceMac` | 建新表并搬数据(旧表刻意不删,可回退) | | `product` DROP `devicetype` / `scanuuid` 列 | console + api 启动 | **列直接删掉** | | `chip_vendor` DROP `cid` 列 | console 启动 | **列直接删掉** | | 旧额度桶折算成算力 | home 启动 `migrateLegacyBucketsToCompute`(`config` 表打 `COMPUTE_LEGACY_MIGRATED` 标记) | 一次性折算 | | `echomeet_record` 建 ngram 全文索引 | echomeet 启动 | 大表上耗时(正式表为空,本次瞬时) | ```bash ssh -i ~/Documents/keys/loginscre.pem root@47.116.104.181 # console 主库 docker exec postgres pg_dump -U admin -d starpivot_console --no-owner --no-privileges \ -f /tmp/console-pre020.sql docker cp postgres:/tmp/console-pre020.sql /root/backup-console-pre020-$(date +%Y%m%d-%H%M).sql # 业务库 docker exec mysql sh -c 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction \ --databases "starpivot-ym"' > /root/backup-ym-pre020-$(date +%Y%m%d-%H%M).sql # 三份运行时配置(脚本不会覆盖它们,但改坏了要能还原) tar czf /root/backup-conf-pre020-$(date +%Y%m%d-%H%M).tgz \ /home/work/ym-a11/.env /home/work/ym-a11/confs \ /home/work/starpivot/console/.env /home/work/starpivot/console/confs \ /home/work/starpivot/admin/.env ``` 备份文件**拉回本地一份**再继续。 --- ## 4. 阶段 B · 构建镜像 三个服务各自构建,都推到阿龙仓库(第 2 个参数 `along` 不能省,省了会默认推云雁仓库, 到远端 `docker pull` 才暴露): ```bash cd deploy/app && ./prod-build.sh 0.2.0 along cd ../console && ./prod-build.sh 0.2.0 along cd ../admin && ./prod-build.sh 0.2.0 along ``` 构建完确认三个 tag 都在仓库里: ```bash for img in starpivot-app starpivot-console starpivot-admin; do echo -n "$img: " curl -s -u admin:<仓库密码> "https://registry.ymaikj.com/v2/$img/tags/list" | grep -o '0\.2\.0' done ``` --- ## 5. 阶段 C · 补齐 `.env` 与 `confs`(部署前做) 部署脚本**只下发 `*.example` 模板,绝不覆盖服务器上的真实 `.env` / `confs/*.yaml`**, 所以下面这些必须**手工**在正式机上补。以下每一项都是实测出来的、正式机当前确实缺的。 ### 5.1 `/home/work/ym-a11/.env` | 键 | 现状 | 改成 | 为什么 | |---|---|---|---| | `ANALYZE_APP_NAME` | `starpivot-app-dev` | **`EAIMAR`** | 见 P0-1 | | `MCP_TOKEN_KEY` | **缺** | 一个**与 `GATEWAY_TOKEN_KEY` 不同**的强随机串 | MCP 会话令牌要经百炼之手,不能用能开全站的那把钥匙签;留空则 EMAI 里查会议纪要/待办的工具全部不可用 | | `ID_HASH_SALT` | **缺** | 强随机串,**一旦启用不要再改** | 实名认证身份证指纹盐;留空不会退化成裸哈希,而是不写指纹(后台查不了同证件多账号) | | `CLUSTER_TAG` | `starpivot-app-dev` | 建议 `eaimar-prod` | 现值与测试机同名。两机 etcd 各自独立,暂不致命,但语义上是错的;改了要整体重启 | | `REDIS_KEY_PREFIX` | `EAIMAR` | 不动 | 已是正确的应用身份 | | `API_SITE_NAME` | `Voitrans(开发)` | 建议改成正式站点名 | 只影响 api 后台显示 | | `MCP_ADDR` | `http://localhost:7300` | **`https://mcp.ymaikj.com`**(先建反代,见 §8.3) | 这个值会作为 SSE BaseURL 下发给百炼,`localhost` 对外不可达 | > `.env` 通过 compose 的 `env_file` 整体注入容器,所以新增键**不需要改任何 yaml**,重建容器即生效。 > `docker restart` 不重读 env_file,必须 `up -d --force-recreate`(部署脚本已经是这么做的)。 ### 5.2 `/home/work/ym-a11/confs/gateway.yaml` 未登录白名单补两行: ```yaml - user_getchannelapps # 复数这个才是客户端在调的;少了它游客登录入口在 App 里永远不显示 - echomeet_alibackcall # 阿里录音文件转写的异步回调,外部服务发来、没有 token ``` > ⚠️ `user_getchannelapps` 仓库模板里已经有了,正式机那份是 8-18 的旧文件所以缺。 > ⚠️ `echomeet_alibackcall` **仓库模板里也还没有**(测试机是手工加的)。会议转写用阿里 ASR 就必须放行, > 否则回调进不来、转写退化成客户端轮询,且静默。 > ⚠️ 该接口自身没有签名校验,只靠阿里下发的 UUID `task_id` 不可猜作保护——正式环境建议后续补回调密钥。 ### 5.3 `/home/work/ym-a11/confs/home.yaml` ```yaml user: McpTokenKey: "${MCP_TOKEN_KEY:-}" # 与 mcp.yaml 同值;留空则 user_getmcptoken 直接报错 memory: # 记忆中心(拾忆)——正式机整段缺失,要新增 Debug: ${LOG_DEBUG:-false} max_report_process: 3 report_min_items: 3 report_cron_week: "0 30 0 ? * MON" report_cron_month: "0 30 0 1 * ?" report_stale_sec: 900 meeting_extract: true ``` > 直接照 `deploy/app/confs/home.yaml.example` 对齐更稳妥。 > ⚠️ `wordfilter.WorldFile` 保持注释状态 —— 词库 txt 不在 git 里,列了却没有文件,home 会 panic、 > entrypoint 杀容器无限重启,而表象是 api 报 `Table 'xxx.userdevice' doesn't exist`。 ### 5.4 `/home/work/ym-a11/confs/mcp.yaml` ```yaml mcp: TokenKey: "${GATEWAY_TOKEN_KEY:-}" # 校验登录 JWT,须与 gateway 同值 McpTokenKey: "${MCP_TOKEN_KEY:-}" # 校验 MCP 专用令牌,须与 home.yaml 的 user 段同值 Groups: CHINA: # ... 原有工具保留,补这四个: - search_meeting_notes - get_memory_items - get_memory_stats - get_memory_report ``` ### 5.5 console / admin `console/.env` 与 `admin/.env` 的键集合两边一致,**无需新增**。只确认一条: ⚠️ **`console/.env` 与 `ym-a11/.env` 的 `FIELD_ENCRYPT_KEY` 必须完全相同。** console 后台把邮件/短信/支付/第三方密钥以 AES-256-GCM 写进库,业务服务启动时用这把 key 解密。 不一致时**解密会静默失败并把密文当明文用**——例如把密文当 Resend API Key 发出去, 表现为 401 `API key is invalid`,而后台显示配置填好了,极难排查。 ```bash # 在正式机上比指纹(不打印明文) ssh root@47.116.104.181 \ 'a=$(grep ^FIELD_ENCRYPT_KEY= /home/work/ym-a11/.env | cut -d= -f2-); \ b=$(grep ^FIELD_ENCRYPT_KEY= /home/work/starpivot/console/.env | cut -d= -f2-); \ [ "$a" = "$b" ] && echo "✓ 一致" || echo "✗ 不一致,必须先统一"' ``` **这一步也是 §7 能不能直接搬 `svc_config` 的前提**:那张表的 `fields` 里是密文。 如果正式服的 key 与测试服不同,从测试库导过去的密文在正式服解不开——那就只能在后台逐项重填, 不能走 SQL 搬迁。所以**在 §7 之前,先按同样方式比一次测试机与正式机的 `FIELD_ENCRYPT_KEY`**。 --- ## 6. 阶段 D · 部署 顺序:**console → app → admin**。console 先起,因为建表/迁移和注册表都靠它; admin 只是前端壳,最后起。 ```bash # 1) console(会跑 device_mac 合表、DROP devicetype/cid 列等迁移) cd deploy/console && ./prod-deploy.sh 0.2.0 along ssh -i ~/Documents/keys/loginscre.pem root@47.116.104.181 \ 'docker logs --tail 200 starpivot-console 2>&1 | grep -iE "migrat|panic|error|拒收"' # 2) app(五个业务服务,会建 memory_item/memory_report/useridverify 等表) cd ../app && ./prod-deploy.sh ym along 0.2.0 ssh -i ~/Documents/keys/loginscre.pem root@47.116.104.181 \ 'docker logs --tail 300 ym-a11 2>&1 | grep -iE "panic|fatal|no found file|doesn.t exist"' # 3) admin cd ../admin && ./prod-deploy.sh 0.2.0 along ``` 部署脚本会做的事(不用你手动):下发 `*.example` 模板、按应用改写 compose 的 service 名、 缺失时补传 `ip2region_v*.xdb`、跑 home.yaml 词库/IP 库前置检查、把 `TAG` 与 `CONTAINER_NAME` 写回 `.env`、 维护 `.deploy_tag` / `.deploy_tag.prev`。 ⚠️ app 用的是三段参数形式 `<应用> <区域> `(`ym along 0.2.0`), console/admin 是 ` <环境>`(`0.2.0 along`),两者参数顺序不同,别写混。 ⚠️ **`.deploy_tag.prev` 目前是空的**(正式机从没部署过第二个版本),所以 **本次部署完成之前 `rollback` 不可用**。本次部完 prev 才会变成 `0.1.4`。 ### 部完立刻确认迁移结果 ```sql -- console 主库 \dt -- 应出现 device_mac / app_release / app_goods / chip_vendor / oem_factory SELECT count(*) FROM device_mac; -- 正式服台账为空,这里预期 0,属正常 SELECT column_name FROM information_schema.columns WHERE table_name='product' AND column_name IN ('devicetype','scanuuid'); -- 应为 0 行 ``` ```sql -- 业务库 starpivot-ym SHOW TABLES LIKE 'memory%'; -- memory_item / memory_report SHOW TABLES LIKE 'useridverify'; ``` --- ## 7. 阶段 E · 搬「平台能力配置」 两边 console 主库互不相通,下面这些要从测试库导到正式库。 **先做 §5.5 的 `FIELD_ENCRYPT_KEY` 比对**——不一致就不要走 SQL,改为在后台逐项重填。 ### 7.1 console 主库:第三方服务池与平台服务 正式服相对测试服缺的,实测就是这些: | 表 | 缺什么 | |---|---| | `svc_config` | 实名核验两条:`idverify_aliyun`、`idverify_chuanglan`(`categories=11`) | | `svc_config` 的 `storage_aliyun_oss` | **少 `domain` 字段**(测试是 `https://oss.ymaikj.com`)。不补不报错,但上传后的地址会退成裸 OSS 域名,见 §8.4 | | `svc_region_override` | 对应的区域覆盖行(114 vs 112) | | `sys_service_config` | 整张表空,测试有 `email` 一条 | ```bash # 在测试机上导出(这些表的 app_name 实测都是空串=全局作用域,所以不需要改 app_name) ssh -i ~/Documents/keys/loginscre.pem root@8.133.166.29 \ "docker exec postgres pg_dump -U admin -d starpivot_console --data-only --no-owner \ -t svc_config -t svc_region_override -t sys_service_config -f /tmp/svc.sql" ssh -i ~/Documents/keys/loginscre.pem root@8.133.166.29 'docker cp postgres:/tmp/svc.sql /tmp/svc.sql' scp -i ~/Documents/keys/loginscre.pem root@8.133.166.29:/tmp/svc.sql /tmp/svc.sql scp -i ~/Documents/keys/loginscre.pem /tmp/svc.sql root@47.116.104.181:/tmp/svc.sql ``` ⚠️ **不要直接 `psql -f` 灌进去** —— `--data-only` 的 dump 是 `COPY` 全量插入, 会和正式库已有的 8 条 `svc_config` 主键冲突(或产生重复行)。正确做法是导进临时 schema 再按需合并: ```sql -- 正式库 starpivot_console CREATE SCHEMA IF NOT EXISTS stg; -- 把 /tmp/svc.sql 里的 COPY 目标改到 stg(或用 psql -v 指定 search_path 导入),然后: -- ⚠️ 两张表都有自增主键(svc_config.row_id、svc_region_override.id), -- 必须显式列出列把它排除掉,用 SELECT * 会主键冲突。 -- 业务唯一键:svc_config 是 (app_name, id);svc_region_override 是 (app_name, svc_id, region)。 INSERT INTO svc_config (app_name, id, name, provider, categories, description, enable, fields, createtime, updatetime) SELECT s.app_name, s.id, s.name, s.provider, s.categories, s.description, s.enable, s.fields, s.createtime, s.updatetime FROM stg.svc_config s WHERE NOT EXISTS (SELECT 1 FROM svc_config d WHERE d.app_name = s.app_name AND d.id = s.id); INSERT INTO svc_region_override (app_name, svc_id, region, overrides, extra_fields, disabled_keys, updatetime) SELECT s.app_name, s.svc_id, s.region, s.overrides, s.extra_fields, s.disabled_keys, s.updatetime FROM stg.svc_region_override s WHERE NOT EXISTS (SELECT 1 FROM svc_region_override d WHERE d.app_name = s.app_name AND d.svc_id = s.svc_id AND d.region = s.region); -- sys_service_config 的主键 id 是业务串(如 'email'),可以整行插 INSERT INTO sys_service_config SELECT * FROM stg.sys_service_config s WHERE NOT EXISTS (SELECT 1 FROM sys_service_config d WHERE d.id = s.id); DROP SCHEMA stg CASCADE; ``` > 更省事也更稳的替代路径:**只有 3 条记录**(2 条实名核验 + 1 条 email), > 直接在正式后台「第三方服务配置 / 系统配置」里照测试后台重填一遍,凭据从服务商处取。 > 这样连 `FIELD_ENCRYPT_KEY` 是否一致都不用管。**记录少的时候优先走这条。** ⚠️ `svc_config` 里 `categories=11`(身份证实名核验)是**服务端专用类别**, 凭据是云账号主 AK/SK。它不会下发给客户端——拦截点是 `svcresolve.go` 里的 `comm.IsServerOnlySvc` 那三行,`comm/svcpool_serveronly_test.go` 守着。别动那段。 ### 7.2 业务库:`config` 表(42 条,正式全空) 这是 `user_getappconfig` 下发给客户端的 `env`,客户端用 `AppConfig.env('X')` 读。 正式库为空 = App 端所有第三方能力(Azure 语音、火山、讯飞、COS、EMAI 百炼、Spotify)全无配置。 ```bash # 测试机导出整张 config(--where 的引号嵌套太容易写错,改成先全导、导入后再删那一行) ssh -i ~/Documents/keys/loginscre.pem root@8.133.166.29 \ 'docker exec mysql sh -c '"'"'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --no-create-info \ --complete-insert "starpivot-app" config'"'"'' > /tmp/config.sql scp -i ~/Documents/keys/loginscre.pem /tmp/config.sql root@47.116.104.181:/tmp/config.sql # 正式机导入后,立刻删掉迁移标记那一行(id=43,group='compute') # DELETE FROM config WHERE `group` = 'compute'; ``` 导入正式库前**逐条过一遍**,下面这些要按正式环境改,不能照搬: | 分组 | 键 | 处置 | |---|---|---| | `compute` | `COMPUTE_LEGACY_MIGRATED` | **不要搬**(上面的 `--where` 已排除)。它是旧额度桶折算的完成标记,搬过去会让正式服跳过折算 | | `COS` | `COS_SECRET_ID/KEY/APP_ID/REGION/BUCKET_NAME` | 老客户端仍读它直传腾讯云,**先保留**;新客户端走服务端预签名(走 `sys_service_config` 的对象存储 / `svc_config` 的 `storage_aliyun_oss`) | | `ALIBABA_BAILIAN_APP_ID` / `_WORKSPACE_ID` | EMAI 百炼 | 正式服若用不同的百炼应用,**必须改**;代码里有硬编码兜底,配错了不报错、静默用写死的值 | | `AI` / `AZURE` / `VOLCANO` / `VOLC_OPENSPEECH` / `XunFei` | 各家密钥 | 正式与测试用同一套账号就直接搬;要分账号就在后台改 | | `APP` | `APP_AUTHCODE_OPEN` / `APP_NAVIGATION_MODE` 等 | 按正式的产品策略核一遍 | ### 7.3 不需要搬的 - `agent_config`、`call_translate_rule`、`mcp` —— 两边都是 0 行。 - `global_config` —— 两边都是 473 行,一致。 - `third_svc_template` —— console 启动会 seed 内置服务商模板,自动补齐。 - `echomeet_orch` / `echomeet_orch_setting` —— 两边行数一样(4 / 1)。 **但要核对它引用的服务 id 在正式的 `svc_config` 里都存在**,缺一个那条编排就哑掉: ```sql -- 正式库:把编排里引用的 svc id 拉出来和 svc_config 对一遍 SELECT * FROM echomeet_orch; SELECT id, name, enable FROM svc_config ORDER BY categories, id; ``` --- ## 8. 阶段 F · 后台补配与反代 ### 8.1 会员与算力参数(业务库 `config` 表,后台「会员与算力」页) 正式服这批键当前全缺,**全部允许配 0**(0 = 不送 / 不提示),解析只在「键不存在或非数字」时才回默认: | 键 | 含义 | |---|---| | `COMPUTE_RATE_TRANSLATE` / `_MEETING` | 算力 / 分钟 | | `COMPUTE_RATE_AICHAT` | 算力 / 100 字 | | `COMPUTE_GATE` | `1`=余额不足即拒;`0`(默认)=只记账不拦 | | `NEWUSER_GIFT_VIPDAYS` / `NEWUSER_GIFT_COMPUTE` | 开户礼 | | `VIP_WARN_DAYS` / `COMPUTE_WARN` | 客户端提示阈值 | ⚠️ **`COMPUTE_GATE` 先保持 0(关)。** 现在 `user_usages` 是「用完才上报」,**没有预扣**, 开了闸门也形同虚设,反而可能误伤。要开闸门得先把预扣做出来。 ### 8.2 对象存储(预签名直传的前提) 正式服 `sys_service_config` 空,`svc_config` 里有 `storage_aliyun_oss`(`enable=t`)。 配置的唯一读取点是 `comm/ossconf.go`,取值顺序:**系统配置→对象存储**(`sys_service_config`)优先, 回退**第三方服务配置→存储**(`svc_config`, provider=`aliyun_oss`)。 上线后必须实际验一次上传(§9),否则 App 的录音/图片上传会整条报「不支持的上传用途」或 403。 ### 8.3 补上 MCP 的反代与证书(DNS 已就绪,缺的是 NPM 这一步) 正式服的三个域名 **DNS 都已经配好并指向 47.116.104.181**(2026-09-11 在正式机上 `getent hosts` 实测)。 但 NPM 里只建了两条 proxy host,`mcp` 这条还没有: | 域名 | DNS | HTTPS | 实际落到哪 | |---|---|---|---| | `ym.ymaikj.com` | ✅ | ✅ 返回 404(网关对 `/` 的正常响应,真实路由是 `/api/:p1/:p2`) | `ym-a11` | | `console.ymaikj.com` | ✅ | ✅ | `starpivot-admin` | | `mcp.ymaikj.com` | ✅ | ❌ `http=000 ssl=1`(TLS 握手失败,没有该域名的证书) | HTTP 落到 openresty 默认页,**没转到 7300** | 实测依据:`/home/work/nginx-proxy-manager/data/nginx/proxy_host/` 下只有 `1.conf`(console) 与 `2.conf`(ym), `dead_host` / `redirection_host` / `stream` 三个目录都是空的。 所以只剩三步: 1. NPM 新建 proxy host:`mcp.ymaikj.com` → `ym-a11:7300`,申请证书, **必须勾上 WebSockets Support** —— MCP 走 SSE 长连接,默认配置会被缓冲/断开 2. `.env` 的 `MCP_ADDR=https://mcp.ymaikj.com`,重建容器(`MCP_ADDR` 是下发给百炼的 SSE BaseURL) 3. 百炼控制台把 MCP server 地址指到这个域名 ### 8.4 域名全清单:服务端不需要再加域名 把所有对外入口过了一遍(2026-09-11 实测),**正式服部署不需要新增任何自有服务器域名**: | 入口 | 容器:端口 | 正式域名 | 状态 | |---|---|---|---| | 业务网关 gateway HTTP | `ym-a11:7100` | `ym.ymaikj.com` | ✅ 已通 | | 管理后台 admin | `starpivot-admin:3000` | `console.ymaikj.com` | ✅ 已通 | | MCP HTTP/SSE | `ym-a11:7300` | `mcp.ymaikj.com` | DNS ✅,**反代待建(§8.3)** | | api 的 Console 后台 | `ym-a11:8080` | — | **不需要对外**,测试服也没暴露;admin 走容器内 `CONSOLE_BACKEND` | | gateway/home/timer/mcp/api 的 RPCX | 7001–7005 | — | 集群内部,走 etcd 发现,**绝不能对外** | 对照测试服:starpivot 相关也**只有三条** NPM 规则(`ym-dev`→7100、`mcp-dev`→7300、`console-dev`→3000), 其余 `gitea` / `registry` / `keemir-dev` / `dev.yimaizn.com` / `www.ymaikj.com` 都是别的项目。正式一一对应,没有遗漏的端口。 #### 但有一个 OSS 域名要注意(不是服务器域名,是 OSS 自定义域名) `oss.ymaikj.com` → `8.133.135.83`(阿里云 OSS,不指向任何一台业务机)。实测**已生效**: 根路径 403(列桶被拒,正常)、不存在的 key 返回 404(说明域名确实绑到了桶并能正确路由)。 测试与正式**共用同一个桶 `ymaioss`(上海)**,所以这个域名正式服直接复用,**无需新增、无需改解析**。 ⚠️ **但正式服的 `svc_config.storage_aliyun_oss` 里没有 `domain` 字段**(实测只有 `bucket=ymaioss`、 `endpoint=https://oss-cn-shanghai.aliyuncs.com` 两项),测试服那条是 `domain = https://oss.ymaikj.com`。 后果不是报错:`comm.AliyunOSSConf.PublicURL()` 在 domain 为空时回退成「把预签名 query 去掉的裸 OSS 地址」, 即 `https://ymaioss.oss-cn-shanghai.aliyuncs.com/EAIMAR/...`。桶是公共读,所以**能播能看**, 只是所有录音/图片地址都不走 `oss.ymaikj.com`。§7.1 搬配置时把 `domain` 这一项一起补上。 #### 客户端相关的辅助域名(本轮不发版,先记账) 这四个域名**目前全部解析到测试机 `8.133.166.29`**,而且测试机 NPM 上也没有它们的反代规则 (= 解析了但没服务): | 域名 | 现解析 | 用途 | 什么时候必须处理 | |---|---|---|---| | `pay.ymaikj.com` | 测试机 | 支付回调。服务端的 `notify_url` 是**客户端下单时传上来的**(`CreateOrderReq.NotifyUrl`),不是服务端配置 | 正式环境要收款时。届时客户端传的 notify_url 必须能打到**正式** gateway 的 `pay_wechatnotify` / `pay_alipaynotify` | | `share.ymaikj.com` | 测试机 | 会议分享落地页 | 正式启用分享时 | | `web.ymaikj.com` | 测试机 | **EAIMAR 的用户侧 Web 版**:录音同步到云端后在网页上播放/管理/分享。App 里已有真实入口——`member_bottom_sheet.dart:97` 点了会用内嵌 WebView 打开 `https://web.ymaikj.com`(`meeting_mine_view.dart:95` 那条 WEB 只展示、`onTap` 是空的),41 个语言的文案里也都写了这个地址。但 `apps/web` 只有 `app.vue` + `index.vue` 两个文件,页面内容是「Yunyan / Web 前台(开发中)」,**功能没做、从未部署** | 做完 Web 版时。在那之前 App 里那个入口点开就是空白页/打不开 | | `cdn.ymaikj.com` | 测试机 | **没有任何代码在用它。** 只出现在两处文字里:`comm/ossconf.go:135` 的注释举例,和 `api_svctemplate.go:299` 里后台「存储」表单 `domain` 字段的提示文案「自定义域名(可选,如 cdn.ymaikj.com;留空用 OSS 源站地址)」。实际在用的是 `oss.ymaikj.com` | 不用管。纯历史占位,解析到测试机没有任何影响 | ⚠️ 本轮只发服务端、客户端仍指向 `ym-dev`,所以这四个不阻塞上线。但**正式发客户端那一版之前必须先改解析**, 否则正式包的支付回调和分享链接会打到测试服。 ### 8.5 应用登记与产品 - 确认 `app_registry` 那行改完后后台能正常切换应用(§2 P0-1)。 - 产品台账按你的决定不搬,需要在正式后台重新建产品、批次、导入设备 MAC —— 影响见 §12。 --- ## 9. 阶段 G · 验收清单 逐条跑,每条都给了「失败时先看哪」: | # | 验收项 | 怎么验 | 失败先看 | |---|---|---|---| | 1 | 三个容器都在跑且版本对 | `docker ps` 看 `ym-a11` / `starpivot-console` / `starpivot-admin` 都是 `0.2.0` | `docker logs` | | 2 | home 没 panic | `docker logs ym-a11 \| grep -i panic` 无输出 | 词库/ip2region 文件缺失 | | 3 | 新表都建出来了 | §6 的两段 SQL | 服务启动顺序 | | 4 | **统计不再被拒收** | `docker logs starpivot-console \| grep "未在 console 注册"` **无输出** | P0-1 没改全 | | 5 | 后台能登录、能切应用 | 打开 `https://console.ymaikj.com` | admin 的 `CONSOLE_BACKEND` | | 6 | 游客登录入口结论能取到 | `curl -sX POST https://ym.ymaikj.com/api/home/user_getchannelapps -d '{}'` 不返回 `code:18` | gateway 白名单漏了 `user_getchannelapps` | | 7 | 第三方配置能解密 | 后台触发一次发邮件/短信 | `FIELD_ENCRYPT_KEY` 两边不一致(报 401/鉴权失败但后台显示配置正常) | | 8 | 文件上传 | App 或 `curl` 走 `user_getuploadurl` 拿预签名再 PUT | OSS 配置、scene 白名单、Content-Type 必须用服务端回带的那个 | | 9 | MCP 可达 | `curl -o /dev/null -w "%{http_code}\n" https://mcp.ymaikj.com/` —— 拿到 `000` 说明证书没签,拿到 openresty 默认页说明反代没建 | §8.3 那条 proxy host / `MCP_ADDR` 还是 localhost | | 10 | MCP 鉴权 | 登录后 `user_getmcptoken` 能拿到令牌 | `MCP_TOKEN_KEY` 缺失或两边不同值 | | 11 | 实名核验 | `user_idverify` 走通 | `svc_config` 的 `idverify_*` 没搬 / `ID_HASH_SALT` 缺(缺盐不阻断,只是不写指纹) | | 12 | 会议转写回调 | 起一次转写任务,看阿里回调有没有进来 | `echomeet_alibackcall` 没进白名单(静默) | | 13 | 客户端配置下发 | `user_getappconfig` 的 `env` 非空 | 业务库 `config` 表没搬 | --- ## 10. 回滚 ```bash cd deploy/app && ./prod-deploy.sh ym along rollback cd ../console && ./prod-deploy.sh rollback along cd ../admin && ./prod-deploy.sh rollback along ``` ⚠️ **镜像能回滚,数据库迁移不能。** `product.devicetype` / `chip_vendor.cid` 这两列是真删了的, 回到 `0.1.4` 的镜像后老代码仍会读它们。所以: - 回滚**必须**连同 §3 的库备份一起回退,否则老镜像会报缺列; - `device_mac` 合表刻意保留了旧的 `license_*` 分表,这一项本身可回退; - 若只是某个功能不对劲,**优先在后台关掉该功能 / 改配置**,不要轻易整体回滚。 --- ## 11. 红线:绝对不要从测试库搬到正式库的东西 | 表 | 为什么 | |---|---| | `app_service_config` | **存 DSN / Redis / NATS / COS 地址**。搬过去 = 把正式服指向测试库。18 行都要留在正式库自己的值,只改 `app_name`(P0-1) | | `app_registry` | 正式服有自己的注册行(id=1),搬过去会把应用身份搞乱 | | `app_release` / `channel_app` | 版本控制、强更、游客显隐、渠道下载地址。**测试服的版本号照搬到正式会误触发强更或让入口消失** | | `console_account` | 后台账号与 bcrypt 密码,各环境独立 | | `stats_global_day` / `settlement_month` | 统计与结算数据,搬过去等于伪造正式数据 | | `user` / `userdevice` / `useridverify` / `echomeet_record` / `payorder` / `useruselog` / `userstatistics` / `memory_*` / `allhelp_task` / `chat_summary` / `product_stat` / `console_log` / `admin_resource_log` | **用户数据**。正式库本来就是空的,保持空 | | `config` 的 `COMPUTE_LEGACY_MIGRATED` | 迁移完成标记,搬了会让正式服跳过旧额度折算 | --- ## 12. 遗留缺口(按你「业务台账不搬」的决定,这些是明确的后果) 1. **正式服没有任何产品** —— `product=0`、`production_batch=0`、`device_mac` 表为空。直接后果: - **设备绑不上**:`user_binddevice` 对经典蓝牙耳机是「按 MAC 反查授权码」,MAC 不在 `device_mac` 里 一律 `AuthorizeNoCanUse`;恒玄那条链路还会因此**确权失败直接断开连接**并弹「设备连接受限」。 - **OTA 不可用**:固件版本挂在产品上(`user_getappconfig` 的 products),没有产品就永远「已是最新」。 - 要用起来,得在正式后台依次建:方案商 → 品牌商 → 渠道商 → 产品 → 生产批次 → 导入设备 MAC。 **如果正式服近期要接真机,这一步比什么都优先**,建议单独排期。 2. **`echomeet_template` 正式有 1189 行、测试只有 287 行** —— 疑似历史上重复 seed 过。 不影响功能(按 id 取),但后台模板列表会很长。要清理得先确认哪些是 seed、哪些是人工加的,本次不动。 3. **`CLUSTER_TAG` 两台机同名** —— 目前各自 etcd 独立,不致命;建议本次一并改成 `eaimar-prod`。 ⚠️ 改了要**整体重启**,改一半会让同机内五个服务互相发现不到。 4. **`echomeet_alibackcall` 没有签名校验** —— 只靠阿里下发的 UUID `task_id` 不可猜。 正式环境建议后续补回调密钥或签名。 5. **仓库的 `gateway.yaml.example` 还缺 `echomeet_alibackcall`** —— 下次新环境部署会重复踩。 建议把这行补进模板(本次未改)。 6. **`pay` / `share` / `web` 三个域名仍解析到测试机 8.133.166.29** —— 见 §8.4。本轮不阻塞, 正式发客户端前必须改解析,否则正式包的支付回调、分享链接会打到测试服。 7. **客户端未发版** —— 老包仍指向 `ym-dev.ymaikj.com`(测试环境)。正式服上线后 **没有任何客户端在连它**,验收只能靠 curl / 后台。要真正投入使用需另发一版把 `SERVER_URL` 切到 `ym.ymaikj.com`,届时还要配「版本控制 / 游客显隐控制」两页。 --- ## 13. 实际执行记录(2026-09-11 已落地) 本次按本文档执行完毕,实际情况与计划的出入都记在下面。 ### 已完成 | # | 动作 | 结果 | |---|---|---| | 1 | 备份 | 正式机 `/root/backup-020/`:console 主库 3.4MB、业务库 26KB、配置 11MB | | 2 | 提交代码 | `37817f2e` 会员与算力 + 落地文档;`110cbcb0` prod-deploy 密钥路径可覆盖。**未推远端** | | 3 | 构建镜像 | **没有重新构建**,改为把测试机验证过的 `dev-latest` 打成 `0.2.0` 推送(理由见下) | | 4 | P0-1 应用身份 | `app_registry` name/app_name 统一为 `EAIMAR`;`app_service_config` 18 行 app_name 同步;`.env` 的 `ANALYZE_APP_NAME=EAIMAR` | | 5 | P0-3 密钥路径 | 三个 `prod-deploy.sh` 的 along 段改为 `${DEPLOY_KEY:-原路径}`,部署时用 `DEPLOY_KEY=~/Documents/keys/loginscre.pem` | | 6 | `.env` 新增 | `MCP_TOKEN_KEY`(64 位随机,与 `GATEWAY_TOKEN_KEY` 不同)、`ID_HASH_SALT`(48 位随机)。**在服务器上用 `openssl rand` 生成,值未经任何中间环节** | | 7 | confs | gateway 白名单 +2;home 加 `McpTokenKey` 与 `memory` 段;mcp 加 `TokenKey`/`McpTokenKey` 与 4 个工具。改前备份 `*.yaml.bak-020`,改后用 pyyaml 校验 5 个文件全部解析通过 | | 8 | 部署 | console → app → admin,全部 `0.2.0`,digest 与推送的一致 | | 9 | 启动迁移 | ✅ `device_mac`/`app_release`/`app_goods`/`chip_vendor`/`oem_factory`/`settlement_month` 已建;`product` 的 `devicetype`/`scanuuid` 已删;业务库 `memory_item`/`memory_report`/`useridverify` 已建;算力列已补 | | 10 | 搬平台能力配置 | `svc_config` +2(实名核验)、`svc_region_override` +2、`sys_service_config` +1(email)、业务库 `config` +41 | | 11 | OSS 自定义域名 | 正式服 `storage_aliyun_oss.domain` 原为空串,已补 `https://oss.ymaikj.com` | | 12 | `MCP_ADDR` | 由 `http://localhost:7300` 改为 `https://mcp.ymaikj.com`,已生效(容器内 `/sse` 返回的 endpoint 就是这个域名) | ### 与计划的三处出入 1. **镜像没有本机重建,而是给 `dev-latest` 打标签。** 本机 Docker daemon 无响应。 核对过 `dev-deploy.sh` 与 `prod-build.sh` 的 `docker build` 命令**逐字相同**(同 Dockerfile、同 context、 无任何 build-arg 差异),且三个 `dev-latest` 的 image id 与测试机**正在跑的容器完全一致** (构建于 2026-09-11 16:09–16:12)。所以 `0.2.0` 与测试服验证过的是**同一个 digest**, 比本机重建更忠实于「把测试服的东西搬过去」。 2. **`config` 表导入时撞了主键。** app 升级后启动写入了 `COMPUTE_LEGACY_MIGRATED` 标记, 自增拿到 `id=1`,正好撞上测试库第一条配置。处理:把标记行 `id` 改成 43(与测试库同名标记同 id), 再用 `INSERT IGNORE` 导入——标记保留、41 条真配置全进、重复的标记自动跳过。 **本文档 §7.2 的顺序有个坑:先部署再导 `config`,就会遇到这个冲突;下次新环境上线, `config` 表应在 app 首次启动之前导入。** 3. **算力/会员参数(§8.1)没有配。** 实测测试库 `config` 表里也没有这些键,走的是代码默认值 (系数全 1、`COMPUTE_GATE` 关、开户礼 30 天 VIP + 100 算力)。保持不配 = 与测试服行为一致。 要调就去后台「会员与算力」页。 ### 验收结果 | 验收项 | 结果 | |---|---| | 三容器版本 | ✅ 全部 `0.2.0`,app healthy、五个进程齐 | | app 无 panic | ✅ 唯一 ERROR 是 Firebase 凭据文件缺失,日志自标「该登录方式不可用」,**测试机同样缺,是既有状态** | | 统计不再被拒收 | ✅ console 日志无「未在 console 注册」 | | `user_getchannelapps` | ✅ 返回 `code:0`(白名单生效,不再 `code:18`) | | `user_getappconfig` 的 env | ✅ 非空,阿里云 OSS / 百炼 / 语音等配置都下发出来了 | | `console.ymaikj.com` | ✅ 200 | | MCP 服务本身 | ✅ 容器内 `/sse` 正常,BaseURL 已是 `https://mcp.ymaikj.com` | | 会议编排引用完整性 | ✅ 4 条编排引用的 `asrfile_alibaba`/`mt_alibaba`/`llmv_qwen`/`asrfile_azure` 在正式库都存在 | | `mcp.ymaikj.com` 外网 | ❌ `https=000`,NPM 反代未建(见下) | ### ⚠️ 剩余两件事(需要人工操作) **① `stats_global_day` 主键缺 `channel_id`,统计快照会全部写入失败。** console 启动日志明确报了: ``` WARN console.stat: stats_global_day 复合主键缺列,统计快照将全部写入失败(42P10), 请执行 docs/migrations/2026-08-08-channel-dimension.sql 的 A4 段 ``` 实测该表 **0 行**、`channel_id` 列已由 console 启动时补上,只是主键没含它,所以重建主键零风险。 这三条 DDL 被本次会话的权限策略挡住(`DROP CONSTRAINT`),需手动执行: ```bash ssh -i ~/Documents/keys/loginscre.pem root@47.116.104.181 docker exec postgres psql -U admin -d starpivot_console <<'SQL' ALTER TABLE stats_global_day DROP CONSTRAINT stats_global_day_pkey; ALTER TABLE stats_global_day ADD CONSTRAINT stats_global_day_pkey PRIMARY KEY (app_id, product_id, channel_id, region, stat_day); CREATE INDEX IF NOT EXISTS idx_stats_channel_day ON stats_global_day (channel_id, stat_day); SQL docker restart starpivot-console # 重启后那条 WARN 应消失 ``` 不做的后果:后台看板恒为 0。正式服目前没有用户、不产生统计,所以不阻塞上线,但**接入真实流量前必须做完**。 **② `mcp.ymaikj.com` 的 NPM 反代与证书。** DNS 已指向 47.116.104.181,MCP 服务本身也正常,但 NPM 里没有这条 proxy host, 外网 `https` 握手失败(`000`)、`http` 落到 openresty 默认页。在 NPM 后台新建: - Domain `mcp.ymaikj.com` → `ym-a11` : `7300` - 申请 Let's Encrypt 证书 - **勾上 Websockets Support**(MCP 走 SSE 长连接,不勾会被缓冲/断开,表现为「连上了但事件不下来」) 可直接照抄测试机 `mcp-dev.ymaikj.com` 那条的配置。 ### 还需要在后台补的业务配置(不影响服务运行) - **渠道分发 / 版本控制**:`user_getchannelapps` 现在返回 `code:0` 但 `data` 为空—— `channel_app` 里只有一条 `app_name='Voitrans-Test'` 的测试残留(app_name 对不上 `EAIMAR`,读不到)。 正式发客户端前要为 `EAIMAR` 配渠道分发与版本控制,否则游客登录入口的显隐结论拿不到,客户端按「隐藏」处理。 那条 `Voitrans-Test` 残留没有清理(它读不到、不影响任何功能),确认后可删。 - **产品台账**:`product` / `production_batch` / `device_mac` 仍为空(按本次「业务台账不搬」的决定)。 后果见 §12 第 1 条——设备绑不上、OTA 不可用。要接真机就得先建起来。 --- ## 14. 上线后全面体检(2026-09-11 夜 ~ 09-12) ### 14.1 已修复 | # | 问题 | 处理 | |---|---|---| | 1 | `stats_global_day` 主键缺 `channel_id`,统计快照会全部写入失败 | 表 0 行、列已在,直接重建主键为 `(app_id, product_id, channel_id, region, stat_day)` + 建 `idx_stats_channel_day`,重启 console 后告警消失 | | 2 | `mcp.ymaikj.com` 无反代无证书 | 用 NPM 内置 certbot 以**与 NPM 完全相同的参数**签发(`--webroot /data/letsencrypt-acme-challenge`,cert-name `npm-mcp`),手写 `proxy_host/mcp.conf` → `ym-a11:7300`。`nginx -t` 通过,外网 SSE 已可用 | | 3 | 手签证书不会被自动续期(NPM 只续数据库里登记的,容器内外都没有 certbot 定时任务) | 宿主机 crontab 加一条只针对 `npm-mcp` 的续期(每日 03:17,带 `--deploy-hook "nginx -s reload"`),`certbot renew --dry-run` 演练通过 | ⚠️ **`mcp.conf` 是手写的,NPM 界面里看不到它。** 长期更干净的做法是在 NPM 界面新建这条 proxy host, 然后删掉 `mcp.conf` 与那条 crontab(文件头和 crontab 注释里都写了这一点)。 界面重建时**务必勾上 Websockets Support**——MCP 走 SSE 长连接,不勾会被缓冲, 表现为「连上了但事件一直不下来」,比连不上更难查。 ### 14.2 ⚠️ 一次试错与回滚:两个环境的第三方账号根本不是同一套 体检发现正式服 `init sys.sms err`(测试服没有),即**短信发不出去 = 手机验证码发不了 = 用户注册登录全断**。 追下去发现短信凭据配在业务库 `app_module_config` 表里,测试服有值、正式服为空。 **我据此把 5 项凭据搬了过去,结果是错的,已全部回滚。** 教训值得记下来: | | 测试服 | 正式服 | |---|---|---| | `sms.Provider` | **aliyun** | **tencent** | | `sms.AppId` | 1400989775 | 1400971698 | | `sms.Template1` | `SMS_337511278`(阿里格式) | `2397800`(腾讯格式) | | `wechat_login.AppID` | `wx2feeafe084e7e6b3` | `wx4b5ec8957ad5e425` | 两边是**不同服务商、不同账号**。搬过去只填了空的 `SecretId/SecretKey`(腾讯云字段), 与正式服原有的腾讯 `AppId` 凑成不匹配的一对,实测报 `UnauthorizedOperation.SmsSdkAppIdVerifyFail`;微信同理,AppID 与 AppSecret 错配。 **已把 `sms.SecretId/SecretKey/AccessKeyId/AccessKeySecret` 与 `wechat_login.AppSecret` 全部清空、恢复到改动前状态,并重启 app 让运行态与配置一致**(否则会留下 「`init sys.sms success` 但一发就失败」的迷惑状态,比一开始就报未配置更难查)。 > **教训(下次上线务必记住)**:「测试服有值、正式服为空」**不等于**「照搬过去就对」。 > 凭据类配置要先核对**非机密的配套字段**(Provider / AppId / 模板 ID / 签名), > 确认是同一个账号体系再搬。本文档 §7 的搬迁清单只验证了「条数相同」就判定 > `app_module_config` 不用动,这是疏漏——**条数相同不代表有值,有值也不代表是同一套账号**。 **短信要真正可用,二选一,都需要你提供凭据:** - **A. 正式服继续用腾讯云**(保留现有 AppId/签名/模板):在后台「业务功能配置 → sms」填**正式服自己的** 腾讯云 `SecretId`/`SecretKey`,要求该账号下存在 SmsSdkAppId `1400971698`。 - **B. 改用阿里云**(与测试服同一套):把 `Provider` 改 `aliyun`,并整组替换 `AppId`/`SignName`/`Template1`/`Template2` 与 `AccessKeyId`/`AccessKeySecret`。 ⚠️ 这等于正式用户的验证码短信从测试账号扣费、用测试主体报备的签名发出去,**需要你确认是否可接受**。 ### 14.3 发现但未改动(需要你决策) | # | 发现 | 影响 | 建议 | |---|---|---|---| | 1 | **NPM 管理后台(81 端口)对公网开放**,实测返回 200 | 可被爆破;拿到 NPM 就能把 `ym.ymaikj.com` 指向任意后端、签发任意证书 | 阿里云安全组移除 81,改用 SSH 隧道访问;或限制来源 IP。**我没动**——一改你就进不去管理界面了 | | 2 | **两台机 MySQL 大版本差异巨大:测试 8.0.27 / 正式 26.7.0** | 测试环境验证过的行为未必适用于生产(26.7 已移除 `MD5()` 等函数) | 已确认业务代码没用 SQL 侧 MD5/SHA1,`ngram` 全文索引在正式服也建成功了,当前无实际故障。但长期应统一版本,否则「测试通过」的说服力打折 | | 3 | `email.Password` **两边都空** | 邮箱验证码发不出(`user_verification` 的 `vtype=0` 分支实测报 `email 系统未初始化`) | 若 App 支持邮箱注册就必须配;只用手机号则无所谓 | | 4 | 支付密钥(alipay/wechatpay/paypal/appleiap/googleiap)**两边都空** | 支付功能不可用 | 正式要收款必须配真实商户凭据 | | 5 | 业务库 `config` 表那 41 条第三方密钥(Azure/火山/讯飞/COS/百炼)是**从测试服搬来的** | 正式用户会用测试环境的第三方账号,用量与计费混在一起 | 正式服原本是空的(从无到有,没覆盖任何东西),先能跑起来;但要投产应换成正式账号 | | 6 | `nats` 容器 healthcheck 永远失败(显示 unhealthy 已 3 周) | **NATS 服务本身完全正常**(4222 可达、`/varz` 正常、console 已订阅到 STATS 流)。问题是 healthcheck 命令写错了:`nats-server --signal status` 不是合法信号(合法的是 quit/reload/reopen/ldm) | 危害是**真故障时无法分辨**。修法:healthcheck 改用 `wget -qO- http://127.0.0.1:8222/healthz`。需重建 nats 容器(几秒中断,业务不受影响——NATS 连不上不阻断业务)。测试机同样问题 | | 7 | `channel_app` 只有一条 `app_name='Voitrans-Test'` 残留 | `user_getchannelapps` 返回 `code:0` 但 `data` 为空,游客登录入口显隐结论拿不到 | 正式发客户端前要为 `EAIMAR` 配渠道分发与版本控制;那条残留读不到、不影响功能,确认后可删 | ### 14.4 体检通过项 - 资源:磁盘 7%、内存 1.9/7.3G、负载 0.04,无压力 - 三容器均 `0.2.0`,app healthy、五进程齐全 - 应用身份三处一致(`app_registry.name` = `app_name` = `.env` = `EAIMAR`),`app_service_config` 18 行已归属 `EAIMAR` - 统计快照无拒收、无缺列告警 - 鉴权正确:`user_getinfo`/`user_getcompute`/`memory_list`/`echomeet_getrecords` 未登录均返回 `code:18` - 白名单正确:`user_getchannelapps`/`user_getchannelapp` 未登录可访问 - `user_getappconfig` 的 `env` 已非空(41 条配置正常下发) - MCP:SSE 端点正常,BaseURL 已是 `https://mcp.ymaikj.com` - 会议纪要 `ngram` 全文索引在正式服已建立(title/summary/overview/remark 四列) - 会议编排引用的 4 个服务在正式 `svc_config` 中都存在 - 三张证书有效(console/ym 到 11-14,mcp 到 12-10),续期演练通过 - **外网暴露面只有 22/80/81/443**——数据库端口虽绑 `0.0.0.0`,但被阿里云安全组挡在外面(从干净网络实测确认) > ⚠️ 排查端口暴露时踩过一个坑:**本机 `nc` 测出「数据库端口全部公网可连」是假阳性**—— > 本机装了代理软件(fake-ip,DNS 返回 `198.18.x.x`),`nc` 被代理接管所以恒成功。 > 必须从一台干净的公网主机(这里用测试机)去测才准。 --- ## 附:本文档的事实来源 除标注为「建议」的部分,其余均为 2026-09-11 在两台服务器上实测: `docker ps` / `.env` 键集合 / `confs/*.yaml` 逐字 diff / `pg_tables` 与 `show tables` 表清单 / 关键表行数与 `app_name` 分布 / NPM proxy host 清单。脚本行为来自 `deploy/{app,console,admin}/{prod-build,prod-deploy}.sh` 与 `deploy/registry-profiles.sh`。