preg_match_all返回空数组怎么办?10分钟快速排查与解决方案大全

preg_match_all空数组排查指南

第一章:preg_match_all返回空数组的常见场景解析

在PHP开发中,preg_match_all 是处理正则匹配的重要函数,用于全局搜索字符串中所有符合模式的内容。然而开发者常遇到其返回空数组的情况,这通常并非函数失效,而是由特定原因导致。

正则表达式语法错误

当正则表达式书写不正确时,如遗漏定界符或使用非法修饰符,preg_match_all 将无法解析模式并返回 false 或空数组。务必确保正则包含有效的分隔符,例如斜杠或井号。
// 正确使用定界符和修饰符
$pattern = '/\d+/'; // 匹配一个或多个数字
$subject = 'Order number: 123, Total: 456';
preg_match_all($pattern, $subject, $matches);

// $matches[0] 将包含 ['123', '456']

目标字符串不包含匹配内容

若传入的主体字符串中没有符合正则模式的子串,preg_match_all 自然返回空结果。建议在调用前验证数据来源,并使用 var_dump 输出调试信息。

大小写敏感性问题

默认情况下,正则匹配区分大小写。若需忽略大小写,应添加 i 修饰符。
  • 检查正则是否包含合法定界符
  • 确认主体字符串中存在预期匹配内容
  • 根据需求添加 i(忽略大小写)或 u(支持UTF-8)修饰符
问题类型解决方案
缺少定界符使用 /pattern/ 或 #pattern# 格式
无匹配内容检查输入字符串与正则逻辑一致性
大小写不匹配添加 i 修饰符

第二章:深入理解preg_match_all函数的工作机制

2.1 正则表达式模式的基本结构与语法要点

正则表达式的模式由普通字符和特殊元字符组成,用于描述文本的匹配规则。元字符如 ^$.*+?[]() 等赋予表达式强大的文本识别能力。
常用元字符及其功能
  • ^:匹配字符串的开始位置
  • $:匹配字符串的结束位置
  • .:匹配除换行符外的任意单个字符
  • *:匹配前面的子表达式零次或多次
  • +:匹配前面的子表达式一次或多次
  • ?:匹配前面的子表达式零次或一次
示例代码:邮箱格式校验
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
该正则表达式从开头(^)匹配字母数字及特定符号组成的用户名部分,接着匹配“@”符号,然后是域名部分,最后以点号和至少两个字母的顶级域名结尾($)。其中 [a-zA-Z0-9._%+-]+ 确保用户名称至少有一个合法字符,\. 转义点号以匹配字面意义。

2.2 匹配模式修饰符对结果的影响分析

在正则表达式中,匹配模式修饰符(flags)显著影响匹配行为和结果输出。常见的修饰符包括 i(忽略大小写)、g(全局匹配)、m(多行模式)等,它们改变了引擎的默认匹配策略。
常用修饰符及其作用
  • i:使匹配不区分大小写,例如 /hello/i 可匹配 "Hello" 或 "HELLO"
  • g:返回所有匹配项而非首个,常用于提取多个结果
  • m:启用多行模式,^ 和 $ 分别匹配每行的起始和结束位置
代码示例与分析
const text = "Hello\nHELLO";
const regex1 = /hello/g;
const regex2 = /hello/ig;
console.log(text.match(regex1)); // null
console.log(text.match(regex2)); // ["Hello", "HELLO"]
上述代码中,仅使用 g 时因大小写不匹配返回 null;加入 i 后成功匹配两处结果,体现修饰符组合对输出的关键影响。

2.3 主题字符串预处理与编码问题排查

在处理消息队列中的主题字符串时,常因编码不一致导致订阅失败或匹配异常。需优先确保字符串标准化。
常见编码问题场景
  • UTF-8 与 GBK 混用导致字符乱码
  • URL 编码未解码即参与匹配
  • 前后空格或不可见控制字符干扰比较
预处理代码示例
func normalizeTopic(topic string) string {
    // 去除首尾空白及控制字符
    trimmed := strings.TrimSpace(topic)
    // 解码 URL 编码字符,如 %E4%B8%AD → "中"
    decoded, _ := url.QueryUnescape(trimmed)
    // 统一转为 UTF-8 标准化形式
    return norm.NFC.String(decoded)
}
该函数依次执行去空、解码、Unicode 标准化,确保主题字符串在不同客户端间具有一致性。其中 norm.NFC 来自 golang.org/x/text/unicode/norm 包,用于规范化合成形式。

2.4 子组捕获与反向引用的实际应用技巧

在正则表达式中,子组捕获不仅用于提取匹配内容,还能通过反向引用增强模式匹配的灵活性。合理使用括号分组和引用编号,可显著提升文本处理效率。
命名捕获提升可读性
使用命名子组使正则更易维护:
(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})
该模式匹配日期如“2024-05-21”,并通过 ?<name> 语法命名捕获组,后续可通过名称引用,增强代码可读性。
反向引用避免重复输入
反向引用常用于匹配重复结构:
(\b\w+\b)\s+\1
此表达式查找连续重复单词,如“the the”。其中 \1 引用第一个捕获组内容,确保前后一致。
  • 反向引用从1开始编号,对应左括号顺序
  • 命名捕获兼容性需注意引擎支持(如PCRE、Python re)
  • 避免过度嵌套,防止性能下降

2.5 preg_match_all与preg_match的行为差异对比

在PHP正则表达式处理中,preg_matchpreg_match_all虽功能相近,但行为存在本质区别。

基础行为差异
  • preg_match仅匹配首次符合条件的结果,执行效率高,适用于精确查找;
  • preg_match_all则会遍历整个字符串,返回所有匹配结果,适合数据提取场景。
代码示例对比

// preg_match:只返回第一个匹配
$subject = "Contact: user@example.com, admin@site.com";
preg_match('/[\w\-\.]+@[\w\-\.]+/', $subject, $matches);
print_r($matches); // 输出: Array ( [0] => user@example.com )

// preg_match_all:返回全部匹配
preg_match_all('/[\w\-\.]+@[\w\-\.]+/', $subject, $matches);
print_r($matches[0]); // 输出: Array ( [0] => user@example.com [1] => admin@site.com )

上述代码中,正则表达式用于提取邮箱地址。preg_match在找到第一个邮箱后立即停止;而preg_match_all持续搜索直至字符串末尾,确保无遗漏。返回结构也不同:preg_match_all将结果组织为二维数组,便于批量处理。

第三章:导致空数组输出的关键原因剖析

3.1 模式语法错误与转义字符遗漏实战检测

在正则表达式和字符串模板处理中,模式语法错误与转义字符遗漏是常见隐患。未正确转义特殊字符(如 .*?())会导致匹配行为异常。
典型错误示例
^\d{3}.\d{3}.\d{4}$
该模式意图匹配电话号码如 123.456.7890,但点号 . 未转义,会匹配任意字符。正确写法应为:
^\d{3}\.\d{3}\.\d{4}$
其中 \. 明确匹配字面量点号,避免语义偏差。
检测建议清单
  • 检查所有特殊字符是否按需转义
  • 使用工具进行静态模式分析
  • 编写单元测试验证匹配准确性

3.2 输入文本中隐藏字符与换行符干扰定位

在文本处理过程中,隐藏字符(如零宽空格、BOM头)和换行符(\n、\r\n)常导致解析错位,严重影响数据定位精度。
常见干扰字符类型
  • \u200B:零宽空格,不可见但占用字符位置
  • \uFEFF:BOM头,常出现在UTF-8文件开头
  • 混合换行符:Windows(\r\n)与Unix(\n)不一致
清洗处理示例

import re

def clean_text(text):
    # 移除各类隐藏字符
    text = re.sub(r'[\u200b\u200c\u200d\ufeff]', '', text)
    # 标准化换行符
    text = re.sub(r'\r\n|\r', '\n', text)
    return text.strip()
该函数通过正则表达式清除零宽字符并统一换行符为 Unix 风格,确保后续解析逻辑的一致性。参数 text 为原始输入字符串,输出为规范化后的文本。

3.3 定界符使用不当引发的匹配失败案例

在正则表达式中,定界符用于标识模式的开始和结束。若使用不当,将直接导致解析错误或匹配失败。
常见定界符误用场景
  • 未转义定界符本身,如在/作为定界符时,路径/path/to/file未转义斜杠
  • 混用不同定界符风格,例如PCRE中使用~却遗漏闭合
代码示例与修正
/^\d{3}-\d{3}-\d{4}$/
该模式用于匹配电话号码,但若字符串包含/(如URL),应更换定界符:
#^https?://example\.com$#i
使用#作为定界符可避免频繁转义/,提升可读性。
推荐实践
选择不常出现在模式中的字符作为定界符,如~#|,并确保首尾一致。

第四章:高效排查流程与实用解决方案

4.1 使用preg_last_error()获取底层错误信息

在PHP正则表达式处理中,当preg_match()preg_replace()等函数执行失败时,往往返回false,但具体原因不明确。preg_last_error()函数可用于获取最后一次PCRE操作的错误代码。
常见错误类型
  • PREG_INTERNAL_ERROR:内部PCRE引擎错误
  • PREG_BAD_UTF8_ERROR:UTF-8字符串格式不合法
  • PREG_BAD_UTF8_OFFSET_ERROR:UTF-8偏移量无效
  • PREG_JIT_STACKLIMIT_ERROR:JIT编译栈溢出
错误码解析示例

