Files
crmeb-mp/AGENTS.md
Karlcarlo b45a190a59 chore: 入库生产基线(2026-10-26 工作树快照)
- 含首页壳/容器页/授权页/分享图/首页配置等此前未入库的在途改动
- 新增未跟踪的 pages/index/mall.vue(第 5 个 tabBar 页,pages.json 已引用)
- 排除本地备份文件(*.bak* / *.origin-bak)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 17:07:49 +08:00

162 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 remote,本仓库是纯本地库**) |
| 版本 | `manifest.json` 的 `versionName` / `versionCode` |
## 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。