# 微信小程序 web-view 壳接入记录 日期:`2026-05-03` ## 1. 目标 本次先用微信小程序 `web-view` 承载现有 H5,不重写 React/Vite 主前端,也不把 SpacetimeDB SDK 或业务规则搬进小程序端。 当前小程序壳只承担五件事: 1. 提供微信开发者工具可识别的 `miniprogram/` 工程根目录。 2. 在原生小程序壳中调用 `wx.login` 获取小程序 `code`。 3. 调用服务器域名下的 `/api/auth/wechat/miniprogram-login`,由 Rust `api-server` 兑换微信身份并签发系统登录态。 4. 若后端返回 `pending_bind_phone`,先在小程序原生层通过 `button open-type="getPhoneNumber"` 取得用户同意后的手机号动态令牌,再调用 `/api/auth/wechat/bind-phone` 完成绑定。 5. 用一个全屏 `web-view` 打开现有 H5 入口,并把系统 `auth_token` 放入 H5 现有登录回调 hash。 重要边界: 1. `openid` 只作为后端微信身份绑定依据,不直接暴露给 H5 当登录凭证。 2. H5 继续消费本系统 JWT,也就是 `#auth_provider=wechat&auth_token=...&auth_binding_status=...`。 3. 这与 [`WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md`](./WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md) 中“微信只提供三方身份,Axum 签发系统 JWT”的边界一致。 ## 2. 文件入口 | 文件 | 说明 | | --- | --- | | `project.config.json` | 指定 `miniprogramRoot: "miniprogram/"`。 | | `miniprogram/app.json` | 小程序全局配置,注册 `pages/web-view/index`。 | | `miniprogram/config.js` | 业务域名入口配置,需要部署时填写。 | | `miniprogram/pages/web-view/index.*` | 最小 web-view 页面。 | | `server-rs/crates/api-server/src/wechat_auth.rs` | 新增小程序登录接口 `/api/auth/wechat/miniprogram-login`。 | | `server-rs/crates/platform-auth/src/lib.rs` | 新增 `jscode2session` 兑换能力。 | ## 3. 需要手工填写的配置 在 `miniprogram/config.js` 中填写: ```js const WEB_VIEW_ENTRY_URL = 'https://你的H5业务域名/'; const API_BASE_URL = 'https://你的服务器域名/'; const MINI_PROGRAM_APP_ID = '你的微信小程序 AppID'; const MINI_PROGRAM_ENV = 'develop'; ``` 约束: 1. 必须是 `https`。 2. 不能是 `localhost` 或 IP。 3. `WEB_VIEW_ENTRY_URL` 域名需要在微信小程序后台配置为业务域名。 4. `API_BASE_URL` 域名需要在微信小程序后台配置为 request 合法域名。 5. H5 页面里的 API、图片、音视频、iframe 等外链也要满足微信侧域名与证书要求。 在 `api-server` 环境变量中填写: ```bash WECHAT_AUTH_ENABLED=true WECHAT_AUTH_PROVIDER=real WECHAT_MINI_PROGRAM_APP_ID="你的微信小程序 AppID" WECHAT_MINI_PROGRAM_APP_SECRET="你的微信小程序 AppSecret" WECHAT_JS_CODE_SESSION_ENDPOINT="https://api.weixin.qq.com/sns/jscode2session" WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT="https://api.weixin.qq.com/cgi-bin/stable_token" WECHAT_PHONE_NUMBER_ENDPOINT="https://api.weixin.qq.com/wxa/business/getuserphonenumber" ``` 如果开放平台网页 OAuth 与小程序使用同一个 AppID/Secret,也可以继续使用已有: ```bash WECHAT_APP_ID="你的微信 AppID" WECHAT_APP_SECRET="你的微信 AppSecret" ``` 但正式部署建议把小程序配置写到 `WECHAT_MINI_PROGRAM_APP_ID` 与 `WECHAT_MINI_PROGRAM_APP_SECRET`,避免和网页 OAuth 配置混淆。 `WEB_VIEW_SOURCE_QUERY` 默认附加: ```text clientType=mini_program clientRuntime=wechat_mini_program ``` 小程序壳调用登录接口时会补传: ```text x-client-type=mini_program x-client-runtime=wechat_mini_program x-client-platform=ios|android|unknown x-client-instance-id=<小程序本地持久化随机值> x-mini-program-app-id= x-mini-program-env= ``` 这些字段会进入 refresh session 的客户端身份快照;URL query 只作为 H5 识别宿主来源的轻量标记,不作为鉴权依据。 ## 4. 登录链路 当前登录链路固定为: 1. 小程序页面启动。 2. 调用 `wx.login` 获取一次性 `code`。 3. 小程序壳请求: ```http POST /api/auth/wechat/miniprogram-login Content-Type: application/json { "code": "wx.login 返回的 code" } ``` 4. `api-server` 调用微信 `jscode2session` 兑换 `openid/unionid`。 5. `api-server` 复用现有微信身份逻辑: - 先按 `unionid` 命中已有身份 - 再按 `openid` 命中已有身份 - 都没有命中时创建 `pending_bind_phone` 的微信壳账号 6. `api-server` 签发系统 access token,并写入 refresh session。 7. 如果返回 `bindingStatus=active`,小程序壳打开: ```text https://你的H5业务域名/#auth_provider=wechat&auth_token=<系统JWT>&auth_binding_status=active ``` 8. 如果返回 `bindingStatus=pending_bind_phone`,小程序壳暂不打开 H5,而是展示原生 `getPhoneNumber` 按钮。用户点击并同意后,小程序把 `bindgetphonenumber` 事件里的 `detail.code` 作为 `wechatPhoneCode` 传给: ```http POST /api/auth/wechat/bind-phone Authorization: Bearer <小程序登录返回的系统JWT> Content-Type: application/json { "wechatPhoneCode": "getPhoneNumber 返回的 code" } ``` 9. `api-server` 通过微信 `stable_token` 获取小程序 `access_token`,再调用 `getuserphonenumber` 换取平台验证后的手机号,并复用现有微信待绑定账号合并逻辑。成功后重新签发 `active` 系统 token。 10. H5 复用 `consumeAuthCallbackResult()` 消费 `auth_token` 并进入现有登录态恢复流程。 补充:H5 里的旧短信验证码绑定页继续保留为非小程序环境兜底;小程序原生手机号授权只替代“手动输入手机号 + 短信验证码”这一步,不代表后台静默读取本机号码。 ## 5. 微信后台配置 至少需要在小程序后台配置: 1. `业务域名`:承载 H5 的域名。 2. `request 合法域名`:`API_BASE_URL` 对应的服务器域名。 3. `socket 合法域名`:若后续小程序原生层直连 WebSocket 才需要;当前不启用。 当前仓库的 H5 仍建议通过同域 `/api/*` 访问 Rust `api-server`,避免在小程序和 H5 中分别维护跨域白名单。 ## 6. 当前不做的事 本次不做原生小程序页面迁移,原因是当前主前端依赖: 1. React DOM 挂载、浏览器 history 和 `window.location`。 2. `localStorage` / `sessionStorage`。 3. 浏览器 `fetch` 与 `ReadableStream` SSE。 4. DOM、Canvas、Three.js 等浏览器渲染能力。 这些能力不能稳定原样运行在原生小程序宿主中。后续如要原生化,应新建小程序端宿主,复用 `packages/shared` 契约和 `api-server` BFF,而不是把 `src/` 整体搬过去。 本次也不做 `openid` query 直登。原因是 `openid` 不是本系统签发的登录凭证,不能表达 token 版本、会话 ID、绑定状态、角色与过期时间,也不能被 H5 直接信任。 ## 7. 验收口径 可重复自动化 smoke: ```bash npm run check:wechat-miniprogram-auth ``` 该命令固定覆盖三段链路: 1. 静态确认 `miniprogram/pages/web-view/index.js` 会请求 `/api/auth/wechat/miniprogram-login`,携带 `mini_program / wechat_mini_program` 客户端来源头,并把 `auth_provider/auth_token/auth_binding_status` 拼入 H5 hash。 2. 运行 `api-server` 定向测试 `wechat_miniprogram_login_returns_system_token_and_marks_session_source`,断言小程序登录返回 `token/bindingStatus/user`、写入 refresh cookie,并且 `/api/auth/sessions` 能看到 `clientType=mini_program`、`clientRuntime=wechat_mini_program`、`miniProgramAppId`。 3. 静态确认小程序壳在 `pending_bind_phone` 时使用 `getPhoneNumber` 和 `wechatPhoneCode` 调用 `/api/auth/wechat/bind-phone`,而不是打开 H5 后再要求手输手机号。 4. 运行前端 `authService` 定向测试,断言 `consumeAuthCallbackResult()` 会消费 `#auth_provider=wechat&auth_token=...&auth_binding_status=...`、保存 access token,并清理地址栏 hash。 手工联调仍按以下口径确认真实微信与域名配置: 1. 微信开发者工具打开项目根目录后,识别 `miniprogram/` 为小程序源码目录。 2. 未填写 `WEB_VIEW_ENTRY_URL` 或 `API_BASE_URL` 时,页面显示配置提示,不出现空白页。 3. 填写已配置业务域名后,小程序先请求 `/api/auth/wechat/miniprogram-login`。 4. 后端返回 `token/bindingStatus/user`,并写入 refresh cookie。 5. 若返回 `pending_bind_phone`,先看到小程序原生授权手机号按钮;用户同意后,小程序请求 `/api/auth/wechat/bind-phone` 且请求体包含 `wechatPhoneCode`。 6. 绑定成功后首页全屏打开 H5,URL hash 中包含 `auth_provider=wechat`、`auth_token`、`auth_binding_status=active`。 7. H5 内 `consumeAuthCallbackResult()` 消费 hash 后,`/api/auth/me` 能返回当前用户。 8. `/api/auth/sessions` 能看到来源为 `mini_program / wechat_mini_program` 的会话记录。