--- title: prop-acc · meter · 场景 - 一次导入整月所有读数(Excel 批量) aliases: - 批量抄表 - Excel 导入抄表 - read-batch-via-excel-import - MeterReadingsImporter - 场景-Excel 批量抄表 tags: - 场景 - prop-acc - 计量表 - 抄表 - 批量导入 audience: - 业务人员 - 抄表员 status: 已发布 sub_feature: meter last_review: 2026-05-26 code_version: 2026-05-22 --- # 场景:一次导入整月所有读数(Excel 批量) 中型物业(几百到上千张表)、未上集抄系统的,抄表员**月底一次性**把全社区抄表数据填入 Excel,业务人员**上传导入**。 ## 典型情境 > [!example] 真实情境 > 嘉禾花园 300 户 + 公共部位 + 商铺,合计 1,200 张表(水电气混)。未上集抄。抄表员李师傅 + 团队**每月最后一周**集中抄表 5-7 天,完成后: > > 1. 整理 Excel(1,200 行,每行一张表的本月读数) > 2. 王主管下载"抄表读数模板" > 3. 抄表数据填入模板对应列 > 4. 上传导入 ## 业务人员视角 ### 第 1 步:下载抄表模板 后台 → 计量表 → 列表 → 顶部 **"下载抄表模板"** 按钮(`ExportMeterReadingsAction`,**注意**:命名误导,实际是下载模板)。 > [!info] 命名问题(issue.md Q5) > `ExportMeterReadingsAction` 的名字让人以为是"导出读数"(把数据从系统导出来),实际是**"下载抄表模板"**(给抄表员填的空模板,预填上次读数)。issue.md Q5 待补:重命名为 `DownloadMeterReadingsTemplateAction`。 下载的 Excel 包含: | 列 | 说明 | 预填 | |---|---|---| | 房号 | asset 编号 | ✅ 系统填(对应每张已建表) | | 表编号 | meter code | ✅ 系统填 | | 费用类型 | 水/电/燃气 | ✅ 系统填 | | 上次读数 | 上月该表 reading | ✅ 系统填(供抄表员对比)| | **本次读数** | 本月该表 reading | **❌ 空,抄表员填**| | 抄表日期 | 本次 read_at | **❌ 空,抄表员填**(或默认月底)| | 备注 | 选填 | ❌ | 预填上次读数让抄表员**对比时有参考**(本次 > 上次 才合理),也防止漏填行(看清楚每行该填什么)。 ### 第 2 步:抄表员填本月数据 抄表员李师傅团队**整月**(或月末几天)做这事: - 现场抄表 + 拍照(用 App / 纸质本) - 数据填入模板 - 完成后交回王主管 ### 第 3 步:上传导入 后台 → 计量表 → 列表 → 顶部 **"导入抄表数据"** 按钮(`ImportActionWithExcel` + `MeterReadingsImporter`)→ 选 asset_type → 上传 → 提交。 系统: 1. 解析 Excel(走 `BaseImporter`) 2. 每行校验(meter 存在?asset 匹配?读数合法?) 3. 批量建 MeterReading(每行一条,`source=manual`,`operated_by=导入操作员`,无 photo_url —— 走批量没拍照) 4. 算 consumption 5. 报告:成功 N 条,失败 M 条 > [!warning] 批量导入无 photo_url > Excel 模板没法批量上传照片。批量导入的 reading **photo_url 为空**。 > > 业务上推荐:**抄表员独立留照片**(手机相册按月归档),业户事后争议时翻照片(虽然不在系统里)。或者: > - 高争议表(住宅)走 [[read-single-meter-manual|单录 + 拍照]] > - 低争议表(商铺 / 公共)走批量导入(节省时间) ### 第 4 步:核对 导入后: - `MetersNeedingReadingListWidget` 显示"本月未抄表"清单 → 此时应 0 条(或个别遗漏) - 抽样验证几张表的 reading 数据正确 - `HighConsumptionReadingsListWidget` 看是否有异常用量 ### 第 5 步:触发账单生成 导入完成后: - **自动触发**(`MeterReadingsImporter` 完成后默认调 `GenerateBillsFromMeterReadingsAction`) - 或**手动触发**(`ListMeters` 上的"生成账单"按钮,选刚导入的 readings) 详见 [[bill-generation-pipeline]]。 ## 系统流程 ```mermaid sequenceDiagram participant 李师傅 participant 王主管 participant Filament participant MeterReadingsImporter participant GenerateBills[GenerateBillsFromMeterReadingsAction] participant 数据库 王主管->>Filament: 下载抄表模板(预填上次读数) Filament-->>王主管: 模板.xlsx 王主管->>李师傅: 转发模板 李师傅->>李师傅: 现场抄表 + 填模板 李师傅->>王主管: 已填模板.xlsx 王主管->>Filament: 导入抄表数据 → 选 asset_type + 上传 Filament->>MeterReadingsImporter: parse + chunk 处理(100/批) loop 每批 MeterReadingsImporter->>数据库: 开启事务 loop 每行 MeterReadingsImporter->>数据库: 校验 meter + asset alt 通过 MeterReadingsImporter->>数据库: 建 MeterReading else 失败 MeterReadingsImporter->>MeterReadingsImporter: 记失败行 end end alt 全成功 MeterReadingsImporter->>数据库: 提交 else 任一失败 MeterReadingsImporter->>数据库: 回滚整批 end end MeterReadingsImporter->>GenerateBills: handle(刚建的 readings) GenerateBills->>数据库: 批量建 Bill + 回写 reading.bill_id MeterReadingsImporter-->>Filament: 报告 Filament-->>王主管: 通知 + 失败行下载 ``` ## 抄表员视角(李师傅) 整月抄表流程: 1. **第 1-5 周(月初)**:正常工作 + 部分非紧急表抄(若有时间) 2. **第 25-30 日(月末)**:集中抄表 - 按楼栋 + 单元顺序(避免漏) - 每户:开门(若有人)/ 看公共表(若装在外) - 读表 → 拍照 → 填模板 3. **30 日 / 月底**:整理完整 Excel → 交回王主管 工作量:1,200 张表 / 月 / 1 抄表员 → 约 40 张 / 天 / 5 天工作。比单录(后台一张张点)快 3-5 倍。 ## 业户视角 业户**无感知** —— 抄表员上门时业户可能在家也可能不在(公共表 / 燃气表通常装在楼道,不用进户)。 ## `MeterReadingsImporter` 双义列名问题(issue.md Q5) > [!warning] 已知 silent corruption 风险 > 当前 `MeterReadingsImporter` 用**一个 Importer 处理两种 Excel layout**(住宅单元 vs 商铺/公共),列 label 是"双义"形式(如 `'层编号/费用类型'`),靠 `$this->options['asset_type']` 决定列含义。 > > **风险**:导入时用户选错 `asset_type`: > - 系统不报错(列存在,数据有值,看着像合法) > - 但数据写到**错误字段**(silent corruption) > - 业务人员事后核对才发现数据错位 > > **修复**(issue.md Q5 待补):拆成 `MeterReadingsImporterForUnit` + `MeterReadingsImporterForShop`,每个列含义固定。 > > **当前预防**:导入时**务必仔细确认** asset_type 选项 + 抽样核对前几行数据。 ## 常见问题 > [!question] 导入失败的常见原因? > - meter 不存在(asset 或 code 找不到对应表) > - meter 已退役(`is_active=false`) > - 读数倒走(本次 < 上次) > - 读数格式错(非数字 / 含中文) > - asset_type 选错(silent corruption,不报错但数据错) > [!question] 已导入想撤销? > Reading 不可改 / 不可删(若已生成 Bill 更严)。撤销 = 走 [[exception-readings-locked-after-bill]] 流程,复杂。 > > **预防**:导入前抽样核对 Excel + 选对 asset_type + 小批量先试。 > [!question] 同一张表本月被重复导入(填了两次 Excel)? > 看 `MeterReadingsImporter` 是否有"同 meter + 同 read_at 不允许"的守护。如果没有 → 系统建两条 reading → 两条都算用量 → 业户被算两遍。**严重 bug**,需要业务人员核对避免重复导入。 > [!question] 部分表本月没抄怎么办? > `MetersNeedingReadingListWidget` 会显示"本月未抄"清单。业务人员可: > - 让抄表员补抄(走 [[read-single-meter-manual|单录]]) > - 让业户自报读数(部分物业接受,但需核对) > - 跳过本月(下月一起算,业户账单可能突然变高) > [!question] 商铺 / 公共表与住宅表能合并导入吗? > 看 Excel 模板设计。当前**asset_type 是必选项**,即一次导入只能处理一种类型。要分两次导入(住宅一次,商铺一次,公共一次)。 > [!question] 导入完成后立即生成账单还是等月底? > 取决于业务流程: > - **导入即生成**:`MeterReadingsImporter` 完成后自动调 GenerateBills(默认推荐) > - **手动触发**:业务人员后续审核 reading 后再触发生成 > > 当前实现应是"自动生成"模式(看 Importer 配置)。 ## 异常分支 - 单张表录入 → [[read-single-meter-manual]] - 集抄自动 → [[read-via-iot-remote-source]] - 已导入发现错 → [[exception-readings-locked-after-bill]] - 高用量异常 → [[exception-high-consumption]] - 抄表完成率审计 → [[audit-meters-needing-reading]] ## 相关文档 - [[meter-vs-meter-reading]] - [[reading-source-and-photo-proof]] - [[bill-generation-pipeline]] - [[read-single-meter-manual]] - [[read-via-iot-remote-source]] - [[init-new-community-batch]]