docs: 新增 vr_ticket openid 问题与主题机制调研文档

- docs/WECHAT_OPENID_ERROR_REPORT.md: vr_ticket 购票流程 openid 未找到 Bug 排错报告

- docs/theme_research.md: 服务端配置联动 & 主题/配色机制 调研报告
master
Council 2026-07-06 11:29:35 +08:00
parent 80ea6bfdef
commit eb45c977d6
2 changed files with 327 additions and 0 deletions

View File

@ -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
// 优先从登录缓存读 openidShopXO 存的是 '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` |

219
docs/theme_research.md Normal file
View File

@ -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 字符串,挂到 `<view :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 L6L170](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 L149L166](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 L1558L1627](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L1558-L1627) | `init_config(num, object, method, params)`:先读本地缓存 → POST `/common/base` 拿全量 → 写入缓存 → 回调 |
| 通用读配置 | [App.vue L1509L1549](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 L1630L1650](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 L2762L2780](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L2762-L2780) | `get_theme_value / set_theme_value`:读写 `uni.storage.theme` |
| 主题色值表 | [App.vue L2725L2755](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L2725-L2755) | `get_theme_color(theme, is_light)`:返回主色 / 浅主色 hex |
| 主题 class | [App.vue L2758L2760](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L2758-L2760) | `get_theme_value_view()``theme-red` |
| Tabbar 主题化 | [App.vue L2783L2837](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L2783-L2837) | `set_tabbar(theme)``uni.setTabBarStyle` + `uni.setTabBarItem` 切换选中色/图标 |
| 静态资源主题路径 | [App.vue L2029L2050](file:///Users/bigemon/WorkSpace/vr-shopxo-uniapp/App.vue#L2029-L2050) | `get_static_url(type, is_plugins)`:返回 `static/app/{theme}/{type}/` |
| 主题 CSS8 套) | [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: <view :class="theme_view"> 触发 .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` / ... 字符串,绑定到根 `<view :class>`
- 自动应用 `theme.css` 里对应主题块的所有工具类。
### 3. `app.globalData.get_theme_color(theme?, is_light?)`
- 返回主题主色 hex 或浅主色 hex。
- 用于 `:color="theme_color"`、`<iconfont :color="theme_color">`、`<checkbox :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` L398L399 | 启动时把当前主题回传给服务端,方便服务端按主题返回差异化数据(如 banner | — |
> ⚠️ 现有实现默认假定后端会把主题字符串放在 `plugins_themestyle_data`**新增 VR 系列组件如果要"自带主题"**(例如 VR 票券背景强制主题色),也建议沿用同一个 key服务端不改VR 组件只需要在 data() 里多写两个字段 `theme_view / theme_color`,零成本接入。
---
## 六、页面侧固定模式("主题消费模板"
所有 40 个 `pages/plugins/**` 页面、首页、购物车、商品详情等都遵循完全相同的模式:
```vue
<template>
<view :class="theme_view + ' page-fade-in'">
<!-- iconfont 等需要动态取色的 -->
<iconfont name="icon-add" :color="theme_color" />
<!-- class 自动换肤 -->
<view class="bg-main cr-main-light">按钮</view>
</view>
</template>
<script>
const app = getApp();
export default {
data() {
return {
theme_view: app.globalData.get_theme_value_view(),
theme_color: app.globalData.get_theme_color(),
};
},
onShow() {
// 极少数页面需要响应主题变化(切换主题/后台切回时)
this.setData({
theme_view: app.globalData.get_theme_value_view(),
theme_color: app.globalData.get_theme_color(),
});
}
}
</script>
```
`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 组件/页面:
- 在最外层 `<view :class="theme_view + ' page-fade-in'">`,享受 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"`,推荐放在页面最外层 `<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 L2809L2835](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` 的典型页面