15 KiB
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 上报 |
pages/annex/link_view/index.vue |
链接型壳页(2026-11-04):链接系统 / 福利系统的小程序码落地。scene=l=<slug> / f=<slug> 或转发 ?t=&k= → 按硬编码的 TARGETS 拼 wenda.douba.top/s/、fuli.douba.top/a/ 用 web-view 打开;不注入身份、不引 mixin;解析不出来走兜底屏(见 §3.9) |
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. 硬约定
- 两个宿主页必须共用 mixin:
mixins/webviewHost.js+mixins/shareCard.js。历史上两页各写一份导致能力分叉(首页壳缺 copyLink / 授权回传 / unionid),别再分叉。 - 身份注入走 query、授权结果走 hash:容器页
_applyIdentity()负责按当前 openid 重写 URL(含_v=缓存击穿);授权回传只走 hash(#openid=&phone=&nickname=&avatar=)——微信 web-view 首次加载丢 query、不丢 hash。 - 拿不到 openid 就不加载 H5(fail-closed):重试 → 兜底屏 → 手动重试;不要匿名加载(H5 会进访客态,反而更难排查)。
- 分享深链必须
stripAuthParams():别把发送者的openid/unionid/t泄露给接收者;接收者由容器页按自己身份重新注入。 - 分享图/标题按
onShareAppMessage(options).webViewUrl当场解析:postMessage 是延迟投递,首页壳又不随 H5 换应用 onLoad,沿用上次结果会「B 应用带 A 应用的图」。 - tabBar 是全局状态:首页壳必须
uni.hideTabBar()(web-view 盖不住原生条,H5 自带底栏会双条);严禁在商城页主动showTabBar配对 —— 会与 DIY 底栏同显成双 tabBar。 - 恢复/落地统一
navigateTo容器页,并保留首页壳作为返回落点;不要直接把首页壳的 web-view src 指向第三方应用(首页壳的buildWebUrl是 hash 注入,会破坏自带 hash 路由的应用)。 - 只用
options.url当显式落地判据:options.scene每次冷启动都有值(1001 最近使用 / 1007 分享 / 1011 扫码…),拿它判落地会导致恢复永远不执行。 - 链接型壳页
pages/annex/link_view的三条同步契约(2026-11-04,方案见 box_appdocs/lianjie-wxacode-plan.md): ① 页内TARGETS(短键 / 地址前缀 / 标识正则)必须与 box_appbackend/app/services/link_wxacode_serv.py的同名表逐字一致,改表 = 两端一起改 + 小程序发版; ② 页面路径必须等于 box_app 生产.env的LINK_WXACODE_PAGE(默认pages/annex/link_view/index),否则已生成的码全部扫不进页面; ③ 该页故意不注入 openid/unionid(两家 H5 走自己的公众号网页授权),所以不要往里加webviewHost/shareCard,也不要往 URL 上带身份参数。wenda.douba.top、fuli.douba.top、dbh5.ywsz.com.cn三个域名必须在微信后台「业务域名」里,体验版真机同样校验(漏一个 = 部分活动白屏)。
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了一层容器页,左上角变成返回箭头,得再点一次返回才真回首页(用户实测)。
- 修法两层,缺一不可:
- 入口判定:
App.vue onLaunch用option.path置globalData.restoreArmed—— 只有pages/index/index/pages/guide/index且无query.url才为 true;首页壳onLoad一次性消费。 (用 path 不用时间窗:引导页可能停很久看开屏广告。) - 抑制 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 的告诫):
- 「是,授权手机号」按钮本身必须是
open-type="getPhoneNumber"—— 手机号没有可编程调起的 API, 不能「先点按钮 → 关弹窗 → 代码调起」。 - 手机号落库的成功判据是
code===200 && result.phone非空(后端解密失败会如实返回 400)。不满足就 留在手机号弹窗报错,绝不写__AUTH_RESULT__、绝不提示成功 —— 假成功会让 H5 闸门拦截 → 用户反复授权 反复被拦(2026-09-18 死循环事故)。 - 判「库里是否已有资料」用
!!(userName || avatar),不能用「记录是否存在」:GET/POST auth/profile对任何 openid 都返回 200 并现场建一条空记录,用存在性判断会 100% 命中「老用户」分支。 - 查库判分支必须走
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),展示的永远是库里已存过的值。 - 本页只采集手机号(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 请求与响应 / 是否命中 / 热启动判定 / 容器页加载与上报):
- 冷启动:点「编译」→ 有 last_app 记录时应直接进上次的 H5 应用(不经过首页)。
- 热启动:点工具栏「切后台」再「切前台」→ 应停在原 H5,不应重载回首页。
- 扫码入口:编译模式选「通过二维码编译」(或扫太阳码)→ 进应用后点左上角「小房子」→ 应停在首页, 不应被送回应用、也不应出现返回箭头。
- 无记录 / 断网冷启动 → 最多约 1.5s 后照常进首页。
- 授权页返回、分享卡片、商城宫格进应用各走一遍。
接口契约与后端验证清单见
box_app/AGENTS.md§5、§6。