闸门:仅改 admin-console/src(A端平台运营后台)。后端 destination 绑定归小齐 D3,本单未改 backend 一行。
真源:docs/delivery/evidence/333/03-隔离域适配矩阵.md(38 缺口域);契约docs/contracts/chain-identity-v1.md;规则backend/app/tenant.py。
时间:2026-09-15 00:1x–00:3x(刘总 23:56 三令 · gpt6 03-夜班分派 · 二号第二件)
矩阵 38 项「适配范围」全部是后端「查询+写入加 destination_id 绑定」,负责人为后端(小齐 D3✓)。
其中 admin-console 直接消费的后端 P0 域:
| 后端域 | 端点数 | 隔离表(前4) | 优先级 | 现状(grep) |
|---|---|---|---|---|
| admin_backend.py | 27 | analytics_events, fulfillment_items, key_balances, merchants | P0 | 无 destination 绑定点(后端缺口) |
| admin_distribution.py | 22 | merchant_bills, merchants, orders | P0 | 无 destination 绑定点(后端缺口) |
| admin_ops.py | 23 | merchants, orders, packages | P0 | 无 destination 绑定点(后端缺口) |
⇒ 二号不可越闸门改 backend。a端可零依赖落地的 P0 = 目的地租户隔离的前端消费侧:
| 文件 | 改动 |
|---|---|
services/http.ts | ApiError 新增 kind 分类;classifyTenant() 按后端 tenant.py 固定文案精确判定:409→tenant_context_required(多目的地回落失效)、404 且文案含目的地→tenant_not_found、403 且文案含目的地/跨租户→tenant_forbidden;其余 403 归 business(不吞权限错误)。网络错误→network、401→unauthorized。409 仅在 detail 命中目的地/租户词时才分类,避免误伤支付状态机等业务 409。后端中文 detail 原样透出,不改写、不静默降级。 |
services/tenant.ts(新建) | switchDestination(id,{scope,reason}) 调 POST /api/miniapp/auth/chain/token-exchange,解包 {ok,data},把受限 JWT 写回 session(保留 adminName/role/phone),并持久化 DestinationContext;getDestinationContext()(过期自动清空,不静默续期);clearDestinationContext()。失败原样抛带 status/kind 的错误,不清空现有上下文。 |
services/contract.ts | 新增 DestinationToken / DestinationTokenResp(token/destination_id/destination_name/scope/expires_at),对齐契约 §3。 |
components/Layout.tsx | 顶栏新增只读目的地上下文条:显式上下文→高亮「目的地:{name} #{id}」(title 带 scope);无显式上下文→中性「目的地:默认(令牌回落)」。不提供切换选择项(理由见阻断 ⓑ)。 |
components/Layout.css | .ac-tenant-ctx / .is-default / .is-explicit / .ac-tenant-dot,复用既有 --color-* 变量。 |
| 项 | 前端 | 后端 | 结果 |
|---|---|---|---|
| 路径 | API_BASE + '/auth/chain/token-exchange' = /api/miniapp/auth/chain/token-exchange | auth_chain.py APIRouter(prefix="/api/miniapp/auth/chain") + @router.post("/token-exchange") | ✅ |
| 方法 | POST | POST | ✅ |
| 鉴权 | 带 Authorization: Bearer <session.token> | Depends(get_current_user) | ✅ |
| 请求体 | {destination_id, scope, reason} | ExchangeReq(同三字段) | ✅ |
| 响应 | 解包 {ok,data} 取 data.* | 返回 {"ok":true,"data":{token,destination_id,destination_name,scope,expires_at}} | ✅ |
| 404/403 | 文案匹配分类 | 「目的地不存在」404 / 「目的地未启用,不得签发访问凭证」403 | ✅ |
| 409 文案 | 匹配「存在多个启用目的地…」分类 | tenant.py _default_destination 多 active 抛 409 同文案 | ✅ |
npx tsc --noEmit:EXIT=0(typecheck.log,空文件=零诊断)。npm run build:EXIT=0(build.log,67 modules,仅 chunk 体积常规提示非错误);构建后已还原被跟踪的 dist。resolve_destination:目的地隔离在 a端数据上真正生效依赖后端查询/写入绑定 + 迁移(目标表加 destination_id 回填)。归小齐 D3,本单不越闸门。/identity 不返回当前 destination_id:a端「目的地切换器」缺可选列表与当前值数据源。tenant.ts 服务层已按契约就绪,切换 UI 在列表端点上线前不渲染选择项(诚实禁用,不臆造列表)。用户 11:4x:ⓐ admin 三域 destination 绑定进行中(admin_ops merchants 段已上 main,resolve_destination 可用);ⓑ destinations 列表端点已上线;ⓒ token admin scope 排下批。⇒ 336 切换器 UI 可开工。
GET /api/miniapp/admin/destinations 上线(admin_ops.py L25),admin 鉴权 _require_admin,返回 {code:0,data:[{destinationId,name,status,merchantCount,orderCount}]}(active 目的地 + 商户/订单计数),实测 200。resolve_destination(Query destination_id:不存在 404 / 未启用 403 / 与凭证不符 403),passport/temples+ai-config 段仍待小齐;⇒ 切换器「目的地上下文透传」对 merchants 段即时生效。| 文件 | 变更 |
|---|---|
components/DestinationSwitcher.tsx(新) | 切换器 UI:点击展开按需拉取 destinations、切换(token-exchange)、回退默认、403/404/409/401 差异提示、当前项高亮 |
components/DestinationSwitcher.css(新) | 切换器/面板/项/错误态样式 |
components/Layout.tsx | 顶栏 336 静态上下文条 → 替换为 <DestinationSwitcher /> |
services/tenant.ts | 新增 listDestinations()(GET destinations,admin 鉴权,失败诚实抛出) |
services/api.ts | 新增 listAdminDestinations()(MINI_ADMIN + '/destinations',CodeEnvelope 解包) |
services/contract.ts | 新增 DestinationList(destinationId/name/status/merchantCount/orderCount) |
GET /api/miniapp/admin/destinations,{code:0,data:[…]},字段=后端 SELECT d.id,name,status + 商户/订单计数 逐项对应。switchDestination(POST token-exchange,chain-identity-v1 §3)。clearDestinationContext → 回退登录主 token(不假装恢复已 revoke 主 token)。npx tsc --noEmit EXIT=0 / npm run build EXIT=0(typecheck-w330.log / build-w330.log;构建后还原 dist)。