diff --git a/docs/WECHAT_OPENID_ERROR_REPORT.md b/docs/WECHAT_OPENID_ERROR_REPORT.md new file mode 100644 index 00000000..4c6f0500 --- /dev/null +++ b/docs/WECHAT_OPENID_ERROR_REPORT.md @@ -0,0 +1,108 @@ +# ShopXO vr_ticket 购票流程 "openid 未找到" 问题分析报告 + +> 更新:2026-07-02 — 已锁定真实根因,非标准 ShopXO 问题,是 vr_ticket 二次开发的 Bug + +--- + +## 📋 完整出错调用链路(vr_ticket 购票 → 一键获取手机号) + +``` +用户点击「一键获取手机号」 + → onGetPhoneNumber(e) + → uni.request POST /user/onekeyusermobilebind + { code, encryptedData, iv, openid: user['weixin_openid'] || '' } ← openid 是空字符串! + → AppMiniUserService::AppMiniOnekeyUserMobileBind() + → AppMiniOnekeyUserMobileDecrypt() + → ParamsChecked(openid) ← 校验失败! + → 返回错误: "openid 未找到" +``` + +--- + +## 🔴 根因(Bug 定位) + +**文件:** `pages/goods-vr-ticket-order/goods-vr-ticket-order.vue` 第 553-554 行 + +```javascript +const user = this.app.globalData.get_login_cache_info() || {}; +const clientType = this.app.globalData.application_client_type(); // → 'weixin' +... +openid: user[clientType + '_openid'] || '', +// 'weixin' + '_openid' = 'weixin_openid' +// 但 cache_user_login_key 里存的是 'openid'(无前缀) +// → undefined → '' → 后端校验失败 +``` + +**三段论:** + +| 层级 | 实际情况 | +|------|---------| +| 前端读取的缓存 | `cache_user_login_key`(通过 `get_login_cache_info()`) | +| 该缓存存储的字段 | `openid`(来自 ShopXO 后端返回的原始字段名) | +| 前端构造的字段名 | `user['weixin_openid']` = `undefined` → `''` | +| 后端校验 | `openid` 不能为空 → **报错 "openid 未找到"** | + +### 为什么 `cache_user_login_key` 里存的是 `openid` 而不是 `weixin_openid`? + +ShopXO 后端 `WeixinUserAuth` [AppMiniUserService.php:212] 的返回结构: +```php +// WeixinUserAuth 返回的是 $ret['data'],其中 'openid' 来自微信原始响应 +// 数据库存的是 weixin_openid,但 API 返回的字段名是 'openid' +$ret = UserService::AuthUserProgram($params, 'weixin_openid'); +// ↑ 存库用的字段 ↑ 返回用的字段 +// $ret['data']['openid'] = 微信原始 openid +``` + +所以 `cache_user_login_key` 存的是 `{ ..., openid: '微信原始openid', ... }`, +前端用 `user['weixin_openid']` 去取 → 取不到 → 空字符串 → 报错。 + +--- + +## 🔧 修复方案 + +### 修改文件:`pages/goods-vr-ticket-order/goods-vr-ticket-order.vue` + +**第 553 行**,把: +```javascript +const user = this.app.globalData.get_login_cache_info() || {}; +const clientType = this.app.globalData.application_client_type(); +... +openid: user[clientType + '_openid'] || '', +``` + +改为: +```javascript +// 优先从登录缓存读 openid(ShopXO 存的是 'openid' 无前缀) +const loginData = this.app.globalData.get_login_cache_info() || {}; +// 备用:从用户信息缓存读 weixin_openid(已登录用户) +const userData = this.app.globalData.get_user_cache_info() || {}; +const openid = loginData.openid || userData.weixin_openid || ''; + +// 没有 openid → 提示用户先完成微信登录 +if (!openid) { + this.app.globalData.showToast('请先完成微信登录后再授权手机号'); + return; +} + +uni.request({ + ... + openid: openid, + ... +}); +``` + +### 可选:后端放宽 openid 校验(针对微信小程序) + +`AppMiniUserService.php` 的 `AppMiniOnekeyUserMobileDecrypt` 中,微信分支其实只需要 `code` 就能换手机号,`openid` 只用于用户关联。如果想解耦,可以让微信分支的 `openid` 校验变为 warn 级别而非 error——但这需要改动 ShopXO 核心代码,不推荐。 + +--- + +## 📁 相关代码文件索引 + +| 文件 | 作用 | +|------|------| +| `vr-shopxo-uniapp/pages/goods-vr-ticket-order/goods-vr-ticket-order.vue:553` | **Bug 所在**:错误读取缓存字段 | +| `vr-shopxo-plugin/shopxo/app/service/AppMiniUserService.php:824` | 后端 `AppMiniOnekeyUserMobileDecrypt` openid 校验 | +| `vr-shopxo-plugin/shopxo/app/service/AppMiniUserService.php:212` | `WeixinUserAuth` 返回字段分析 | +| `vr-shopxo-uniapp/App.vue:671` | `get_login_cache_info()` 读取 `cache_user_login_key` | +| `vr-shopxo-uniapp/App.vue:639` | `get_user_cache_info()` 读取 `cache_user_info_key` | diff --git a/docs/theme_research.md b/docs/theme_research.md new file mode 100644 index 00000000..7a416f84 --- /dev/null +++ b/docs/theme_research.md @@ -0,0 +1,219 @@ +# vr-shopxo-uniapp —— 服务端配置联动 & 主题/配色机制 调研报告 + +> 调研对象:`/Users/bigemon/WorkSpace/vr-shopxo-uniapp`(uniapp + 多端 shopxo 客户端) +> 目的:在为后续新增 VR 系列组件/页面提供可复用机制之前,把现有的「服务端下发 → 主题/配色 → 页面消费」整条链路说清楚。 + +--- + +## 一、TL;DR + +项目使用「**本地默认值 + 服务端一次下发 + 本地缓存**」三段式做配置联动,主题/配色只是这套机制里的一个特殊子集: + +1. 客户端本地写死默认值(`App.vue globalData.data`,例如 `default_theme: 'red'`、`default_language: 'zh'`、`currency_symbol`、`static_url` 等),保证首屏可立即渲染。 +2. 启动时调用 `init_config()` 远程拉取 `/api.php?s=common/base` 接口的完整配置 JSON,落盘 `uni.setStorageSync('cache_config_info_key', data)`。 +3. 各业务模块通过 `app.globalData.get_config('a.b.c')` 从本地缓存读字段,多级 key 用 `.` 分割。 +4. 主题是其中最特殊的一个:**它在响应结果中以 `plugins_themestyle_data` 字符串下发**(如 `red/yellow/...`),落地后由 `App.vue` 把它写到 `uni.storage.theme`,并触发 `set_tabbar(theme)` 重绘原生 tabbar。 +5. 页面通过两个全局 helper 消费主题: + - `get_theme_value_view()` → 返回 `theme-red` 这种 class 字符串,挂到 `` 的最外层; + - `get_theme_color()` → 返回该主题的主色/浅主色 hex,喂给 `:color="theme_color"` 这类需要动态取色的组件。 +6. CSS 端则用 `common/css/theme.css` 里为 8 套主题(`red/yellow/black/green/orange/blue/brown/purple`)预制的工具类(`.bg-main / .cr-main / .br-main / .bg-main-light / .cr-main-light` ...)按主题自动换肤。 +7. 静态图片资源也是按主题分目录组织:`static/images/{theme}/...`,tabbar 图、插件业务图都按主题选;插件专用主题资源走 `get_static_url(type, is_plugins)` 拼成 `static/app/{theme}/{type}/`。 + +--- + +## 二、源码位置(全部已验证存在) + +| 角色 | 位置 | 说明 | +|---|---|---| +| 默认配置中心 | [App.vue L6–L170](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L6-L170) | `globalData.data` 中的所有默认值:`request_url`、`static_url`、`default_theme`、`default_language`、`currency_symbol`、`system_tabbar` 等 | +| 缓存 key 常量 | [App.vue L149–L166](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L149-L166) | `cache_config_info_key: 'cache_config_info_key'` 等 | +| 启动入口 | [App.vue L3397](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L3397) | `this.globalData.init_config()` 在 `onLaunch` 调用 | +| 远程拉取实现 | [App.vue L1558–L1627](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L1558-L1627) | `init_config(num, object, method, params)`:先读本地缓存 → POST `/common/base` 拿全量 → 写入缓存 → 回调 | +| 通用读配置 | [App.vue L1509–L1549](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L1509-L1549) | `get_config(key, default_value)`:支持 `a.b.c` 多级 key,缺失回落 `default_value` 或 `globalData` 默认值 | +| 主题落地 | [App.vue L1630–L1650](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L1630-L1650) | `init_config_result_handle`:调用 `set_theme_value(data.plugins_themestyle_data)` 与 `set_tabbar(...)` | +| 主题读/写 | [App.vue L2762–L2780](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L2762-L2780) | `get_theme_value / set_theme_value`:读写 `uni.storage.theme` | +| 主题色值表 | [App.vue L2725–L2755](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L2725-L2755) | `get_theme_color(theme, is_light)`:返回主色 / 浅主色 hex | +| 主题 class | [App.vue L2758–L2760](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L2758-L2760) | `get_theme_value_view()` → `theme-red` | +| Tabbar 主题化 | [App.vue L2783–L2837](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L2783-L2837) | `set_tabbar(theme)`:`uni.setTabBarStyle` + `uni.setTabBarItem` 切换选中色/图标 | +| 静态资源主题路径 | [App.vue L2029–L2050](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L2029-L2050) | `get_static_url(type, is_plugins)`:返回 `static/app/{theme}/{type}/` | +| 主题 CSS(8 套) | [common/css/theme.css](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/common/css/theme.css) | 每套主题约 80 行工具类:`bg-main / bg-main-light / bg-main-pair / cr-* / br-* / .nav-active-line` ... | +| 主题图片资源 | `static/images/{black\|blue\|brown\|green\|orange\|purple\|red\|yellow}/` | tabbar 图、按钮背景等按主题分目录 | +| 接入示例页面 | `pages/{index,cart,goods-category,...}/...`、`pages/plugins/**` | 全部使用 `theme_view: app.globalData.get_theme_value_view()` + `theme_color: app.globalData.get_theme_color()` 模式 | +| VR 钱包插件 | `pages/plugins/vr-ticket-wallet/` | 已存在的 VR 系列插件,结构上跟主题机制完全契合 | + +--- + +## 三、完整执行链路(可视化) + +```mermaid +sequenceDiagram + autonumber + participant App as App.vue onLaunch + participant Cache as uni.storage + participant API as 服务端 /api.php?s=common/base + participant Page as 业务页面 / 组件 + participant CSS as common/css/theme.css + + Note over App: 首屏立即使用 globalData.data 默认值渲染 + App->>App: init_config() + App->>Cache: getStorageSync(cache_config_info_key) + alt 有缓存(上次启动留下的) + App->>App: init_config_result_handle(cache) + end + App->>API: POST common/base (is_key=1) + API-->>App: { code:0, data:{ plugins_themestyle_data, config.*, plugins_*, ... } } + App->>Cache: setStorageSync(cache_config_info_key, data) + App->>App: init_config_result_handle(data) + App->>Cache: setStorageSync('theme', data.plugins_themestyle_data) + App->>App: set_tabbar(theme) — uni.setTabBarStyle/Item + App-->>Page: 通知 common_data_init_config_back_list 回调 + Page->>Cache: get_config('xxx') / get_theme_color() / get_theme_value_view() + Page->>Page: data() 重新计算 theme_view/theme_color + Page->>CSS: 触发 .theme-red .bg-main 等规则 +``` + +--- + +## 四、关键 API 一览(VR 组件直接复用即可) + +### 1. `app.globalData.get_config(key, default_value?)` +```js +// 用法 +app.globalData.get_config('config.home_site_name', '默认名') +app.globalData.get_config('plugins_themestyle_data') // → 'red' +app.globalData.get_config('plugins_base.thirdpartylogin.data') +``` +- 数据源:`uni.storage['cache_config_info_key']` +- 支持 `.` 分级 key,命中失败会回落到 `default_value` 或 `globalData.data` 同名字段(如 `currency_symbol`)。 + +### 2. `app.globalData.get_theme_value_view()` +- 返回 `theme-red` / `theme-yellow` / ... 字符串,绑定到根 ``。 +- 自动应用 `theme.css` 里对应主题块的所有工具类。 + +### 3. `app.globalData.get_theme_color(theme?, is_light?)` +- 返回主题主色 hex 或浅主色 hex。 +- 用于 `:color="theme_color"`、``、`` 等不能走 class 的场景。 +- 注意:**这是**「代码内 hardcode 的 8 套色值表」**与 `theme.css` 里的色值保持一致**,所以两套数据源要同时改(这是一个已知的耦合点,见下)。 + +### 4. `app.globalData.get_static_url(type, is_plugins)` +- 返回 `static_url + static/app/{theme}/{type}/` 或 `static_url + static/{theme}/{type}/` +- VR 组件需要主题化背景图时,直接 `image :src="app.globalData.get_static_url('vr-banner')"` 即可。 + +--- + +## 五、对服务端字段的依赖约定(要清楚) + +| 字段 | 出现位置 | 含义 | 默认值来源 | +|---|---|---|---| +| `plugins_themestyle_data` | 服务端下发的最外层字符串 | 主题名(8 选 1) | `App.vue` `default_theme = 'red'` | +| `config.*` | 站点设置(站点名、logo、协议 URL 等) | 站点级公共配置 | `App.vue` `application_title`、`home_site_*` | +| `plugins_*` | 插件配置 | 各业务插件开关/数据 | 各插件 `App.vue` 内的 hardcode | +| `is_key=1` | `common/base` 请求参数 | 服务端只返回 key 不返回大字段(精简包体) | — | +| 请求 URL 参数 `&theme=red` | `App.vue` L398–L399 | 启动时把当前主题回传给服务端,方便服务端按主题返回差异化数据(如 banner) | — | + +> ⚠️ 现有实现默认假定后端会把主题字符串放在 `plugins_themestyle_data`,**新增 VR 系列组件如果要"自带主题"**(例如 VR 票券背景强制主题色),也建议沿用同一个 key,服务端不改;VR 组件只需要在 data() 里多写两个字段 `theme_view / theme_color`,零成本接入。 + +--- + +## 六、页面侧固定模式("主题消费模板") + +所有 40 个 `pages/plugins/**` 页面、首页、购物车、商品详情等都遵循完全相同的模式: + +```vue + + + +``` + +`common/css/theme.css` 在 `App.vue` 顶部通过 `@import './common/css/theme.css';`([App.vue L3426](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L3426))全局生效。 + +--- + +## 七、给后续 VR 系列组件/页面的扩展建议 + +> 既然现有机制已经够用,**不要新增第二套主题系统**,否则会出现「同一页同时存在两种主题类名」的混乱。统一复用现有 8 套即可。 + +### 1. 接入"主题感知"成本极低(推荐做法) + +对每个新增 VR 组件/页面: + +- 在最外层 ``,享受 class 换肤。 +- 在 `data()` 里加 `theme_view` 与 `theme_color` 两字段,从 `app.globalData.get_theme_*()` 取值。 +- 需要用图片的地方,资源放 `static/app/{theme}/vr-xxx/`,组件内用 `get_static_url('vr-xxx')` 拼路径。 +- 这样**不需要改 App.vue、不需要改 CSS**,服务端也无需额外配置。 + +### 2. 给"VR 主题"做差异化时(可选进阶) + +如果某些 VR 场景需要「**主题色 + 品牌专属渐变**」叠加,建议两步: + +- **服务端扩展字段**:在 `common/base` 返回里新增 `plugins_vr_theme_extras`(或在 `plugins_*` 下挂一个 VR 专属段),下发一组 hex 或图片 URL。 +- **客户端扩展 helper**:在 `App.vue` 里新增 `get_vr_theme_assets(name)` 方法,从 `get_config('plugins_vr_theme_extras.xxx')` 取值。 +- **CSS 不动**:所有新视觉差异都走「行内样式 / 数据驱动」,避免污染 `theme.css`。 + +### 3. 几个需要避坑的点 + +| 坑 | 说明 | 解法 | +|---|---|---| +| 主题色双源 | `App.vue get_theme_color()` 的 hex 表与 `theme.css` 里的 hex 重复 | 加新主题时**两处都要改**,建议改为「`theme.css` 单一来源,JS 通过 `getComputedStyle` 读 `--main-color`」——但这是大重构,需单独评估 | +| 主题切换不广播 | 当前没有事件总线,`set_theme_value` 后页面需要自己 `onShow` 重读 | 新增 VR 组件时若需要"运行中切主题也能立即生效",建议引入 `uni.$emit / uni.$on('theme-change', ...)`,在 `set_theme_value` 内 emit | +| 缓存时机 | 首屏渲染走默认值,远程配置到达后页面不会自动 re-render(数据写死到 `data()` 里) | VR 关键业务页面建议在 `onShow` 重读 `theme_view/theme_color`;列表类在 `onPullDownRefresh` 重新拉数据 | +| 离线/服务端挂 | `init_config` 已做 20 次重试 + 本地缓存 fallback([App.vue L1609](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L1609)) | VR 组件不要自己再发 `common/base` 请求,**直接调 `get_config` 即可** | + +### 4. 建议的最小 VR 主题规范(落地清单) + +如果要为 VR 系列另起一份"主题约定文档",建议覆盖: + +1. **页面骨架**:必含 `:class="theme_view"`,推荐放在页面最外层 ``。 +2. **取色**:动态色一律 `app.globalData.get_theme_color()`;静态色走 `theme.css` 工具类(`.bg-main` / `.cr-main` / `.bg-main-light` 等)。 +3. **图片**:主题相关图放 `static/app/{theme}/vr-.../`,通过 `get_static_url('vr-...')` 取;与主题无关的图(如商品主图)放 `static/images/common/`。 +4. **TabBar**:VR 系列新增 tab 项需要在 `App.vue` 的 `set_tabbar()` switch 里补一项 case(参考 [App.vue L2809–L2835](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L2809-L2835))。 +5. **服务端字段**:优先复用 `plugins_themestyle_data`,**不要**新增 `vr_theme` 等并行字段,否则主题永远只对一半生效。 +6. **离线降级**:依赖任何 `get_config('...')` 字段时都加默认参数;不要假设 `plugins_vr_xxx` 一定存在。 + +--- + +## 八、待用户确认的设计决策 + +> [!IMPORTANT] +> 是否要把"VR 主题"做成「复用 8 套主题」还是「新增第 9 套 VR 专属主题(vr-cyan / vr-purple-grad 之类)」? + +> [!IMPORTANT] +> 是否要在本次顺便把"主题色双源(JS hex 表 vs theme.css)"统一为 CSS 变量单一来源?工作量约 1 个工作日。 + +> [!TIP] +> 是否需要新增 `theme-change` 事件总线,让运行中切主题能即时通知所有页面? + +--- + +## 九、参考阅读清单 + +- [App.vue](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue) —— 配置中心 / 主题注册 / TabBar +- [common/css/theme.css](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/common/css/theme.css) —— 8 套主题的工具类 +- [static/images/](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/static/images) —— 按主题分目录的图片资源 +- [pages/plugins/vr-ticket-wallet/records/index.vue](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/pages/plugins/vr-ticket-wallet/records/index.vue) —— 已存在的 VR 插件页面(最佳参考模板) +- [pages/index/index.vue](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/pages/index/index.vue) —— 标准主题消费模板 +- [pages/buy/buy.vue](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/pages/buy/buy.vue) —— 同时使用 `theme_view` + `theme_color` 的典型页面