166 lines
14 KiB
Markdown
166 lines
14 KiB
Markdown
# AGENTS.md · CRMEB 小程序(web-view 宿主)
|
||
|
||
> 在本仓库工作前先读。**跨仓库的权威约定在 `box_app/AGENTS.md`**(WSL:`/home/jone/workspace/box_app/AGENTS.md`)——
|
||
> 三仓布局、身份契约、接口命名、部署流程都在那里。本文件只写小程序侧特有的部分。
|
||
|
||
---
|
||
|
||
## 1. 这是什么
|
||
|
||
CRMEB 商城小程序(uniapp,manifest `name=DoubaShop`),同时是**整个 H5 生态的 web-view 宿主**:
|
||
|
||
- **首页壳** `pages/index/index.vue` —— tabBar 首页,硬编码加载 `https://mp.yizhezhe.com/mama-box/`;
|
||
H5 应用宫格在 mama-box 内用 `location.href` 换应用,**本页不会重新 onLoad**。
|
||
- **容器页** `pages/annex/web_view/index.vue` —— 太阳码 / 分享落地 / 商城宫格 / JumpPath 的通用宿主。
|
||
- H5 应用(30+)只在 box_app 后台登记,本仓库没有它们的源码。
|
||
|
||
| 项 | 值 |
|
||
| --- | --- |
|
||
| 本地路径 | `/mnt/c/Users/Jone/Documents/HBuilderProjects/crmeb`(Windows:`C:\Users\Jone\Documents\HBuilderProjects\crmeb`) |
|
||
| 发布 | HBuilderX 发行 → 微信小程序上传 → 微信审核 → 发布 |
|
||
| 远程仓库 | `git@gitea.nexjs.com:jone/crmeb-mp.git`(2026-10-26 补)—— `main` = 可发布小程序基线;App 开发在 `feat/app-android` 分支 |
|
||
| 版本 | `manifest.json` 的 `versionName` / `versionCode` |
|
||
|
||
> ⚠️ HBuilderX 打包按**当前工作树**,不看分支。发小程序版前先切回 `main`;
|
||
> App 专属代码一律包 `#ifdef APP-PLUS`,否则会被打进小程序包。
|
||
|
||
## 2. 关键文件
|
||
|
||
| 文件 | 作用 |
|
||
| --- | --- |
|
||
| `pages/index/index.vue` | 首页壳:身份 fail-closed、加载 mama-box、**冷启动恢复 last_app**、分享卡片、tabBar 显隐 |
|
||
| `pages/annex/web_view/index.vue` | 容器页:太阳码 scene 解析、身份注入(query)、`_reportLastApp` 上报 |
|
||
| `mixins/webviewHost.js` | 宿主公共能力:postMessage 分发(location/share/copyLink/setTitle/requestProfile)、原生授权面板、授权回传、静默登录复用 |
|
||
| `mixins/shareCard.js` | 分享图:按当前 H5 URL 反查 box_app App 表;朋友圈本地图转换 |
|
||
| `App.vue` | 冷启动静默登录、`globalData`(`spid` / `appShowAt`)、`onShow`(前台化时间戳) |
|
||
| `pages/annex/auth/index.vue` | 独立授权页(`navigateTo` 进入,**先手机号 → 再问头像昵称**,结束写 `__AUTH_RESULT__` 后 `navigateBack`,见 §4.4) |
|
||
| `pages/guide/index.vue` | `pages.json` 第一个页面(冷启动入口,无广告时 `switchTab` 到首页壳) |
|
||
|
||
## 3. 硬约定
|
||
|
||
1. **两个宿主页必须共用 mixin**:`mixins/webviewHost.js` + `mixins/shareCard.js`。历史上两页各写一份导致能力分叉(首页壳缺 copyLink / 授权回传 / unionid),别再分叉。
|
||
2. **身份注入走 query、授权结果走 hash**:容器页 `_applyIdentity()` 负责按当前 openid 重写 URL(含 `_v=` 缓存击穿);授权回传只走 hash(`#openid=&phone=&nickname=&avatar=`)——微信 web-view **首次加载丢 query、不丢 hash**。
|
||
3. **拿不到 openid 就不加载 H5**(fail-closed):重试 → 兜底屏 → 手动重试;**不要匿名加载**(H5 会进访客态,反而更难排查)。
|
||
4. **分享深链必须 `stripAuthParams()`**:别把发送者的 `openid/unionid/t` 泄露给接收者;接收者由容器页按自己身份重新注入。
|
||
5. **分享图/标题按 `onShareAppMessage(options).webViewUrl` 当场解析**:postMessage 是延迟投递,首页壳又不随 H5 换应用 onLoad,沿用上次结果会「B 应用带 A 应用的图」。
|
||
6. **tabBar 是全局状态**:首页壳必须 `uni.hideTabBar()`(web-view 盖不住原生条,H5 自带底栏会双条);**严禁在商城页主动 `showTabBar` 配对** —— 会与 DIY 底栏同显成双 tabBar。
|
||
7. **恢复/落地统一 `navigateTo` 容器页**,并保留首页壳作为返回落点;不要直接把首页壳的 web-view src 指向第三方应用(首页壳的 `buildWebUrl` 是 **hash** 注入,会破坏自带 hash 路由的应用)。
|
||
8. **只用 `options.url` 当显式落地判据**:`options.scene` **每次冷启动都有值**(1001 最近使用 / 1007 分享 / 1011 扫码…),拿它判落地会导致恢复永远不执行。
|
||
|
||
## 4. 踩坑:宿主生命周期(2026-09-22,都付过线上代价)
|
||
|
||
### 4.1 热启动 vs 冷启动
|
||
|
||
- 用户点右上角胶囊圆点「关闭」**不是销毁**,只是切后台。马上再进是**热启动**:页面栈与 web-view 都在,
|
||
**只触发 `onShow`,不触发 `onLoad`**;被回收后才是冷启动。
|
||
- 所以「退出小程序再进要回到上次的 H5 应用」这类需求,**只写在 `onLoad` 里会失败** —— 用户测的正是「关闭后马上再进」。
|
||
- 反过来,`onShow` 里**无条件重载 web-view** 会把用户从 H5 应用踢回首页。首页壳曾有一段「切回首页 tab 就重载」的老逻辑,
|
||
热启动也走它,于是把恢复好的应用冲掉(`shareHref` 因 postMessage 延迟还停在 mama-box)。
|
||
- **判据**:切 tab **不触发** `App.onShow`,热启动**会**。做法:`App.vue onShow` 记 `globalData.appShowAt`,
|
||
页面 `onShow` 里 `Date.now() - appShowAt < 2000` 视为热启动恢复 → **跳过重载**。
|
||
- 改任何 `onShow` 逻辑前,先想清楚触发来源(App 前后台切换 / 页内跳转 / 切 tab)。
|
||
|
||
### 4.2 显式落地后点「小房子」回首页,不能被恢复逻辑再送回去
|
||
|
||
- 扫码(太阳码)/ 分享落地时,用户直接落在**容器页**,页面栈只有它 → 左上角是**「小房子」**,点它回小程序首页。
|
||
- 如果这时「上次应用恢复」还生效,就会把用户又送回刚扫码的那个应用:表现为**点了小房子回不了家**,
|
||
而且因为多push了一层容器页,左上角变成**返回箭头**,得再点一次返回才真回首页(用户实测)。
|
||
- **修法两层,缺一不可**:
|
||
1. **入口判定**:`App.vue onLaunch` 用 `option.path` 置 `globalData.restoreArmed` ——
|
||
只有 `pages/index/index` / `pages/guide/index` 且无 `query.url` 才为 true;首页壳 `onLoad` 一次性消费。
|
||
(用 path 不用时间窗:引导页可能停很久看开屏广告。)
|
||
2. **抑制 H5 兜底**:这类「明确不恢复」的首页加载,以及显式落地/恢复后的返回落点,
|
||
`loadMamaBox(true)` / `_loadWithCachedIdentity` / `onShow` 重载 / `retryLoad` **都要带 `#norestore=1`**,
|
||
否则 mama-box 的兜底恢复会绕开小程序判定又把用户送走。
|
||
- 反向:小程序**尝试恢复但取数失败/超时**时**不带** `norestore`,让 mama-box 兜底重试一次。
|
||
|
||
### 4.3 回到首页要清 last_app(两侧互补,2026-09-22)
|
||
|
||
现象:进入 H5 应用 → **返回**回到 mama-box 首页 → 再冷启动进小程序,又被送回刚退出的应用。
|
||
记录的是「最后进入的 H5 应用」,人已经回到首页了 → 必须清掉。清除动作分两侧,互补覆盖:
|
||
|
||
| 路径 | 谁清 | 判定 |
|
||
| --- | --- | --- |
|
||
| H5 应用自己的返回(`history.back()` / 后退重载) | **mama-box** `clearLastAppOnBackHome()` | `pageshow.persisted` / `navigation.type === 'back_forward'` |
|
||
| H5 应用以 `location.href` 回 mama-box | **mama-box**(同上) | `document.referrer` 是 `https://mp.yizhezhe.com/<app>/` |
|
||
| 容器页**返回箭头**回首页(host 重载,不带 referrer) | **首页壳** `_clearLastApp()` | `onShow` 决定重载回 mama-box 时(`reloadBase` 命中 `/mama-box`) |
|
||
|
||
- 接口:`DELETE /api/mp/public/last-app?openid=`(幂等、fail-soft)。
|
||
- ⚠️ **绝不能改成「mama-box 每次加载都清」**:恢复流程会把 mama-box 当**返回落点**加载,
|
||
误清会毁掉刚用上的记录(实测:普通加载与 `#norestore=1` 落点加载都不清)。
|
||
- ⚠️ 首页壳只在**重载目标是 mama-box** 时清;重载目标是某个应用(`shareHref` 指向它)时不能清。
|
||
|
||
### 4.4 独立授权页:只采集手机号,再按库里资料决定是否问复用(2026-09-22 改版)
|
||
|
||
页面交互按 `box_app/design/授权流程演示/index.html` 重做,链路与契约(`backUrl` / `__AUTH_RESULT__` 形状 /
|
||
宿主 `consumeAuthResult`)**都没变**,只改了页内流程:
|
||
|
||
```
|
||
S0 半弹窗问手机号 ─「否」→ 直接 navigateBack(不写结果)
|
||
└「是,授权手机号」= <button open-type="getPhoneNumber"> 本身 → 拿 code
|
||
→ POST /api/mp/public/auth/profile?code=… 落库手机号
|
||
→ GET /api/mp/public/auth/profile?openid=… 查库(判分支、取值展示)
|
||
├ 库里有昵称/头像 → 问「是否复用」→ 是/否都收尾返回
|
||
└ 库里没有 → **不追问、不给表单**,直接收尾返回
|
||
收尾 = 写 __AUTH_RESULT__(手机号 + 库里真实值) → navigateBack
|
||
```
|
||
|
||
五个必须守住的点(前三条照抄演示页 README 的告诫):
|
||
|
||
1. **「是,授权手机号」按钮本身必须是 `open-type="getPhoneNumber"`** —— 手机号没有可编程调起的 API,
|
||
不能「先点按钮 → 关弹窗 → 代码调起」。
|
||
2. **手机号落库的成功判据是 `code===200 && result.phone` 非空**(后端解密失败会如实返回 400)。不满足就
|
||
**留在手机号弹窗**报错,绝不写 `__AUTH_RESULT__`、绝不提示成功 —— 假成功会让 H5 闸门拦截 → 用户反复授权
|
||
反复被拦(2026-09-18 死循环事故)。
|
||
3. **判「库里是否已有资料」用 `!!(userName || avatar)`,不能用「记录是否存在」**:`GET/POST auth/profile`
|
||
对任何 openid 都返回 200 并现场建一条空记录,用存在性判断会 100% 命中「老用户」分支。
|
||
4. **查库判分支必须走 `GET`,不能拿手机号那次 `POST` 的返回凑**(2026-09-22 修):后端的
|
||
「先取后授权」兜底回填(本地 → 上游用户中心 → `CrmebService.user_info_by_openid` 同步 CRMEB 商城)
|
||
**只在 `get_user_auth`(GET)里做,`apply_auth`(POST)只 find_one/建空记录**。用户只在商城
|
||
有昵称/头像时,POST 返回仍是空 → 会误判成新用户,白丢「是否复用」那一屏。
|
||
⚠️ 顺带明确:这一屏的昵称/头像**不是**当场从微信取的(`getUserProfile` 已废弃,昵称/头像只能由
|
||
用户手势触发 `input type=nickname` / `chooseAvatar`),展示的永远是**库里已存过的值**。
|
||
5. **本页只采集手机号**(2026-09-22 产品定案):库里没有昵称/头像时**不追问、不给表单**,
|
||
直接收尾返回 —— 所以 `chooseAvatar` / `input type=nickname` / `/api/upload` 这条采集链
|
||
已从本页移除,昵称/头像改由小程序其它流程 / 上游用户中心 / 商城同步维护。
|
||
⚠️ 由此:`get_user_auth`(GET) 里读 CRMEB 商城用户表回填 `nickname`/`avatar` 的兜底
|
||
(`CrmebService.user_info_by_openid`,命中标 `fromCRMEB=true`)**必须保留**(2026-09-22 确认)——
|
||
它是头像目前**唯一稳定来源**(上游用户中心只有 `nickname`、没有 avatar,本页又不再采集)。
|
||
去掉它 = 头像恒空、「是否复用」那一屏基本不再出现、H5 侧展示一起变空。
|
||
口径:H5 侧仍只读 box_app(不直连商城);box_app 对 H5 是上游、对商城是下游。
|
||
|
||
其它:手机号落库与随后的昵称头像**解耦**(所以「不同步直接返回」不会丢手机号);「复用」是普通 button
|
||
(不调微信接口),会再 POST 一次把 `authorized` 置位;工具宫格数据来自 `GET /api/mp/apps`
|
||
(**仅展示、不可点击**,未授权时右上角显示小锁),拉取失败只是宫格留空、不阻塞授权。
|
||
|
||
⚠️ **CSS 陷阱(2026-09-22 真机截图返工)**:页面顶部有条 `button` 全局 reset
|
||
(`.auth-page button { padding:0; border:none; … }`,特异性 (0,1,1),scoped 编译后约 (0,2,1)),
|
||
它**高于**单类选择器 `.bsec` / `.bmain` / `.cta`((0,1,0))。所以按钮自己的 `padding` / `border`
|
||
规则必须写成 `.auth-page .bsec { … }` 这样带前缀提权,否则会被 reset 清成 0 ——
|
||
表现是「否」没有边框只剩文字、所有按钮高度塌成一行字高。同理 `.bwx` 这种只改底色的变体
|
||
要写成 `.auth-page .bmain.bwx`,否则会被同级的 `.auth-page .bmain` 底色盖掉。
|
||
|
||
### 4.5 授权页宫格的内容风险(未决)
|
||
|
||
宫格按演示页照搬了「已有 36.5万+ 位妈妈在用」+ 三个内联 SVG 插画头像(`SOCIAL_COUNT` 常量),
|
||
以及「全部免费 / 换手机不丢」等承诺 —— **演示页 README 明确标注这些是占位数据**,
|
||
上线前必须核实真实数字并取得产品/合规确认,否则涉嫌虚假宣传。
|
||
|
||
## 5. 本仓库状态提醒
|
||
|
||
- ⚠️ `mixins/shareCard.js` **未被 git 跟踪**(untracked),但两个宿主页都 import 它 —— 提交时别漏,否则一旦丢工作区这份文件就没了。
|
||
- `pages/index/index.vue.origin-bak`(及若干 `.bak_*`)是原装修首页/历史备份,**别当改动目标**,也别删(回退时要用)。
|
||
- 仓库里有多次会话遗留的未提交改动(`manifest.json` / `pages.json` / `pages/index/mall.vue` 等),动手前先 `git status` 分清哪些是本次的。
|
||
|
||
## 6. 自测
|
||
|
||
微信开发者工具(HBuilderX 运行到小程序)。**Console 过滤 `[enter-app]`** 可看到完整判定链
|
||
(入口路径 / restoreArmed / last-app 请求与响应 / 是否命中 / 热启动判定 / 容器页加载与上报):
|
||
1. **冷启动**:点「编译」→ 有 last_app 记录时应直接进上次的 H5 应用(不经过首页)。
|
||
2. **热启动**:点工具栏「切后台」再「切前台」→ 应停在原 H5,**不应重载回首页**。
|
||
3. **扫码入口**:编译模式选「通过二维码编译」(或扫太阳码)→ 进应用后点左上角「小房子」→ **应停在首页**,
|
||
不应被送回应用、也不应出现返回箭头。
|
||
4. 无记录 / 断网冷启动 → 最多约 1.5s 后照常进首页。
|
||
5. 授权页返回、分享卡片、商城宫格进应用各走一遍。
|
||
接口契约与后端验证清单见 `box_app/AGENTS.md` §5、§6。
|