$pattern = '/[\x80-\xFF]/u';
$result = preg_match($pattern, "invalid\xFF");

if ($result === false) {
    $errorCode = preg_last_error();
    echo "正则匹配失败,错误码: $errorCode";
}
上述代码尝试匹配非法UTF-8序列,若启用Unicode模式会触发PREG_BAD_UTF8_ERROR。通过preg_last_error()可捕获该异常,便于调试复杂正则场景。

4.2 借助var_dump和正则调试工具验证模式

在PHP开发中,准确验证正则表达式匹配结果至关重要。var_dump函数能够输出变量的完整类型与结构,是调试匹配结果的首选工具。
基本调试流程
使用preg_match捕获数据后,立即结合var_dump查看返回值:
$pattern = '/\d{3}-\d{3}-\d{4}/';
$subject = 'Contact: 123-456-7890';
preg_match($pattern, $subject, $matches);
var_dump($matches);
上述代码将输出匹配的电话号码详情。$matches[0]包含完整匹配项,便于确认模式是否精准捕获目标内容。
常用正则调试工具
  • RegExr:实时高亮匹配文本
  • Debuggex:可视化正则结构图
  • PHP Live Regex:集成var_dump输出预览
结合本地var_dump与在线工具,可大幅提升模式验证效率。

4.3 分步测试法缩小问题范围的操作策略

在复杂系统调试中,分步测试法是定位故障的核心手段。通过隔离变量、逐层验证,可高效锁定异常源头。
测试步骤分解原则
遵循“从外到内、由简至繁”的逻辑顺序,优先验证输入输出接口,再深入内部逻辑模块。
  1. 确认外部依赖服务状态正常
  2. 检查配置参数与环境一致性
  3. 执行最小可运行单元测试
  4. 逐步接入完整调用链路
代码层验证示例

// TestUserService_GetUser 模拟用户服务的分步测试
func TestUserService_GetUser(t *testing.T) {
    mockDB := new(MockDatabase)           // 步骤1:模拟数据库依赖
    mockDB.On("Query", "123").Return(user, nil)

    service := UserService{DB: mockDB}
    result, err := service.GetUser("123") // 步骤2:调用目标方法

    assert.NoError(t, err)                // 步骤3:验证返回结果
    assert.Equal(t, "Alice", result.Name)
}
上述代码通过 Mock 机制隔离数据层,确保测试仅聚焦业务逻辑正确性。mockDB 模拟预期响应,避免真实数据库连接带来的不确定性,从而精准判断问题是否存在于服务层。

4.4 常见修复方案汇总与代码示例演示

乐观锁机制避免并发冲突
在高并发场景下,多个请求同时修改同一数据易导致脏写。采用版本号字段实现乐观锁可有效解决该问题。
UPDATE user SET balance = balance - 100, version = version + 1 
WHERE id = 1 AND version = 3;
该SQL仅当当前版本为3时更新成功,防止旧版本覆盖新数据。
重试机制增强系统容错性
对于临时性故障(如网络抖动),引入指数退避重试策略可显著提升稳定性。
  • 首次失败后等待1秒重试
  • 每次间隔倍增,最多重试5次
  • 结合熔断机制防止雪崩

第五章:总结与最佳实践建议

构建高可用系统的监控策略
现代分布式系统必须依赖实时监控来保障稳定性。推荐使用 Prometheus 采集指标,结合 Grafana 实现可视化。以下是一个典型的 Prometheus 配置片段,用于抓取 Kubernetes 节点指标:

scrape_configs:
  - job_name: 'kubernetes-nodes'
    static_configs:
      - targets: ['node-exporter:9100']
    relabel_configs:
      - source_labels: [__address__]
        regex: '(.*):(.*)'
        target_label: instance
        replacement: '${1}'
安全加固的关键步骤
生产环境应遵循最小权限原则。以下是常见的安全实践清单:
  • 禁用 root 用户 SSH 登录
  • 配置防火墙(如 iptables 或 UFW),仅开放必要端口
  • 定期轮换密钥和证书
  • 启用审计日志并集中存储
  • 使用 SELinux 或 AppArmor 强化进程隔离
性能调优的实际案例
某电商平台在大促期间遭遇数据库瓶颈。通过分析慢查询日志,发现未合理使用索引。优化后,响应时间从 850ms 降至 90ms。关键措施包括:
  1. 为高频查询字段添加复合索引
  2. 调整 PostgreSQL 的 shared_buffers 至物理内存的 25%
  3. 引入 Redis 缓存热点商品数据
