Compare commits
2 Commits
d67494595e
...
898d3a93a7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
898d3a93a7 | ||
|
|
a7ac2d50b9 |
8
.obsidian/workspace.json
vendored
8
.obsidian/workspace.json
vendored
@@ -197,6 +197,10 @@
|
||||
},
|
||||
"active": "849c5ff8936a2b67",
|
||||
"lastOpenFiles": [
|
||||
"prop-acc/concepts/meter/reading-source-and-photo-proof.md",
|
||||
"prop-acc/concepts/meter/bill-generation-pipeline.md",
|
||||
"prop-acc/concepts/meter/multiplier-and-tiered-pricing.md",
|
||||
"prop-acc/concepts/meter/replacement-chain.md",
|
||||
"prop-acc/concepts/meter/meter-vs-meter-reading.md",
|
||||
"prop-acc/concepts/meter",
|
||||
"prop-acc/scenarios/prepaid/audit-low-balance-and-overdue.md",
|
||||
@@ -221,10 +225,6 @@
|
||||
"prop-acc/scenarios/deposit/deposit-first-time-renovation.md",
|
||||
"prop-acc/scenarios/deposit/deposit-additional-topup.md",
|
||||
"prop-acc/maps/prepaid-knowledge-map.md",
|
||||
"prop-acc/concepts/prepaid/auto-deduction-design.md",
|
||||
"prop-acc/maps/deposit-knowledge-map.md",
|
||||
"prop-acc/concepts/prepaid/consume-via-bill-collection-type.md",
|
||||
"prop-acc/concepts/prepaid/transaction-types.md",
|
||||
"prop-acc/concepts/prepaid",
|
||||
"prop-acc/scenarios/deposit",
|
||||
"prop-acc/concepts/deposit",
|
||||
|
||||
294
prop-acc/concepts/meter/bill-generation-pipeline.md
Normal file
294
prop-acc/concepts/meter/bill-generation-pipeline.md
Normal file
@@ -0,0 +1,294 @@
|
||||
---
|
||||
title: prop-acc · meter · 账单生成的三层分层
|
||||
aliases:
|
||||
- 账单生成 pipeline
|
||||
- Calculator Service Action 三层
|
||||
- MeterBillCalculator
|
||||
- MeterBillGenerationService
|
||||
- GenerateBillsFromMeterReadingsAction
|
||||
tags:
|
||||
- 概念
|
||||
- prop-acc
|
||||
- 计量表
|
||||
- 架构
|
||||
- 计费
|
||||
audience:
|
||||
- 架构师
|
||||
- 业务人员
|
||||
status: 已发布
|
||||
sub_feature: meter
|
||||
last_review: 2026-05-25
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 账单生成的三层分层
|
||||
|
||||
从"一条 MeterReading"到"一张 Bill",中间经过三层代码,每层职责清晰、可独立测试:
|
||||
|
||||
1. **`MeterBillCalculator`** — 纯算函数(无 DB IO),拿用量 + RatePlan,算出金额
|
||||
2. **`MeterBillGenerationService`** — 查费率 + 找业主 + 建 Bill 数据库记录
|
||||
3. **`GenerateBillsFromMeterReadingsAction`** — 业务入口,接收一组 reading,触发整个流程
|
||||
|
||||
> [!info] 为什么这种分层是 prop-acc 的"样板"
|
||||
> issue.md Q5 提到 meter 是 prop-acc 里**最成熟**的模块,后续 deposit / prepaid / adhoc 学习了它的"业务分层方法"。这条 Calculator → Service → Action 的链路设计,**值得作为新模块的参考蓝本**。
|
||||
|
||||
## 三层职责清单
|
||||
|
||||
| 层 | 类名 | 职责 | 依赖 |
|
||||
|---|---|---|---|
|
||||
| **Calculator** | `MeterBillCalculator` | 纯算:用量 → 金额(阶梯 + 倍率 + min/max)| 无(纯函数)|
|
||||
| **Service** | `MeterBillGenerationService` | 业务编排:从 Reading 查 fee_type → 查 RatePlan → 找 asset 业主 → 调 Calculator → 建 Bill | DB |
|
||||
| **Action** | `GenerateBillsFromMeterReadingsAction` | 业务入口:接收一组 Reading → 校验 → 逐条调 Service | DB + 事件 |
|
||||
|
||||
## 调用栈
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 业务[业务人员/定时任务]
|
||||
participant Filament
|
||||
participant Action[GenerateBillsFromMeterReadingsAction]
|
||||
participant Service[MeterBillGenerationService]
|
||||
participant Calc[MeterBillCalculator]
|
||||
participant DB
|
||||
|
||||
业务->>Filament: 触发"生成本月账单"按钮
|
||||
Filament->>Action: handle([reading1, reading2, ..., readingN])
|
||||
|
||||
loop 每个 reading
|
||||
Action->>Action: 校验:reading.bill_id == null
|
||||
Action->>Service: generateBillForReading(reading)
|
||||
Service->>DB: 查 Meter.fee_type → 查 RatePlan + RateTier
|
||||
Service->>DB: 查 asset_id 关联的业户(community_asset_users)
|
||||
Service->>Calc: calculate(consumption, ratePlan, min, max)
|
||||
Calc-->>Service: amount = 148
|
||||
Service->>DB: 建 Bill(amount, asset, resident, sourceable=reading)
|
||||
Service->>DB: 更新 reading.bill_id = bill.id
|
||||
Service-->>Action: ok
|
||||
end
|
||||
|
||||
Action-->>Filament: 完成 + 报告(已生成 N 张 Bill)
|
||||
```
|
||||
|
||||
## 每层细节
|
||||
|
||||
### Layer 1:`MeterBillCalculator`(纯算)
|
||||
|
||||
```php
|
||||
// 伪代码示意
|
||||
class MeterBillCalculator
|
||||
{
|
||||
public function calculate(
|
||||
float $consumption,
|
||||
RatePlan $ratePlan,
|
||||
?float $minAmount,
|
||||
?float $maxAmount,
|
||||
): float {
|
||||
$amount = $this->calculateTiered($consumption, $ratePlan->tiers);
|
||||
if ($maxAmount !== null && $amount > $maxAmount) {
|
||||
$amount = $maxAmount;
|
||||
}
|
||||
if ($minAmount !== null && $amount < $minAmount) {
|
||||
$amount = $minAmount;
|
||||
}
|
||||
return $amount;
|
||||
}
|
||||
|
||||
public function calculateTiered(float $consumption, Collection $tiers): float
|
||||
{
|
||||
// progressive 累进算法,见 multiplier-and-tiered-pricing
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- 无 DB 查询
|
||||
- 无业务对象关联(只接受参数)
|
||||
- 完全可测试(给定输入 → 期望输出)
|
||||
- 单元测试覆盖率 100% 容易达到
|
||||
|
||||
### Layer 2:`MeterBillGenerationService`(业务编排)
|
||||
|
||||
```php
|
||||
// 伪代码示意
|
||||
class MeterBillGenerationService
|
||||
{
|
||||
public function __construct(
|
||||
private MeterBillCalculator $calculator,
|
||||
) {}
|
||||
|
||||
public function generateBillForReading(MeterReading $reading): Bill
|
||||
{
|
||||
$meter = $reading->meter;
|
||||
$feeType = $meter->feeType;
|
||||
$ratePlan = $feeType->currentRatePlan;
|
||||
$asset = $meter->asset;
|
||||
$resident = $this->findCurrentResident($asset); // 关键业务逻辑
|
||||
|
||||
$amount = $this->calculator->calculate(
|
||||
$reading->consumption,
|
||||
$ratePlan,
|
||||
$ratePlan->min_amount,
|
||||
$ratePlan->max_amount,
|
||||
);
|
||||
|
||||
return DB::transaction(function () use ($reading, $amount, /* ... */) {
|
||||
$bill = Bill::create([
|
||||
'community_id' => $meter->community_id,
|
||||
'asset_id' => $meter->asset_id,
|
||||
'resident_id' => $resident?->id,
|
||||
'fee_type_id' => $meter->fee_type_id,
|
||||
'amount' => $amount,
|
||||
'sourceable_type' => MeterReading::class,
|
||||
'sourceable_id' => $reading->id,
|
||||
'status' => BillStatus::Unpaid,
|
||||
'due_at' => /* 计算到期日 */,
|
||||
]);
|
||||
|
||||
$reading->update(['bill_id' => $bill->id]);
|
||||
|
||||
return $bill;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- 查 DB(RatePlan / asset / resident)
|
||||
- 业务规则(找当前业主、计算到期日)
|
||||
- 事务边界(确保 Bill 建 + Reading 回写同时成功)
|
||||
- 不直接处理 UI / 用户输入
|
||||
|
||||
### Layer 3:`GenerateBillsFromMeterReadingsAction`(入口)
|
||||
|
||||
```php
|
||||
// 伪代码示意
|
||||
class GenerateBillsFromMeterReadingsAction
|
||||
{
|
||||
public function __construct(
|
||||
private MeterBillGenerationService $service,
|
||||
) {}
|
||||
|
||||
public function handle(Collection $readings): array
|
||||
{
|
||||
$generated = [];
|
||||
$skipped = [];
|
||||
|
||||
foreach ($readings as $reading) {
|
||||
if ($reading->bill_id !== null) {
|
||||
$skipped[] = ['reading' => $reading, 'reason' => 'already_billed'];
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$bill = $this->service->generateBillForReading($reading);
|
||||
$generated[] = $bill;
|
||||
} catch (\Exception $e) {
|
||||
$skipped[] = ['reading' => $reading, 'reason' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'generated' => $generated,
|
||||
'skipped' => $skipped,
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- 业务入口(给 Filament / Console / 定时任务调用)
|
||||
- 处理批量
|
||||
- 容错(单条失败不影响其他)
|
||||
- 返回结构化结果供调用方报告 / 推送
|
||||
|
||||
## 调用方
|
||||
|
||||
`GenerateBillsFromMeterReadingsAction` 的调用方:
|
||||
|
||||
| 调用方 | 场景 |
|
||||
|---|---|
|
||||
| **Filament Action**(`ViewMeter` 上的"生成账单"按钮)| 业务人员手动触发(单表 / 多表)|
|
||||
| **`MeterReadingsImporter`**(批量导入完成后)| 导入抄表数据后自动触发账单生成 |
|
||||
| **`MeterReadingsRelationManager`** 单录后(可选)| 抄表后即生成账单(配置项)|
|
||||
| **定时任务**(月初自动)| 待补(类似 prepaid 的 [[../prepaid/auto-deduction-design]]) |
|
||||
|
||||
**关键**:所有调用方**共用同一份 Action**,业务逻辑只写一遍。
|
||||
|
||||
## 为什么分层
|
||||
|
||||
| 不分层(全部塞 Filament Action)| **三层分层(本设计)** |
|
||||
|---|---|
|
||||
| 一个 Filament Action 500+ 行 | Calculator 100 行 + Service 200 行 + Action 100 行 |
|
||||
| 测试要 mock UI 才能跑 | Calculator 直接单元测试 |
|
||||
| 复用困难(其他调用方要复制粘贴)| 多调用方共用 Action / Service |
|
||||
| 业务逻辑藏在 UI 层(违反层次原则)| 业务逻辑在 Action 层,UI 只是入口 |
|
||||
| 重构成本极高 | 各层独立演化 |
|
||||
|
||||
issue.md 多处提到"清理内联业务逻辑"的迁移(deposit / prepaid / adhoc 都做过),meter 模块**一开始就是这样设计的**,所以没经历这种痛。
|
||||
|
||||
## 测试金字塔
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Action 层<br/>少数 Feature 测试] --> B[Service 层<br/>中等数 Feature 测试]
|
||||
B --> C[Calculator 层<br/>大量 Unit 测试]
|
||||
```
|
||||
|
||||
- Calculator:大量边界情况单元测试(0 用量、负用量、阶梯边界、min max 触发等)
|
||||
- Service:Feature 测试覆盖业务规则(找业户失败、RatePlan 不存在等)
|
||||
- Action:Feature 测试覆盖批量逻辑(部分失败、空集合、重复 reading 等)
|
||||
|
||||
## 性能与并发
|
||||
|
||||
- 单 reading 生成 Bill: ~50ms(查 RatePlan + 业户 + 建 Bill + 更新 reading)
|
||||
- 批量 100 reading: ~5 秒(顺序执行)
|
||||
- 大规模批量(>1000):分 chunk 处理(Action 内置 / 调用方分批)
|
||||
- 并发安全:每次 `generateBillForReading` 在事务内,reading.bill_id 是数据库约束(unique 也好,not null check 也好),不会重复建 Bill
|
||||
|
||||
## 异常处理
|
||||
|
||||
| 异常 | Service 行为 | Action 报告 |
|
||||
|---|---|---|
|
||||
| RatePlan 不存在 | 抛 `RatePlanNotFoundException` | skipped[reason=rate_plan_not_found] |
|
||||
| asset 没绑业户 | 视设计 — 抛 / 建匿名 Bill / 不建 | skipped[reason=no_resident] |
|
||||
| Bill 建表 DB 错误 | 抛(回滚事务)| skipped[reason=db_error] |
|
||||
| reading 已有 bill_id | Service 不接 / Action 跳过 | skipped[reason=already_billed] |
|
||||
| consumption 是负数(读数倒走)| 视设计 — 抛 / 容错 | skipped[reason=negative_consumption] |
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
在 Filament 后台**几乎感受不到**这三层:
|
||||
|
||||
- 看到的就是 `ViewMeter` 或 `ListMeters` 上的"生成账单"按钮
|
||||
- 点击 → 弹出选择 reading 的 Modal → 提交
|
||||
- 系统报告"X 张账单已生成"
|
||||
- 失败的 reading 列出原因
|
||||
|
||||
底层的 Calculator / Service / Action 对业务人员透明。
|
||||
|
||||
## 架构师视角
|
||||
|
||||
这套分层是**评审新功能**的参考:
|
||||
|
||||
| 新需求 | 该改哪一层 |
|
||||
|---|---|
|
||||
| 加新计价算法(累退 / 二次方等)| Calculator 层(纯算)|
|
||||
| 加业户关联规则(房屋多业主时按比例分摊)| Service 层(业务编排)|
|
||||
| 加新触发场景(微信小程序业户主动结账)| Action 层(入口)+ 新 controller |
|
||||
| 改账单字段(加新字段、改 due_at 计算)| Service 层 |
|
||||
| 加批量优化(并行 / 队列)| Action 层 |
|
||||
|
||||
每层都是**单一职责**,改动局部、不会污染其他层。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[meter-vs-meter-reading]]
|
||||
- [[multiplier-and-tiered-pricing]]
|
||||
- [[decommission-and-locking]]
|
||||
- [[generate-bill-tiered-pricing]]
|
||||
- [[generate-bill-with-multiplier]]
|
||||
- [[generate-bill-min-max-cap]]
|
||||
- [[../prepaid/auto-deduction-design]](类似的"Service + Action"分层借鉴)
|
||||
252
prop-acc/concepts/meter/decommission-and-locking.md
Normal file
252
prop-acc/concepts/meter/decommission-and-locking.md
Normal file
@@ -0,0 +1,252 @@
|
||||
---
|
||||
title: prop-acc · meter · 表退役与读数锁定
|
||||
aliases:
|
||||
- 表退役
|
||||
- decommission
|
||||
- 读数锁定
|
||||
- reading lock
|
||||
- MeterDecommissionReason
|
||||
tags:
|
||||
- 概念
|
||||
- prop-acc
|
||||
- 计量表
|
||||
- 数据完整性
|
||||
audience:
|
||||
- 业务人员
|
||||
- 架构师
|
||||
status: 已发布
|
||||
sub_feature: meter
|
||||
last_review: 2026-05-25
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 表退役与读数锁定
|
||||
|
||||
Meter / MeterReading 模块有**两套不可变保护机制**保证数据完整性:
|
||||
|
||||
1. **表退役 (Decommission)** — 表停用后只读,不能改 / 不能删 / 不能继续抄
|
||||
2. **读数锁定 (Reading lock)** — Reading 一经创建不可改;**一旦生成 Bill 更不可改 / 不可删**
|
||||
|
||||
两套机制保证**审计可追溯**,防止"事后改数据"导致账单与历史不符。
|
||||
|
||||
## 第 1 套:表退役机制
|
||||
|
||||
### 5 种退役原因
|
||||
|
||||
`MeterDecommissionReason` 枚举:
|
||||
|
||||
| 枚举 | 中文 | 业务场景 |
|
||||
|---|---|---|
|
||||
| `Damaged` | 损坏 | 表内电路烧 / 表头读不出 / 物理损坏 |
|
||||
| `Replaced` | 更换 | 校验未通过 / 老化主动换,新表带 `-R1` 后缀(详见 [[replacement-chain]]) |
|
||||
| `Removed` | 拆除 | 房屋拆迁 / 业主搬走永久弃用 / 重装时拆掉 |
|
||||
| `Expired` | 到期 | 表的法定使用年限到(法律规定,需校验后定换或保留)|
|
||||
| `Calibration` | 校验 | 送检校验中暂停使用(校验后可重启 / 退役)|
|
||||
|
||||
### 退役后的行为
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> InUse : 装机
|
||||
InUse --> InUse : 抄表 / 编辑配置
|
||||
InUse --> Decommissioned : decommission()
|
||||
Decommissioned --> [*]
|
||||
Decommissioned --> Decommissioned : 只读,不可改 / 不可删 / 不能抄
|
||||
```
|
||||
|
||||
退役表(`is_active=false` + `decommissioned_at` 已填)的能力对照:
|
||||
|
||||
| 操作 | InUse(`is_active=true`)| **Decommissioned(`is_active=false`)** |
|
||||
|---|---|---|
|
||||
| `EditAction`(改 code / multiplier / fee_type) | ✅ | **❌**(UI 灰化 + Policy 拦截)|
|
||||
| `ReplaceMeterAction`(换表)| ✅ | **❌**(已退役无可换)|
|
||||
| 录新 reading | ✅ | **❌**(业务上无表可读)|
|
||||
| 看历史 reading | ✅ | ✅(只读)|
|
||||
| 看历史 Bill | ✅ | ✅(只读)|
|
||||
| `DeleteAction`(物理删除)| **❌**(UI 移除 + Policy 拦截)| **仅允许"已退役 + 无任何读数"**(罕见)|
|
||||
|
||||
> [!warning] 为什么退役表不能改 code / multiplier
|
||||
> 退役表是**历史档案**。改 code / fee_type / multiplier 不会回填到历史 reading / Bill,会让"历史档案"和"当时实际计费配置"对不上号:
|
||||
>
|
||||
> | 反例 | 后果 |
|
||||
> |---|---|
|
||||
> | 已退役表把 multiplier 从 1 改成 10 | 业户翻历史账单,看到 reading consumption 显示翻 10 倍,但当时账单金额没翻 → 业户困惑、质疑系统数据准确性 |
|
||||
> | 已退役表把 code 改个名 | 历史抄表照片上的物理表号对不上系统 code → 审计追溯断链 |
|
||||
>
|
||||
> `EditAction` 在 `is_active=false` 时**三处**(Table 行 / ViewMeter / EditMeter)隐藏,`MeterPolicy::update()` 服务端兜底。
|
||||
|
||||
### 退役表的物理删除
|
||||
|
||||
`MeterPolicy::delete()` 默认拦截 —— 退役表是**历史档案**,正常情况下不该删。**唯一允许**:
|
||||
|
||||
```php
|
||||
// MeterPolicy::delete()
|
||||
public function delete(AuthUser $user, Meter $meter): bool
|
||||
{
|
||||
return $user->can('delete meters')
|
||||
&& ! $meter->is_active // 必须先退役
|
||||
&& ! $meter->hasReadings(); // 且无任何读数(罕见,只在"误建表后未抄过"清理)
|
||||
}
|
||||
```
|
||||
|
||||
**业务场景**:某物业误建了张表(填错 asset),还没抄表就发现错了 → 退役 + 删除。
|
||||
**反例**:已抄过表的表**绝不能删**,删了会级联抹掉历史 reading + Bill 关联,等于消灭审计证据。
|
||||
|
||||
> [!info] 历史:issue.md Q5 第二轮的修复
|
||||
> 原本 `MeterPolicy` 是**空壳**(从父类继承默认 allow),Table 上的 `DeleteAction` / `DeleteBulkAction` / `EditMeter` 上的 `DeleteAction` 全暴露 → 业务人员一键级联删历史。第二轮修复:
|
||||
>
|
||||
> - 删 Table 行 `DeleteAction`
|
||||
> - 删 Table toolbar `DeleteBulkAction`
|
||||
> - 删 EditMeter 页 `DeleteAction`
|
||||
> - `MeterPolicy::delete()` 服务端兜底
|
||||
>
|
||||
> 三处 UI 入口移除 + 一层 Policy 防御 = 双保险。
|
||||
|
||||
## 第 2 套:Reading 锁定机制
|
||||
|
||||
### Reading 创建后不可改
|
||||
|
||||
`MeterReading` 一经创建**只读**(模型设计,无 Update 入口):
|
||||
|
||||
| 字段 | 创建后可改吗 |
|
||||
|---|---|
|
||||
| `current_reading` | ❌ |
|
||||
| `consumption` | ❌(算出来的)|
|
||||
| `read_at` | ❌ |
|
||||
| `source` | ❌ |
|
||||
| `operated_by` | ❌ |
|
||||
| `photo_url` | ❌ |
|
||||
| `memo` | ❌(严格)|
|
||||
| `bill_id` | ❌(由系统填写,业务人员不动)|
|
||||
|
||||
**业务上"修正错误读数"** 走专门流程([[exception-readings-locked-after-bill]]):
|
||||
|
||||
1. **如果还没生成 Bill**:理论上可以删 reading + 重建(看 Policy 允许)
|
||||
2. **如果已生成 Bill**:必须先**作废 Bill** → 改 reading(实际是建新 reading + 标旧 reading 作废)→ 重生成 Bill
|
||||
|
||||
### 已生成 Bill 的 Reading 更严
|
||||
|
||||
如果 `MeterReading.bill_id != null`,有**双锁**:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Reading 创建] --> B[Reading 只读<br/>第 1 锁]
|
||||
B --> C{有 bill_id 吗?}
|
||||
C -->|有| D[更不可改<br/>更不可删<br/>第 2 锁]
|
||||
C -->|无| E[暂时可删<br/>看 Policy]
|
||||
```
|
||||
|
||||
`MeterReadingPolicy`:
|
||||
|
||||
```php
|
||||
// MeterReadingPolicy.php(伪代码)
|
||||
public function update(AuthUser $user, MeterReading $reading): bool
|
||||
{
|
||||
// 几乎永远 false(Reading 不可改)
|
||||
return false;
|
||||
}
|
||||
|
||||
public function delete(AuthUser $user, MeterReading $reading): bool
|
||||
{
|
||||
return $user->can('delete meter readings')
|
||||
&& $reading->bill_id === null; // 已生成 Bill 不可删
|
||||
}
|
||||
```
|
||||
|
||||
UI 同步:
|
||||
|
||||
- `MeterReadingsRelationManager` 上 `EditAction->visible(fn ($r) => $r->bill_id === null)`
|
||||
- 已生成 Bill 的 reading 在 UI 上 Edit / Delete 按钮**自动灰化**
|
||||
|
||||
> [!info] 历史:issue.md Q5 第二轮的修复
|
||||
> 原本 `MeterReadingsRelationManager` 行级 `DeleteAction` 完全暴露 → 已落账的 reading 可被删除 → Bill 数据脱节、审计断链。第二轮修复:
|
||||
>
|
||||
> - 删 `MeterReading.DeleteAction`
|
||||
> - `EditAction` 加 `->visible(bill === null)`
|
||||
> - `MeterReadingPolicy::update()` / `delete()` 服务端兜底要求 `bill === null`
|
||||
|
||||
## 两套机制的协同
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[业务人员发现某 reading 数据错] --> B{Reading 有 bill 吗?}
|
||||
|
||||
B -->|没有| C[尝试删 reading<br/>看 Policy 是否允许]
|
||||
C --> D[重建 reading]
|
||||
D --> E[重生成 Bill]
|
||||
|
||||
B -->|有| F[先作废 Bill<br/>本身是复杂流程]
|
||||
F --> G[Reading 上 bill_id 解除<br/>视设计]
|
||||
G --> H[新建一条修正 reading<br/>+ 标旧 reading 作废]
|
||||
H --> I[重生成 Bill]
|
||||
```
|
||||
|
||||
第二种情况(已生成 Bill)是 issue.md Q5 "待补 / 已知问题"段中提到的:
|
||||
|
||||
> **"作废已生成 Bill 的 MeterReading"组合流程**:当前 Reading 一旦生成 Bill 即锁定。要修正错误读数需要先**作废 Bill** 再改 Reading 再重新生成。这个组合流程类似 AdHocEvent 的 `VoidAction`(级联废 + 留 voided 审计),需要单独设计。
|
||||
|
||||
**当前实施**:不支持自动化,运维 / 高权限人员手工通过 tinker 处理。常见场景见 [[exception-readings-locked-after-bill]]。
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
### 退役表
|
||||
|
||||
通常场景:
|
||||
|
||||
- 检定到期 → `Calibration` → 送校验 → 校验后视情况
|
||||
- 校验未过 → `Replaced` → 走 [[replacement-chain|换表]]
|
||||
- 物理损坏 → `Damaged` → 走 [[replacement-chain|换表]] 或 `Removed` 不换
|
||||
- 业户搬走永久弃用 → `Removed`
|
||||
- 法定使用年限到 → `Expired` → 视情况换 / 退役
|
||||
|
||||
### 已锁定的 Reading
|
||||
|
||||
业务上常见的"修正错误读数"诉求:
|
||||
|
||||
- 抄表员手抖录错(2800 录成 2080)→ 已生成 Bill → 走作废 Bill 流程
|
||||
- 集抄数据错 → 同上
|
||||
- 业户拍照说"实际不是这个数字"→ 拿物理表当前读数对照 → 决定修正与否
|
||||
|
||||
修正不是常规操作,**预防胜于补救** —— 录入时多审核 + 拍照存证([[reading-source-and-photo-proof]])。
|
||||
|
||||
## 架构师视角
|
||||
|
||||
退役 + 锁定两套机制是**严肃的数据治理**:
|
||||
|
||||
- 保证账单与抄表历史**一致性**(不能修改产生过账单的数据)
|
||||
- 保证物业有**审计抗辩能力**(被业户质疑时拿出原始记录)
|
||||
- 保证**长期数据可信**(5 年、10 年后的查询仍准确)
|
||||
|
||||
替代方案(允许改)的代价:
|
||||
|
||||
- 业户质疑账单 → 物业拿不出真实凭证
|
||||
- 内审查不出账面历史 vs 当时实际配置不一致
|
||||
- 法律纠纷物业举证不利
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 已退役表的 Reading 还能新建吗?
|
||||
> 不能。物理表不存在了,业务上无可读。系统层面 `MeterReadingsRelationManager` 在 `is_active=false` 时隐藏 Create 按钮(应该实现,若没有需补)。
|
||||
|
||||
> [!question] 错误退役(其实表还好)能撤销吗?
|
||||
> Policy 设计上**不允许**(`is_active` 从 false 改 true 没 UI 入口)。如果真需要撤销:
|
||||
>
|
||||
> - tinker 直接改字段(运维操作,留备注)
|
||||
> - 或退役表保留 + 建一张新表(`is_active=true`,但失去更换链关联)
|
||||
>
|
||||
> **预防胜于补救**:退役前确认。
|
||||
|
||||
> [!question] Reading 和 Bill 哪个先?
|
||||
> 时间上:Reading 先(抄表),Bill 后(生成账单)。数据上:Reading 创建即写入,Bill 是 reading 的派生。一旦 Bill 创建,会回写 `Reading.bill_id`。
|
||||
|
||||
> [!question] 删 Bill 会自动解锁 Reading 吗?
|
||||
> 当前**不会自动**(Reading.bill_id 不会因为 Bill 删除而自动 nullify,看具体 cascade 配置)。Bill 作废后,需要业务流程明确"是否要重新生成"——如果重新生成,新 Bill 创建时回写 `Reading.bill_id`(覆盖旧的)。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[meter-vs-meter-reading]]
|
||||
- [[replacement-chain]]
|
||||
- [[bill-generation-pipeline]]
|
||||
- [[exception-readings-locked-after-bill]]
|
||||
- [[decommission-without-replacement]]
|
||||
- [[replace-broken-meter]]
|
||||
244
prop-acc/concepts/meter/multiplier-and-tiered-pricing.md
Normal file
244
prop-acc/concepts/meter/multiplier-and-tiered-pricing.md
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: prop-acc · meter · 倍率与阶梯计价
|
||||
aliases:
|
||||
- 倍率
|
||||
- 阶梯计价
|
||||
- multiplier
|
||||
- tiered pricing
|
||||
- min max 封顶
|
||||
tags:
|
||||
- 概念
|
||||
- prop-acc
|
||||
- 计量表
|
||||
- 计费
|
||||
audience:
|
||||
- 业务人员
|
||||
- 财务
|
||||
- 架构师
|
||||
status: 已发布
|
||||
sub_feature: meter
|
||||
last_review: 2026-05-25
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 倍率与阶梯计价
|
||||
|
||||
抄表生成账单的金额由三层叠加算出:**倍率(multiplier)** × **阶梯计价(tiered)** + **min/max 封顶**。这三层都在 `MeterBillCalculator` 内实现(纯算,无 DB IO),是 prop-acc 模块中**最严谨的算法之一**。
|
||||
|
||||
## 一句话总览
|
||||
|
||||
```
|
||||
本月用量 = (current_reading - previous_reading) × multiplier
|
||||
本月金额 = 阶梯计价(本月用量, RatePlan)
|
||||
最终金额 = clamp(本月金额, min, max)
|
||||
```
|
||||
|
||||
详见下文每层展开。
|
||||
|
||||
## 第 1 层:倍率(multiplier)
|
||||
|
||||
### 用途
|
||||
|
||||
工业 / 集团表的**实际计量值远大于表头数字**:
|
||||
|
||||
| 表型 | 表头读数 | multiplier | 实际用量 |
|
||||
|---|---|---|---|
|
||||
| 家用单相表 | 280 度 | 1 | 280 度 |
|
||||
| 三相工业表 | 28 度 | **10** | 280 度 |
|
||||
| 大型集团表(高压侧)| 28 度 | **100** | 2,800 度 |
|
||||
|
||||
物理原理:工业表为了在小表头显示大用量,内部有变压 / 分流比。multiplier 就是这个比值。
|
||||
|
||||
### 字段精度
|
||||
|
||||
```php
|
||||
// migration
|
||||
$table->decimal('multiplier', 10, 4);
|
||||
// model casts
|
||||
'multiplier' => 'decimal:4',
|
||||
```
|
||||
|
||||
decimal(10,4) 精度足够工业场景(常见 1.0 / 10.0 / 100.0 / 1000.0,极少数 0.5 / 1.25 等特殊变比)。
|
||||
|
||||
### 用量计算
|
||||
|
||||
```
|
||||
consumption = (current_reading - previous_reading) × multiplier
|
||||
```
|
||||
|
||||
例:三相表上月 280,本月 308,multiplier=10:
|
||||
|
||||
```
|
||||
consumption = (308 - 280) × 10 = 280 度
|
||||
```
|
||||
|
||||
业户账单按"280 度"算,不是"28 度"。
|
||||
|
||||
## 第 2 层:阶梯计价(Tiered Pricing)
|
||||
|
||||
### 用途
|
||||
|
||||
水电气**阶梯递增**:用得越多,单价越高。鼓励节约,符合国家政策。
|
||||
|
||||
例:某市水阶梯计价:
|
||||
|
||||
| 阶梯 | 月用水(吨)| 单价 |
|
||||
|---|---|---|
|
||||
| 第一阶梯 | 0-20 | 3.0 元/吨 |
|
||||
| 第二阶梯 | 21-30 | 4.5 元/吨 |
|
||||
| 第三阶梯 | 30 以上 | 6.0 元/吨 |
|
||||
|
||||
业户本月用 35 吨:
|
||||
|
||||
| 段 | 用量 | 单价 | 段金额 |
|
||||
|---|---|---|---|
|
||||
| 0-20 吨(第一阶梯) | 20 | 3.0 | 60 |
|
||||
| 21-30 吨(第二阶梯) | 10 | 4.5 | 45 |
|
||||
| 31-35 吨(第三阶梯) | 5 | 6.0 | 30 |
|
||||
| **合计** | **35** | | **135** |
|
||||
|
||||
### Progressive 累进 vs Full-tier 简陋实现
|
||||
|
||||
> [!info] 本系统采用 **progressive 累进**(正确算法),不是 full-tier 简陋实现。
|
||||
|
||||
| 算法 | 35 吨水 |
|
||||
|---|---|
|
||||
| **Progressive 累进**(本系统)| 20 × 3 + 10 × 4.5 + 5 × 6 = 135 元 ✅ |
|
||||
| Full-tier 简陋(错)| 35 × 6 = 210 元 ❌(整月用量按最高阶梯计)|
|
||||
| Mixed(更错)| 35 × 4.5 = 157.5 元 ❌ |
|
||||
|
||||
progressive 是国家政策标准做法,简陋实现是市场上劣质系统的常见 bug。`MeterBillCalculator::calculateTiered()` 实现得对,所以我们的账单准确。
|
||||
|
||||
### RatePlan + RateTier 模型
|
||||
|
||||
阶梯定义在 `RatePlan` + `RateTier` 模型里(不属于本子模块,在更通用的费率模块):
|
||||
|
||||
```
|
||||
RatePlan ─── RateTier (1..n)
|
||||
├── tier=1, lower=0, upper=20, unit_price=3.0
|
||||
├── tier=2, lower=20, upper=30, unit_price=4.5
|
||||
└── tier=3, lower=30, upper=null, unit_price=6.0
|
||||
```
|
||||
|
||||
`fee_type_id`(在 Meter 上)指向 RatePlan,Calculator 拿到 RatePlan → 按 tier 累加段金额。
|
||||
|
||||
## 第 3 层:min / max 封顶
|
||||
|
||||
### 用途
|
||||
|
||||
防止**极端用量**导致离谱账单:
|
||||
|
||||
| 场景 | 问题 | 封顶方案 |
|
||||
|---|---|---|
|
||||
| 业户家漏水,1 月用 1000 吨 | 按阶梯算 ~ 6000+ 元 | `max=2000` 封顶,差额走维修保险 |
|
||||
| 表故障读 0 度 | 业户不付钱了 | `min=20` 兜底(至少收基础费)|
|
||||
| 业户家整月没人(零用量)| 看物业政策 | `min=20` 仍要收(物业服务费性质)|
|
||||
|
||||
### 实现
|
||||
|
||||
`RatePlan` 上有 `min_amount` / `max_amount` 字段:
|
||||
|
||||
```
|
||||
final_amount = max(min_amount, min(calculated_amount, max_amount))
|
||||
```
|
||||
|
||||
例:计算出 ¥6,000 但 `max_amount=2000` → 实际收 ¥2,000,差额 ¥4,000 由物业 / 维修保险承担(账面体现为"封顶减免")。
|
||||
|
||||
### 业务上的取舍
|
||||
|
||||
> [!warning] 封顶不是万能的
|
||||
> max 封顶让业户感激,但物业要承担差额。建议:
|
||||
>
|
||||
> - 设合理的 max(覆盖正常波动范围)
|
||||
> - 异常用量先排查([[exception-high-consumption]])再决定是否减免
|
||||
> - 封顶降低收入,需评估对物业财务可持续性影响
|
||||
|
||||
min 类似:
|
||||
|
||||
> [!info] min 的政策意义
|
||||
> min 通常对应"管网维护费 / 基础服务费",即使零用量也要分摊管网成本。但在国家政策严格的地区,要明确告知业户"min 是什么"。
|
||||
|
||||
## 完整算法流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[抄表 current_reading] --> B[查询上次 reading]
|
||||
B --> C[计算 consumption =<br/>(current - previous) × multiplier]
|
||||
C --> D[加载 RatePlan + RateTier]
|
||||
D --> E[Calculator.calculateTiered<br/>按阶梯累加段金额]
|
||||
E --> F{有 min/max?}
|
||||
F -->|有 max & 金额超| G[封顶到 max]
|
||||
F -->|有 min & 金额低| H[补到 min]
|
||||
F -->|正常范围| I[原值]
|
||||
G --> J[最终账单金额]
|
||||
H --> J
|
||||
I --> J
|
||||
```
|
||||
|
||||
## 完整算例
|
||||
|
||||
电费阶梯(假设):
|
||||
|
||||
| 阶梯 | 用电度数 | 单价 |
|
||||
|---|---|---|
|
||||
| 1 | 0-200 | 0.5 |
|
||||
| 2 | 200-400 | 0.6 |
|
||||
| 3 | 400+ | 0.8 |
|
||||
|
||||
`min_amount=10`,`max_amount=500`。
|
||||
|
||||
工业表(三相,multiplier=10),5 月抄表 308,上月 280:
|
||||
|
||||
```
|
||||
1. consumption = (308 - 280) × 10 = 280 度
|
||||
2. 阶梯:200 × 0.5 + 80 × 0.6 = 100 + 48 = 148 元
|
||||
3. 封顶:10 ≤ 148 ≤ 500 → 不动
|
||||
4. 最终账单:148 元
|
||||
```
|
||||
|
||||
家用表(multiplier=1),5 月抄表 1100,上月 800:
|
||||
|
||||
```
|
||||
1. consumption = (1100 - 800) × 1 = 300 度
|
||||
2. 阶梯:200 × 0.5 + 100 × 0.6 = 100 + 60 = 160 元
|
||||
3. 封顶:10 ≤ 160 ≤ 500 → 不动
|
||||
4. 最终账单:160 元
|
||||
```
|
||||
|
||||
漏水 case(家用表,水阶梯,multiplier=1),本月用 1000 吨:
|
||||
|
||||
```
|
||||
1. consumption = 1000 吨
|
||||
2. 阶梯:20 × 3 + 10 × 4.5 + 970 × 6 = 60 + 45 + 5820 = 5925 元
|
||||
3. 封顶:5925 > max_amount(假设 2000)→ 封到 2000
|
||||
4. 最终账单:2000 元(差额 3925 由物业 / 维修保险承担)
|
||||
```
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
- **配置阶梯**:在 RatePlan + RateTier 模型里设置(不在 meter 子模块,通常运营 / 财务总监来设)
|
||||
- **配置倍率**:建表时 / 后续编辑表时填(`MeterForm`,默认 1)
|
||||
- **核对账单**:看到异常金额时,顺着 Calculator 算法手工验算
|
||||
|
||||
## 财务视角
|
||||
|
||||
- 阶梯计价 = 政策合规
|
||||
- 倍率确保工业表账单正确
|
||||
- min/max 封顶 = 风险控制 + 业户友好(max)
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户**不需要懂这套算法**,看到的是账单金额。但**对账时**要能理解:
|
||||
|
||||
- 上月读数 → 本月读数 → 用量(度 / 吨 / 立方米)
|
||||
- 用量 → 阶梯单价 → 金额
|
||||
- 如有封顶 → 凭证显示
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[meter-vs-meter-reading]]
|
||||
- [[bill-generation-pipeline]]
|
||||
- [[generate-bill-tiered-pricing]]
|
||||
- [[generate-bill-with-multiplier]]
|
||||
- [[generate-bill-min-max-cap]]
|
||||
- [[exception-high-consumption]]
|
||||
206
prop-acc/concepts/meter/reading-source-and-photo-proof.md
Normal file
206
prop-acc/concepts/meter/reading-source-and-photo-proof.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: prop-acc · meter · 抄表来源与拍照存证
|
||||
aliases:
|
||||
- 抄表来源
|
||||
- 抄表拍照存证
|
||||
- manual vs remote
|
||||
- MeterReadingSource
|
||||
tags:
|
||||
- 概念
|
||||
- prop-acc
|
||||
- 计量表
|
||||
- 数据字典
|
||||
audience:
|
||||
- 业务人员
|
||||
- 抄表员
|
||||
- 财务
|
||||
status: 已发布
|
||||
sub_feature: meter
|
||||
last_review: 2026-05-25
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 抄表来源与拍照存证
|
||||
|
||||
每条 `MeterReading` 有 **`source` 字段**标记**抄表数据来源**(`manual` 手抄 vs `remote` 集抄),配合 **`photo_url`** 拍照存证,保证抄表数据**可追溯、可核对、有凭据**。
|
||||
|
||||
## `MeterReadingSource` 枚举
|
||||
|
||||
```php
|
||||
enum MeterReadingSource: string
|
||||
{
|
||||
case Manual = 'manual'; // 抄表员手动录入
|
||||
case Remote = 'remote'; // 集抄系统(IoT)自动上报
|
||||
}
|
||||
```
|
||||
|
||||
只有两种,简洁明了。
|
||||
|
||||
## 两种来源对照
|
||||
|
||||
| 维度 | `manual`(手抄)| `remote`(集抄 / IoT)|
|
||||
|---|---|---|
|
||||
| 触发 | 抄表员到现场读表 + 录入 | 物联网集抄系统定时推送 |
|
||||
| 数据流 | 抄表员手机 / 抄表机 → App / 后台 | IoT 网关 → 后端 API → 系统 |
|
||||
| 速度 | 慢(几天到几周完成全社区)| 快(几小时全社区)|
|
||||
| 准确性 | 中(可能手抖 / 看错)| 高(设备直接传)|
|
||||
| 拍照存证 | **推荐**(`photo_url` 必填或强烈推荐)| 自动 |
|
||||
| 操作员(`operated_by`)| 必填(抄表员 ID)| null 或系统账号 |
|
||||
| 业务上常见 | 中小社区 / 老旧设备 | 新建社区 / 升级改造后 |
|
||||
| 故障率 | 抄表员漏抄、误抄、不录入 | IoT 设备掉线、数据丢失 |
|
||||
|
||||
## 拍照存证(`photo_url`)
|
||||
|
||||
### 为什么要拍照
|
||||
|
||||
抄表是物业**收业户钱**的依据。万一业户质疑账单金额,物业要拿出证据"5 月 X 日,您家表头读数确实是 280 度,这是抄表当天的照片"。
|
||||
|
||||
> [!info] 真实情境
|
||||
> 张阿姨 5 月电费账单 ¥168(280 度),她声称"我家不可能用这么多",要看证据。
|
||||
>
|
||||
> 物业打开后台 → 找 5 月那条 reading → 看 `photo_url` → 给业户看现场拍的表头照片("280" 清晰可见,旁边日期戳)→ **业户无话可说**。
|
||||
|
||||
### 实施细节
|
||||
|
||||
| 字段 | 实施 |
|
||||
|---|---|
|
||||
| `photo_url` | 存储到对象存储(S3 / 阿里云 OSS / 本地)的 URL |
|
||||
| 上传时机 | 抄表 App 录入读数同时上传(强制)|
|
||||
| 上传时机(批量导入)| Excel 批量导入时**没有拍照** → 业务上要求抄表员当场拍 + 单独留存,导入时只录数字 |
|
||||
| 上传时机(remote)| 集抄无拍照(IoT 设备本身就是凭证)|
|
||||
| 数据保留 | 法律 / 业务规定保留期(通常 3-5 年),与账单同周期 |
|
||||
|
||||
### 业务流程上的强制度
|
||||
|
||||
| 物业政策 | 实施 |
|
||||
|---|---|
|
||||
| **必须拍照**(严)| `MeterReadingsRelationManager` Form 上 `photo` 字段标 `->required()`,无照片不能提交 |
|
||||
| **建议拍照**(中)| Form 上 `photo` 可空,UI 上有"建议拍照"提示 |
|
||||
| **不强制**(松)| Form 上 `photo` 可空,无任何提示 |
|
||||
|
||||
当前实现**看具体配置**(应该是建议拍照,可空)。生产环境**强烈推荐"必须拍照"**,避免后续举证困难。
|
||||
|
||||
## 集抄(remote)的对接
|
||||
|
||||
### 数据流
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Meter[物理表]
|
||||
participant Gateway[IoT 网关]
|
||||
participant Backend[第三方集抄平台]
|
||||
participant API[本系统 API]
|
||||
participant DB
|
||||
|
||||
Note over Meter: 物理表通过 RS485 / NB-IoT 等连网关
|
||||
|
||||
loop 定时(每天/每月)
|
||||
Meter->>Gateway: 读数推送
|
||||
Gateway->>Backend: 上传(GSM/4G/有线)
|
||||
Backend->>API: HTTP POST(批量推 reading)
|
||||
API->>DB: 建 MeterReading(source=remote)
|
||||
end
|
||||
```
|
||||
|
||||
### 触发账单生成
|
||||
|
||||
集抄推数后**通常立即触发账单生成**(详见 [[bill-generation-pipeline]]):
|
||||
|
||||
- 集抄 API 接收 → 写 MeterReading → 同事务内或立即触发 `GenerateBillsFromMeterReadingsAction`
|
||||
- 或者每月固定时间批量(避免账单生成时机不一致)
|
||||
|
||||
### 集抄掉线的兜底
|
||||
|
||||
| 集抄掉线 | 处置 |
|
||||
|---|---|
|
||||
| 个别表掉线(少数)| 抄表员补抄(`source=manual` + 备注"集抄掉线")|
|
||||
| 大面积掉线(网关故障)| 集抄运维介入 + 物业短期手抄兜底 + 排查恢复 |
|
||||
| 长期掉线(>2 周)| 重新评估集抄设备的可靠性 |
|
||||
|
||||
## 手动抄表的实施
|
||||
|
||||
### 工具
|
||||
|
||||
- 抄表员手机 App(物业自研 / 第三方)
|
||||
- 老式:纸质本子 + 后台输入(误差大,逐步淘汰)
|
||||
|
||||
### 流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[抄表员到现场] --> B[读表头]
|
||||
B --> C[拍照]
|
||||
C --> D[App 上录入<br/>读数 + 上传照片]
|
||||
D --> E[同步到后台]
|
||||
E --> F[业务人员审核 / 直接生成账单]
|
||||
```
|
||||
|
||||
### 操作员追踪
|
||||
|
||||
每条 `MeterReading` 有 `operated_by` 字段(抄表员 ID):
|
||||
|
||||
- 业户对账单有疑问 → 后台查 → 看是谁抄的
|
||||
- 抄表员考核:本月抄了多少表、有无遗漏
|
||||
- 异常追责:某抄表员的读数频繁错 → 培训 / 换人
|
||||
|
||||
## 业务人员视角
|
||||
|
||||
后台 `ViewMeter` → 看 Reading 列表(`MeterReadingsRelationManager`),每条 reading 显示:
|
||||
|
||||
| 列 | 内容 |
|
||||
|---|---|
|
||||
| 抄表日期 | `read_at` |
|
||||
| 读数 | `current_reading` |
|
||||
| 用量 | `consumption` |
|
||||
| 来源 | `manual` / `remote`(图标区分)|
|
||||
| 抄表员 | `operated_by`(`manual` 时显示)|
|
||||
| 拍照 | `photo_url`(有图标,点开看照片)|
|
||||
| 备注 | `memo` |
|
||||
|
||||
## 抄表员视角
|
||||
|
||||
抄表员李师傅每月 25-30 号集中抄表:
|
||||
|
||||
- 拿手机 App 出门
|
||||
- 按片区(楼栋)走,App 自动按楼层 / 房号给清单
|
||||
- 每家:开门(若有人)→ 找表 → 读数 → 拍照 → 录入
|
||||
- 数据**实时上传**(网络可用时)或**离线缓存**(无网时回去再传)
|
||||
- 当月所有表抄完 → App 显示"完成度 100%" → 提交
|
||||
|
||||
每天 KPI:60-100 户(看小区密度)。
|
||||
|
||||
## 业户视角
|
||||
|
||||
业户**通常感知不到抄表细节**,只看月底账单。对账单有异议时:
|
||||
|
||||
- 联系物业询问
|
||||
- 物业出示 `photo_url` 证据
|
||||
- 如果证据充分 → 业户认可
|
||||
- 如果证据不足(无照片 + 抄表员说不清)→ 物业可能减免 / 让业户支付平均月用量
|
||||
|
||||
## 系统视角:不可变 + 双锁
|
||||
|
||||
详见 [[meter-vs-meter-reading]] "两者的契约" + [[decommission-and-locking]] "已生成 Bill 的 Reading 锁定" 段。
|
||||
|
||||
- MeterReading 一经创建**不可改**
|
||||
- 一旦生成 Bill 后**更不可改**(Policy 双锁)
|
||||
- 任何错误走"作废 Bill → 改 Reading → 重生成 Bill"的组合流程(详见 [[exception-readings-locked-after-bill]])
|
||||
|
||||
## 待补 / 已知问题
|
||||
|
||||
| 项 | 状态 |
|
||||
|---|---|
|
||||
| 集抄回调签名校验 / 防重放 | 未文档化(可能已实现,看具体集成 |
|
||||
| 抄表照片的对象存储清理(留存期满)| 未实现,需求看物业法务定 |
|
||||
| 抄表员位置 / 时间戳防作弊(GPS + 时间)| 未实现,部分物业有此需求 |
|
||||
| 拍照强制 + AI 识别照片内读数比对录入 | 未实现,高大上但实施成本高 |
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[meter-vs-meter-reading]]
|
||||
- [[bill-generation-pipeline]]
|
||||
- [[decommission-and-locking]]
|
||||
- [[read-single-meter-manual]]
|
||||
- [[read-batch-via-excel-import]]
|
||||
- [[read-via-iot-remote-source]]
|
||||
- [[read-with-photo-proof]]
|
||||
205
prop-acc/concepts/meter/replacement-chain.md
Normal file
205
prop-acc/concepts/meter/replacement-chain.md
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: prop-acc · meter · 表更换链
|
||||
aliases:
|
||||
- 表更换链
|
||||
- Meter Replacement Chain
|
||||
- replaced_meter_id
|
||||
- R1 R2 后缀
|
||||
tags:
|
||||
- 概念
|
||||
- prop-acc
|
||||
- 计量表
|
||||
- 数据模型
|
||||
audience:
|
||||
- 业务人员
|
||||
- 抄表员
|
||||
- 架构师
|
||||
status: 已发布
|
||||
sub_feature: meter
|
||||
last_review: 2026-05-25
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 表更换链
|
||||
|
||||
物理表会**老化、损坏、定期校验**。旧表换新表后,系统通过 **`replaced_meter_id`** 字段把新表指回旧表,**初始读数继承**(避免业户被白嫖一段用量)。新表编号自动加 **`-R1` / `-R2` / ...** 后缀,**整条更换链**(代代相承)在数据库里可追溯。
|
||||
|
||||
## 为什么要更换链
|
||||
|
||||
> [!info] 真实情境
|
||||
> 张阿姨家电表(编号 E-501)用了 8 年,2026 年 5 月 物业例行校验发现读数跳变(怀疑表内电路老化),要换新表。
|
||||
>
|
||||
> 换表那天:
|
||||
> - 旧表 E-501 最后读数 = 5,000 度
|
||||
> - 新表 E-501-R1 出厂 = 0 度,但**初始读数继承 5,000 度**
|
||||
> - 否则:新表从 0 起 → 业户下个月看着账单(假设用了 50 度)→ 但系统算的 "5050 - 0 = 5050 度"全要业户付 → 灾难
|
||||
|
||||
更换链保证**用量计算连续**,业户感知无差异。
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 字段关系
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Meter 旧表<br/>E-501<br/>final_reading=5000<br/>is_active=false<br/>decommissioned_at=2026-05-15<br/>decommission_reason=Replaced] -->|被 replaced_meter_id 指回| B[Meter 新表<br/>E-501-R1<br/>initial_reading=5000<br/>is_active=true<br/>installed_at=2026-05-15<br/>replaced_meter_id=旧表 ID]
|
||||
```
|
||||
|
||||
### 字段语义
|
||||
|
||||
| Meter 字段 | 旧表(被换)| 新表(替代)|
|
||||
|---|---|---|
|
||||
| `code` | E-501 | **E-501-R1**(`nextReplacementCode()` 自动算)|
|
||||
| `is_active` | **false** | true |
|
||||
| `installed_at` | 8 年前 | 2026-05-15(换表当天)|
|
||||
| `decommissioned_at` | **2026-05-15** | null |
|
||||
| `decommission_reason` | `Replaced`(枚举,见 [[decommission-and-locking]])| null |
|
||||
| `final_reading` | **5000**(换表前最后读数)| null |
|
||||
| `initial_reading` | (历史值,不动)| **5000**(继承自旧表)|
|
||||
| `replaced_meter_id` | null(无上一代)| **旧表 ID** |
|
||||
|
||||
## 换表后的抄表数据
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[E-501 最后 reading<br/>2026-04-30 → 5000 度] --> B[换表 2026-05-15]
|
||||
B --> C[E-501-R1 第一次抄表<br/>2026-05-31 → ?]
|
||||
|
||||
C --> D[计算 5月用量]
|
||||
D --> E{用量公式}
|
||||
E -->|新表上累计读数| F[假设新表读到 50 度<br/>但 initial_reading=5000<br/>所以 current = 5050]
|
||||
F --> G[consumption = (5050 - 5000) * multiplier = 50 度]
|
||||
```
|
||||
|
||||
**关键**:新表 `initial_reading=5000` **不是**抄表的起点,而是用于计算用量的**基准**。下次抄表时:
|
||||
|
||||
```
|
||||
current_reading(新表表头读数 + initial_reading)= 50 + 5000 = 5050
|
||||
previous_reading = 5000(继承自旧表最后读数)
|
||||
consumption = (current - previous) × multiplier = (5050 - 5000) × 1 = 50
|
||||
```
|
||||
|
||||
业户付的就是 5 月实际用的 50 度,不是 5050。
|
||||
|
||||
> [!warning] 抄表员要注意
|
||||
> 抄表系统**显示给抄表员的数字是物理表头的数字**(50),系统**内部存的是叠加值**(5050)。如果系统设计不一致(让抄表员录 5050),会让人困惑。当前实现需查 `MeterReadingsRelationManager` / `MeterReadingsImporter` 看具体如何处理。
|
||||
|
||||
## 整条链的追溯
|
||||
|
||||
一张表可能多次换:
|
||||
|
||||
```
|
||||
E-501 (原生) → E-501-R1 (第一次换) → E-501-R2 (第二次换) → E-501-R3 (第三次换)
|
||||
```
|
||||
|
||||
每张表 `replaced_meter_id` 指上一代。后台 / API 可以:
|
||||
|
||||
```php
|
||||
// 找当前在役表
|
||||
$current = Meter::where('asset_id', $assetId)
|
||||
->where('fee_type_id', $feeType)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
// 顺着链向上追溯
|
||||
$predecessors = [];
|
||||
$cursor = $current;
|
||||
while ($cursor->replaced_meter_id) {
|
||||
$cursor = Meter::find($cursor->replaced_meter_id);
|
||||
$predecessors[] = $cursor;
|
||||
}
|
||||
// $predecessors 现在是 [R2, R1, 原生] 的逆序数组
|
||||
```
|
||||
|
||||
业务上可用于:
|
||||
|
||||
- 业户对历史用量有异议:看哪张表抄出来的
|
||||
- 表更换历史报表
|
||||
- 长期累计用量
|
||||
|
||||
## `nextReplacementCode()` 实现
|
||||
|
||||
代码 `Meter::nextReplacementCode($oldMeterCode)` 算法:
|
||||
|
||||
```php
|
||||
// 伪代码
|
||||
function nextReplacementCode(string $oldCode): string
|
||||
{
|
||||
// E-501 → E-501-R1
|
||||
// E-501-R1 → E-501-R2
|
||||
// E-501-R2 → E-501-R3
|
||||
if (preg_match('/^(.*)-R(\d+)$/', $oldCode, $matches)) {
|
||||
return $matches[1] . '-R' . ($matches[2] + 1);
|
||||
}
|
||||
return $oldCode . '-R1';
|
||||
}
|
||||
```
|
||||
|
||||
业务人员**不需要手动想**新表编号,系统自动算。
|
||||
|
||||
## 操作:`ReplaceMeterAction`
|
||||
|
||||
后台 → 计量表 → 找旧表 → 进 `ViewMeter` → 点 `ReplaceMeterAction`。
|
||||
|
||||
Modal 表单:
|
||||
|
||||
| 字段 | 填什么 |
|
||||
|---|---|
|
||||
| **旧表最后读数(`final_reading`)** | 现场拍照确认,如 `5000` |
|
||||
| **退役原因(`decommission_reason`)** | 选 `Replaced`(其他选项见 [[decommission-and-locking]])|
|
||||
| **退役日期** | 默认今天 |
|
||||
| **新表编号** | 自动 `E-501-R1`(可改但不推荐)|
|
||||
| **新表 multiplier** | 默认继承旧表(可改)|
|
||||
| **新表安装日期** | 默认今天 |
|
||||
| 备注 | "校验未通过,换新表" |
|
||||
|
||||
提交后系统在一个事务内:
|
||||
|
||||
1. 旧表 `is_active=false`, `decommissioned_at=今天`, `decommission_reason=Replaced`, `final_reading=5000`
|
||||
2. 建新表 `is_active=true`, `installed_at=今天`, `replaced_meter_id=旧表.id`, `initial_reading=5000`, `multiplier=继承`
|
||||
|
||||
## 常见问题
|
||||
|
||||
> [!question] 旧表读数比新表初始低,会发生吗?
|
||||
> 不会。新表的 `initial_reading` 就是旧表的 `final_reading`,逻辑上必然相等。
|
||||
|
||||
> [!question] 换表时业户家正在用电怎么处理?
|
||||
> 实际换表过程要断电断水短暂时间,业户可感知。系统层面:
|
||||
>
|
||||
> - 旧表 `decommissioned_at` 和新表 `installed_at` 都填换表那天
|
||||
> - 中间用电量(几分钟到几小时)的微小差异通常忽略
|
||||
> - 严格的物业可在换表说明里告知业户"换表过程几分钟用电不计费"
|
||||
|
||||
> [!question] 换表后旧表的历史 MeterReading 还能查吗?
|
||||
> 能。每条 reading 都关联 `meter_id`(旧表 ID),不会因换表丢失。审计可完整追溯。
|
||||
|
||||
> [!question] 误换表(其实不该换)能撤销吗?
|
||||
> 不能直接撤销(MeterReading 不可变,Meter 状态也不轻易回滚)。要修复:
|
||||
>
|
||||
> - 物理上把新表退役(`is_active=false`, `decommission_reason=Removed`)
|
||||
> - 把旧表重启(`is_active=true`, `decommissioned_at=null`)→ 但这种"复活"操作在 Policy 层可能被守护拒绝([[decommission-and-locking]])
|
||||
>
|
||||
> **预防胜于补救**:换表前确认。
|
||||
|
||||
> [!question] 同一张表换好多次,链很长怎么办?
|
||||
> 链长本身不是问题,系统正常处理。如果链过长(>10 代),通常说明该表频繁出问题,业务上应:
|
||||
>
|
||||
> - 排查表的型号 / 安装环境
|
||||
> - 考虑改型号 / 换品牌
|
||||
> - 长链不影响数据查询性能(关联查询逐级递归,但物业表数量通常不大)
|
||||
|
||||
> [!question] 新表 `multiplier` 与旧表不同可以吗?
|
||||
> 可以,但**强烈不推荐**。如果新换的表倍率不同(例如旧 1x → 新 10x),用量计算公式就变,容易让业户困惑。除非业务上有明确升级原因(从普通家用表换成工业表),否则**继承旧表 multiplier**。
|
||||
|
||||
## 异常分支
|
||||
|
||||
- 表损坏(非校验)→ 走 [[replace-broken-meter]] 场景(meter_decommission_reason=Damaged)
|
||||
- 不换表只退役 → [[decommission-without-replacement]]
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [[meter-vs-meter-reading]]
|
||||
- [[decommission-and-locking]]
|
||||
- [[multiplier-and-tiered-pricing]]
|
||||
- [[replace-broken-meter]]
|
||||
- [[decommission-without-replacement]]
|
||||
@@ -26,7 +26,7 @@ last_review: 2026-05-25
|
||||
| **一次性收费** | IC 卡、装修证、泳票等单次购买 | [adhoc 知识地图](maps/adhoc-knowledge-map.md) | ✅ 28 篇 |
|
||||
| **保证金** | 装修押金等代管资金,完工后退还 | [deposit 知识地图](maps/deposit-knowledge-map.md) | ✅ 25 篇 |
|
||||
| **预存款** | 业户预存,自动抵扣月度账单 | [prepaid 知识地图](maps/prepaid-knowledge-map.md) | ✅ 23 篇 |
|
||||
| **计量表** | 水表/电表/燃气表,抄表生成账单 | _待补_ | 🚧 |
|
||||
| **计量表** | 水表/电表/燃气表,抄表生成账单 | [meter 知识地图](maps/meter-knowledge-map.md) | 🟡 6 概念已完成,14 场景待补 |
|
||||
| **账单** | 周期性账单 + 计量账单 | _待补_ | 🚧 |
|
||||
| **收款订单** | 一次收款的支付方式、银行账户记录 | _待补_ | 🚧 |
|
||||
| **收据** | 成功收款后生成的凭证 | _待补_ | 🚧 |
|
||||
|
||||
@@ -22,7 +22,7 @@ last_review: 2026-05-25
|
||||
| adhoc | 一次性收费 | IC 卡、装修证、泳票等单次购买 | [adhoc 知识地图](adhoc-knowledge-map.md) | ✅ 25 场景 + 3 概念 |
|
||||
| prepaid | 预存款 | 业户预存,自动抵扣月度账单 | [prepaid 知识地图](prepaid-knowledge-map.md) | ✅ 16 场景 + 6 概念 + 1 地图 = 23 篇 |
|
||||
| deposit | 保证金 | 装修押金等代管资金,完工后退还 | [deposit 知识地图](deposit-knowledge-map.md) | ✅ 18 场景 + 6 概念 + 1 地图 = 25 篇 |
|
||||
| meter | 计量表 | 水表/电表/燃气表,抄表生成账单 | _待补_ | 🚧 |
|
||||
| meter | 计量表 | 水表/电表/燃气表,抄表生成账单 | [meter 知识地图](meter-knowledge-map.md) | 🟡 6 概念已完成,14 场景待补 |
|
||||
| billing | 账单 | 周期性账单 + 计量账单 | _待补_ | 🚧 |
|
||||
| payment-order | 收款订单 | 一次收款的支付方式、银行账户记录 | _待补_ | 🚧 |
|
||||
| receipt | 收据 | 成功收款后生成的凭证 | _待补_ | 🚧 |
|
||||
|
||||
134
prop-acc/maps/meter-knowledge-map.md
Normal file
134
prop-acc/maps/meter-knowledge-map.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: prop-acc · meter · 知识地图
|
||||
aliases:
|
||||
- meter 知识地图
|
||||
- 计量表知识地图
|
||||
tags:
|
||||
- 规范
|
||||
- prop-acc
|
||||
- 知识地图
|
||||
- 计量表
|
||||
sub_feature: meter
|
||||
audience:
|
||||
- 业务人员
|
||||
- 抄表员
|
||||
- 财务
|
||||
status: 已发布
|
||||
last_review: 2026-05-25
|
||||
code_version: 2026-05-22
|
||||
---
|
||||
|
||||
# 计量表(meter)知识地图
|
||||
|
||||
> 本子模块 = Meter(物理表配置)+ MeterReading(不可变抄表流水)。覆盖物业计量收费(水电气)的全生命周期 —— 建表、抄表、生成账单、表更换、表退役、异常处理。
|
||||
|
||||
## 这是什么?
|
||||
|
||||
物业管理水表 / 电表 / 燃气表的**计量计费基础设施**。从"业户家里有一张物理表"到"每月物业费账单里有一行水电费",中间走的就是本模块。
|
||||
|
||||
> [!info] meter 是 prop-acc 里**最成熟**的子模块
|
||||
> issue.md Q5 评估:数据模型对齐市场标准 ~90%,业务分层清楚(`Calculator → Service → Action`),完整的换表链、倍率支持、阶梯计价、min/max 封顶、抄表来源跟踪、拍照存证、初始化批量导入。**后续 deposit / prepaid / adhoc 模块的分层方法是从 meter 学的**。
|
||||
|
||||
## 与其他子模块的关系
|
||||
|
||||
| 关系 | 说明 |
|
||||
|---|---|
|
||||
| **下游产 Bill,不直接产 Receipt** | 抄表 → 生成 Bill → 业户付账单时走 [adhoc 的 CollectionOrder + Receipt 体系](../concepts/adhoc/collection-order-and-receipt.md) |
|
||||
| **业户付账单的资金可来自预存款** | 走 [prepaid 的 consume](../concepts/prepaid/consume-via-bill-collection-type.md) 模式 —— Bill.amount 自动从预存款余额扣 |
|
||||
| **本模块不涉及押金** | 计量表是日常计费工具,无押金概念 |
|
||||
|
||||
## 核心特性(与其他模块对比)
|
||||
|
||||
| 维度 | meter | deposit / prepaid |
|
||||
|---|---|---|
|
||||
| 主对象类型 | **物理硬件**(表)| 抽象账户 |
|
||||
| 主对象有 balance | ❌ | ✅ |
|
||||
| 流水方向 | 单向(只录读数,无 +/-) | 双向(deposit / refund / forfeit / consume) |
|
||||
| 直接产 Receipt | ❌(走 Bill 中转) | ✅ |
|
||||
| 表更换 / 退役机制 | ✅(`replaced_meter_id` 链 + 5 种退役原因) | N/A |
|
||||
| 来源标记(manual/remote) | ✅ | ❌ |
|
||||
| 拍照存证 | ✅ | ❌ |
|
||||
|
||||
## 核心概念(6 篇)
|
||||
|
||||
| 文档 | 一句话 |
|
||||
|---|---|
|
||||
| [计量表与抄表流水](../concepts/meter/meter-vs-meter-reading.md) | 双对象(物理表配置 + 不可变读数流水),与"账户+流水"模式的差异 |
|
||||
| [表更换链](../concepts/meter/replacement-chain.md) | `replaced_meter_id` + 自动 `-R1` 后缀 + 初始读数继承,保证用量计算连续 |
|
||||
| [倍率与阶梯计价](../concepts/meter/multiplier-and-tiered-pricing.md) | 倍率(工业表 10x/100x) + 阶梯计价(progressive 累进) + min/max 封顶 |
|
||||
| [账单生成的三层分层](../concepts/meter/bill-generation-pipeline.md) | Calculator(纯算)→ Service(查费率 + 找业主)→ Action(入口),prop-acc 的样板 |
|
||||
| [抄表来源与拍照存证](../concepts/meter/reading-source-and-photo-proof.md) | `manual` 手抄 vs `remote` 集抄 + `photo_url` 凭证,业户争议时的证据 |
|
||||
| [表退役与读数锁定](../concepts/meter/decommission-and-locking.md) | 5 种退役原因 + Reading 双锁机制(创建即不可改,有 Bill 更不可改) |
|
||||
|
||||
## 场景手册(14 篇,**待补充 ✋**)
|
||||
|
||||
> 🚧 概念骨架已就位,场景文档将在下一轮(轮 2)产出。预定结构如下。
|
||||
|
||||
### 📦 表管理(4 篇)
|
||||
|
||||
- 🚧 [新社区批量建表 + 初始读数 Excel 导入](../scenarios/meter/init-new-community-batch.md)
|
||||
- 🚧 [单独新增一张表(后台单录)](../scenarios/meter/register-single-meter.md)
|
||||
- 🚧 [换表:旧表故障/退役,新表带 -R1 后缀,初始读数继承](../scenarios/meter/replace-broken-meter.md)
|
||||
- 🚧 [退役不换表(房屋拆除 / 业户永久弃用)](../scenarios/meter/decommission-without-replacement.md)
|
||||
|
||||
### 📊 抄表(4 篇)
|
||||
|
||||
- 🚧 [单张表后台手动录入](../scenarios/meter/read-single-meter-manual.md)
|
||||
- 🚧 [一次导入整月所有读数(Excel 批量)](../scenarios/meter/read-batch-via-excel-import.md)
|
||||
- 🚧 [集抄系统自动推送(`source=remote`)](../scenarios/meter/read-via-iot-remote-source.md)
|
||||
- 🚧 [抄表拍照存证(物理表头照片)](../scenarios/meter/read-with-photo-proof.md)
|
||||
|
||||
### 💰 账单生成(3 篇)
|
||||
|
||||
- 🚧 [阶梯水电价生成账单(progressive 累进算例)](../scenarios/meter/generate-bill-tiered-pricing.md)
|
||||
- 🚧 [工业表 10x 倍率生成账单](../scenarios/meter/generate-bill-with-multiplier.md)
|
||||
- 🚧 [单笔账单上下限封顶(防异常用量爆账)](../scenarios/meter/generate-bill-min-max-cap.md)
|
||||
|
||||
### 🛡️ 异常 / 审计(3 篇)
|
||||
|
||||
- 🚧 [高用量异常(漏水 / 电器故障),`HighConsumptionReadingsListWidget` 预警](../scenarios/meter/exception-high-consumption.md)
|
||||
- 🚧 [已生成 Bill 的 Reading 锁定,要修正需作废 Bill](../scenarios/meter/exception-readings-locked-after-bill.md)
|
||||
- 🚧 [待抄表清单 + 月度抄表完成率(`MetersNeedingReadingListWidget`)](../scenarios/meter/audit-meters-needing-reading.md)
|
||||
|
||||
## 跨域引用
|
||||
|
||||
本子模块引用以下跨域共享概念:
|
||||
|
||||
- [业户](../../cross/concepts/resident.md) — 账单关联业户
|
||||
- [房屋单元](../../cross/concepts/housing-unit.md) — `asset_id`,表绑定房屋
|
||||
- [组织结构](../../cross/concepts/org-hierarchy.md) — `community_id`,物业项目归属
|
||||
|
||||
## 跨子模块引用
|
||||
|
||||
- [adhoc · CollectionOrder 与 Receipt](../concepts/adhoc/collection-order-and-receipt.md) — 计量账单付款时走的凭证体系
|
||||
- [prepaid · Consume 走 CollectionType=Bill 的设计](../concepts/prepaid/consume-via-bill-collection-type.md) — 计量账单可由预存款抵扣
|
||||
- [deposit · 账户与流水](../concepts/deposit/deposit-account-vs-transaction.md) — 账户+流水模式对比
|
||||
|
||||
## 相关代码
|
||||
|
||||
- 模型:[`Meter.php`](../../../packages/prop-acc/src/Models/Meter.php)、[`MeterReading.php`](../../../packages/prop-acc/src/Models/MeterReading.php)
|
||||
- 枚举:`MeterReadingSource`(2 种)、`MeterDecommissionReason`(5 种)
|
||||
- Policy:`MeterPolicy`(3 个方法)、`MeterReadingPolicy`(2 个方法)
|
||||
- 业务层:
|
||||
- [`MeterBillCalculator`](../../../packages/prop-acc/src/Services/MeterBillCalculator.php)(纯算)
|
||||
- [`MeterBillGenerationService`](../../../packages/prop-acc/src/Services/MeterBillGenerationService.php)(业务编排)
|
||||
- [`GenerateBillsFromMeterReadingsAction`](../../../packages/prop-acc/src/Actions/Meters/GenerateBillsFromMeterReadingsAction.php)(入口)
|
||||
- Filament Resource:[`packages/prop-acc/src/Filament/Resources/Meters/`](../../../packages/prop-acc/src/Filament/Resources/Meters/)
|
||||
- Importers:`MeterReadingsImporter`、`MeterInitializationImporter`(继承 `BaseImporter`)
|
||||
- Dashboard / Widgets:`MeterDashboard`、`MetersNeedingReadingListWidget`、`HighConsumptionReadingsListWidget`、`MonthlyConsumptionByFeeTypeChart`、`MeterStatsOverviewWidget`
|
||||
- 业务设计决策:`packages/prop-acc/issue.md` 的 Q5 段
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [prop-acc 域知识地图](knowledge-map.md)
|
||||
- [prop-acc 域首页](../index.md)
|
||||
- [adhoc 子模块知识地图](adhoc-knowledge-map.md)
|
||||
- [deposit 子模块知识地图](deposit-knowledge-map.md)
|
||||
- [prepaid 子模块知识地图](prepaid-knowledge-map.md)
|
||||
- [跨域协作地图](../../cross/maps/cross-domain-map.md)
|
||||
|
||||
---
|
||||
|
||||
> [!info] 概念已完成,场景待补
|
||||
> 本轮(轮 1)产出:6 个概念 + 本子模块地图 + 域总图更新。
|
||||
> 下一轮(轮 2)产出:14 个场景文档,基于本知识地图骨架填充。
|
||||
Reference in New Issue
Block a user