# 项目错误经验记录 本文档记录在开发过程中遇到的典型错误及其解决方案,供团队参考避免重复踩坑。 --- ## 1. 支付金额计算顺序错误(2026-07-02) ### 问题描述 在 `GamePayService::orderData()` 方法中,`$payData['pay_amount']` 的计算依赖于 `Pay::calPayAmount()` 方法。当使用专属币(ptb_amt)且存在折扣时,计算结果偏高。 ### 错误代码 ```php // Pay.php 第166-176行 $diff = bcsub($amount, $ptb_amount, 2); // 先扣除专属币 $raw_amount = bcmul($diff, $discount, 4); // 再应用折扣 ``` ### 正确逻辑 ```php $discounted = bcmul($amount, $discount, 2); // 先应用折扣 $result = bcsub($discounted, $ptb_amount, 2); // 再扣除专属币 ``` ### 影响范围 - **触发条件**:专属币 > 0 且折扣 < 1 - **影响接口**:所有使用 `calPayAmount()` 的支付入口 - **业务影响**:用户实际支付金额比正确金额偏高 ### 根本原因 1. 开发时未理清业务规则:**折扣应作用于原价,专属币抵扣折扣后的金额** 2. 缺乏单元测试覆盖边界场景 3. 新旧逻辑迁移时引入错误(旧逻辑只处理折扣,新逻辑增加专属币扣除) ### 解决方案 调整 `calPayAmount()` 中的计算顺序,确保先折扣后扣除。 ### 预防措施 - 涉及金额计算的修改必须编写单元测试 - 明确业务规则文档化(折扣、抵扣的优先级) - 代码审查时重点关注计算顺序 --- ## 2. API调用丢失折扣参数(2026-07-02) ### 问题描述 在 `api/controller/v1/Pay.php` 中调用 `calPayAmount()` 时未传递 `$discount` 参数,导致折扣配置失效。 ### 错误代码 ```php // api/controller/v1/Pay.php 第257、260行 $arrPayAmount = $payLogic->calPayAmount($amount, $ptb_amt); // 缺少 $discount $arrPayAmount = $payLogic->calPayAmount($amount); // 缺少 $discount ``` ### 影响范围 - **触发条件**:所有通过API发起的支付请求 - **业务影响**:游戏配置的折扣活动完全失效 ### 根本原因 1. 方法签名 `$discount = 1` 的默认值掩盖了参数缺失 2. 调用方未遵循方法契约(应传递完整参数) ### 解决方案 补充 `$discount` 参数传递: ```php $discount = getDiscount($member_id, $game_id, $channel_id); $arrPayAmount = $payLogic->calPayAmount($amount, $ptb_amt, $discount); ``` ### 预防措施 - 关键业务参数不应有默认值(或默认值为异常值如 `null`) - 静态分析工具检查函数调用参数完整性 - 统一支付入口,避免多处重复逻辑 --- ## 3. 调试代码残留(2026-07-02) ### 问题描述 生产代码中残留 `dump()` 调试语句,影响性能和输出。 ### 错误代码 ```php // common.php 第639行 dump($ancestors); // Pay.php 第550行 dump($channel); ``` ### 影响 - 输出额外内容,可能破坏JSON响应 - 性能损耗(尤其循环中) - 暴露内部数据结构 ### 预防措施 - 代码提交前搜索 `dump(`、`var_dump(`、`print_r(` - IDE配置保存时自动移除调试语句 - Code Review 时重点关注 --- ## 4. 登录时未添加新设备到常用设备列表(2026-07-02) ### 问题描述 用户登录时,新的设备号不会被添加到常用设备列表中,数据库中 `nw_subaccount.recent_devices` 字段始终只保存一个设备。 ### 错误代码 ```php // api/controller/v1/Login.php 第594行 $memberDevice->addDevice($userinfo['id'], $gameid, $imeil, $device, false, true); // ↑ // $onlyUpdateExisting = true ``` ### 正确逻辑 ```php $memberDevice->addDevice($userinfo['id'], $gameid, $imeil, $device, false, false); // ↑ // $onlyUpdateExisting = false ``` ### 影响范围 - **触发条件**:用户使用新设备登录 - **影响接口**:`api/controller/v1/Login.php` 登录接口 - **业务影响**:常用设备列表无法正常维护,设备切换验证功能异常 ### 根本原因 1. `MemberDevice::addDevice()` 的 `$onlyUpdateExisting` 参数含义混淆: - `true` = 仅更新已存在设备,不添加新设备 - `false` = 不存在时添加新设备 2. 登录接口错误地传入 `true`,与注册接口逻辑不一致 3. 注册接口正确传入 `false`(或使用默认值) ### 解决方案 将 Login.php 中 `$onlyUpdateExisting` 参数改为 `false`,使登录与注册保持一致,都允许添加新设备。 ### 预防措施 - 关键业务参数应使用有意义的常量或枚举,避免裸露的 `true/false` - 相同功能的代码应保持一致的调用方式 - 代码审查时对比相似场景的实现 --- ## 5. GamePayService 中 real_amount 与 pay_amount 概念混淆(2026-07-02) ### 问题描述 在 `GamePayService::gamePay()` 方法中,`$payData['real_amount']` 和 `$payData['pay_amount']` 两个字段容易混淆,需要明确区分其业务含义和计算逻辑。 ### 字段定义对比 | 字段 | 含义 | 考虑代金券 | 考虑币抵扣 | 考虑折扣 | |------|------|------------|------------|----------| | `pay_amount` | 折扣后应付金额 | ❌ | ❌ | ✅ | | `real_amount` | 实际需第三方支付金额 | ✅ | ✅ | ✅ | ### 计算公式 ``` pay_amount = amount * discount real_amount = (amount - coupon_amount - ptb_amount/coin_amount) * discount ``` ### 数据流向 ``` $data['amount'] (原始充值金额) ↓ 代金券抵扣 → $amount = amount - coupon_amount ↓ calPayAmount() / calCoinPayAmount() → $arrPayAmount ↓ orderData() → $payData['real_amount'] + $payData['pay_amount'] ``` ### 关键代码位置 1. **代金券抵扣**:`GamePayService.php` 第215-235行 2. **币抵扣计算**:`Pay::calPayAmount()` / `Pay::calCoinPayAmount()` 3. **orderData赋值**:`GamePayService.php` 第583-619行 ### 易错点 1. **`pay_amount` 不考虑代金券和币抵扣**:仅反映折扣因素 2. **`real_amount` 是最终支付金额**:传给第三方支付渠道的金额 3. **`real_amount = 0` 时直接标记支付成功**:无需调用第三方支付 ### 具体示例 假设:充值金额 `100元`,折扣 `0.9`,代金券 `10元`,专属币 `20元` ``` 原始金额: 100元 代金券抵扣后: 100 - 10 = 90元 折扣后应付: 100 * 0.9 = 90元 → pay_amount 实际支付: (100 - 10 - 20) * 0.9 = 63元 → real_amount ``` ### 预防措施 - 修改金额计算逻辑前,先明确字段的业务定义 - 建议在代码注释中标注字段含义和计算公式 - 涉及金额的接口必须编写单元测试覆盖边界场景 --- *最后更新:2026-07-02* --- ## 6. 定时任务循环中使用致命错误导致后续任务中断(2026-07-03) ### 问题描述 在 PolyChannelSmsWarn.php 定时任务脚本中,当处理多个渠道的短信预警时,第一个渠道发送失败后,后续渠道不会被执行。 ### 错误代码 #### 问题1:致命错误导致脚本终止 `php // fetchContent 方法 if(\ === false) { // ❌ E_USER_ERROR 是致命错误,会立即终止整个 PHP 脚本执行 trigger_error("[CURL_" . curl_errno(\) . "]: " . curl_error(\), E_USER_ERROR); } ` #### 问题2:number_format 重复调用 `php // execute 方法中的短信发送逻辑 \ = number_format(\['total_advance'] - \, 2); // 返回值: "-42,299.56"(带逗号的字符串) \ = ['name' => \['channel_name'], 'money' => number_format(\, 2)]; // ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ // 对带逗号的字符串再次 number_format 会报错 // 报错信息:A non well formed numeric value encountered ` ### 正确逻辑 #### 修复1:改为抛出异常 `php // fetchContent 方法 if(\ === false) { \ = "[CURL_" . curl_errno(\) . "]: " . curl_error(\); curl_close(\); // ✅ 抛出异常,可被 try-catch 捕获 throw new \RuntimeException(\); } ` #### 修复2:添加 try-catch 捕获异常 `php foreach ( \ as \ ) { try { // ... 原有的发送逻辑 ... } catch (\Exception \) { // ✅ 记录错误日志,继续执行下一个渠道 \->writeln(date('H:i:s')." [\['channel_id']] 处理异常: ".\->getMessage()); continue; } } ` #### 修复3:避免 number_format 重复调用 `php \ = number_format(\['total_advance'] - \, 2); // ❌ 错误写法:对已经格式化的字符串再次格式化 // \ = ['name' => \['channel_name'], 'money' => number_format(\, 2)]; // ✅ 正确写法:直接使用已格式化的值 \ = ['name' => \['channel_name'], 'money' => \]; ` ### 影响范围 - **触发条件**:定时任务处理多个渠道,且至少有一个渠道发送失败 - **影响脚本**:pplication/crontab/PolyChannelSmsWarn.php - **业务影响**:短信预警通知不完整,可能导致部分渠道余额不足未被及时发现 ### 根本原因 1. **致命错误处理不当**:在循环中使用 E_USER_ERROR 会导致整个脚本终止,无法容错 2. **number_format 理解偏差**: umber_format() 返回的是带千分位逗号的字符串,不能直接再次格式化 ### 预防措施 #### 代码层面 - **循环中禁止使用致命错误**:E_USER_ERROR、xit()、die() 等会导致脚本终止的语句 - **使用 try-catch 包裹可能失败的操作**:HTTP请求、数据库操作、文件操作等 - **number_format 返回值是字符串**:如需再次参与计算,应使用原始数值而非格式化后的字符串 #### 测试层面 - **多场景测试**:测试部分成功、部分失败的混合场景 - **边界值测试**:测试负数、零、超大数等特殊情况 #### 架构层面 - **批量任务设计**:单个任务失败不应影响其他任务 - **日志记录**:记录每个子任务的执行结果,便于排查问题 ### 相关文件 - www/new_sdk/application/crontab/PolyChannelSmsWarn.php ### 关键教训 > **在循环处理多个任务时,永远不要使用会导致脚本终止的错误处理方式(如 E_USER_ERROR),而应该使用 ry-catch 捕获异常,确保单个失败不会影响其他任务的执行。** > > ** umber_format() 返回的是带千分位的字符串,不是数值类型,不能直接再次格式化或参与需要数值的运算。** --- *最后更新:2026-07-03*