本文整理 DGJ2 模板下载、文件上传、Excel 解析、逐行校验、批量落库、错误文件、同步导出、OSS 下载和大文件治理。项目中存在多代 Excel 实现,维护前必须先识别当前入口使用哪套工具。
适用于秒杀活动、商品/价格/客户基础资料、采购销售单据、盘点库存、微仓、活动、财务流水和报表的导入导出开发与故障排查。
1. 业务目标
- 从页面入口定位控制器、解析工具、Service、业务表和文件存储位置。
- 保证模板、解析、错误文件和导出字段四者口径一致。
- 区分“全量失败”“逐行部分成功”“仅解析预览”“异步任务”四种原子性。
- 正确处理编码、长数字、金额、日期、空单元格、公式和超过 26 列的数据。
- 控制大文件的内存、执行时间、数据库批次、临时文件和 OSS 生命周期。
- 让用户能知道成功多少、失败多少、失败在哪一行、如何修正后重试。
2. 导入导出全景
flowchart LR
T["下载模板"] --> F["用户填写文件"]
F --> U["上传/拉取OSS文件"]
U --> P["Excel解析"]
P --> H["表头映射"]
H --> V["逐行格式与业务校验"]
V --> MODE{"处理模式"}
MODE -- 全量原子 --> ALL["任一错误全部拒绝"]
MODE -- 部分成功 --> PART["有效行批量落库"]
MODE -- 预览 --> PRE["返回标准化列表"]
ALL --> RESP["错误摘要"]
PART --> ERR["错误Excel/下载地址"]
PRE --> SAVE["前端确认后统一保存"]
RESP --> FIX["修正后重试"]
ERR --> FIX
导出主链:
flowchart LR
Q["查询条件与权限"] --> D["查询/聚合数据"]
D --> M["字段映射与格式化"]
M --> W["写Excel/CSV"]
W --> MODE{"返回方式"}
MODE -- 同步流 --> HTTP["php://output"]
MODE -- 本地文件 --> LOCAL["UPLOAD_PATH"]
MODE -- 对象存储 --> OSS["上传OSS"]
OSS --> CLEAN["删除临时文件"]
HTTP --> USER["用户下载"]
LOCAL --> USER
OSS --> USER
3. 入口地图
| 业务 | 入口 | 说明 |
|---|---|---|
| 基础资料 | application/controllers/basedata/Import.php | 采购、销售退货、调拨等多种历史导入 |
| 商品/客户/货位 | application/controllers/basedata/Invlocation.php | 多套解析和错误文件逻辑集中区 |
| 价格管理 | application/controllers/basedata/PriceManager.php | 价格导入、校验和批次更新 |
| 秒杀活动 | application/controllers/inner/activity/FlashActivity.php | OPS 模板、商品导入、活动/商品导出 |
| 普通活动 | application/controllers/inner/activity/Activity.php | 多类活动商品、服务站、套包导入 |
| 采购 | application/controllers/scm/InvPo.php | 采购导出、到货/退货导入 |
| 销售 | application/controllers/scm/InvSa.php、PdImport.php | 销售、出库、导入错误文件 |
| 库存/盘点 | application/controllers/scm/InvOi.php | 安全库存、盘点和库存差异 |
| 财务 | application/controllers/scm/Payment.php、Receipt.php | 收付款、账户、授信流水导出 |
| 微仓 | application/Services/MoveMall/MoveStoSer.php、MoveSupplySer.php | 商品和供给导入 |
| 报表 | application/controllers/reports/*.php | 采购、销售、库存、利润等报表导出 |
| CLI 导入任务 | application/controllers/tasks/Import.php | 历史数据批处理和缓存初始化 |
4. Excel 工具代际
4.1 工具矩阵
| 工具 | 底层库 | 读/写 | 典型特点 | 主要风险 |
|---|---|---|---|---|
Components/ExcelHelper.php | PHPExcel | 读写 | 使用广、支持错误文件和报表 | 通用导出列数组仅 A-Z |
Utils/NewExcelUtil.php | PHPExcel | 读写 | 返回 A/B/C 列键和错误表头 | 上传类型配置过宽、列仅 A-Z |
Services/BaseData/ExcelService.php | PHPExcel/旧 Excel reader | 读写 | OPS/站管家双上传、模板缓存、OSS | 全表进内存、始终 Excel5 写出 |
Services/Help/ExcelService.php | 同类实现 | 读写 | Help 域复用 | 与 BaseData 同名,引用易混 |
Components/ExportExcel.php | PhpSpreadsheet | 写 | 主子行合并、样式、OSS | 同步构建整本工作簿,内存高 |
Components/Excel.php | PHPExcel | 写 | 简单导出 | 能力有限 |
| 控制器自定义 | PHPExcel/sys_csv | 多为写 | 快速满足历史页面 | 契约、编码、列宽和上限不统一 |
4.2 选型原则
- 先复用业务模块已经使用的工具,避免同一模板读写采用不同数据类型规则。
- 超过 26 列时不能直接使用
ExcelHelper::exportExcel/exportExcel2的固定 A-Z 列数组。 - 主单加明细需要纵向合并时可考虑
ExportExcel。 - 纯大数据导出优先评估流式 CSV,而不是把整本 Excel 放内存。
- OPS 需要返回下载 URL 时使用“本地临时文件 -> OSS -> 删除本地”模式。
- 错误文件必须保留原始列和错误原因,不能只返回一段截断错误文本。
代码审计提示:项目里不止一种“列序号转字母”实现,部分历史算法在 Z、AA 等边界存在可疑下标计算。新增宽表导出应直接使用库提供的列转换 API,并用 25/26/27/51/52 列做边界测试。
5. 文件类型与上传
统一可信扩展名为:
xls
xlsx
RegularConst::EXCEL_EXTENSION 和 ExcelHelper::readExcel 都按扩展名选择 Excel5/Excel2007 Reader。
5.1 站管家普通上传
ExcelService::upload 使用 CI Upload:
allowed_types=xls|xlsx- 文件名重命名为唯一值
- 保存到
UPLOAD_PATH/excel/upload/{sid} - 读取第一张 Sheet
- 第一行作为表头
- 后续行按表头列数补空值并 trim
5.2 OPS 上传
OPS 传入对象存储文件描述,Service 通过文件服务获取临时下载地址,再保存到本地站点目录后解析。业务文档和日志不得记录真实签名 URL、存储凭证或完整请求头。
5.3 历史上传风险
NewExcelUtil::upload_file 当前配置 allowed_types=*,实际安全依赖后续 Reader 是否能解析。新功能不要复制这一模式;应同时验证:
- 扩展名白名单。
- MIME/文件签名。
- 文件大小。
- 文件名不参与最终路径。
- Reader 只读数据,不执行宏和外部链接。
- 解析失败后删除临时文件。
6. 解析行为
ExcelHelper::readExcel 的关键行为:
- 根据扩展名选择 Excel5 或 Excel2007。
- 加载整个工作簿。
- 只取第 1 个 Sheet。
- 调用
toArray()返回二维数组。
这意味着:
- 公式单元格、日期序列值和格式化显示值要单独验证。
- 大文件会把整张表和对象模型放入 PHP 内存。
- 第二个及后续 Sheet 默认被忽略。
- 合并单元格通常只有左上角有值。
- 隐藏行、隐藏列不会自动代表忽略。
7. 表头契约
7.1 固定列序
历史导入常用 A/B/C 位置映射。模板一旦插列、换序或改标题,解析可能整体错位。
7.2 标题映射
秒杀导入采用标题映射:
- trim 表头。
- 删除空格和换行。
- 用别名字典映射到字段。
- 只要必填标题存在,允许用户调整列顺序。
flowchart LR
HEADER["Excel第一行"] --> NORMAL["去空格/换行"]
NORMAL --> ALIAS["标题别名 -> 字段"]
ALIAS --> REQUIRED{"必填字段齐全?"}
REQUIRED -- 否 --> ERR["提示下载最新模板"]
REQUIRED -- 是 --> MAP["field -> column index"]
7.3 表头版本化建议
模板发生不兼容变化时建议新增隐藏或显式版本列,而不是靠标题猜测:
template_code: flash_activity_goods
template_version: 2
generated_at: YYYY-MM-DD HH:mm:ss
当前多数历史模板没有统一版本机制,修改时必须兼容已有文件或明确停止旧模板。
8. 单元格数据类型
| 类型 | 常见问题 | 推荐处理 |
|---|---|---|
| 物料编码/单号 | Excel 转科学计数法、去前导 0 | 模板设文本,导出显式 string |
| 金额 | 浮点误差、元/分混淆 | 明确单位,Decimal 校验,限制小数位 |
| 数量 | 小数位和负数规则不同 | 按业务单位校验精度和符号 |
| 日期 | Excel serial、文本、时区混用 | 统一解析后格式化 Y-m-d |
| 百分比 | 0.8、80%、8折 混用 | 模板说明和后端接受值必须一致 |
| 空值 | 空字符串、null、0 混淆 | 每字段定义是否允许空和默认值 |
| 布尔 | 是/否、1/0、TRUE/FALSE | 显式映射,拒绝未知文本 |
| 公式 | 显示值与公式内容不同 | 决定读取计算值还是拒绝公式 |
ExcelHelper::exportExcel 把所有值写为字符串;exportExcel2 仅把 PHP int 写为数字;reportExportExcel 尝试按值推断类型。因此同一数据用不同工具导出,Excel 中的排序和求和表现可能不同。
9. 导入原子性模型
| 模式 | 行错误时 | 适用场景 | 必须返回 |
|---|---|---|---|
| 全量原子 | 一行错,整批不落库 | 配置规则、强一致活动 | 全部错误摘要 |
| 部分成功 | 正确行落库,错误行回传 | 大批基础资料 | 成功数、失败数、错误文件 |
| 解析预览 | 不落库,返回标准化数据 | 需要前端确认的订单/活动 | 标准行和校验结果 |
| 异步任务 | 接收后排队 | 超大数据或慢外部依赖 | task_id、进度、结果文件 |
产品、前端和后端必须对原子性达成一致。否则用户看到“导入失败”后重试,可能把已成功行重复写入。
10. 通用导入流程
sequenceDiagram
participant U as 用户
participant C as Controller
participant X as Excel工具
participant S as BusinessService
participant DB as Database
participant O as OSS/下载目录
U->>C: 上传xls/xlsx
C->>C: 权限/大小/类型检查
C->>X: 解析第一个Sheet
X-->>C: header + rows
C->>S: 标准化与批量校验
S->>S: 行内校验+跨行重复+库内冲突
alt 全量通过
S->>DB: 事务/分批落库
C-->>U: 成功数量
else 部分成功
S->>DB: 写入有效行
S->>O: 生成错误文件
C-->>U: 成功/失败数+下载地址
else 全量拒绝
C-->>U: 行号化错误摘要
end
11. 三层校验
11.1 文件级
- 文件存在、非空。
- 扩展名/MIME/签名允许。
- 文件大小和行数不超限。
- 工作簿可被 Reader 打开。
- 第一个 Sheet 存在。
11.2 行级
- 必填字段。
- 数字、金额、日期、枚举格式。
- 字符长度和特殊字符。
- 物料/客户/供应商/仓库存在。
- 数量和价格边界。
11.3 集合级
- 文件内重复。
- 与数据库现有数据冲突。
- 同活动/订单唯一键冲突。
- 总数量、总金额和明细合计。
- 业务状态是否允许批量修改。
- 同一批次内关联行是否完整。
12. 批量查询与落库
避免逐行查库和逐行写库:
- 收集文件内所有 SKU、客户、仓库等业务键。
- 去重后分批查询。
- 在内存建立映射。
- 逐行校验时只查映射。
- 有效数据按 200/500/1000 等受控批次写入。
- 每批记录数量和失败原因。
项目已有批次案例:价格导入 500 行一批、部分商品/缓存任务 1000 或 2000 一批、秒杀库存查询 200 一批。批次值要根据 SQL 长度、锁时间、内存和外部服务限制确定,不能机械复制。
13. 错误返回设计
理想错误行包含:
| 字段 | 含义 |
|---|---|
| 原始行号 | 从 Excel 第 2 行开始的真实行号 |
| 原始业务键 | SKU、单号、客户编码等 |
| 字段名 | 失败字段 |
| 原始值 | 用户填写内容,注意脱敏 |
| 错误码 | 稳定机器码,可选 |
| 错误原因 | 可操作中文说明 |
一行存在多个错误时,可在同一次校验中汇总,避免用户反复上传才能发现下一个问题。
14. 错误文件
项目常见目录:
UPLOAD_PATH/download/import/error/
UPLOAD_PATH/download/import/error/{sid}/
UPLOAD_PATH/excel/download/error/{sid}/
常见返回字段并不统一:errorFile、error_file、fail_file、file_url、errorFileName。修改接口时必须以实际前端消费字段为准。
错误文件最低要求:
- 保留原模板列顺序。
- 最后一列增加“错误原因”。
- 只包含失败行,或明确标识成功/失败。
- 业务编码按文本写出。
- 文件名不可冲突。
- 下载地址有合理有效期和访问控制。
- 定期清理本地临时文件。
15. 模板缓存
BaseData/ExcelService::downloadTemplate 使用表头内容生成 hash 文件名:
模板物理文件 = hash(header) + .xls
用户下载文件名 = 业务标题 + .xls
优点:相同表头可复用已生成模板。风险:模板样式、说明或数据验证改变但表头没变时,hash 不变,旧文件仍可能被复用。模板缓存键应包含完整模板版本,而不只包含 header。
16. 同步下载、文件 URL 与 OSS
| 模式 | 行为 | 适用 | 风险 |
|---|---|---|---|
| HTTP 流 | Writer 写 php://output 后 exit | 小型同步导出 | 超时后无法恢复、响应前不能有输出 |
| 本地下载 | 保存到 UPLOAD_PATH,再由下载接口读取 | 错误文件、模板 | 磁盘堆积、路径校验 |
| OSS URL | 临时文件上传后返回 URL | OPS、异步导出 | URL 过期、上传失败、临时文件清理 |
ExportExcel::saveToOss 和秒杀 streamExcel 都是“生成本地文件 -> 上传 OSS -> 删除本地文件”。故障可能发生在三个阶段,日志要区分生成失败、上传失败和删除失败。
17. 下载安全
下载接口不能简单信任用户传入的相对路径。至少需要:
- 对路径
realpath。 - 确认最终路径位于允许的下载根目录下。
- 限定扩展名。
- 校验当前用户对 sid/业务文件的访问权限。
- 文件不存在返回业务错误,不暴露服务器绝对路径。
- OSS URL 不进入长期日志。
inner/Base::download 当前按用户 file 参数拼接到 UPLOAD_PATH,维护该公共入口时应重点验证目录穿越防护,不要扩大可下载目录。
18. Excel 公式注入
任何用户可控文本如果以 = + - @ 开头,Excel 打开时可能被解释为公式。错误文件和导出文件尤其容易原样回写用户输入。
安全策略:
- 业务编码统一显式写字符串。
- 对用户自由文本检查公式前缀。
- 必要时前置单引号或使用显式 string 类型。
- 不允许导入宏文件。
- 不自动访问工作簿外部链接。
19. 超过 26 列
ExcelHelper::exportExcel/exportExcel2 的列数组只定义 A-Z,超过 26 列会越界。秒杀活动列表导出有 28 列,因此控制器使用:
PHPExcel_Cell::stringFromColumnIndex($index)
来生成 A、B、...、Z、AA、AB。
维护规则:新增导出列前先数列数;超过 26 列时使用可靠的列转换 API,不要自行拼接字母。
20. 大文件资源模型
PHP Excel 库的内存消耗远大于文件大小,因为单元格会变成对象。
flowchart TD
FILE["上传文件"] --> OBJECTS["Workbook/Sheet/Cell对象"]
OBJECTS --> ARRAY["toArray二维数组"]
ARRAY --> MAP["业务映射/错误数组"]
MAP --> WRITER["错误/导出Workbook"]
WRITER --> PEAK["内存峰值叠加"]
大文件治理:
- 设置业务行数上限,而不是只提高
memory_limit。 - 只读需要的 Sheet、列和行。
- 分批查询和落库。
- 错误数据过多时流式写错误文件。
- 导出超过阈值改异步任务或 CSV。
- 避免同时保留原二维数组、标准化数组、错误数组和完整 Workbook。
- 记录总行数、有效行、失败行、耗时和峰值内存。
21. 秒杀商品导入业务契约
入口:inner/activity/FlashActivity::importFlashActivityGoods。
重要语义:导入接口只解析并返回 goodsList,不直接写活动商品表;最终由活动保存接口统一落库。
sequenceDiagram
participant OPS as OPS页面
participant C as FlashActivity Controller
participant S as FlashActivitySer
participant X as ExcelHelper
participant DB as FlashActivity/Goods
OPS->>C: activity_id + file_path + 页面活动上下文
C->>S: assertImportGoodsAllowed
C->>C: 从OPS文件服务下载本地文件
C->>S: importGoods
S->>X: readExcel
X-->>S: 二维行
S->>S: 表头映射/逐行归一/冲突校验
S-->>OPS: import_count + goodsList
OPS->>C: saveFlashActivity(goodsList)
C->>DB: 事务保存活动和商品
21.1 请求示例
字段名以真实网关映射为准:
POST /inner/activity/flashActivity/importFlashActivityGoods
Content-Type: multipart/form-data
activity_id=123
activity_type=2
stock_scope=1
file_path=<OPS文件描述>
活动 ID 缺失时,控制器历史兼容会尝试从 Referer 查询参数获取。新前端应显式传 ID,不应依赖 Referer。
21.2 返回示例
{
"import_count": 2,
"goodsList": [
{
"id": 0,
"sku_id": "SKU001",
"price_mode": 1,
"fixed_price": 990,
"locked_qty": 0,
"used_qty": 0,
"version": 0,
"reason": ""
}
]
}
示例只表示字段结构;价格内部单位应以秒杀 Service 的归一逻辑为准。
22. 秒杀导入模板
模板文件名:秒杀活动商品导入模板.xlsx。
| 列 | 语义 | 核心规则 |
|---|---|---|
| 物料编码 | SKU | 必须能找到物料 |
| 价格维度 | 一口价/固定折扣 | 接受文本或枚举值 |
| 折扣 | 0-1 小数 | 最多 2 位小数,内部乘 100 |
| 折扣价 | 固定价格 | 与价格维度联动 |
| 仓库 | 指定仓 | 仅中文、英文、数字 |
| 促销数量 | 活动数量 | 单品活动可按业务语义为空 |
| 生产日期推算方式 | 无/按月/按天 | 决定倒推字段语义 |
| 倒推月数/天数从 | 起始范围 | 与推算方式联动 |
| 倒推月数/天数到 | 结束范围 | 起止关系必须合法 |
必填表头检查允许列换序,并兼容部分导出标题别名,例如“折扣价/固定价/一口价”“仓库/指定仓编号/指定仓编码”。
23. 秒杀导入校验顺序
- 活动存在且不处于进行中;新建活动 ID 可为 0。
- 本地导入文件存在。
- 至少有表头和一行明细。
- 表头包含物料编码或物料 ID,以及全部规则字段。
- 跳过全空行。
- 按真实 Excel 行号解析。
- 仓库文本字符校验。
- 根据 SKU 或物料 ID 定位物料。
- 解析价格维度和日期推算方式。
- 归一价格、折扣、数量、仓库、效期规则。
- 校验单行业务规则。
- 按活动商品唯一键检查文件内重复。
- 与活动当前商品检查冲突。
- 任一错误存在时整批拒绝。
- 清理运行时元数据并返回待保存商品。
24. 秒杀唯一键
当前导入重复键由以下字段组合:
inv_id
+ warehouseNo(仅仓库范围商品)
+ date_calc_mode
+ backward_from
+ backward_to
因此同一物料在不同指定仓或不同生产日期倒推区间可形成不同规则;完全相同组合不允许重复。
flowchart LR
ROW["标准化商品行"] --> SCOPE{"是否仓库范围?"}
SCOPE -- 是 --> W["保留warehouseNo"]
SCOPE -- 否 --> E["warehouseNo置空"]
W --> KEY["invId|warehouse|dateMode|from|to"]
E --> KEY
KEY --> DUP{"文件内/现有商品已存在?"}
DUP -- 是 --> ERR["带行号拒绝"]
DUP -- 否 --> OK["加入goodsList"]
25. 秒杀错误策略
秒杀当前是全量原子校验:逐行收集错误,合并当前商品冲突后,如果有任何错误则抛出格式化错误摘要,不返回错误 Excel。
优点:不会出现只导入半批规则。限制:超多错误可能导致响应过长。后续增强可保留全量原子性,同时生成错误文件并只在响应返回前 N 条摘要。
26. 秒杀模板与导出可回导
商品明细导出有 23 列,模板只有 9 列。导入别名兼容商品导出的关键列,因此可在一定程度上“导出后修改再导入”,但要注意:
- 导出含只读物料/分类/品牌字段,导入会忽略。
- 导出“一口价”可映射到模板“折扣价”。
- 导出“数量”可映射到“促销数量”。
- 活动进行中仍禁止批量导入。
- 导入只返回新 goodsList,不代表自动覆盖原商品。
locked_qty/used_qty等运行态字段不会从 Excel 回写。
27. 秒杀导出流程
三个 OPS 接口:
| 接口方法 | 内容 | 行维度 |
|---|---|---|
downloadFlashActivityGoodsTemplate | 空模板 | 只有表头 |
exportFlashActivityGoods | 单活动商品明细 | 一商品一行 |
exportFlashActivity | 活动列表加商品 | 一活动商品组合一行 |
活动列表导出最多读取 5000 个活动,再批量查询商品。活动没有商品时仍输出一行活动信息。
sequenceDiagram
participant OPS as OPS
participant C as FlashActivity
participant S as FlashActivitySer
participant F as FileProvider
S-->>C: filename + columns + data
C->>C: 自定义PHPExcel写本地xlsx
C->>F: uploadFileToOSS
F-->>C: fileUrl
C->>C: 删除本地临时文件
C-->>OPS: JSON下载URL
当前自定义方法把 Sheet 标题设为 ERROR_INFO,即使导出并非错误文件。这不影响数据,但属于可维护性遗留,修改时应回归前端下载契约。
28. 基础资料导入
基础资料常见业务键:
| 模块 | 主键/匹配键 | 常见动态规则 |
|---|---|---|
| 商品 | SKU、物料编码、简码 | 品牌分类、单位、价格、套包 |
| 客户/供应商 | 联系人编号/名称/手机号 | 类别、结算方式、期初余额 |
| 货位 | 仓库+货位码 | 是否默认、停用状态 |
| 客户类别价格 | SKU+客户类别 | 动态类别列、价格精度 |
| 商品价格 | SKU+价格类型 | 最低价、指导价、特殊价 |
basedata/Invlocation.php 存在多套 importError/importError2/importNewError。改表头时必须定位对应入口,不能只修改其中一个同名错误文件方法。
29. 采购销售导入
订单类导入除字段格式外,还要校验:
- 单据状态是否允许导入。
- 供应商/客户是否属于当前 sid。
- 仓库、货位和门店数据范围。
- SKU 是否存在且业务可采购/销售。
- 数量、价格、折扣、税率和金额守恒。
- 文件内重复商品的合并或拒绝规则。
- 库存、可退数量、锁定数量。
- 导入结果是“商品预览”还是“直接生成单据”。
订单导入重试前必须确认是否已经生成业务单号,避免重复单据。
30. 库存盘点导入
盘点、安全库存和货位调整特别关注:
- 当前盘点单状态。
- SKU 对应
invId。 - 仓库/货位是否属于当前站点。
- 盘点数量允许的小数位。
- 重复 SKU+货位如何处理。
- 导入值是“最终数量”还是“增减差额”。
- 落库是否立即产生库存流水。
- 错误行成功重试是否会重复盘点。
盘点修复必须与库存一致性手册联动验证实时库存、流水和盘点明细。
31. 报表导出
报表导出通常经历:查询条件 -> 报表 Service -> 汇总/合计 -> reportExportExcel。
reportExportExcel 支持:
- 主标题和查询条件副标题。
- 多行/合并表头。
- 尾部合计行。
- 最多定义到 AB 的固定列数组。
- 数字值按默认 Binder 推断类型。
报表导出必须与页面保持:
- 相同筛选条件。
- 相同状态排除规则。
- 相同金额、税额、成本和利润口径。
- 相同数据权限和门店范围。
- 相同总计口径。
32. CSV 导出
项目部分列表使用 sys_csv,商品价格有流式 CSV 示例。CSV 更适合大数据,但需处理:
- UTF-8 BOM,避免 Excel 中文乱码。
- 逗号、双引号和换行转义。
- 长数字前导 0。
- 公式注入。
- 日期和金额格式。
- 不能提供复杂样式、合并单元格和多 Sheet。
33. 常用排查命令
# 按业务找模板、导入、导出入口
rg -n "download.*Template|import.*Goods|export.*Goods|导入|导出" application/controllers application/Services application/service
# 确定使用哪代Excel工具
rg -n "ExcelHelper|NewExcelUtil|ExcelService|ExportExcel|PHPExcel|PhpSpreadsheet|sys_csv" application
# 查错误文件返回字段和生成位置
rg -n "errorFile|error_file|fail_file|file_url|createError|importError" application
# 查大文件保护和批次
rg -n "memory_limit|set_time_limit|array_chunk|limit.*5000" application/controllers application/Services application/service
# 秒杀模板/导入/导出
rg -n "buildGoodsImportTemplate|importGoods|buildGoodsExport|buildActivityExport|goodsImportHeaderAliasMap" application/Services/Activity/FlashActivitySer.php
# 报表导出
rg -n "reportExportExcel|exportExcel|sys_csv" application/controllers/reports application/Services/Report
34. 日志定位
一次导入建议记录这些非敏感指标:
request_id
sid(内部日志)
业务类型/template_code/template_version
原始文件扩展名和大小
总行数/空行数/有效行数/失败行数
批次数和每批耗时
生成业务单号或任务ID
错误文件内部标识
总耗时/峰值内存
禁止记录完整 OSS 签名 URL、用户隐私列、整行原始数据和认证信息。
35. 故障一:提示模板表头错误
- 下载当前环境最新模板。
- 比较第一行标题,注意隐藏空格和换行。
- 确认使用正确业务入口,避免把导出文件导入另一模块。
- 检查是否插列、合并表头或使用第二行作为标题。
- 对标题映射型导入,检查别名是否覆盖旧标题。
- 对位置映射型导入,检查列顺序是否完全一致。
36. 故障二:物料编码变成科学计数法
原因通常是 Excel 把长编码当数字。处理:
- 模板编码列预设文本格式。
- 用户填写前不要先粘贴为数字。
- 导出使用显式 string。
- 后端不要先转 float/int 再转字符串。
- 错误文件也必须保持文本。
已丢失末位精度的编码无法通过格式切换恢复,必须重新从原始数据填写。
37. 故障三:导入成功数和数据库不一致
flowchart TD
A["响应成功数不一致"] --> B{"接口是预览模式?"}
B -- 是 --> R1["确认后续保存是否执行"]
B -- 否 --> C{"部分成功模式?"}
C -- 是 --> D["核对错误行/批次回滚"]
C -- 否 --> E["核对事务提交"]
D --> F{"重复键被更新还是跳过?"}
E --> G{"后置任务失败?"}
F --> H["按业务唯一键查库"]
G --> H
不要只按文件总行数核对;空行、重复行、更新行和预览行都会影响统计。
38. 故障四:部分成功后重试重复
- 确认错误文件是否只含失败行。
- 查业务唯一键和已成功记录。
- 确认导入语义是插入、覆盖还是 upsert。
- 重试只上传错误文件修正后的行。
- 如接口没有幂等,先清理重复风险,不要整批原文件重传。
39. 故障五:导出为空但页面有数据
检查:
- 导出是否收到与页面相同筛选参数。
- 分页参数是否错误带入导出。
- 导出权限与页面查询权限是否不同。
- 导出 Service 是否固定上限或日期范围。
- 状态、门店、sid、时间边界是否一致。
- 页面数据是否来自缓存而导出读数据库。
- 商品筛选时是否先求活动 ID,空集合提前返回。
40. 故障六:导出超时或内存溢出
- 查筛选范围和预计行数。
- 查是否一次加载全量明细。
- 查是否同时保留多份二维数组。
- 查列数、样式和合并单元格数量。
- 减小查询批次不一定降低 Workbook 内存。
- 改为异步分段查询、流式 CSV 或多文件压缩包。
- 仅临时提高
memory_limit不是最终方案。
41. 故障七:下载地址打不开
| 阶段 | 检查 |
|---|---|
| 本地生成 | 文件是否存在、大小是否大于 0 |
| 上传 OSS | 返回数组是否有 fileUrl |
| 临时 URL | 是否过期、域名是否可访问 |
| 本地清理 | 是否在上传成功前被删除 |
| API 契约 | 前端期望 URL 字符串还是对象字段 |
| 权限 | 下载接口是否允许当前用户访问 |
42. 数据修复门槛
导入引起的数据异常修复前:
- 保存原始文件的受控副本和 hash。
- 明确导入接口、时间、操作者、sid、请求 ID。
- 确认是预览、全量、部分成功还是异步模式。
- 统计成功业务键、失败业务键和重复业务键。
- 检查是否生成订单、库存流水、财务单或 MQ。
- 先设计业务层回滚,不直接按行删除主表。
- 用同一模板版本做修复后验证。
43. 完整回归矩阵
| 维度 | 用例 |
|---|---|
| 文件 | xls、xlsx、空文件、损坏文件、错误扩展、超大文件 |
| Sheet | 单 Sheet、多 Sheet、空第一 Sheet、隐藏行列 |
| 表头 | 最新、旧版、换序、缺列、重复列、空格换行 |
| 单元格 | 空、0、负数、超长、科学计数、公式、特殊字符 |
| 业务 | 正常、库内不存在、状态不允许、权限不足 |
| 集合 | 文件内重复、库内重复、跨行合计错误 |
| 结果 | 全成功、部分成功、全部失败、预览后保存 |
| 重试 | 相同文件重复、仅错误行重试、超时后重试 |
| 导出 | 空数据、1 行、上限行数、超过 26 列、中文文件名 |
| 下载 | 同步流、本地文件、OSS、URL 过期、本地清理失败 |
| 安全 | 公式注入、目录穿越、跨 sid 下载、恶意文件 |
| 性能 | 行数上限、内存峰值、数据库批次、并发导入 |
44. 改动风险分级
| 级别 | 改动 | 风险 |
|---|---|---|
| 高 | 公共 ExcelHelper/ExcelService | 影响大量业务入口 |
| 高 | 上传/下载路径和权限 | 文件泄露或任意文件访问风险 |
| 高 | 导入原子性从全量改部分成功 | 重试和数据一致性语义改变 |
| 高 | 大批订单/库存直接落库 | 可能产生下游流水和 MQ |
| 中 | 模板表头/列序 | 旧文件兼容和错误文件错位 |
| 中 | 金额/数量数据类型 | 可能出现 100 倍或精度错误 |
| 中 | OSS 临时/永久参数 | 下载有效期和存储成本变化 |
| 低 | 文件名、列宽、Sheet 标题 | 仍需回归中文下载和前端契约 |
45. 上线验收
45.1 模板
- 下载文件可打开。
- 标题、顺序、必填说明和示例正确。
- 编码列为文本。
- 模板版本与后端兼容。
45.2 导入
- 总数、成功数、失败数可解释。
- 每条错误带真实 Excel 行号。
- 全量/部分/预览行为符合约定。
- 重复上传不会产生不可控重复。
- 权限、sid、门店范围不越界。
45.3 错误文件
- 地址可下载且权限正确。
- 原始值和错误原因完整。
- 长编码和中文不乱码。
- 文件会按生命周期清理。
45.4 导出
- 与页面筛选、状态和合计一致。
- 金额、数量、日期、长编码格式正确。
- 空数据和上限数据均可用。
- 超过 26 列没有截断。
- OSS 上传失败有明确错误,不返回空 URL。
46. 证据来源
| 结论 | 代码证据 |
|---|---|
| Excel 仅支持 xls/xlsx | RegularConst::EXCEL_EXTENSION、ExcelHelper::readExcel |
| 第一 Sheet 全量 toArray | application/Components/ExcelHelper.php |
| 通用导出 A-Z 上限 | ExcelHelper::exportExcel/exportExcel2 |
| OPS/站管家双上传 | Services/BaseData/ExcelService.php::upload |
| 模板按表头 hash 缓存 | ExcelService::downloadTemplate |
| 本地/OSS 两种创建 | ExcelService::create |
| PhpSpreadsheet 主子行导出 | application/Components/ExportExcel.php |
| 秒杀导入只返回预览 | FlashActivitySer::importGoods |
| 秒杀活动进行中禁导入 | assertImportGoodsAllowed |
| 秒杀标题别名和换序 | buildExcelHeaderMap/goodsImportHeaderAliasMap |
| 秒杀 28 列自定义导出 | FlashActivity::exportFlashExcel |
| 秒杀活动导出上限 5000 | FlashActivitySer::buildActivityExport |
| 部分历史导入返回错误文件 | basedata/Invlocation.php、basedata/Import.php |
| 报表多行表头与合计 | ExcelHelper::reportExportExcel |
47. 待环境确认项
- Web/PHP-FPM 实际上传大小、请求超时、执行超时和内存限制。
- Nginx/Caddy/网关对大上传和大下载的限制。
- OSS 临时 URL 有效期、永久文件策略和清理任务。
UPLOAD_PATH磁盘容量、错误文件清理周期和访问控制。- 各业务前端实际消费的错误文件字段名。
- 线上 Excel 库版本、公式计算和日期解析行为。
- 秒杀错误摘要最大长度及 OPS 网关响应限制。
- 报表导出实际数据上限和异步导出能力。
- 公共下载接口目录穿越防护的现网验证结果。
这些环境项未确认前,不应仅凭本地小文件测试判断可以支撑生产大批量导入导出。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 同步导入 | PC 基础资料/业务页上传 | 文件、模板类型、sid、操作人 | basedata/Import.php 等 | Import Service、ExcelHelper | 导入批次 + 行号 | 合法行写业务表,错误行形成反馈 |
| 活动导入 | OPS 活动接口 | 活动 ID、商品 Excel | inner/activity/FlashActivity.php | FlashActivity Import Service | 活动 ID + SKU | 活动商品批量新增/更新 |
| 异步导入 | 大文件上传后 task | 上传文件路径、任务 ID、批次 | tasks/Import.php | Import Service | task/batch ID | 分批处理并记录进度/错误 |
| 导出/模板 | PC/报表下载 | 筛选条件、列配置、模板类型 | 各 Controller/reports | ExportExcel/Report Service | export task/request ID | 生成只读文件,不改业务事实 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 上传解析 | Controller/ExcelHelper | request_id、文件名、模板类型、批次 | 表头识别、解析行数明确 | 格式/编码/大小/表头失败 | 批次 + 原始行号进入校验 | | 行校验 | Import Service | 批次、sheet/row、业务键 | 有效/无效计数可对账 | 错误未带行号、规则不一致 | 行号关联错误反馈和业务键 | | 批量落库 | Service/DB 日志 | batch ID、分片页、主键、影响行数 | 成功数 + 失败数 = 处理数 | 部分 commit、duplicate、超时 | 业务键回查目标表 | | 导出生成 | ExportExcel/Report | request/task ID、筛选摘要、行数 | 文件生成且行列数正确 | 内存溢出、超时、乱码、公式注入 | 查询条件与文件抽样行关联 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 文件接收 | Controller + UPLOAD_PATH | 事务外 | 文件类型、大小、用户 | 临时文件/任务记录 | 上传状态初始化;业务表零变化 | request ID、文件 hash、任务 ID |
| 解析校验 | ExcelHelper/Import Service | 事务外 | 表头、每行单元格、字典值 | 错误集合/临时数据 | 原始行 -> 标准字段;错误行不落业务表 | 行号、字段、错误原因 |
| 分批写入 | 领域 Import Service | 每批/每行事务需明确 | 目标记录、唯一键、当前值 | 目标业务表 | insert/update old -> new;失败批次 rollback 范围明确 | batch、业务键、影响行数 |
| 后置刷新 | Cache/Search/MQ | commit 后异步 | 成功写入的业务键 | 缓存、索引、MQ | DB 已变 -> 派生层待刷新 | 消费 ACK、索引/缓存回查 |
| 导出 | ExportExcel/Report Service | 只读 | 查询结果、列定义 | 下载文件 | DB 不变;危险公式值转义 | SQL 条件、行数、抽样字段 |
子模块追踪:excel-upload-parse 文件上传、解析与表头契约
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 上传解析 | PC/OPS 上传 xls/xlsx/csv | file ID、模板版本 | application/Components/ExcelHelper.php | MIME、大小、sheet、固定列序/标题 | 解析阶段 不写业务表;事务外生成临时文件 | request ID + file ID + rows | 类型/表头错误零业务写入并清临时文件 |
| 表头回查 | 模板错误或列偏移 | expected/actual headers | application/Components/ExcelHelper.php | 模板版本和列索引 | 查询只读 不写 | file ID + header diff | 历史模板明确版本,禁止静默错位 |
子模块追踪:excel-validate-write 三层校验与批量落库
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 校验 | 文件/行/集合三级校验 | task、rowNo、业务键 | application/controllers/tasks/Import.php | 模板、类型、引用、文件内/库内重复 | 校验阶段不写;生成 valid/error 集合 | task + row/error counts | 错误带行号字段;部分成功策略须明确 |
| 落库 | 校验通过后分批写 | batch、business keys | application/controllers/basedata/Import.php | 当前 DB 和幂等键 | 每批本地事务 none/old -> imported/new | task + batch + affected rows | 中断只重跑失败批,成功键零增量 |
子模块追踪:excel-error-file 错误返回与错误文件
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 生成错误 | 校验出现失败行 | task、rowNo、field/code | application/Components/ExportExcel.php | 脱敏原行和错误列表 | 业务 DB 不变;事务外文件 pending -> ready/failed | task + error rows + file result | 文件失败不改变导入结果;不回显敏感值 |
| 下载 | 用户取错误文件 | file/task ID、user/sid | application/controllers/basedata/Import.php | 文件归属、过期与权限 | 下载只读 不写 | request ID + file ID + HTTP code | 地址失效重生成文件,不重跑导入 |
子模块追踪:excel-download 模板缓存、下载与 OSS
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 模板生成 | 下载业务模板 | template code/version、sid | application/Components/ExportExcel.php | 列定义、下拉数据、缓存版本 | 事务外 old template -> current,业务 DB 不变 | request ID + template/version + size | 列合同变更必须失效旧缓存 |
| 文件交付 | 流式下载或 OSS URL | file/task ID | application/Components/ExcelHelper.php | 状态、归属、URL 有效期 | 下载只读 不写 | file ID + storage + HTTP code | OSS 配置待环境确认;超时只刷新 URL |
子模块追踪:excel-security 公式注入、列数与大文件安全
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 安全校验 | 公式前缀、超列或大文件 | file、row/column、size | application/Components/ExcelHelper.php | 单元格类型、列索引、资源上限 | 校验/转义不写业务表 | file ID + row/column + reason | 超限尽早失败并清临时文件 |
| 资源控制 | 异步批处理/流式导出 | task、batch、memory/time | application/Components/ExportExcel.php | 水位和已写行 | 文件事务外 processing -> success/failed | task + rows + resource usage | 从稳定水位重建,避免重复行 |
子模块追踪:flash-excel 秒杀商品导入导出
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 导入 | OPS 秒杀商品文件 | activity/file、SKU/效期/仓库 | application/controllers/inner/activity/FlashActivity.php -> application/Services/Activity/FlashActivitySer.php | 活动态、唯一键、价格数量冲突 | 全部通过后本地事务 old goods -> imported goods | request ID + activity/file + errors | 一行失败按合同整批拒绝,不留部分商品 |
| 导出回导 | 导出并验证可回导 | activity/task ID | application/Services/Activity/FlashActivitySer.php | 商品、数量显示、仓库名 | 查询只读 不写;文件异步生成 | activity/task + rows | 列版本一致;文件失败只重做导出 |
子模块追踪:base-excel 基础资料导入
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 导入 | 商品、价格、仓位文件 | sid、task、业务编码 | application/controllers/basedata/Import.php | 编码、唯一性、状态、引用 | 每批本地事务基础表 old -> new | task + row + business code | 引用无效零写入该批;成功批不重做 |
| 刷新 | 基础资料提交后 | changed codes、batch | application/controllers/inner/CacheManage.php | 已提交行和派生缓存 | commit 后事务外失效缓存/索引 | batch + code + result | DB 正确只补派生层 |
子模块追踪:trade-excel 采购、销售与盘点导入
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 交易导入 | 采购/销售/盘点文件 | sid、task、来源/业务单 | application/controllers/scm/InvPo.php、application/controllers/scm/InvSa.php | 状态、基础资料、数量、来源唯一性 | 每单本地事务调用领域 Service,none -> created | task + row + bill/source ID | 禁止绕过 Service 直插;来源键幂等 |
| 验收 | 成功数与 DB 不符 | task、成功键 | application/controllers/scm/InvOi.php | 任务、主明细和库存副作用 | 查询只读 不写 | task + counts + billNos | 只重跑失败键,避免重复库存 |
子模块追踪:report-export 报表与 CSV 导出
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 导出查询 | 页面筛选导出 | sid、filters、report code | application/Components/ExportExcel.php | 权限、时间/状态/软删口径、总数 | 业务查询只读;任务 pending -> processing | request/task + filters + rows | 页面/导出复用口径;空文件先比参数 |
| 文件完成 | Excel/CSV 生成 | task、row count、file ID | application/Components/ExportExcel.php | 批次、列、文件态 | 事务外 processing -> success/failed | task/file + written rows | 超时用分批流式重建,不改业务数据 |