From db47e0f3f0f84a3fb3b3e39b9c5b9626b8a7776a Mon Sep 17 00:00:00 2001 From: Rodger-Wang <1367893453@qq.com> Date: Sat, 29 Aug 2026 17:50:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=B8=E5=AE=A2=E7=99=BB=E5=BD=95=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E6=94=B9=E4=B8=BA=E5=90=8E=E5=8F=B0=E6=8C=89=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 需求:后台一个开关控制游客登录按钮显隐,且只对指定版本号的客户端生效, 其余版本一律隐藏(应用商店送审场景:只给正在过审的那一版开)。 白名单语义(comm.ChannelApp.ShowTourists): - tourists=false → 全部隐藏 - tourists=true 且 touristsversion 空 → 全部隐藏(没点名任何版本 = 谁都不放行) - tourists=true 且 touristsversion=X → 只有版本号等于 X 的客户端可见 留空按"全隐藏"而非"全放行"是刻意选的失败方向:判宽的后果是不该露游客入口的版本 露了出去(过审风险、白嫖新用户开户礼),判严只是入口少显示。要全部隐藏就取消勾选, 后台 saveChannelApp 会拒绝"勾了开关却没填版本"这种等于没开的配置。 判定放在服务端,与 NeedForceUpdate/IsReviewing 同规矩:客户端在 user_getchannelapps 的请求里带 version,服务端把每条渠道配置的 tourists 换算成结论再下发。版本比较各端 各写一遍必然出现实现不一致,而这个开关直接关系到过审,判错代价高。 touristsversion 是 channel_app 的后加列,console 启动时用 ADD COLUMN IF NOT EXISTS 补上——CreateTable 对已存在的表跳过 AutoMigrate,不补就永远没这列。 注:db.proto / user_msg.proto / CLAUDE.md 里另有先于本次改动就存在的编辑,一并带上。 Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 267 ++++++++- apps/admin/app/pages/channels.vue | 32 +- apps/proto/db.proto | 23 + apps/proto/user/user_msg.proto | 46 +- apps/services/comm/channelapp.go | 38 +- apps/services/comm/channelapp_test.go | 41 +- .../modules/console/api_channelapp.go | 11 + apps/services/modules/console/api_config.go | 5 + .../modules/user/api_getchannelapp.go | 2 +- .../modules/user/api_getchannelapps.go | 5 +- apps/services/modules/user/model_user.go | 32 +- apps/services/pb/db.pb.go | 411 ++++++++++---- apps/services/pb/user_msg.pb.go | 516 ++++++++++++++++-- 13 files changed, 1251 insertions(+), 178 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 14b2f3a0..9d6c2780 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,6 +23,7 @@ yunyan-sas/ │ │ ├── pb/ # protoc 生成的 *.pb.go(DO NOT EDIT) │ │ ├── swagger/、utils/ │ ├── admin/ # 管理后台前端(Nuxt 4 SSR + Tailwind + DaisyUI 4 + Pinia) +│ ├── client/ # 移动端 App(Flutter 3.41 + GetX,pubspec name: eaimar) │ └── web/ # 用户侧前端骨架(Nuxt 4) └── deploy// # 每服务一份 deploy.sh + docker-compose.yml + env/ ``` @@ -52,6 +53,20 @@ pnpm build # 产出 .output/(SSR) 本地开发需同时起 console 后端(默认代理到 `127.0.0.1:8080`,见下文 proxy)。超管账号见各 `console.yaml` 的 `AdminAccount/AdminPassword`。 +### Client 移动端(在 `apps/client/` 下执行) + +```bash +cp .env.example .env # 首次必做,缺 .env 直接起不来(见下文) +flutter pub get +flutter run # 需先 `flutter devices` 确认真机/模拟器 +flutter analyze +flutter build apk --debug +./build_channels.sh # 多渠道打包(Android) +``` + +Flutter 3.41.5 stable(本机 `~/flutter`)。只支持 Android / iOS 两端(pubspec 的 `platforms:` 已锁死)。 +iOS 首次还需 `cd ios && pod install`。 + ### 构建镜像与部署 ```bash @@ -136,7 +151,12 @@ ym 首次部署容器一直叫 `starpivot-app` 正是如此)。service 名变 ``` 就会被注册成路由 `_`(如 `api_getfactorys`)。新增接口 = 新建 `modules//api_xxx.go` 加一个符合签名的方法即可,**无需手动注册**。后缀 `Check` 的方法不注册。 - **错误处理约定**:成功填 `resp`;失败构造 `&pb.ErrorData{Code: pb.ErrorCode_XXX, Message: ...}` 并 `return`。 -- **pb 包**:`pb/*.pb.go` 由 `apps/proto/` 下 `.proto` 经 `protoc` + `protoc-gen-go` v1.36 生成,**手动改 .pb.go 无效**;改协议要改 `.proto` 再重新生成。注意 console 模块有本地 gorm struct(非 pb)的 model 文件,见 `modules/console/model_*.go`。 +- **pb 包**:`pb/*.pb.go` 由 `apps/proto/` 下 `.proto` 经 `protoc` + `protoc-gen-go` **v1.36.6** 生成 + (`cd apps/proto && python3 pb.py`,需 `~/go/bin` 在 PATH 里)。 + ⚠️ **别照 go.mod 的 `google.golang.org/protobuf v1.36.2` 去装生成器**——那是运行时库版本, + 生成器版本写在每个 `.pb.go` 头部(`protoc-gen-go v1.36.6`)。装错版本会让 `pb.py` 把 + **全部 22 个文件**降版重写,产出几千行与本次改动无关的 diff。生成后务必 + `git diff --stat -- apps/services/pb/` 确认只动了你改的那几个,**手动改 .pb.go 无效**;改协议要改 `.proto` 再重新生成。注意 console 模块有本地 gorm struct(非 pb)的 model 文件,见 `modules/console/model_*.go`。 ## Admin 前端约定(关键,易踩坑) @@ -150,6 +170,251 @@ ym 首次部署容器一直叫 `starpivot-app` 正是如此)。service 名变 - **后端返回常多包一层**:如 `getfactorys → {factorys}`、`getproducts → {products}`、`getproduct → {product}`;保存 payload 也常包一层(厂家 `{Factory:{...}}`、产品 `{product:{...}}`);统计 trend 日期字段是 `stat_day`。列表 ID 多以十六进制展示(`id.toString(16).toUpperCase()`)。 - 样式:`app/assets/css/console.css` 由 nuxt.config 引用;DaisyUI 固定 4.x。 +## Client 移动端约定(Flutter,2026-08-28 并入 monorepo) + +`apps/client` 从 `/Users/yunyanzhineng/work/eaimar_client-main` 搬入,**未带原 `.git`**,历史留在原仓库。 + +⚠️ 搬仓时源仓库 .gitignore 忽略的文件**不会跟过来**,已手工补回:`.env`、`.mcp.json`、`.metadata`、 +`.vscode/settings.json`。(`.icon_preview`、`.vercel`、空的 `.claude` 判定为本机产物,未搬。) + +### 与后端怎么对上(联动开发的关键链路) + +- 客户端所有业务请求:`POST {SERVER_URL}/api/home/<模块>_<方法小写>` + —— 路径由 gateway 的 `this.gin.Any("/api/:param1/:param2", this.api)` + ([wservice_comp.go:66](apps/services/modules/gateway/wservice_comp.go#L66))接住,转发到对应服务里 + **反射注册**出来的同名方法。所以**后端加一个 `api_xxx.go` 方法 = 客户端立刻多一个可调接口**, + 两边都不用改路由表,只需在 [lib/data/services/network/api.dart](apps/client/lib/data/services/network/api.dart) 补一个静态方法。 +- `SERVER_URL` 在 `apps/client/.env` 里,走 `flutter_dotenv`; + [dio_manager.dart](apps/client/lib/data/services/network/dio_manager.dart) 的 `baseUrl` 再按 + 地区/渠道在 `SERVER_URL` 与 `HW_SERVER_URL` 间选一个(`HW_SERVER_URL` 留空会**回退**到 `SERVER_URL`,不会变成空 host)。 + `lib/data/services/network/nw_api.dart` 里那个 `baseApi` 常量已注释废弃,别照着它改。 +- **当前这份 client 对应 `ym` 应用**:真实 `.env` 里 `SERVER_URL=HW_SERVER_URL=https://ym-dev.ymaikj.com` + (阿龙测试环境),`Channel=xiaomi`。别照着 `nw_api.dart` 里注释掉的 `api.voitrans.net` 改。 +- **本机联调**:`SERVER_URL=http://<局域网IP>:7100`(gateway HTTP 监听端口,见 + [deploy/app/confs/gateway.yaml.example](deploy/app/confs/gateway.yaml.example))。 + 真机调试别写 `127.0.0.1` —— 那是手机自己。 +- **两套 "env" 别混**: + - `dotenv.env['X']` = `apps/client/.env`,本机/渠道级,编译进包; + - `AppConfig.env('X')`([appconfig.dart](apps/client/lib/data/models/appconfig.dart))= 后端 + `user_getappconfig` 下发的配置(env / agents / mcps / products),由 **console 后台**维护。 +- 客户端 `UserGetAppConfigResp`、`DBAgent`、`DBProduct` 等模型是**手写镜像**服务端 + `pb.UserGetAppConfigResp`([user_msg.proto:168](apps/proto/user/user_msg.proto#L168))的, + **没有代码生成把两边绑在一起**。改 proto 字段后要手动同步 + [appconfig_model.dart](apps/client/lib/data/models/appconfig_model.dart) 并重跑 + `dart run build_runner build`(生成 `*.g.dart`)。服务端已为此留了兼容层, + 见 [modules/user/compat.go](apps/services/modules/user/compat.go):**Dart 里 non-nullable 字段一旦后端不下发就是 + TypeError,整个 `fromJson` 失败**,表现为设备列表/应用配置整体空白——所以后端删字段前先看这个文件。 +- `user` 模块的 v2/v3 api 把 `GetAppConfig`/`GetAgents` 列入 `EncryptMsgs`(加密协议), + 抓包看到的是密文不是明文 JSON,别以为接口挂了。 + +### 游客登录与功能闸门(2026-08-29 改) + +游客登录**走服务端** `api_sgin` 的 `Tourists` 分支(`stype=6` + `phonemac=设备id`),拿真实雪花 uid 与 JWT。 +早先是 `_guestLoginLocal()` 本地伪造会话(token 形如 `guest-xxx`),网关解不出 uid, +游客一个后端请求都发不出去——实名认证接口自然也调不通。 + +- 服务端 `findtouristformac`([model_user.go](apps/services/modules/user/model_user.go))按设备找回游客账号。 + ⚠️ **不能改用现成的 `findformac`**:`phonemac` 是所有登录方式建号时都会写的字段, + 同一台设备先用手机号登录过、再点游客登录会串进那个正式账号。该函数额外要求 + `phone/mail/account/*openid` 全为空,只有游客账号满足。 +- ⚠️ 找回逻辑原先是注释掉的,恒定走新建账号分支,后果是游客每次登录都是新账号: + 实名记录留不住,且新账号分支紧接着发**新用户开户礼**,等于可以无限白嫖。 +- **功能闸门不再按游客身份拦,一律按实名状态拦**:`GuestGuard` 已删除,所有调用点改用 + `IdVerifyGuard.blocked()`([id_verify_guard.dart](apps/client/lib/core/utils/id_verify_guard.dart))。 + 游客点功能弹实名窗,认证通过就能用。卡片角标从「仅浏览」改成「未认证」(复用 `idVerifyNotDone`)。 +- `User.isGuest` 现在读登录时写下的 `login_is_tourist` 标记(游客拿到的是正经 uid/JWT, + 光看 uid/token 认不出来)。**它已不是功能闸门**,只用于展示层区分身份;登出时随会话一起清。 +- 老装机升级上来时本地还留着伪造的 `guest-` token,过不了网关, + splash 的 `_validateTokenAsync` 会清掉会话把人踢回登录页——这是预期行为。 + +#### 游客登录入口的显隐:后台开关 + 版本限定(2026-08-29 加) + +入口显隐由后台「SaaS 管理 → 渠道分发」按 (应用, 渠道) 配置,**判定在服务端** +([comm.ChannelApp.ShowTourists](apps/services/comm/channelapp.go),同 `NeedForceUpdate`/`IsReviewing` 的规矩): + +**白名单语义:只有被点名的那一个版本可见,其余一律隐藏。** + +| `tourists` | `touristsversion` | 效果 | +|---|---|---| +| 关 | 任意 | 全部版本隐藏 | +| 开 | 空 | 全部版本隐藏(没点名任何版本 = 谁都不放行;后台已拦住这种等于没开的配置) | +| 开 | `1.0.2` | **只有 1.0.2 可见,其余版本一律隐藏** | + +留空按「全隐藏」而非「全放行」是刻意选的失败方向:判宽的后果是不该露游客入口的版本 +露了出去(过审风险、白嫖开户礼),判严只是入口少显示。**要全部隐藏就取消勾选, +不要靠留空版本号表达**——后台 `saveChannelApp` 会拒绝「勾了开关却没填版本」。 + +⚠️ **这不向后兼容**:升级前 `tourists=true` 且没填版本的存量配置,升级后入口会消失。 +部署后必须去后台把生效版本填上(或确认就是要关掉)。 + +第三行是送审场景:只给正在过审的那一版开游客登录。客户端在 `user_getchannelapps` +的请求里带 `version`(净版本号,不含构建号),服务端把每条渠道配置的 `tourists` +**换算成结论**再下发——客户端拿到的 `tourists` 已经是「这一版该不该显示」, +**不要再拿 `touristsversion` 自己比一遍**。没上报版本时按不匹配处理(隐藏), +点名了版本却认不出是哪一版只能从严。 + +- ⚠️ **已发布的老客户端关不掉**:这次改动之前 `_checkGuestLoginConfig()` 恒定写死 + `true`([login_controller.dart](apps/client/lib/modules/login/controllers/login_controller.dart)), + 入口根本不读服务端配置。所以「其他版本全部隐藏」只对装了本次改动及以后版本的客户端成立, + 存量包无论后台怎么配都仍会显示入口。 +- 入口所在的 `_buildSocialLoginSection` 必须是 `Obx` 不能是 `Builder`:配置是异步拿到的, + `Builder` 只渲染 onInit 时的初值 `false`,入口再也不会出现。 +- 拿不到配置(无网/接口失败)时按**隐藏**处理。反过来默认显示会在送审期把不该露的入口露给审核员。 + 代价是断网启动后 10 分钟内(`checkForUpdate` 的缓存窗口)入口不出现。 +- `touristsversion` 是 `channel_app` 表的后加列,console 启动时由 + [api_config.go](apps/services/modules/console/api_config.go) 的 `ADD COLUMN IF NOT EXISTS` 补上 + (`CreateTable` 对已存在的表跳过 AutoMigrate,不补就永远没这列)。 + +### EMAI 助手的端侧指令(2026-08-29 接入) + +百炼多模态会在 `RespondingContent` 这一帧里下发端侧函数调用,客户端原先**整个丢掉**了 +(只取了 event/text/finished/state 四个字段),所以助手嘴上说"闹钟已经定好",实际什么都没做。 + +链路:协议层解析 → [assistant_directive_service.dart](apps/client/lib/data/services/assistant_directive_service.dart) +分发 + 落本地记录 → 「代办」页展示(`CalendarController._rebuildEvents`)。 +App 内对话页与耳机唤醒(`BesAiSessionService`)两条链路都往同一个 service 送,按指令 id 去重。 + +抓包实测(原始帧见 `test/assistant_directive_test.dart` 的用例,全是真实 payload): + +- 指令在 `payload.output.extra_info.tool_calls`,`arguments` 是**字符串形式的 JSON**,要二次 parse。 +- ⚠️ 参数值是**中文自然语言**,哪怕用户全程说英文:`repeat="周一"`、`content="开会"`。别指望 `MON` 这种枚举。 +- ⚠️ `extra_info.tool_infos` 是**另一回事**——服务端插件(如 weather)**已经执行完**的结果,端侧不用做事,两者不能混。 +- ⚠️ `finish_reason` 决定这一帧的形状:`stop` = 有文案(可能顺带指令); + `command_calls` = **纯指令帧,text/spoken 都是空,而且这一轮不会有 RespondingStarted/Ended**。 + 照着 text 建气泡会在聊天记录里留下永远填不上的空泡。 +- ⚠️ **`time` 不能无条件相信**:实测说 "3:30pm"、文案写"下午3:30",`time` 却给 `"03:30"` + (同批样本里 5pm→17:00、9pm→21:00 又是对的)。`AssistantReminder.disambiguateAfternoon` + 按同帧文案里的"下午/晚上/pm"做保守校正,删了会把闹钟定到凌晨。 + +已知指令(探测所得,**不完整,完整清单要从百炼控制台导**):`SET_clock`(time/date/content/repeat)、 +`ROUTE_map`(endLoc_city/endLoc_poi)、`SEND_message`(contact/content)、 +`MAKE_A_PHONE_CALL_phone_call`(contact_name)、`INCREASE_DEFAULT_volume`(无参)。 +目前**只有 `SET_clock` 真正落地**,其余记一条 `unsupported` 流水而不是丢掉。 +`SEND_message` / `MAKE_A_PHONE_CALL_phone_call` 列在 `_declined` 里——本版已移除电话/联系人能力, +接了也执行不了,**要治本得去百炼后台把这两个工具摘掉**。 + +另有两处模型侧的坑,不是客户端问题:**"取消闹钟"和"记一下待办"都不下发任何指令**,模型却回"已经帮你取消了"; +`SEND_message` 的 content 出现过幻觉(说的是"我会迟到",下发的是"项目进展如何了?")。 + +**当前边界**:指令只在 EMAI 会话建立时(对话页打开或耳机唤醒)才收得到, +「App 没打开也能定闹钟」需要常驻的会话持有者,光把执行代码挪到原生层不解决。 +落地也**只到本地**(GetStorage),服务端同步接口未开发——两个模型的 `synced` 字段和 `toJson()` +已按可直接上传的形状设计,接口就绪后补上传即可,不用改结构。 + +### Android 渠道:国内五家共用一个包(2026-08-29 起) + +华为/荣耀/小米/OPPO/vivo **发同一个 APK**,不再按渠道分别打包(`build_channels.sh` 只剩 +google 的 aab 需要单独出)。原来靠打包时写进 `.env` 的 `Channel` 区分的逻辑,现在一律 +**运行时按设备品牌推导**: + +- `getAndroidStoreInfoByBrand()`([version_update_service.dart](apps/client/lib/data/services/version_update_service.dart)) + 把 `_channel` 设成品牌推导出的渠道,`.env` 的 `Channel` **只作兜底**(品牌认不出的三星/魅族等才用)。 + 渠道统计、游客登录/跳过设备绑定开关、版本更新地址都走这一个值。 +- ⚠️ `GLOBAL_GOOGLE` 与 `OFFICIAL` **不参与品牌推导**:前者装在华为手机上仍须连 `HW_SERVER_URL` + (`dio_manager` 的 baseUrl 只认这个值),后者是官方/内测包才显示的"工具打断"菜单开关。 + 国内包的兜底值别填这两个。 +- ⚠️ `.env` 的 `Channel` 必须写 `UserChannel.fromName` 认识的全名(`CHINA_XIAOMI`), + 写成 `xiaomi` 会被解析成 `UNKNOWN`。 +- 华为/荣耀商店要求"每次启动重新确认隐私政策",这条判断在 + [login_controller.dart](apps/client/lib/modules/login/controllers/login_controller.dart) 的 + `_initDeviceInfo()` 里,用 `DeviceBrand.isHuaweiOrHonor(brand, manufacturer)` + ([device_brand.dart](apps/client/lib/core/utils/device_brand.dart),两个字段任一命中即算, + 老荣耀机型 brand=HONOR/厂商=HUAWEI)。**别改回读 `versionService.cachedChannel`**——那个值在版本 + 检查跑完前会回退成 `GLOBAL_GOOGLE`,无网启动时合规逻辑直接失效。 + `_hasAcceptedPrivacy` 初值 `false`,品牌读出来之前默认"未同意",失败方向偏向合规。 +- 口径偏差是已知的取舍:判断依据从"从哪家商店下载"变成了"用哪家的手机"。审核实测都在对应 + 品牌真机上做,能过审;反向(小米商店的包装在华为手机上)只是多弹一次。 + +### 必备本机文件 + +- **`.env` 是硬性前置**:`main.dart` 启动第一步就 `dotenv.load(fileName: ".env")`,且 `.env` 同时被 + `pubspec.yaml` 声明为 asset —— 文件不存在**编译期就报 asset 缺失**。已提供 `.env.example`(key 与真实 + `.env` 逐项对齐)。`.env` **在 .gitignore 里,不入库**(源仓库 `eaimar_client-main` 的 .gitignore + 同样有 `.env*`,本来就从不进 git —— 所以**换机器/重新拉代码后必须手工补 `.env`**,这是搬仓时最容易漏的一步)。 +- 真实 `.env` 里 `VOLCANO_*` / `OPENAI_*` **全是空值**:这些第三方能力已改由后端 `AppConfig.env()` 下发, + 本机只在绕过后端直连调试时才填。实际必填的只有 `SERVER_URL` 和 `Channel`。 +- `.mcp.json` 同样被 pubspec 声明为 asset(`open_ai_service_adapter.dart` 里的加载代码虽已注释, + 删文件仍会构建失败)。里面是两个真实 MCP server(`mcp.ideapsound.com` / `api.deapsound.com`),会入库。 + +### 仓库卫生(重要) + +`apps/client/.gitignore` 挡掉的构建产物占 5.8G 中的 5.6G:`build/`(5.2G)、`ios/Pods/`(311M)、 +`android/.gradle/`(120M)。实际入库约 **118MB / 3200 文件**,其中 ~40MB 是构建必需的预编译 SDK +(`local_plugins/azure_speech` 的 MicrosoftCognitiveServicesSpeech.xcframework、`device_jieli` 的 JL_BLEKit 等)。 +**`build/` 与 `.dart_tool/` 的规则故意不带前导斜杠**,这样才能一并覆盖 `local_plugins/*/android/build` 等嵌套产物。 + +⚠️ `android/app/sign/eaimar.keystore`(Android 发版签名库)目前**会入库** —— 与本仓库"私有仓库下接受真实凭据" +的既有取舍一致,且不入库会导致其他人打不出可升级的正式包。仓库若要公开,这个必须先摘出去。 + +### 服务器域名(2026-08-28 统一) + +自有服务器域名**一律 `ymaikj.com`**,上一代的 `voitrans.net` / `voitrans.com` / `ideapsound.com` / +`deapsound.com` 已全部清除(含 41 个语言文件里用户可见的 `web.voitrans.com`)。现役清单: + +| 用途 | 域名 | 位置 | +|---|---|---| +| 业务后端 | `ym-dev.ymaikj.com` | `.env` 的 `SERVER_URL`/`HW_SERVER_URL` | +| 支付回调 | `pay.ymaikj.com` | `pay_service.dart`、`goods_base_controller.dart` | +| 会议分享 | `share.ymaikj.com` | `share_bottom_sheet.dart` | +| Web 站点 | `web.ymaikj.com` | `member_bottom_sheet.dart`、`meeting_mine_view.dart`、各语言文案 | +| 微信 Universal Link | `ymaikj.com` | `login_service.dart`、`pay_service.dart`、`ios/Runner/Runner.entitlements` | +| MCP(死配置) | `mcp.ymaikj.com`、`api.ymaikj.com` | `.mcp.json` | + +⚠️ **微信 Universal Link 改了代码还不够**,三处必须完全一致,否则微信登录与微信支付**静默失败**: +① `universalLink: 'https://ymaikj.com/app/'`(两个 dart 文件) +② `Runner.entitlements` 的 `applinks:ymaikj.com` +③ **微信开放平台后台登记的 Universal Link** + `https://ymaikj.com/.well-known/apple-app-site-association` +代码侧(①②)已改完,**③ 属于外部系统,发版前务必确认已同步**。 + +⚠️ 另外两处含 `voitrans` 但**不是域名、不要改**: +- `lib/firebase_options.dart` 的 `projectId: 'voitrans'` / `'voitrans-e0e3d'` —— Firebase 项目标识, + 改它要在 Firebase 控制台另建项目并换掉 `google-services.json` / `GoogleService-Info.plist`。 +- `lib/core/translations/` 里的 key 名(`copyrightVoitrans`、`voitransPrivateCloud`…)—— 是标识符不是文案, + 用户不可见,改了要 41 个语言文件连同引用处一起动。 + +`share_bottom_sheet.dart` 的分享链接格式是 `share.ymaikj.com/share/{serverHost}/ymaikj.com/{id}/{token}`, +**路径第二段是品牌标识、不是要请求的域名**,由 share 端解析,别当成域名误改。 + +### 其他 + +- 状态管理 GetX;目录分层见 `apps/client/README.md`(`core/` `data/` `modules/` `routes/`)。 +- `local_plugins/` 下 25 个本地插件用 `path:` 依赖,蓝牙/语音/音乐等 native 能力都在这里。 + `asr_tr_tts_manager` 在 pubspec 里**是注释掉的**(缺 deepvoice 的 `audio_recorder` 插件,打开会 Gradle 报错)。 +- `doc/` 下 41 个语种目录是国际化文案,`tools/i18n_audit.py` 做占位符校验。 + +## 第三方服务配置:服务端专用类别(2026-08-28 新增) + +`svc_config`(后台「第三方服务配置」)默认是**面向客户端**的:[svcresolve.go](apps/services/modules/user/svcresolve.go) +的 `resolveThirdSvcs` 会把 `encrypted` 字段**解密成明文**塞进 `user_getthirdsvcs_v2` / +`user_getappconfig_v3` 的响应下发。 + +但有些第三方能力只能由服务端调用,凭据是**云账号主 AK/SK**(拿到就能操作整个云账号)。 +这类服务放进 `svc_config` 是为了统一后台管理入口,**必须在下发口拦掉**: + +- `comm.SvcCatIdVerify = 11`(身份证实名核验)登记在 [comm/svcpool.go](apps/services/comm/svcpool.go) + 的 `serverOnlySvcCats` 里;`resolveThirdSvcs` 用 `comm.IsServerOnlySvc` 整条跳过。 +- ⚠️ **`svcresolve.go` 里那三行过滤是唯一拦截点**,删掉等于把主账号密钥明文发给每个 App 用户。 + `comm/svcpool_serveronly_test.go` 守着这条底线。 +- 新增服务端专用类别:`serverOnlySvcCats` 加一行 + admin `serviceconfig.vue` 的 `SERVER_ONLY_SCATS` 加一项 + (前端那份只管界面提示,**不产生拦截效果**)。 + +### 身份证实名核验 + +- 服务商:阿里云 `Id2MetaVerify`(`cloudauth.aliyuncs.com`,2019-03-07,`ResultObject.BizCode` 1=一致/2=不一致) + 与腾讯云 `IdCardVerification`(`faceid.tencentcloudapi.com`,2018-03-01,`Result` "0"=一致/"-1"=不一致)。 + 实现在 [sys/idverify/](apps/services/sys/idverify/),**无状态工厂**(不同于其它 sys 子系统的启动单例)—— + 因为配置在 svc_config 里按应用作用域存,调用时才解析。 +- **调用失败 ≠ 不一致**:两个 provider 都只在服务商明确判定时返回 `Matched`,其余(网络/鉴权/额度耗尽/ + 未知返回码)一律返回 error。把它们当成"不一致"会导致服务商欠费时全体用户看到"您填的信息有误"。 +- 客户端接口:`user_idverify`(提交核验)、`user_getidverify`(查状态),见 + [api_idverify.go](apps/services/modules/user/api_idverify.go)。 +- **合规口径**:身份证号**全文不落库**,只存掩码(`4403**********1234`)与加盐 SHA-256 指纹。 + 盐来自 `ID_HASH_SALT` 环境变量,**留空则不写指纹**(绝不退化成裸哈希——身份证号空间有限, + 裸哈希可被彻底反查)。盐一旦启用不要再改,否则历史指纹全部对不上。 +- **一证可绑多号**:不拦截,但按指纹回查,后台用户页显示「同证件账号数」。 +- 限流:同账号 10 分钟内失败 5 次即拒(`idVerifyFailWindow`/`idVerifyFailLimit`)——服务商按次计费。 +- 本地先校验身份证格式(GB 11643 校验位),把明显无效的输入挡在计费调用之前。 + ## 配置与密钥约定(重要) - 各 `conf/*.yaml` 与 `deploy//deploy.sh` 中**写死了真实的 DSN / 密钥 / 服务器 IP / 仓库密码,且纳入 git**——这是用户在私有仓库下**明确接受**的取舍。修改这些文件时注意它们会随仓库提交。 diff --git a/apps/admin/app/pages/channels.vue b/apps/admin/app/pages/channels.vue index 11ffe4c3..1dda54dd 100644 --- a/apps/admin/app/pages/channels.vue +++ b/apps/admin/app/pages/channels.vue @@ -69,7 +69,9 @@
强制更新 - 游客登录 + + {{ c.touristsversion ? `游客登录 仅 ${c.touristsversion}` : '游客登录 未填版本(不生效)' }} + 可跳过绑定 已停用
@@ -175,6 +177,16 @@ 允许游客登录 +
+ 生效版本(必填) + +

+ 白名单语义:只有填在这里的版本可见,其余版本一律隐藏。 + 送审场景就是只给正在过审的那一版开。版本号要与客户端上报的一致(不含构建号,如 1.0.2)。 + 留空等于所有版本都看不到入口——要全部隐藏请直接取消上面的勾选。 +

+

⚠ {{ touristsWarn }}

+