diff --git a/.claude/api-data-null-analysis.md b/.claude/api-data-null-analysis.md new file mode 100644 index 0000000..7916462 --- /dev/null +++ b/.claude/api-data-null-analysis.md @@ -0,0 +1,306 @@ +# API返回data为null问题深度分析报告 + +## 问题描述 +用户报告通过Postman请求所有API接口都返回 `"data": null`,即使代码中明确传递了数据给 `$this->success()` 方法。 + +## 分析过程 + +### 1. 架构分析 + +#### 控制器继承链 +``` +Ccblife extends Common extends Api +``` + +- **Ccblife.php**: `/Users/billy/Code/fengketrade.com/addons/shopro/controller/Ccblife.php` +- **Common.php**: `/Users/billy/Code/fengketrade.com/addons/shopro/controller/Common.php` +- **Api.php**: `/Users/billy/Code/fengketrade.com/application/common/controller/Api.php` + +#### success/error/result方法定义 + +**Api.php (第170-219行)**: +```php +protected function success($msg = '', $data = null, $code = 1, $type = null, array $header = []) +{ + $this->result($msg, $data, $code, $type, $header); +} + +protected function error($msg = '', $data = null, $code = 0, $type = null, array $header = []) +{ + $this->result($msg, $data, $code, $type, $header); +} + +protected function result($msg, $data = null, $code = 0, $type = null, array $header = []) +{ + $result = [ + 'code' => $code, + 'msg' => $msg, + 'time' => Request::instance()->server('REQUEST_TIME'), + 'data' => $data, + ]; + $type = $type ? : $this->responseType; + if (isset($header['statuscode'])) { + $code = $header['statuscode']; + unset($header['statuscode']); + } else { + $code = $code >= 1000 || $code < 200 ? 200 : $code; + } + $response = Response::create($result, $type, $code)->header($header); + throw new HttpResponseException($response); +} +``` + +**Common.php (第30-85行)**: +- success/error/result方法**已被注释**,从git历史看从未启用过 +- 因此Ccblife控制器使用的是Api基类的方法 + +### 2. 可能原因排查 + +#### ✅ 已排除的原因 + +1. **Request Filter机制** + - Api.php第102行: `$this->request->filter('trim,strip_tags,htmlspecialchars');` + - 检查ThinkPHP源码第1095行: `elseif (is_scalar($value))` + - **结论**: filter只处理标量值,不会处理数组/对象,不是问题根源 + +2. **全局响应钩子** + - 检查 `application/tags.php`: 无response相关钩子 + - 检查 `application/common/behavior/Common.php`: 无response处理 + - **结论**: 无全局钩子修改响应数据 + +3. **Response处理流程** + - 检查 `thinkphp/library/think/Response.php` + - 检查 `thinkphp/library/think/response/Json.php` + - **结论**: Response类正常进行json_encode,无数据篡改 + +4. **Common.php重写** + - Common.php中success/error/result方法已注释 + - **结论**: 使用的是Api基类的标准实现 + +### 3. 实际测试结果 + +#### 测试1: init接口(成功返回数据) +```bash +curl -X GET "http://fengketrade.test/addons/shopro/index/init" -H "platform: H5" +``` + +**响应**: +```json +{ + "code": 1, + "msg": "初始化", + "time": "1760930544", + "data": { + "app": { ... }, // 完整的数据对象 + "platform": { ... }, + "template": { ... }, + "chat": { ... } + } +} +``` + +**结论**: `/addons/shopro/index/init` 接口**data字段正常返回数据**! + +#### 测试2: decryptParam接口(返回data:null) +```bash +curl -X POST http://fengketrade.test/addons/shopro/ccblife/decryptParam \ + -H "Content-Type: application/json" \ + -d '{"ccbParamSJ":"test"}' +``` + +**响应**: +```json +{ + "code": 0, + "msg": "解密失败: ", + "time": "1760930527", + "data": null +} +``` + +**分析**: +- code=0 表示这是error响应 +- 触发代码: `Ccblife.php` 第354行 `$this->error('解密失败: ' . $e->getMessage());` +- **error()方法签名**: `error($msg = '', $data = null, $code = 0, ...)` +- **调用方式**: 只传了第一个参数$msg,第二个参数$data默认值就是null +- **结论**: 这是**预期行为**,错误响应时确实data为null + +#### 测试3: page接口(返回data:null) +```bash +curl -X GET "http://fengketrade.test/addons/shopro/index/page?id=1" +``` + +**响应**: +```json +{ + "code": 0, + "msg": "记录未找到", + "time": "1760930559", + "data": null +} +``` + +**分析**: +- code=0 表示error响应 +- 触发代码: `Index.php` 第116行 `$this->error(__('No Results were found'));` +- **结论**: 错误响应,data为null是**预期行为** + +### 4. Ccblife.php中的success调用检查 + +```php +// 第107行 - login方法 +$this->success(__('Logged in successful'), [ + 'token' => $token, + 'user_info' => $userInfo, + 'redirect_url' => $redirectUrl +]); + +// 第149行 - autoLogin方法 +$this->success('登录成功', [ + 'token' => $token, + 'user_id' => $userInfo['user_id'], + 'is_new_user' => $userInfo['is_new'], + 'userInfo' => $userInfo +]); + +// 第347行 - decryptParam方法 +$this->success('解密成功', $decryptedParams); +``` + +**结论**: 所有success调用**都正确传递了data参数**。 + +### 5. 问题根源推断 + +基于测试结果,我认为问题可能是: + +#### 可能性1: 用户测试的是错误场景 +- 用户可能测试的接口都触发了异常或错误条件 +- error()方法调用时很多地方只传了$msg参数,没有传$data +- **导致data为null是正常的错误响应行为** + +#### 可能性2: 参数传递问题 +- 用户Postman请求时可能缺少必要参数或token +- 导致接口执行异常分支,返回error响应 +- **需要检查请求头和请求体是否完整** + +#### 可能性3: 特定接口存在问题 +- 并非"所有接口"都返回data:null +- init接口已验证可正常返回data +- **需要用户明确具体哪些接口有问题** + +## 关键发现 + +### ✅ 正常工作的部分 +1. Api基类的success/error/result方法实现正确 +2. Response JSON序列化正常 +3. init接口验证可以正常返回完整data数据 +4. Ccblife.php中success方法调用语法正确 + +### ❓ 需要用户确认的信息 +1. 具体测试了哪几个API接口? +2. 这些接口的完整请求URL、Headers、Body是什么? +3. 是否有token认证? +4. 返回的完整JSON响应(包括msg和code字段)? +5. 是否所有接口返回的code都是0(错误)? + +### ⚠️ 潜在问题点 +1. **error调用缺少data参数** + - 在很多地方调用error()时只传了msg + - 如: `$this->error('错误信息');` + - 应该改为: `$this->error('错误信息', ['detail' => ...]);` + +2. **异常处理可能吞掉data** + ```php + // Ccblife.php 第113-115行 + } catch (\Exception $e) { + $this->error($e->getMessage()); // 只传msg,data为null + } + ``` + +## 修复建议 + +### 短期修复 +如果确实需要在error响应中也返回数据,修改所有error调用: + +```php +// 修改前 +$this->error('参数解密失败'); + +// 修改后 +$this->error('参数解密失败', [ + 'error_code' => 'DECRYPT_FAILED', + 'timestamp' => time() +]); +``` + +### 长期优化 +1. **统一异常处理** + ```php + } catch (\Exception $e) { + Log::error('错误: ' . $e->getMessage()); + $this->error('操作失败', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + } + ``` + +2. **规范化响应格式** + - success时data必须有值(至少是空数组[]) + - error时data可以包含错误详情 + - 避免所有error都返回data:null + +3. **增加调试日志** + ```php + protected function result($msg, $data = null, $code = 0, $type = null, array $header = []) + { + // 添加调试日志 + if (config('app_debug')) { + Log::debug('API Response', [ + 'msg' => $msg, + 'data' => $data, + 'code' => $code + ]); + } + + $result = [ + 'code' => $code, + 'msg' => $msg, + 'time' => Request::instance()->server('REQUEST_TIME'), + 'data' => $data, + ]; + // ... 后续代码 + } + ``` + +## 下一步行动 + +1. **用户需要提供**: + - 具体的API接口URL列表 + - 完整的Postman请求示例(包括Headers和Body) + - 完整的响应JSON(包括code和msg字段) + +2. **开发者需要检查**: + - 是否所有"data:null"的响应code都是0(错误响应)? + - 是否success响应(code=1)也出现data:null? + - 是否有日志记录显示data确实被构建了但最后变成null? + +3. **验证测试**: + - 测试已验证正常的init接口 + - 对比init和问题接口的差异 + - 使用相同的Postman配置测试两个接口 + +## 结论 + +目前**没有发现系统性的问题导致所有API的data都为null**。测试验证了: +- init接口可以正常返回完整data +- decryptParam和page接口返回data:null是因为触发了error分支 + +**强烈建议用户提供具体的问题接口和完整请求信息,以便进一步定位问题。** + +当前的data:null很可能是: +1. 错误响应的正常行为(code=0) +2. 调用error时未传data参数 +3. 请求参数不完整触发异常分支 + +而非框架层面的全局性问题。 diff --git a/public/ccblife-demo.html b/public/ccblife-demo.html index 098bbaf..edf2e23 100644 --- a/public/ccblife-demo.html +++ b/public/ccblife-demo.html @@ -194,10 +194,18 @@ 登录状态: 未登录 +
+ 自动登录: + 待检测 +
建行用户ID: -
+
+ 登录时间: + - +
建行参数: @@ -211,9 +219,10 @@ - + + - +
@@ -263,8 +272,157 @@ document.getElementById('bridge-status').innerHTML = '已就绪'; }); + + // 自动登录逻辑 + autoLoginOnLoad(); }; + // 页面加载时自动登录 + function autoLoginOnLoad() { + console.log('[自动登录] 开始检测...'); + + // 更新状态显示 + document.getElementById('auto-login-status').innerHTML = + '检测中...'; + + // 获取 URL 参数 + var params = CcbLifeBridge.getUrlParams(); + + // 检查是否有建行参数 + if (params.ccbParamSJ) { + console.log('[自动登录] 检测到 ccbParamSJ 参数'); + + document.getElementById('auto-login-status').innerHTML = + '正在执行...'; + + // 检查是否已经登录过(避免重复登录) + var token = localStorage.getItem('ccb_token'); + var loginTimestamp = localStorage.getItem('ccb_login_timestamp'); + var now = Date.now(); + + // 如果已登录且登录时间在30分钟内,跳过自动登录 + if (token && loginTimestamp && (now - parseInt(loginTimestamp)) < 30 * 60 * 1000) { + console.log('[自动登录] 已存在有效登录,跳过'); + document.getElementById('auto-login-status').innerHTML = + '已跳过(使用缓存)'; + showResult('✅ 检测到已登录状态\n\n使用缓存的 Token\n\n如需重新登录,请点击"测试登录入库"按钮'); + return; + } + + // 显示自动登录提示 + showResult('🔄 检测到建行参数,正在自动登录...\n\n1. 解密建行参数\n2. 验证用户信息\n3. 创建/更新用户\n4. 生成登录 Token'); + showLoading(true); + + // 延迟500ms执行,确保页面渲染完成 + setTimeout(function() { + executeAutoLogin(params.ccbParamSJ); + }, 500); + } else { + console.log('[自动登录] 未检测到 ccbParamSJ 参数'); + + document.getElementById('auto-login-status').innerHTML = + '未触发(无参数)'; + + // 检查本地缓存 + var cachedToken = localStorage.getItem('ccb_token'); + var cachedUserInfo = localStorage.getItem('ccb_user_info'); + + if (cachedToken && cachedUserInfo) { + try { + var user = JSON.parse(cachedUserInfo); + console.log('[自动登录] 使用缓存的登录信息'); + updateLoginStatus({ userInfo: user }); + showResult('✅ 使用缓存的登录状态\n\n用户信息:\n' + JSON.stringify(user, null, 2)); + } catch(e) { + console.error('[自动登录] 解析缓存失败', e); + } + } else { + console.log('[自动登录] 无缓存,显示未登录状态'); + } + } + } + + // 执行自动登录 + function executeAutoLogin(ccbParamSJ) { + console.log('[自动登录] 开始调用登录接口'); + + // 调用后端登录接口 + fetch('/addons/shopro/ccblife/login?ccbParamSJ=' + encodeURIComponent(ccbParamSJ), { + method: 'GET' + }) + .then(response => response.json()) + .then(data => { + showLoading(false); + + console.log('[自动登录] 接口返回:', data); + + if (data.code === 1) { + var loginData = data.data; + + // 保存 token 和用户信息 + localStorage.setItem('ccb_token', loginData.token); + localStorage.setItem('ccb_user_info', JSON.stringify(loginData.user_info)); + localStorage.setItem('ccb_login_timestamp', Date.now().toString()); + + // 更新自动登录状态 + document.getElementById('auto-login-status').innerHTML = + '成功'; + + // 更新登录时间 + document.getElementById('login-time').textContent = + new Date().toLocaleString('zh-CN', { hour12: false }); + + // 更新页面显示 + updateLoginStatus({ userInfo: loginData.user_info }); + + // 触发自定义事件 + window.dispatchEvent(new CustomEvent('ccb:login:success', { + detail: loginData + })); + + console.log('[自动登录] 登录成功!'); + + showResult('✅ 自动登录成功!\n\n' + + '用户昵称: ' + loginData.user_info.nickname + '\n' + + '手机号: ' + loginData.user_info.mobile + '\n' + + '建行用户ID: ' + loginData.user_info.ccb_user_id + '\n\n' + + 'Token: ' + loginData.token.substr(0, 20) + '...\n\n' + + '是否新用户: ' + (loginData.user_info.is_new ? '是' : '否') + '\n' + + '登录时间: ' + new Date().toLocaleString('zh-CN', { hour12: false }) + '\n\n' + + '跳转URL: ' + loginData.redirect_url + ); + + // 如果有重定向URL且不是默认首页,可以自动跳转(可选) + // if (loginData.redirect_url && loginData.redirect_url !== '/pages/index/index') { + // setTimeout(function() { + // window.location.href = loginData.redirect_url; + // }, 2000); + // } + + } else { + console.error('[自动登录] 登录失败:', data.msg); + + // 更新自动登录状态 + document.getElementById('auto-login-status').innerHTML = + '失败'; + + showResult('❌ 自动登录失败\n\n错误信息: ' + data.msg + '\n\n' + + (data.data ? '详细信息:\n' + JSON.stringify(data.data, null, 2) + '\n\n' : '') + + '请尝试手动点击"测试登录入库"按钮'); + } + }) + .catch(error => { + showLoading(false); + console.error('[自动登录] 请求异常:', error); + + // 更新自动登录状态 + document.getElementById('auto-login-status').innerHTML = + '异常'; + + showResult('❌ 自动登录请求失败\n\n' + error.message + '\n\n请检查网络连接或手动点击"测试登录入库"按钮'); + }); + } + // 更新环境状态 function updateEnvStatus() { var isInApp = CcbLifeBridge.isInCcbApp(); @@ -518,6 +676,31 @@ }, 5000); // 每5秒查询一次 } + // 强制自动登录(忽略缓存) + function forceAutoLogin() { + var params = CcbLifeBridge.getUrlParams(); + + if (!params.ccbParamSJ) { + showResult('错误: URL 中没有 ccbParamSJ 参数\n\n请从建行生活 App 跳转到此页面,或在 URL 中添加 ccbParamSJ 参数'); + return; + } + + // 清除登录时间戳,强制重新登录 + localStorage.removeItem('ccb_login_timestamp'); + + showResult('🔄 强制重新登录...\n\n忽略缓存,重新执行登录流程'); + showLoading(true); + + // 更新状态 + document.getElementById('auto-login-status').innerHTML = + '强制执行...'; + + // 延迟执行 + setTimeout(function() { + executeAutoLogin(params.ccbParamSJ); + }, 500); + } + // 测试登录入库 function testLogin() { var params = CcbLifeBridge.getUrlParams(); @@ -544,10 +727,15 @@ // 保存 token 和用户信息 localStorage.setItem('ccb_token', loginData.token); localStorage.setItem('ccb_user_info', JSON.stringify(loginData.user_info)); + localStorage.setItem('ccb_login_timestamp', Date.now().toString()); // 更新页面显示 updateLoginStatus({ userInfo: loginData.user_info }); + // 更新登录时间 + document.getElementById('login-time').textContent = + new Date().toLocaleString('zh-CN', { hour12: false }); + showResult('✅ 登录成功!\n\n' + '用户信息:\n' + JSON.stringify(loginData.user_info, null, 2) + '\n\n' + 'Token:' + loginData.token.substr(0, 20) + '...\n\n' +