preg_match_all返回空数组?常见错误排查与正确用法详解,新手必看

第一章:preg_match_all返回空数组的典型现象

在使用 PHP 的 preg_match_all 函数进行正则匹配时,开发者常遇到函数返回空数组的情况。这种现象通常并非函数本身出错,而是由正则表达式书写不当、目标字符串不匹配或修饰符使用错误导致。

常见原因分析

  • 正则表达式语法错误,例如未正确转义特殊字符
  • 目标字符串中不存在符合模式的内容
  • 遗漏定界符或使用了不支持的修饰符
  • 编码问题导致字符串与正则无法匹配(如 UTF-8 与 ASCII 混用)

调试方法与代码示例

通过以下代码可验证匹配结果并排查问题:
// 示例:提取所有邮箱地址
$pattern = '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/';
$subject = '联系邮箱:admin@example.com 和 support@domain.org';

$matches = [];
$result = preg_match_all($pattern, $subject, $matches);

if ($result === false) {
    echo "正则表达式错误";
} else {
    var_dump($matches[0]); // 输出匹配到的邮箱
}
上述代码中,$matches 是一个引用数组,用于存储所有匹配结果。若返回为空数组且无报错,则说明未找到匹配项。

常见正则修饰符对照表

修饰符作用
i忽略大小写匹配
u启用 UTF-8 模式,处理多字节字符
s使点号 '.' 匹配包括换行在内的所有字符
m启用多行模式,^ 和 $ 可匹配每行起止位置
若字符串包含中文或特殊符号,应确保正则表达式使用 u 修饰符,例如:
$pattern = '/\p{Han}+/u'; // 匹配连续的汉字
$subject = '你好世界 World';
preg_match_all($pattern, $subject, $matches);
var_dump($matches[0]); // 输出: array('你好世界')

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

2.1 函数语法与参数详解:深入解析模式匹配流程

在函数式编程中,模式匹配是核心机制之一,它允许根据输入数据的结构执行不同的逻辑分支。该机制不仅提升了代码可读性,还增强了类型安全性。
模式匹配的基本语法结构
func matchValue(x interface{}) string {
    switch v := x.(type) {
    case int:
        return "整数类型"
    case string:
        return "字符串类型"
    default:
        return "未知类型"
    }
}
上述代码展示了Go语言中通过类型断言实现的模式匹配。x.(type) 是类型开关的关键语法,变量 v 将绑定到具体类型实例,进而执行对应分支逻辑。
匹配优先级与穷尽性检查
  • 模式按书写顺序自上而下匹配,优先匹配最先符合的分支
  • 必须覆盖所有可能情况以避免运行时遗漏
  • 编译器可在部分语言(如Rust、Scala)中静态验证穷尽性

2.2 捕获组与分隔符的作用:影响结果的关键因素

在正则表达式中,捕获组和分隔符的设计直接影响匹配结果的结构与提取效率。捕获组通过圆括号 () 定义,用于提取子字符串。
捕获组的基本用法
(\d{4})-(\d{2})-(\d{2})
该正则用于匹配日期格式如 2023-05-10。三个捕获组分别提取年、月、日。第一个组 (\d{4}) 捕获年份,第二个和第三个依次捕获月份和日期。
分隔符的影响
使用不同的分隔符会影响匹配精度:
  • 连字符 - 常见于日期
  • 斜杠 / 多用于路径或URL
  • 点号 . 需转义以避免通配符含义
合理设计分隔符可提升正则表达式的鲁棒性与可读性。

2.3 模式修饰符对匹配行为的影响:实战案例分析

在正则表达式中,模式修饰符显著改变匹配行为。例如,i 修饰符启用不区分大小写的匹配,而 g 实现全局搜索。
常见修饰符效果对比
  • i:忽略大小写,如 /hello/i 可匹配 "Hello" 或 "HELLO"
  • g:全局匹配,返回所有结果而非首个匹配项
  • m:多行模式,使 ^$ 匹配每行起止位置
实战代码示例
const text = "Hello\nHELLO";
const regex = /^hello$/gm;
console.log(text.match(regex)); // 输出: ["Hello", "HELLO"]
上述代码中,g 确保找到所有匹配,m 使行首行尾锚点在多行中生效,结合 i(隐含需求)可完整覆盖大小写变体。

2.4 匹配失败的底层原因:从正则引擎角度剖析