部署流程标准化
阶段操作工具示例
代码提交触发 CI 流水线GitHub Actions
测试运行单元与集成测试Jenkins, Testify
部署蓝绿发布至生产Kubernetes, Argo Rollouts
<?php namespace app\common\service\wareHousFee\jingdong\wareHousingFees; use app\common\model\wareHousFee\jingdong\wareHousingFees\WareHousingFeesItemizationModel; use app\common\model\wareHousFee\jingdong\wareHousingFees\WareHousingFeesItemVeryModel; use app\common\model\wareHousFee\jingdong\wareHousingFees\WareHousingFeesQuoteModel; use app\common\model\wareHousFee\newFie\camelCaseFee\CamelCaseFeeItemizationModel; use think\db\exception\DataNotFoundException; use think\db\exception\DbException; use think\db\exception\ModelNotFoundException; use think\facade\Log; class WareHousingFeesService { public function __construct(){} /** * @notes 京东仓储服务费核对逻辑 * @param $yearMonth * @return bool * @throws DataNotFoundException * @throws DbException * @throws ModelNotFoundException * @author 胡军 * @date 2025/06/27 */ public function wareHousingFeeVerifyDo($yearMonth):bool{ $monthTimeRange = $this->getMonthTimeRange($yearMonth); //获取时间范围内的数据并进行分组统计 //total_quantity 总数 //total_settlement_amount 总的结算金额 $itemizationMonthList = WareHousingFeesItemizationModel::whereBetween('business_time', [$monthTimeRange['startTime'], $monthTimeRange['endTime']]) ->field([ 'document_number', 'document_type', 'SUM(quantity) as total_quantity', 'SUM(settlement_amount) as total_settlement_amount' ]) ->group('document_number, document_type') ->select() ->toArray(); //一次性读取报价单避免foreach循环 提升效率 $quoteList = WareHousingFeesQuoteModel::select()->toArray(); $quoteListRst = []; foreach ($quoteList as $item) { $quoteListRst[$item['service_type']] = $item; } if(!empty($quoteListRst)){ foreach($itemizationMonthList as $key => $value){ //初始化理论金额为0 $itemizationMonthList[$key]['theoretical_amount'] = 0; if($value['document_type'] == '出库单' && !empty($quoteListRst['出库单'])){ //$value['total_quantity'] 数量 if($value['total_quantity'] <= 3){ //理论金额 $itemizationMonthList[$key]['theoretical_amount'] = $quoteListRst['出库单']['first_three_items']; } else { $itemizationMonthList[$key]['theoretical_amount'] = $quoteListRst['出库单']['first_three_items'] + ($value['total_quantity'] - 3) * $quoteListRst['出库单']['additional_items']; } } if($value['document_type'] == '退供单' && !empty($quoteListRst['退供单'])){ $itemizationMonthList[$key]['theoretical_amount'] = $quoteListRst['退供单']['first_three_items'] * $value['total_quantity']; } if($value['document_type'] == '退货单' && !empty($quoteListRst['退货单'])){ if($value['total_quantity'] <= 3){ $itemizationMonthList[$key]['theoretical_amount'] = $quoteListRst['退货单']['first_three_items']; } else { $itemizationMonthList[$key]['theoretical_amount'] = $quoteListRst['退货单']['first_three_items'] + ($value['total_quantity'] - 3) * $quoteListRst['退货单']['additional_items']; } } //正常计算出来的理论金额不应该是0 那么这个时候就要记录日志便于排查 if($itemizationMonthList[$key]['theoretical_amount'] == 0){ //echo $value['document_number'].PHP_EOL; Log::warning('【京东仓储服务费明细核对】--月份为:'.$yearMonth."的京东仓储服务费订单明细核对匹配不到京东仓储服务费报价表la_storage_service_quotes当中的类型,明细单据编号为".$value['document_number']); unset($itemizationMonthList[$key]); continue; }else{ //差异:结算金额-理论金额 $itemizationMonthList[$key]['balance'] = $value['total_settlement_amount'] - $itemizationMonthList[$key]['theoretical_amount']; } } //批量分批次更新数据库 $status = true; $batchSize = 50; // 每批10条记录 $totalCount = count($itemizationMonthList); $batchCount = ceil($totalCount / $batchSize); $itemizationModel = new WareHousingFeesItemVeryModel(); for ($i = 0; $i < $batchCount; $i++) { $batchData = array_slice($itemizationMonthList, $i * $batchSize, $batchSize); $documentNumbers = array_column($batchData, 'document_number'); // 提取当前批次的所有单据号 try { $itemizationModel->startTrans(); // 批量删除操作(根据单据号) if (!empty($documentNumbers)) { $itemizationModel->whereIn('document_number', $documentNumbers)->delete(); } // 使用批量更新替代循环单条更新 $itemizationModel->saveAll($batchData); $itemizationModel->commit(); } catch (\Exception $e) { $itemizationModel->rollback(); //记录日志 Log::error('【京东仓储服务费明细核对异常】--月份为:'.$yearMonth."的费用核对发生错误:" . $e->getMessage()); //其中一个批次数据处理失败则直接退出循环 不再继续执行后续批次的数据处理 报错给前端显示 $status = false; break; } } return $status; }else{ return false; } } /** * @notes 根据年月比如2025-05获取当月时间范围 精确到秒 (git有问题未解决 暂时无法写入common.php当中 临时放这) * @param string $yearMonth * @return array * @author 胡军 * @date 2025/06/27 */ private function getMonthTimeRange(string $yearMonth): array { // 验证输入格式 (YYYY-MM) if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $yearMonth)) { throw new InvalidArgumentException('输入格式不正确,必须为YYYY-MM格式'); } list($year, $month) = explode('-', $yearMonth); // 构建开始时间 $startTime = "{$year}-{$month}-01 00:00:00"; // 使用DateTime类计算当月最后一天 $lastDay = (new \DateTime("{$year}-{$month}-01")) ->modify('last day of this month') ->format('d'); // 构建结束时间 $endTime = "{$year}-{$month}-{$lastDay} 23:59:59"; return [ 'startTime' => $startTime, 'endTime' => $endTime ]; } } 这是我原有的代码业务逻辑,后来我修改为了如下: <?php namespace app\common\service\wareHousFee\jingdong\wareHousingFees; use app\common\model\wareHousFee\jingdong\wareHousingFees\WareHousingFeesItemizationModel; use app\common\model\wareHousFee\jingdong\wareHousingFees\WareHousingFeesQuoteModel; use think\facade\Db; use think\facade\Log; class WareHousingFeeService { public function __construct(){} /** * @notes 京东仓储服务费核对逻辑 * @param $yearMonth * @return bool * @throws DataNotFoundException * @throws DbException * @throws ModelNotFoundException * @author 胡军 * @date 2025/06/27 */ public function wareHousingFeeVerifyDo(string $yearMonth): bool { // 1. 获取月份时间范围 $monthTimeRange = $this->getMonthTimeRange($yearMonth); if (!$monthTimeRange) { Log::error("无效的月份格式: {$yearMonth}"); return false; } // 2. 预加载报价数据 $quoteList = WareHousingFeesQuoteModel::where('service_type', 'IN', ['出库单', '退供单', '退货单']) ->column('first_three_items,additional_items', 'service_type'); //print_r($quoteList);die; /*Array ( [出库单] => Array ( [first_three_items] => 1.500 [additional_items] => 0.250 [service_type] => 出库单 ) [退供单] => Array ( [first_three_items] => 0.500 [additional_items] => 0.500 [service_type] => 退供单 ) [退货单] => Array ( [first_three_items] => 3.000 [additional_items] => 0.500 [service_type] => 退货单 ) )*/ if (empty($quoteList)) { Log::warning("京东仓储服务费报价表为,月份: {$yearMonth}"); return false; } // 3. 配置参数(从环境变量获取或使用默认值) $batchSize = 500; // 批次大小即每次读取的数据条数 $reconnectInterval = 200; // 每10万条重新连接一次数据库(200批次=10万条) $gcInterval = 20; // 垃圾回收间隔 $updateCount = 0; // 处理计数 $batchCount = 0; // 批次计数 $status = true; // 整体状态 // 性能监控变量 $startTime = microtime(true); $startMemory = memory_get_usage(); try { // 4. 创建基础查询 $query = WareHousingFeesItemizationModel::whereBetween('business_time', [ $monthTimeRange['startTime'], $monthTimeRange['endTime'] ]); // 5. 分批次处理数据 $query->chunk($batchSize, function($items) use ( &$updateCount, &$batchCount, &$status, $quoteList, $reconnectInterval, $gcInterval, $yearMonth, $startTime, $startMemory ) { $batchCount++; //print_r($items->toArray());die; // 6. 分组统计(按单据号和单据类型分组) $groupedData = []; foreach ($items as $item) { $key = $item->document_number . '|' . $item->document_type; //echo $key; //ESL00000022972118012|出库单 //ESL00000022971633940|退货单 //ESL00000022971819716|退供单 //ESL00000022972118012|出库单 if (!isset($groupedData[$key])) { $groupedData[$key] = [ 'document_number' => $item->document_number, 'document_type' => $item->document_type, 'total_quantity' => 0, 'total_settlement_amount' => 0 ]; } $groupedData[$key]['total_quantity'] += $item->quantity;// 数量累加 $groupedData[$key]['total_settlement_amount'] += $item->settlement_amount;//结算金额累加 } /*echo "<pre>"; print_r($groupedData); echo "</pre>";die; Array ( [ESL00000022972118012|出库单] => Array ( [document_number] => ESL00000022972118012 [document_type] => 出库单 [total_quantity] => 4 [total_settlement_amount] => 3 ) [ESL00000022971633940|退货单] => Array ( [document_number] => ESL00000022971633940 [document_type] => 退货单 [total_quantity] => 1 [total_settlement_amount] => 1.5 ) [ESL00000022971819716|退供单] => Array ( [document_number] => ESL00000022971819716 [document_type] => 退供单 [total_quantity] => 1 [total_settlement_amount] => 0 ) )*/ // 7. 计算理论金额和差额 $updateData = []; foreach ($groupedData as $data) { //单据类型 $docType = $data['document_type']; //数量累加 $totalQty = $data['total_quantity']; //结算金额累加 $settlementAmount = $data['total_settlement_amount']; //理论金额 $theoreticalAmount = 0; //判断类型 根据报价单以及数量 拿到具体的首三件以及大于三件的具体价格进行计算 switch ($docType) { case '出库单': if (isset($quoteList['出库单'])) { $quote = $quoteList['出库单']; $theoreticalAmount = ($totalQty <= 3) ? $quote['first_three_items'] : $quote['first_three_items'] + ($totalQty - 3) * $quote['additional_items']; } break; case '退供单': if (isset($quoteList['退供单'])) { $quote = $quoteList['退供单']; $theoreticalAmount = $quote['first_three_items'] * $totalQty; } break; case '退货单': if (isset($quoteList['退货单'])) { $quote = $quoteList['退货单']; $theoreticalAmount = ($totalQty <= 3) ? $quote['first_three_items'] : $quote['first_three_items'] + ($totalQty - 3) * $quote['additional_items']; } break; } // 跳过理论金额为0的异常情况 if ($theoreticalAmount == 0) { Log::warning("京东仓储费计算失败: {$data['document_number']}, 单据类型: {$docType}"); continue; } // 计算费用差额: 结算金额累加 - 理论金额 $balance = $settlementAmount - $theoreticalAmount; $updateData[] = [ //单据编号 'document_number' => $data['document_number'], //单据类型 'document_type' => $docType, //总数量 'total_quantity' => $totalQty, //结算总金额 'total_settlement_amount' => $settlementAmount, //理论金额 'theoretical_amount' => $theoreticalAmount, //差异 'balance' => $balance, //所属年月 'yearMonth' => $yearMonth ]; } // 8. $updateData为最后要插入结果表的数据 批量更新数据库(支持插入和更新) if (!empty($updateData)) { $this->batchUpsert($updateData); $updateCount += count($updateData); } // 9. 内存管理(释放不再使用的变量) unset($items, $groupedData, $updateData); // 10. 定期执行垃圾回收 if ($batchCount % $gcInterval === 0) { gc_collect_cycles(); } // 11. 定期重连数据库(避免长连接超时) if ($batchCount % $reconnectInterval === 0) { $this->reconnectDatabase(); Log::info("京东仓储服务费用核对已处理 {$updateCount} 条,进行数据库重连"); } // 12. 输出进度日志(每10批次记录一次) if ($batchCount % 10 === 0) { $memUsage = round((memory_get_usage() - $startMemory) / 1024 / 1024, 2); $timeElapsed = round(microtime(true) - $startTime, 2); Log::info("处理进度: {$updateCount}条, 内存占用: {$memUsage}MB, 耗时: {$timeElapsed}秒"); } }); }catch (\Throwable $e) { Log::error("京东仓储费核对系统运行异常: {$e->getMessage()}"); $status = false; } finally { Db::close(); // 最终关闭数据库连接 } // 13. 生成最终统计日志 $peakMemory = round(memory_get_peak_usage() / 1024 / 1024, 2); $totalTime = round(microtime(true) - $startTime, 2); Log::info("京东仓储费核对完成: {$yearMonth}, 处理单据: {$updateCount}条, 峰值内存: {$peakMemory}MB, 总耗时: {$totalTime}秒"); return $status; } // 数据库重连方法,用于长时间运行的任务,避免连接超时 private function reconnectDatabase() { try { $connection = \think\facade\Db::connect(); $connection->close(); // 关闭当前连接 $connection->connect(); // 重新建立连接 } catch (\Exception $e) { // 记录重连失败日志,但不中断程序执行 Log::error('【菜鸟退货入仓费用核对数据库重连失败】' . $e->getMessage()); } } // 获取月份时间范围(返回包含开始和结束时间的数组) private function getMonthTimeRange(string $yearMonth): ?array { if (!preg_match('/^\d{4}-\d{2}$/', $yearMonth)) { return null; } $startTime = date('Y-m-01 00:00:00', strtotime($yearMonth)); $endTime = date('Y-m-t 23:59:59', strtotime($yearMonth)); return ['startTime' => $startTime, 'endTime' => $endTime]; } // 批量插入或更新数据(使用INSERT ON DUPLICATE KEY UPDATE语法) private function batchUpsert(array $data): void { if (empty($data)) return; // 按1000条数据分块处理,避免SQL语句过长 // TODO:根据自己服务器性能适度调整大小 反复测试找到最合适的值即可 $chunks = array_chunk($data, 1000); foreach ($chunks as $chunk) { $values = []; $params = []; // 构建SQL语句的VALUES部分 foreach ($chunk as $row) { $values[] = "(?, ?, ?, ?, ?, ?, ?)"; $params[] = $row['document_number']; $params[] = $row['document_type']; $params[] = $row['total_quantity']; $params[] = $row['total_settlement_amount']; $params[] = $row['theoretical_amount']; $params[] = $row['balance']; $params[] = $row['yearMonth']; } // 构建完整的INSERT ON DUPLICATE KEY UPDATE语句 // 使用了 MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE 语法。这个语法允许我们尝试插入一行,如果该行已经存在(根据唯一索引或主键判断),则改为执行更新操作 // 务必创建la_storage_service_yes 表的唯一键索引:ALTER TABLE `la_storage_service_yes` ADD UNIQUE KEY `idx_document_number` (`document_number`); // 否则ON DUPLICATE KEY UPDATE语法不生效!!! $sql = "INSERT INTO la_storage_service_yes (document_number, document_type, total_quantity, total_settlement_amount, theoretical_amount, balance, yearMonth) VALUES " . implode(',', $values) . " ON DUPLICATE KEY UPDATE document_type = VALUES(document_type), total_quantity = VALUES(total_quantity), total_settlement_amount = VALUES(total_settlement_amount), theoretical_amount = VALUES(theoretical_amount), balance = VALUES(balance), yearMonth = VALUES(yearMonth)"; try { Db::execute($sql, $params); } catch (\Exception $e) { Log::error("京东仓储费批量更新失败: {$e->getMessage()}\nSQL语句: {$sql}"); throw $e; } } } } 就发现2000条数据最后就只读取了500条进行了处理 如果像你说的一样不累加岂不是修改了我的原有逻辑?
07-08
/** * @notes 京东收到派服务费核对逻辑 * @param $yearMonth * @return bool * @throws DataNotFoundException * @throws DbException * @throws ModelNotFoundException * @author 胡军 * @date 2025/07/01 */ public function deliveryPrimaryRegionsVerifyDo($yearMonth):bool{ $monthTimeRange = $this->getMonthTimeRange($yearMonth); $status = true; // 初始化状态 // 使用use关键字将$yearMonth传入闭包 如果闭包返回 false,chunk() 会立即停止处理后续数据块 DeliveryPrimaryRegionsItemizationModel::whereBetween('order_time', [ $monthTimeRange['startTime'], $monthTimeRange['endTime'] ])->chunk(100, function ($items) use ($yearMonth,&$status) { // 临时存储当前批次的结果 $itemizationMonthList = []; foreach ($items as $item) { //print_r($item->id."处理完成".PHP_EOL); $processRet = $this->processItem($item, $yearMonth); if(!empty($processRet)){ $itemizationMonthList[] = $processRet; } } // 优化后的事务处理->将整个批次作为一个事务处理,减少事务开启次数 $itemizationModel = new DeliveryPrimaryRegionsItemizationModel(); try { $itemizationModel->startTrans(); // 直接处理整个批次数据 $itemizationModel->saveAll($itemizationMonthList); $itemizationModel->commit(); } catch (\Exception $e) { $itemizationModel->rollback(); Log::error('【京东收派服务费明细核对】--月份为:'.$yearMonth."的费用核对发生错误:" . $e->getMessage()); $status = false; return false; } }); return $status; } // 将处理逻辑封装到单独方法当中 private function processItem($item,$yearMonth) { //实际配送特惠送区域型 $deliveryPrimary = DeliveryPrimaryRegionsQuoteModel::where('origin_province','=',$item['origin_province']) ->where('origin_location','=',$item['origin_city']) ->where('destination_province','=',$item['destination_province']) ->where('destination_location','=',$item['destination_city']) ->find(); if(!empty($deliveryPrimary)){ $deliveryPrimaryData = $deliveryPrimary->toArray(); //1、实际配送特惠送区域型->始发省&始发市&目的省&目的市去匹配对应收派服务费报价表中的始发省&始发市&目的省&目的市对应的京东标快分区值 $item['actual_delivery_area_type'] = $deliveryPrimaryData['jd_standard_partition']; }else{ //记录错误日志 Log::warning('【京东收派服务费明细核对】--月份为:'.$yearMonth."的明细表la_primary_regions_mx当中的[始发省&始发市&目的省&目的市] 没有匹配到 京东收派服务费报价表:la_primary_regions_quote对应的 [始发省&始发市&目的省&目的市],明细记录id为".$item['id']); return []; } //本应发货特惠送区域类型 $laPrimaryRegions = DeliveryPrimaryRegionsModel::where('province','=',$item['destination_province']) ->where('city','=',$item['destination_city'])->find(); if(!empty($laPrimaryRegions)){ //2、本应发货特惠送区域类型->目的省&目的市匹配对应一级区域表中的省&市对应的京东标快分区值 $laPrimaryRegionsData = $laPrimaryRegions->toArray(); $item['expected_delivery_area_type'] = $laPrimaryRegionsData['jd_zone']; }else{ //记录错误日志 Log::warning('【京东收派服务费明细核对】--月份为:'.$yearMonth."的明细表la_primary_regions_mx当中的[目的省&目的市] 没有匹配到 京东收派服务费一级区域表:la_primary_regions对应的 [目的省&目的市],明细记录id为".$item['id']); return []; } //3、是否跨仓->当实际配送特惠送区域型=实际配送特惠送区域时写入不跨仓否则跨仓 $item['is_cross_warehouse'] = $item['actual_delivery_area_type'] == $item['expected_delivery_area_type'] ? '2' : '1'; //4、理论重量: 收派服务费核对明细的平台订单号关联la_scm_day_outbound_data当中的billNo字段,查询该订单billNo下货品goodsNo及数量quantity,再通过关联基础数据物料la_warehouse_facility表的单品重量(通过条码)计算该订单下各货品的重量,并按照订单号分组、求和。 $scmDayOutboundData = ScmDayOutboundDataModel::where('billNo','=',$item['platform_order_number'])->select()->toArray(); if(!empty($scmDayOutboundData)){ $weightTotal = 0; foreach($scmDayOutboundData as $k => $v){ //skuBarcode $goodsWeightModel = GoodsWeightModel::where('barcode','=',$v['skuBarcode'])->field('single_item_weight')->find(); if(!empty($goodsWeightModel)){ $goodsWeight = $goodsWeightModel->toArray(); //la_scm_day_outbound_data数量*la_warehouse_facility单品重量(kg) $weightTotal += $v['quantity'] * $goodsWeight['single_item_weight']; }else{ //记录错误日志 Log::warning('【京东收派服务费明细核对】--月份为:'.$yearMonth."的明细表平台订单号关联la_scm_day_outbound_data当中的billNo字段 但是billNo字段关联基础数据物料la_warehouse_facility表的条码skuBarcode失败,明细记录id为".$item['id']); return []; } } $item['theoretical_weight'] = $weightTotal; }else{ Log::warning('【京东收派服务费明细核对】--月份为:'.$yearMonth."的明细表la_primary_regions_mx当中的平台订单号platform_order_number 没有匹配到 每日出库数据明细表:la_scm_day_outbound_data对应的关联单号billNo,明细记录id为".$item['id']); return []; } //5、重量取整-> 理论重量向上取0.5 if(is_numeric($item['theoretical_weight'])) { //如果是浮点数那么向上取0.5 $floatValue = (float)$item['theoretical_weight']; $item['weight_rounded'] = ceil($floatValue * 2) / 2; }else{ //如果是整数那么就直接赋值即可 $item['weight_rounded'] = $item['theoretical_weight']; } //6、重量差异-> 计费重量-重量取整 $item['weight_difference'] = $item['billing_weight'] - $item['weight_rounded']; //7、金额核验->(存在重复代码待优化 赶工期......) // ①费用类型=快递改址费 5; // ②费用类型=保价费 0.3; // ③费用类型=快递_转寄服务费 5; // ④费用类型=快递超长超重,【计费重量】*1元/kg // ⑤费用类型=快递运费,始发省&始发市&目的省&目的市,匹配对应 收派服务费报价表中的 始发省&始发市&目的省&目的市 对应的报价,使用【计费重量】按照报价计算: // 若 计费重量<=1 计算公式为:首重金额*0.28; // 若1<计费用重量<=30 计算公式为:(首重金额+(计费重量-1)*续重1金额)*0.28; // 若 计费重量>30 计算公式为:(首重金额+(计费重量-1)*续重1金额+(计费重量-30)*续重2金额)*0.28 if($item['fee_type'] == '快递改址费' || $item['fee_type'] == '快递_转寄服务费'){ $item['amount_verification'] = 5; }elseif($item['fee_type'] == '保价费'){ $item['amount_verification'] = 0.3; }elseif($item['fee_type'] == '快递超长超重'){ $item['amount_verification'] = $item['billing_weight'] * 1; }elseif($item['fee_type'] == '快递运费'){ if($item['billing_weight'] <= 1){ $item['amount_verification'] = $deliveryPrimary->first_weight * 0.28; }elseif($item['billing_weight'] <= 30){ $item['amount_verification'] = ($deliveryPrimary->first_weight + ($item['billing_weight'] - 1) * $deliveryPrimary->continued_weight1) * 0.28; }else{ // >30的情况 $item['amount_verification'] = ($deliveryPrimary->first_weight + ($item['billing_weight'] - 1) * $deliveryPrimary->continued_weight1 + ($item['billing_weight'] - 30) * $deliveryPrimary->continued_weight2) * 0.28; } }else{ Log::warning('【京东收派服务费明细核对】--月份为:'.$yearMonth."的明细表la_primary_regions_mx当中的费用类型fee_type 没有匹配到 京东收派服务费报价表:la_delivery_primary_regions对应的费用类型,明细记录id为".$item['id']); return []; } //8、核验差异—>结算金额-金额核验 $item['verification_difference'] = $item['settlement_amount'] - $item['amount_verification']; //9、理论计费->(存在重复代码待优化 赶工期......) // ①费用类型=快递改址费 5; // ②费用类型=保价费 0.3; // ③费用类型=快递_转寄服务费 5; // ④费用类型=快递超长超重,【计费重量】*1元/kg // ⑤费用类型=快递运费, 始发省&始发市&目的省&目的市去匹配对应 收派服务费报价表中的 始发省&始发市&目的省&目的市 当中对应的报价,使用【理论重量】向上取整 1,按照报价计算 // 若理论重量取整后<=1 计算公式为->首重*0.28; // 若1<理论重量<=30 计算公式为:(首重+(理论重量-1)*续重1)*0.28; // 若理论重量>30 计算公式为:(首重+(理论重量-1)*续重1+(理论重量-30)*续重2)*0.28 if($item['fee_type'] == '快递改址费' || $item['fee_type'] == '快递_转寄服务费'){ $item['theoretical_billing'] = 5; }elseif($item['fee_type'] == '保价费'){ $item['theoretical_billing'] = 0.3; }elseif($item['fee_type'] == '快递超长超重'){ $item['theoretical_billing'] = $item['billing_weight'] * 1; }elseif($item['fee_type'] == '快递运费'){ $roundedUp = ceil($item['theoretical_weight']); if($roundedUp <= 1){ $item['theoretical_billing'] = $deliveryPrimary->first_weight * 0.28; }elseif($roundedUp <= 30){ $item['theoretical_billing'] = ($deliveryPrimary->first_weight + ($item['billing_weight'] - 1) * $deliveryPrimary->continued_weight1) * 0.28; }else{ // >30的情况 $item['theoretical_billing'] = ($deliveryPrimary->first_weight + ($item['billing_weight'] - 1) * $deliveryPrimary->continued_weight1 + ($item['billing_weight'] - 30) * $deliveryPrimary->continued_weight2) * 0.28; } }else{ Log::warning('【京东收派服务费明细核对】--月份为:'.$yearMonth."的明细表la_primary_regions_mx当中的费用类型fee_type 没有匹配到 京东收派服务费报价表:la_delivery_primary_regions对应的费用类型,明细记录id为".$item['id']); return []; } //10、理论差异->结算金额-理论计费 $item['theoretical_difference'] = $item['settlement_amount'] - $item['theoretical_billing']; //11、不跨仓收费->(存在重复代码待优化 赶工期......) // ①费用类型=快递改址费 5; // ②费用类型=保价费 0.3; // ③费用类型=快递_转寄服务费 5; // ④费用类型=快递超长超重,【计费重量】*1元/kg // ⑤费用类型=快递运费,目的省&目的市,匹配对应 收派服务费报价表中的 目的省&目的市 对应的报价,使用【计费重量】按照一级区域报价计算 // 若计费重量<=1 计算公式为:首重*0.28; // 若1<计费用重量<=30 计算公式为:(首重+(计费重量-1)*续重1)*0.28; // 若计费重量>30 计算公式为:(首重+(计费重量-1)*续重1+(计费重量-30)*续重2)*0.28; $deliveryPrimaryRegions = DeliveryPrimaryRegionsModel::where('province','=',$item['destination_province']) ->where('city','=',$item['destination_city']) ->find(); if(empty($deliveryPrimaryRegions)){ //记录错误日志 Log::warning('【京东收派服务费明细核对】--月份为:'.$yearMonth."的明细表la_primary_regions_mx当中的[目的省&目的市] 没有匹配到 京东收派服务费一级区域表:la_primary_regions对应的 [目的省&目的市],明细记录id为".$item['id']); return []; } if($item['fee_type'] == '快递改址费' || $item['fee_type'] == '快递_转寄服务费'){ $item['non_cross_warehouse_fee'] = 5; }elseif($item['fee_type'] == '保价费'){ $item['non_cross_warehouse_fee'] = 0.3; }elseif($item['fee_type'] == '快递超长超重'){ $item['non_cross_warehouse_fee'] = $item['billing_weight'] * 1; }elseif($item['fee_type'] == '快递运费'){ if($item['billing_weight'] <= 1){ $item['non_cross_warehouse_fee'] = $deliveryPrimaryRegions->first_wt * 0.28; }elseif($item['billing_weight'] <= 30){ $item['non_cross_warehouse_fee'] = ($deliveryPrimaryRegions->first_wt + ($item['billing_weight'] - 1) * $deliveryPrimaryRegions->add_wt1) * 0.28; }else{ // >30的情况 $item['non_cross_warehouse_fee'] = ($deliveryPrimaryRegions->first_wt + ($item['billing_weight'] - 1) * $deliveryPrimaryRegions->add_wt1 + ($item['billing_weight'] - 30) * $deliveryPrimaryRegions->add_wt2) * 0.28; } }else{ Log::warning('【京东收派服务费明细核对】--月份为:'.$yearMonth."的明细表la_primary_regions_mx当中的[目的省&目的市] 没有匹配到 京东收派服务费一级区域表:la_primary_regions对应的 [目的省&目的市],明细记录id为".$item['id']); return []; } //12、跨仓差异->结算金额-不跨仓收费 $item['cross_warehouse_difference'] = $item['settlement_amount'] - $item['theoretical_billing']; return $item->toArray(); } /** * @notes 根据年月比如2025-05获取当月时间范围 精确到秒 (git有问题未解决 暂时无法写入common.php当中 临时放这) * @param string $yearMonth * @return array * @author 胡军 * @date 2025/07/01 */ private function getMonthTimeRange(string $yearMonth): array { // 验证输入格式 (YYYY-MM) if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $yearMonth)) { throw new InvalidArgumentException('输入格式不正确,必须为YYYY-MM格式'); } list($year, $month) = explode('-', $yearMonth); // 构建开始时间 $startTime = "{$year}-{$month}-01 00:00:00"; // 使用DateTime类计算当月最后一天 $lastDay = (new \DateTime("{$year}-{$month}-01")) ->modify('last day of this month') ->format('d'); // 构建结束时间 $endTime = "{$year}-{$month}-{$lastDay} 23:59:59"; return [ 'startTime' => $startTime, 'endTime' => $endTime ]; } 这是我核算100万数据的代码,虽然使用了分块处理但是我感觉还有优化的间,我之前优化完的代码就很好我可以提供给你代码做参考: /** * @notes 非销售出库明细核对逻辑(优化版) * @param $yearMonth * @return bool * @throws DataNotFoundException * @throws DbException * @throws ModelNotFoundException * @author 胡军 * @date 2025/06/25 */ public function nonSalesFeeVerifyDo($yearMonth): bool { // 获取指定月份的时间范围(月初到月末) $monthTimeRange = $this->getMonthTimeRange($yearMonth); // 预加载并缓存报价数据,使用键值对存储(仓库名 => 报价信息) // 避免在后续循环中重复查询数据库,提高性能 $warehouseQuotes = []; $warehouseList = NonSalesItemizationModel::whereBetween('business_time', [ $monthTimeRange['startTime'], $monthTimeRange['endTime'] ])->group('warehouse_name')->column('warehouse_name'); // 批量获取所有仓库的报价记录 foreach ($warehouseList as $warehouse) { // 获取该仓库的报价记录 $quoteResult = NonSalesQuoteModel::where('warehouse', $warehouse)->select()->toArray(); if (!empty($quoteResult)) { $newQuoteResult = []; foreach ($quoteResult as $item) { $newQuoteResult[$item['fee_item']] = $item; } $warehouseQuotes[$warehouse] = $newQuoteResult; } } $status = true; // 处理状态标识 $batchSize = 500; // 批次大小即每次读取的数据条数 $reconnectInterval = 200; // 每10万条重新连接一次数据库(200批次=10万条) $updateCount = 0; // 记录已更新的总记录数 $batchCount = 0; // 记录当前批次数 try { // 使用游标分批处理数据,每次只加载少量数据到内存 // 避免一次性加载大量数据导致内存溢出 $query = NonSalesItemizationModel::whereBetween('business_time', [ $monthTimeRange['startTime'], $monthTimeRange['endTime'] ]); $query->chunk($batchSize, function ($items) use ( &$status, // 引用传递处理状态 &$updateCount, // 引用传递更新计数 &$batchCount, // 引用传递批次计数 $warehouseQuotes, // 仓库报价数据 $reconnectInterval, // 重连间隔 $yearMonth ) { $batchCount++; $updateBuffer = []; // 批量更新缓冲区,存储待更新的数据 // 遍历当前批次的所有明细记录 foreach ($items as $item) { $warehouseName = $item->warehouse_name; $itemData = $item->toArray(); // 检查仓库是否有对应的报价数据 if (isset($warehouseQuotes[$warehouseName])) { $quoteData = $warehouseQuotes[$warehouseName]; // 处理数量数据,确保不为 $boxCount = !empty($itemData['box_count']) ? $itemData['box_count'] : 0; $looseItems = !empty($itemData['loose_items']) ? $itemData['loose_items'] : 0; // 计算理论费用 $theoreticalFee = $boxCount * $quoteData['整箱']['amount'] + $looseItems * $quoteData['拆零']['amount']; // 计算费用差异 $feeDifference = $itemData['charge_amount'] - $theoreticalFee; } else { // 记录无匹配报价的日志,方便后续排查 Log::warning('【菜鸟非销售出库费明细核对报价单仓库名称不匹配】--月份为:' . $yearMonth . "的订单明细核对匹配不到非销售出库费报价表la_nonsales_fee_quote当中的仓库名称,明细数据id为" . $itemData['id']); // 对于未匹配到的记录,设置为null以便标识 $theoreticalFee = null; $feeDifference = null; } // 准备更新数据,存入缓冲区 $updateBuffer[$itemData['id']] = [ 'theoretical_fee' => $theoreticalFee, // 理论费用 'fee_difference' => $feeDifference // 费用差异 ]; } // 使用高效的批量更新方法将缓冲区数据写入数据库 $this->batchUpdate('la_nosales_fee', $updateBuffer, [ 'theoretical_fee', 'fee_difference' ]); $updateCount += count($updateBuffer); // 更新总计数 // 定期重连数据库,避免长时间运行导致连接断开 if ($batchCount % $reconnectInterval === 0) { $this->reconnectDatabase(); Log::info("菜鸟非销售出库费用核对已处理 {$updateCount} 条,进行数据库重连"); } // 释放内存,避免内存泄漏 unset($items, $updateBuffer); }); } catch (\Exception $e) { // 记录异常信息,确保错误可追溯 Log::error('【菜鸟非销售出库费明细核对异常】--月份为:' . $yearMonth . "的费用核对发生错误:" . $e->getMessage()); $status = false; // 标记处理失败 } // 记录处理完成信息,包含更新的总记录数 Log::info("菜鸟非销售出库费用核对处理完成,共更新 {$updateCount} 条记录"); return $status; } /** * 高效批量更新方法(单SQL更新多条) * @param string $table 表名 * @param array $data 更新数据,格式:[id => ['field1' => 'value1', 'field2' => 'value2']] * @param array $fields 需要更新的字段 */ private function batchUpdate(string $table, array $data, array $fields) { // 数据为时直接返回,避免无效操作 if (empty($data) || empty($fields)) return; $cases = []; // 存储CASE WHEN语句的数组 $ids = []; // 存储所有需要更新的ID $params = []; // 存储预处理语句的参数 // 构建CASE WHEN更新语句(每个字段一个CASE WHEN) foreach ($fields as $field) { $cases[$field] = 'CASE id '; // 为每个ID和对应字段值构建WHEN子句 foreach ($data as $id => $row) { $cases[$field] .= "WHEN {$id} THEN ? "; // 占位符用于预处理语句 $params[] = $row[$field]; // 对应的值 $ids[] = $id; // 记录ID } $cases[$field] .= 'END'; // 结束CASE语句 } // 去重并拼接ID列表 $idsStr = implode(',', array_unique($ids)); // 构建SET子句(每个字段的CASE WHEN语句) $setClauses = []; foreach ($fields as $field) { $setClauses[] = "{$field} = {$cases[$field]}"; } // 构建完整的UPDATE SQL语句 /* 最后的执行sql可以视为(案例sql): UPDATE la_reverse_delivery_fee SET theoretical_amount = CASE id WHEN 1001 THEN 100 -- 当ID=1001时,更新为100 WHEN 1002 THEN 200 -- 当ID=1002时,更新为200 END, fee_difference = CASE id WHEN 1001 THEN 5 -- 当ID=1001时,更新为5 WHEN 1002 THEN 10 -- 当ID=1002时,更新为10 END WHERE id IN (1001,1002); 其实就是巧妙地利用了 SQL 的CASE WHEN语法,将多行更新合并为单条 SQL,是一种常见的数据库优化技巧! */ $sql = "UPDATE {$table} SET " . implode(', ', $setClauses) . " WHERE id IN ({$idsStr})"; // 执行预处理语句,提高安全性和性能 Db::execute($sql, $params); } // 数据库重连方法,用于长时间运行的任务,避免连接超时 private function reconnectDatabase() { try { $connection = \think\facade\Db::connect(); $connection->close(); // 关闭当前连接 $connection->connect(); // 重新建立连接 } catch (\Exception $e) { // 记录重连失败日志,但不中断程序执行 Log::error('【菜鸟非销售出库费用核对数据库重连失败】' . $e->getMessage()); } } /** * @notes 根据年月比如2025-05获取当月时间范围 精确到秒 * @param string $yearMonth * @return array * @author 胡军 * @date 2025/06/25 */ private function getMonthTimeRange(string $yearMonth): array { // 验证输入格式 (YYYY-MM) if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $yearMonth)) { throw new InvalidArgumentException('输入格式不正确,必须为YYYY-MM格式'); } list($year, $month) = explode('-', $yearMonth); // 构建开始时间 $startTime = "{$year}-{$month}-01 00:00:00"; // 使用DateTime类计算当月最后一天 $lastDay = (new \DateTime("{$year}-{$month}-01")) ->modify('last day of this month') ->format('d'); // 构建结束时间 $endTime = "{$year}-{$month}-{$lastDay} 23:59:59"; return [ 'startTime' => $startTime, 'endTime' => $endTime ]; } 请根据我后面给你的优化完的代码使用的技术方案来优化我给你的要优化的代码,记住一定最后要提供完整代码并且我的原有的注释信息请保留下来方便我阅读
07-09
/** * @notes 菜鸟退货入仓费明细核对逻辑(优化版) * @param y e a r M o n t h ∗ @ r e t u r n b o o l ∗ @ t h r o w s D a t a N o t F o u n d E x c e p t i o n ∗ @ t h r o w s D b E x c e p t i o n ∗ @ t h r o w s M o d e l N o t F o u n d E x c e p t i o n ∗ @ a u t h o r 胡军 ∗ @ d a t e 2025 / 06 / 26 ∗ / p u b l i c f u n c t i o n c o m p u t e R e t u r n I t e m V e r i f y D o ( yearMonth∗@returnbool∗@throwsDataNotFoundException∗@throwsDbException∗@throwsModelNotFoundException∗@author胡军∗@date2025/06/26∗/publicfunctioncomputeReturnItemVerifyDo( yearMonth): bool { // 获取指定月份的时间范围(月初到月末) m o n t h T i m e R a n g e = monthTimeRange= this->getMonthTimeRange( y e a r M o n t h ) ; / / 预加载并缓存报价数据,使用键值对存储(仓库名 = > 报价信息) / / 避免在后续循环中重复查询数据库,提高性能 yearMonth);//预加载并缓存报价数据,使用键值对存储(仓库名=>报价信息)//避免在后续循环中重复查询数据库,提高性能 warehouseQuotes = []; w a r e h o u s e L i s t = C o m p u t e R e t u r n I t e m i z a t i o n M o d e l : : w h e r e B e t w e e n ( ′ b u s i n e s s t i m e ′ , [ warehouseList=ComputeReturnItemizationModel::whereBetween( ′ business t ​ ime ′ ,[ monthTimeRange[‘startTime’], m o n t h T i m e R a n g e [ ′ e n d T i m e ′ ] ] ) − > g r o u p ( ′ w a r e h o u s e n a m e ′ ) − > c o l u m n ( ′ w a r e h o u s e n a m e ′ ) ; / / 批量获取所有仓库的首件和每增加一件报价记录 ( 同一个仓库报价必须是两条并且首件报价在前每增加一件报价在后否则计算错误 ) f o r e a c h ( monthTimeRange[ ′ endTime ′ ]])−>group( ′ warehouse n ​ ame ′ )−>column( ′ warehouse n ​ ame ′ );//批量获取所有仓库的首件和每增加一件报价记录(同一个仓库报价必须是两条并且首件报价在前每增加一件报价在后否则计算错误)foreach( warehouseList as ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: …记录 firstResult = ComputeReturnFeeQuoteModel::where(‘warehouse’, w a r e h o u s e ) − > o r d e r ( ′ i d a s c ′ ) − > f i n d ( ) ; warehouse)−>order( ′ idasc ′ )−>find(); firstOrderQuote = f i r s t R e s u l t ? firstResult? firstResult->toArray() : []; // 获取该仓库的每增加一件报价记录 l a s t R e s u l t = C o m p u t e R e t u r n F e e Q u o t e M o d e l : : w h e r e ( ′ w a r e h o u s e ′ , lastResult=ComputeReturnFeeQuoteModel::where( ′ warehouse ′ , warehouse)->order(‘id desc’)->find(); l a s t O r d e r Q u o t e = lastOrderQuote= lastResult ? l a s t R e s u l t − > t o A r r a y ( ) : [ ] ; / / 仅当首件和续件报价都存在时才保存(确保数据完整性) i f ( ! e m p t y ( lastResult−>toArray():[];//仅当首件和续件报价都存在时才保存(确保数据完整性)if(!empty( firstOrderQuote) && !empty(ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … warehouseQuotes[ w a r e h o u s e ] = [ ′ f i r s t ′ = > warehouse]=[ ′ first ′ => firstOrderQuote, // 首件价格信息 ‘last’ => ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 61: …]; }̲ } … status = true; // 处理状态标识 b a t c h S i z e = 500 ; / / 批次大小即每次读取的数据条数 batchSize=500;//批次大小即每次读取的数据条数 reconnectInterval = 200; // 每10万条重新连接一次数据库(200批次=10万条) u p d a t e C o u n t = 0 ; / / 记录已更新的总记录数 updateCount=0;//记录已更新的总记录数 batchCount = 0; // 记录当前批次数 try { // 使用游标分批处理数据,每次只加载少量数据到内存 // 避免一次性加载大量数据导致内存溢出 q u e r y = C o m p u t e R e t u r n I t e m i z a t i o n M o d e l : : w h e r e B e t w e e n ( ′ b u s i n e s s t i m e ′ , [ query=ComputeReturnItemizationModel::whereBetween( ′ business t ​ ime ′ ,[ monthTimeRange[‘startTime’], m o n t h T i m e R a n g e [ ′ e n d T i m e ′ ] ] ) ; monthTimeRange[ ′ endTime ′ ]]); query->chunk( b a t c h S i z e , f u n c t i o n ( batchSize,function( items) use ( &ParseError: KaTeX parse error: Expected 'EOF', got '&' at position 51: … &̲ updateCount, // 引用传递更新计数 & b a t c h C o u n t , / / 引用传递批次计数 batchCount,//引用传递批次计数 warehouseQuotes, // 仓库报价数据 r e c o n n e c t I n t e r v a l , / / 重连间隔 reconnectInterval,//重连间隔 yearMonth ) { b a t c h C o u n t + + ; batchCount++; updateBuffer = []; // 批量更新缓冲区,存储待更新的数据 // 遍历当前批次的所有明细记录 foreach ( i t e m s a s itemsas item) { w a r e h o u s e N a m e = warehouseName= item->warehouse_name; i t e m D a t a = itemData= item->toArray(); t h e o r e t i c a l F e e = 0 ; / / 检查仓库是否有对应的报价数据 i f ( i s s e t ( theoreticalFee=0;//检查仓库是否有对应的报价数据if(isset( warehouseQuotes[ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … quoteData = w a r e h o u s e Q u o t e s [ warehouseQuotes[ warehouseName]; // 将CN订单SKU总重量(克)转换为千克 b u b b l e V o l u m e K g = bubbleVolumeKg= itemData[‘bubble_volume_1kg’] / 1000; q u a n t i t y = quantity= itemData[‘total_goods_quantity’]; // 确定价格类型 p r i c e T y p e = priceType= bubbleVolumeKg <= 0.5 ? ‘tiny’ : ( b u b b l e V o l u m e K g < = 3 ? ′ s m a l l ′ : ( bubbleVolumeKg<=3? ′ small ′ :( bubbleVolumeKg <= 5 ? ‘medium’ : ( b u b b l e V o l u m e K g < = 10 ? ′ l a r g e ′ : ′ t e n ′ ) ) ) ; / / 计算费用 bubbleVolumeKg<=10? ′ large ′ : ′ ten ′ )));//计算费用 theoreticalFee += ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: …irst']["price_{ priceType}"]; if (ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … theoreticalFee += ( q u a n t i t y − 1 ) ∗ quantity−1)∗ quoteData[‘last’]["price_{ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 11: priceType}̲"]; … yearMonth . “的订单明细核对匹配不到退货入仓费报价表la_compute_return_fee_quote当中的仓库名称,明细数据id为” . i t e m D a t a [ ′ i d ′ ] ) ; / / 未匹配时设置为 n u l l 以便标识 itemData[ ′ id ′ ]);//未匹配时设置为null以便标识 theoreticalFee = null; } // 准备更新数据,存入缓冲区 u p d a t e B u f f e r [ updateBuffer[ itemData[‘id’]] = [ ‘theoretical_fee’ => t h e o r e t i c a l F e e , / / 理论费 用 ′ f e e d i f f e r e n c e ′ = > theoreticalFee,//理论费用 ′ fee d ​ ifference ′ => itemData[‘billing_amount’] - (ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 71: … }̲ … this->batchUpdate(‘la_compute_return_fee’, u p d a t e B u f f e r , [ ′ t h e o r e t i c a l f e e ′ , ′ f e e d i f f e r e n c e ′ ] ) ; updateBuffer,[ ′ theoretical f ​ ee ′ , ′ fee d ​ ifference ′ ]); updateCount += count( u p d a t e B u f f e r ) ; / / 更新总计数 / / 定期重连数据库,避免长时间运行导致连接断开 i f ( updateBuffer);//更新总计数//定期重连数据库,避免长时间运行导致连接断开if( batchCount % ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … this->reconnectDatabase(); Log::info(“菜鸟退货入仓费用核对已处理 {ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 13: updateCount}̲ 条,进行数据库重连"); … items, ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 29: …); }̲); } ca… e) { // 记录异常信息,确保错误可追溯(保留原始注释逻辑) Log::error(‘【菜鸟退货入仓费明细核对异常】–月份为:’ . y e a r M o n t h . " 的费用核对发生错误 : " . yearMonth."的费用核对发生错误:". e->getMessage()); ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 37: …标记处理失败 }̲ // 记录处… updateCount} 条记录”); return $ status; } /** * 高效批量更新方法(单SQL更新多条) * @param string $ table 表名 * @param array $ data 更新数据,格式:[id => ['field1' => 'value1', 'field2' => 'value2']] * @param array $ fields 需要更新的字段 */ private function batchUpdate(string $ table, array $ data, array $ fields) { // 数据为时直接返回,避免无效操作 if (empty($ data) || empty($ fields)) return; $ cases = []; // 存储CASE WHEN语句的数组 $ ids = []; // 存储所有需要更新的ID $ params = []; // 存储预处理语句的参数 // 构建CASE WHEN更新语句(每个字段一个CASE WHEN) foreach ($ fields as $ field) { $ cases[$ field] = 'CASE id '; // 为每个ID和对应字段值构建WHEN子句 foreach ($ data as $ id => $ row) { $ cases[$ field] .= "WHEN {$ id} THEN ? "; // 占位符用于预处理语句 $ params[] = $ row[$ field]; // 对应的值 $ ids[] = $ id; // 记录ID } $ cases[$ field] .= 'END'; // 结束CASE语句 } // 去重并拼接ID列表 $ idsStr = implode(',', array_unique($ ids)); // 构建SET子句(每个字段的CASE WHEN语句) $ setClauses = []; foreach ($ fields as $ field) { $ setClauses[] = "{$ field} = {$ cases[$ field]}"; } // 构建完整的UPDATE SQL语句(保留SQL案例注释) /* 最后的执行sql可以视为(案例sql): UPDATE la_reverse_delivery_fee SET theoretical_amount = CASE id WHEN 1001 THEN 100 -- 当ID=1001时,更新为100 WHEN 1002 THEN 200 -- 当ID=1002时,更新为200 END, fee_difference = CASE id WHEN 1001 THEN 5 -- 当ID=1001时,更新为5 WHEN 1002 THEN 10 -- 当ID=1002时,更新为10 END WHERE id IN (1001,1002); 其实就是巧妙地利用了 SQL 的CASE WHEN语法,将多行更新合并为单条 SQL,是一种常见的数据库优化技巧! */ $ sql = "UPDATE {$ table} SET " . implode(', ', $ setClauses) . " WHERE id IN ({$ idsStr})"; // 执行预处理语句,提高安全性和性能 Db::execute($ sql, $ params); } // 数据库重连方法,用于长时间运行的任务,避免连接超时 private function reconnectDatabase() { try { $ connection = \think\facade\Db::connect(); $ connection->close(); // 关闭当前连接 $ connection->connect(); // 重新建立连接 } catch (\Exception $ e) { // 记录重连失败日志,但不中断程序执行 Log::error('【菜鸟退货入仓费用核对数据库重连失败】' . $ e->getMessage()); } } /** * @notes 根据年月比如2025-05获取当月时间范围 精确到秒 * @param string y e a r M o n t h ∗ @ r e t u r n a r r a y ∗ @ a u t h o r 胡军 ∗ @ d a t e 2025 / 06 / 23 ∗ / p r i v a t e f u n c t i o n g e t M o n t h T i m e R a n g e ( s t r i n g yearMonth∗@returnarray∗@author胡军∗@date2025/06/23∗/privatefunctiongetMonthTimeRange(string yearMonth): array { // 验证输入格式 (YYYY-MM) if (!preg_match(‘/^\d{4}-(0[1-9]|1[0-2]) / ′ , / ′ , yearMonth)) { throw new InvalidArgumentException(‘输入格式不正确,必须为YYYY-MM格式’); } list( y e a r , year, month) = explode(’-', y e a r M o n t h ) ; / / 构建开始时间 yearMonth);//构建开始时间 startTime = “{ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 6: year}̲-{ month}-01 00:00:00”; // 使用DateTime类计算当月最后一天 ParseError: KaTeX parse error: Undefined control sequence: \DateTime at position 17: …lastDay = (new \̲D̲a̲t̲e̲T̲i̲m̲e̲("{ year}-{ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 7: month}̲-01")) … endTime = "{ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 6: year}̲-{ month}-{ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 9: lastDay}̲ 23:59:59"; … startTime, ‘endTime’ => $ endTime ]; } 这是我之前的业务模块处理100万数据核对的代码,也是您给出的方案,那么我上次提问的代码: /** * @notes 京东仓储服务费核对逻辑 * @param y e a r M o n t h ∗ @ r e t u r n b o o l ∗ @ t h r o w s D a t a N o t F o u n d E x c e p t i o n ∗ @ t h r o w s D b E x c e p t i o n ∗ @ t h r o w s M o d e l N o t F o u n d E x c e p t i o n ∗ @ a u t h o r 胡军 ∗ @ d a t e 2025 / 06 / 27 ∗ / p u b l i c f u n c t i o n w a r e H o u s i n g F e e V e r i f y D o ( yearMonth∗@returnbool∗@throwsDataNotFoundException∗@throwsDbException∗@throwsModelNotFoundException∗@author胡军∗@date2025/06/27∗/publicfunctionwareHousingFeeVerifyDo( yearMonth):bool{ m o n t h T i m e R a n g e = monthTimeRange= this->getMonthTimeRange( y e a r M o n t h ) ; / / 获取时间范围内的数据并进行分组统计 / / t o t a l q u a n t i t y 总数 / / t o t a l s e t t l e m e n t a m o u n t 总的结算金额 yearMonth);//获取时间范围内的数据并进行分组统计//total q ​ uantity总数//total s ​ ettlement a ​ mount总的结算金额 itemizationMonthList = WareHousingFeesItemizationModel::whereBetween(‘business_time’, [ m o n t h T i m e R a n g e [ ′ s t a r t T i m e ′ ] , monthTimeRange[ ′ startTime ′ ], monthTimeRange[‘endTime’]]) ->field([ ‘document_number’, ‘document_type’, ‘SUM(quantity) as total_quantity’, ‘SUM(settlement_amount) as total_settlement_amount’ ]) ->group(‘document_number, document_type’) ->select() ->toArray(); //一次性读取报价单避免foreach循环 提升效率 q u o t e L i s t = W a r e H o u s i n g F e e s Q u o t e M o d e l : : s e l e c t ( ) − > t o A r r a y ( ) ; quoteList=WareHousingFeesQuoteModel::select()−>toArray(); quoteListRst = []; foreach ( q u o t e L i s t a s quoteListas item) { q u o t e L i s t R s t [ quoteListRst[ item[‘service_type’]] = ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 16: item; }̲ if(!em… quoteListRst)){ foreach( i t e m i z a t i o n M o n t h L i s t a s itemizationMonthListas key => ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … itemizationMonthList[ k e y ] [ ′ t h e o r e t i c a l a m o u n t ′ ] = 0 ; i f ( key][ ′ theoretical a ​ mount ′ ]=0;if( value[‘document_type’] == ‘出库单’ && !empty(ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … // value[‘total_quantity’] 数量 if(ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … itemizationMonthList[ k e y ] [ ′ t h e o r e t i c a l a m o u n t ′ ] = key][ ′ theoretical a ​ mount ′ ]= quoteListRst[‘出库单’][‘first_three_items’]; } else { i t e m i z a t i o n M o n t h L i s t [ itemizationMonthList[ key][‘theoretical_amount’] = q u o t e L i s t R s t [ ′ 出库 单 ′ ] [ ′ f i r s t t h r e e i t e m s ′ ] + ( quoteListRst[ ′ 出库单 ′ ][ ′ first t ​ hree i ​ tems ′ ]+( value[‘total_quantity’] - 3) * ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 63: … }̲ … value[‘document_type’] == ‘退供单’ && !empty(ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … itemizationMonthList[ k e y ] [ ′ t h e o r e t i c a l a m o u n t ′ ] = key][ ′ theoretical a ​ mount ′ ]= quoteListRst[‘退供单’][‘first_three_items’] * ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 43: … }̲ … value[‘document_type’] == ‘退货单’ && !empty(ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … if( value[‘total_quantity’] <= 3){ i t e m i z a t i o n M o n t h L i s t [ itemizationMonthList[ key][‘theoretical_amount’] = ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 64: … }̲ else { … itemizationMonthList[ k e y ] [ ′ t h e o r e t i c a l a m o u n t ′ ] = key][ ′ theoretical a ​ mount ′ ]= quoteListRst[‘退货单’][‘first_three_items’] + ( v a l u e [ ′ t o t a l q u a n t i t y ′ ] − 3 ) ∗ value[ ′ total q ​ uantity ′ ]−3)∗ quoteListRst[‘退货单’][‘additional_items’]; } } //正常计算出来的理论金额不应该是0 那么这个时候就要记录日志便于排查 if( i t e m i z a t i o n M o n t h L i s t [ itemizationMonthList[ key][‘theoretical_amount’] == 0){ //echo v a l u e [ ′ d o c u m e n t n u m b e r ′ ] . P H P E O L ; L o g : : w a r n i n g ( ′ 【京东仓储服务费明细核对】 − − 月份为 : ′ . value[ ′ document n ​ umber ′ ].PHP E ​ OL;Log::warning( ′ 【京东仓储服务费明细核对】−−月份为: ′ . yearMonth.“的京东仓储服务费订单明细核对匹配不到京东仓储服务费报价表la_storage_service_quotes当中的类型,明细单据编号为”. v a l u e [ ′ d o c u m e n t n u m b e r ′ ] ) ; u n s e t ( value[ ′ document n ​ umber ′ ]);unset( itemizationMonthList[ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 55: … }̲else{ … itemizationMonthList[ k e y ] [ ′ b a l a n c e ′ ] = key][ ′ balance ′ ]= value[‘total_settlement_amount’] - i t e m i z a t i o n M o n t h L i s t [ itemizationMonthList[ key][‘theoretical_amount’]; } } //批量分批次更新数据库 s t a t u s = t r u e ; status=true; batchSize = 50; // 每批10条记录 t o t a l C o u n t = c o u n t ( totalCount=count( itemizationMonthList); b a t c h C o u n t = c e i l ( batchCount=ceil( totalCount / b a t c h S i z e ) ; batchSize); itemizationModel = new WareHousingFeesItemVeryModel(); for ( i = 0 ; i=0; i < b a t c h C o u n t ; batchCount; i++) { b a t c h D a t a = a r r a y s l i c e ( batchData=array s ​ lice( itemizationMonthList, i ∗ i∗ batchSize, b a t c h S i z e ) ; batchSize); documentNumbers = array_column(ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … itemizationModel->startTrans(); // 批量删除操作(根据单据号) if (!empty(ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … itemizationModel->whereIn(‘document_number’, ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 50: … }̲ … itemizationModel->saveAll( b a t c h D a t a ) ; batchData); itemizationModel->commit(); } catch (\Exception ParseError: KaTeX parse error: Expected '}', got 'EOF' at end of input: … itemizationModel->rollback(); //记录日志 Log::error(‘【京东仓储服务费明细核对异常】–月份为:’. y e a r M o n t h . " 的费用核对发生错误 : " . yearMonth."的费用核对发生错误:". e->getMessage()); //其中一个批次数据处理失败则直接退出循环 不再继续执行后续批次的数据处理 报错给前端显示 ParseError: KaTeX parse error: Expected 'EOF', got '}' at position 61: … }̲ } … status; }else{ return false; } } /** * @notes 根据年月比如2025-05获取当月时间范围 精确到秒 (git有问题未解决 暂时无法写入common.php当中 临时放这) * @param string $ yearMonth * @return array * @author 胡军 * @date 2025/06/27 */ private function getMonthTimeRange(string $ yearMonth): array { // 验证输入格式 (YYYY-MM) if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $ yearMonth)) { throw new InvalidArgumentException('输入格式不正确,必须为YYYY-MM格式'); } list($ year, $ month) = explode('-', $ yearMonth); // 构建开始时间 $ startTime = "{$ year}-{$ month}-01 00:00:00"; // 使用DateTime类计算当月最后一天 $ lastDay = (new \DateTime("{$ year}-{$ month}-01")) ->modify('last day of this month') ->format('d'); // 构建结束时间 $ endTime = "{$ year}-{$ month}-{$ lastDay} 23:59:59"; return [ 'startTime' => $ startTime, 'endTime' => $ endTime ]; } 是不是也可以按照这种优化思路去改造呢?
07-08
【四轴飞行器】非线性三自由度四轴飞行器模拟器研究(Matlab代码实现)内容概要:本文围绕非线性三自由度四轴飞行器模拟器的研究展开,重点介绍了基于Matlab的建模仿真方法。通过对四轴飞行器的动力学特性进行分析,构建了非线性状态间模型,并实现了姿态位置的动态模拟。研究涵盖了飞行器运动方程的建立、控制系统设计及数值仿真验证等环节,突出非线性系统的精确建模仿真优势,有助于深入理解飞行器在复杂工况下的行为特征。此外,文中还提到了多种配套技术如PID控制、状态估计路径规划等,展示了Matlab在航航天仿真中的综合应用能力。; 适合人群:具备一定自动控制理论基础和Matlab编程能力的高校学生、科研人员及从事无人机系统开发的工程技术人员,尤其适合研究生及以上层次的研究者。; 使用场景及目标:①用于四轴飞行器控制系统的设计验证,支持算法快速原型开发;②作为教学工具帮助理解非线性动力学系统建模仿真过程;③支撑科研项目中对飞行器姿态控制、轨迹跟踪等问题的深入研究; 阅读建议:建议读者结合文中提供的Matlab代码进行实践操作,重点关注动力学建模控制模块的实现细节,同时可延伸学习文档中提及的PID控制、状态估计等相关技术内容,以全面提升系统仿真分析能力。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值