回溯机制与贪婪匹配
正则引擎在执行匹配时,常采用回溯算法尝试所有可能路径。当使用贪婪量词(如 *+)时,引擎会尽可能多地捕获字符,随后在无法继续时逐步释放字符以尝试匹配。
a.*b
该模式试图匹配以 a 开头、b 结尾的字符串。若文本为 axbxb,引擎首次捕获整个字符串,但在末尾未能找到 b 时将逐个回退,直至找到合适位置。
常见失败场景对比
场景原因解决方案
过度回溯模式复杂导致性能下降使用非捕获组或惰性匹配
字符编码不匹配未启用 Unicode 模式添加 u 标志

2.5 多重匹配与偏移量控制:确保完整遍历目标文本

在正则表达式处理中,单一匹配往往无法覆盖目标文本中的所有符合条件的子串。为了实现完整遍历,必须启用多重匹配机制,并精确控制匹配的起始偏移量。
偏移量递增策略
每次成功匹配后,需将当前匹配结束位置作为下一次搜索的起始偏移,避免遗漏相邻或重叠的模式。
  • 初始偏移设为0
  • 每次匹配后更新偏移至匹配结束位置
  • 循环直至无更多匹配项
let regex = /ab/g;
let text = "ababcab";
let match;
while ((match = regex.exec(text)) !== null) {
  console.log(`匹配内容: ${match[0]}, 位置: ${match.index}`);
}
上述代码中,g 标志启用全局匹配,regex.exec() 返回每次匹配结果并自动更新内部偏移,确保遍历整个字符串。

第三章:常见错误场景及排查方法

3.1 忽略定界符导致模式解析失败:经典陷阱演示

在正则表达式或字符串解析场景中,开发者常因忽略定界符而导致模式匹配失败。一个典型案例如下:
^\d{3}-\d{2}-\d{4}$
该正则本意是匹配格式为 123-45-6789 的社会保险号,但在某些语言(如PHP)中若未使用定界符包裹,则会引发语法错误或解析异常。例如,正确写法应为:
preg_match('/^\d{3}-\d{2}-\d{4}$/', $input)
其中斜杠 / 作为定界符标识模式起止。若省略,引擎将无法识别模式边界。
常见定界符使用对比
语言是否需要显式定界符示例
PHP/pattern/
JavaScript/pattern/
Goregexp.MustCompile("pattern")
忽视这一差异会导致跨语言移植时的隐蔽错误。

3.2 转义字符处理不当引发的匹配遗漏:修复策略

在正则表达式或字符串解析场景中,转义字符(如反斜杠 \)若未被正确识别,常导致模式匹配失败或数据误判。
常见问题示例
例如,在路径匹配中,Windows 路径 C:\temp\file.txt 若直接用于正则表达式,反斜杠会被视为转义符而非字面量,造成匹配遗漏。
// 错误写法:未处理转义
pattern := "C:\temp\file.txt"
matched, _ := regexp.MatchString(pattern, filePath) // 可能无法匹配
该代码中,\t 被解释为制表符,而非路径中的 \t 字符。
修复方案
  • 使用原始字符串(raw string)避免转义解析
  • 对特殊字符进行双重转义
  • 预处理输入,统一转义格式
// 正确写法:使用原始字符串
pattern := `C:\temp\file.txt` // Go 中反引号表示原始字符串
matched, _ := regexp.MatchString(pattern, filePath) // 正确匹配
通过使用原始字符串,确保反斜杠作为字面量参与匹配,从根本上规避转义错误。

3.3 UTF-8编码与中文文本匹配问题:跨语言支持方案

在处理多语言文本时,UTF-8 编码成为跨语言支持的核心。它以变长字节(1–4 字节)表示 Unicode 字符,对中文等非拉丁字符提供良好兼容。
中文字符的 UTF-8 编码特征
中文汉字通常占用 3 个字节,例如“中”的 UTF-8 编码为 E4 B8 AD。正则表达式若未正确识别字节边界,可能导致匹配错位。
常见匹配问题示例

// 错误的字符串截取可能导致乱码
const text = "中文测试";
console.log(text.substring(0, 2)); // 可能输出乱码字符
上述代码因按字符索引截断 UTF-8 字节流,破坏了多字节编码结构。
解决方案对比
方案优势局限性
使用 Unicode-aware API准确处理多语言字符部分旧环境不支持
转为 Unicode 码点操作避免字节级错误性能开销略高
推荐始终使用支持 Unicode 的正则引擎(如 ES6 的 /u 标志)和安全字符串方法,确保跨语言文本处理的准确性。

第四章:正确使用preg_match_all的最佳实践

4.1 构建可靠的正则表达式:从需求到实现的转化

在实际开发中,正则表达式的构建需从明确需求出发,逐步转化为精确的模式匹配逻辑。首先应分析目标文本的结构特征,识别关键标识符与可变部分。
常见匹配场景示例
例如,验证邮箱格式时,需涵盖用户名、@符号、域名及顶级域:

^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
该表达式中,^$ 确保完整匹配;[a-zA-Z0-9._%+-]+ 允许合法用户名字符;@ 字面量分隔本地域与域名;末尾 \.[a-zA-Z]{2,} 强制至少两个字母的顶级域。
构建步骤清单
  • 明确匹配目标(如电话号码、URL等)
  • 分解字符串结构为固定与可变部分
  • 选择合适的元字符与量词
  • 通过测试用例验证边界情况

4.2 结果数组结构解析与数据提取技巧

在处理API响应或数据库查询结果时,结果数组通常以嵌套JSON形式存在。理解其层级结构是高效提取数据的前提。
典型结构示例
[
  {
    "id": 1,
    "name": "Alice",
    "meta": {
      "active": true,
      "roles": ["admin", "user"]
    }
  }
]
该结构包含基础字段(id, name)和嵌套对象(meta),需逐层访问。
数据提取方法
  • 使用点符号访问嵌套属性:item.meta.active
  • 结合map()批量提取特定字段
  • 利用解构赋值简化深层取值
安全取值建议
为避免undefined错误,推荐使用可选链操作符:
const role = data[0]?.meta?.roles[0] || 'guest';
此方式能有效防止因层级缺失导致的运行时异常,提升代码健壮性。

4.3 性能优化建议:避免回溯失控和冗余匹配

在正则表达式处理中,回溯失控是导致性能急剧下降的常见原因。当模式包含大量可选分支或嵌套量词时,引擎可能尝试指数级的匹配路径。
使用非捕获组与惰性匹配
优先采用非贪婪量词和非捕获组以减少不必要的分支尝试:
(?:https?://)(\S+?)
上述模式中,(?:...) 避免创建捕获组,? 使 \S+ 惰性匹配,尽早结束。
避免嵌套量词
(a+)* 类结构易引发灾难性回溯。应重构为原子组或固化分组:
(?>a+)+
使用占有型括号 (?>...) 防止回退,提升执行效率。
  • 优先使用字符类而非多选分支,如 [abc] 优于 a|b|c
  • 限制量词范围,例如用 {1,10} 替代 +*

4.4 实际应用场景示例:日志解析与HTML标签提取

在运维监控和数据清洗场景中,正则表达式广泛应用于日志解析与HTML标签提取。
日志行结构化提取
以Nginx访问日志为例,匹配IP、时间、请求方法与状态码:
^(\d+\.\d+\.\d+\.\d+) - - \[(.*?)\] "(GET|POST) (.*?) HTTP.*" (\d{3})
该模式逐段捕获客户端IP、访问时间、HTTP方法、路径及响应状态码,便于后续导入数据库或进行异常分析。
HTML标签内容抽取
从网页片段中提取所有链接文本与URL:
<a\s+href=["']([^"']+)["']>(.*?)</a>
使用非贪婪匹配分离URL与锚文本,适用于爬虫预处理或内容审计。
  • 日志解析提升故障排查效率
  • HTML提取支持信息聚合与安全检测

第五章:总结与进阶学习建议

持续构建实战项目以巩固技能
真实项目是检验技术掌握程度的最佳方式。建议开发者定期参与开源项目或自主搭建全栈应用,例如使用 Go 构建 RESTful API 并集成 PostgreSQL 数据库:

package main

import (
    "database/sql"
    "net/http"
    _ "github.com/lib/pq"
)

func main() {
    db, _ := sql.Open("postgres", "user=dev dbname=appdb sslmode=disable")
    defer db.Close()

    http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
        rows, _ := db.Query("SELECT id, name FROM users")
        defer rows.Close()
        // 处理结果集...
    })

    http.ListenAndServe(":8080", nil)
}
制定系统化的学习路径
避免碎片化学习,推荐按阶段提升能力:
  1. 掌握核心语言特性与并发模型
  2. 深入理解依赖管理与模块化设计
  3. 学习微服务架构与 gRPC 通信机制
  4. 实践 CI/CD 流程,集成 GitHub Actions 自动化部署
利用社区资源加速成长
积极参与技术社区能有效解决实际问题。以下平台值得长期关注:
  • Gopher Slack 频道中的 #performance 与 #databases 讨论组
  • GitHub 上高星项目如 gin-gonic/ginhashicorp/nomad
  • Go 官方博客发布的性能优化案例分析
[本地开发] → [Git 提交] → [CI 测试] → [Docker 构建] → [K8s 部署]
/** * @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 ]; } 我需要通过上面的代码去处理了数据库100万的数据进行费用核对,但是我感觉性能肯定是受影响的,请根据我的代码业务逻辑不变的前提下,去优化一下代码 ,提升整体性能
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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值