# 财务计费领域技术方案

**领域编号**: 4.4  
**微服务**: ether-finance  
**最后更新**: 2026-04-26

> **更新记录**:
> - 2026-04-26: 反向同步实际代码实现到文档。主要变更：修正 FeeItem/FeeBill/FeePayment 的实现状态标注为"未实现"（原标注为"已实现"与实际代码不符）；修正费用催缴/滞纳金的实现状态标注为"未实现"；新增能耗计量管理章节（EnergyMeter/EnergyConsumption，当前在 module-mdm 中实现）；新增能源类型枚举（EnergyType）；新增记录方式枚举（RecordMethod）；明确能耗管理与财务计费的边界。

---

## 一、领域概述

### 1.1 领域职责

财务计费领域负责物业费用管理全生命周期：
- 收费项目管理（物业费、停车费、水电费等）
- 账单生成与管理
- 支付处理与对账
- 费用催缴通知
- 财务报表与统计

### 1.2 核心概念

| 概念 | 说明 | 对应实体 | 实现状态 |
|------|------|----------|----------|
| **收费项目** | 可收费的项目定义 | FeeItem | 未实现 |
| **账单** | 应收费用单据 | FeeBill | 未实现 |
| **支付记录** | 支付流水记录 | FeePayment | 未实现 |
| **退款** | 退款申请与处理 | FeeRefund | 未实现 |
| **能源计量点** | 能耗数据采集点 | EnergyMeter | 已实现(module-mdm) |
| **能耗记录** | 抄表数据与费用计算 | EnergyConsumption | 已实现(module-mdm) |

---

## 二、领域模型

### 2.1 聚合根设计

#### FeeItem（收费项目）

```java
@Entity
@Table(name = "fin_fee_item")
@Data
public class FeeItem {
    @Id
    private UUID id;
    private UUID projectId;
    
    private String code;           // 项目编码
    private String name;           // 项目名称
    private FeeType type;          // PROPERTY/PARKING/WATER/ELECTRICITY/GAS/OTHER
    
    // 计费规则
    private BillingMethod billingMethod; // FIXED/AREA/USAGE/CUSTOM
    private BigDecimal unitPrice;  // 单价
    private String unit;           // 单位: 元/月、元/㎡·月、元/度
    
    // 账期
    private Integer billDay;       // 账单日(每月几号)
    private Integer dueDay;        // 到期日(每月几号)
    private Integer overdueDay;    // 逾期日(每月几号)
    
    // 滞纳金
    private Boolean enableLateFee;
    private BigDecimal lateFeeRate; // 滞纳金比例(日利率)
    private BigDecimal maxLateFee;  // 滞纳金上限
    
    // 状态
    private Boolean enabled;
    
    // 审计字段
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// 收费类型枚举
public enum FeeType {
    PROPERTY("物业费"),
    PARKING("停车费"),
    WATER("水费"),
    ELECTRICITY("电费"),
    GAS("燃气费"),
    HEATING("供暖费"),
    REPAIR_FUND("维修基金"),
    OTHER("其他");
}

// 计费方式枚举
public enum BillingMethod {
    FIXED("固定金额"),
    AREA("按面积"),
    USAGE("按用量"),
    CUSTOM("自定义");
}
```

#### FeeBill（收费账单）

```java
@Entity
@Table(name = "fin_fee_bill")
@Data
public class FeeBill {
    @Id
    private UUID id;
    private UUID projectId;
    
    private String billNo;         // 账单编号
    
    // 关联信息
    private UUID feeItemId;        // 收费项目
    private UUID spaceNodeId;      // 关联房产
    private UUID ownerId;          // 关联业主
    
    // 账期
    private String billPeriod;     // 账期: 2024-02
    private LocalDate billDate;    // 账单日期
    private LocalDate dueDate;     // 到期日期
    private LocalDate overdueDate; // 逾期日期
    
    // 用量（按用量计费）
    private BigDecimal usageAmount; // 用量
    private String usageUnit;      // 用量单位
    
    // 金额
    private BigDecimal amount;     // 应收金额
    private BigDecimal lateFee;    // 滞纳金
    private BigDecimal discount;   // 优惠金额
    private BigDecimal payableAmount; // 应付金额 = amount + lateFee - discount
    private BigDecimal paidAmount; // 已付金额
    
    // 状态
    private BillStatus status;     // UNPAID/PARTIAL_PAID/PAID/OVERDUE/CANCELLED
    
    // 关联支付记录
    @OneToMany(mappedBy = "bill")
    private List<FeePayment> payments;
    
    // 审计字段
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// 账单状态枚举
public enum BillStatus {
    UNPAID("未缴费"),
    PARTIAL_PAID("部分缴费"),
    PAID("已缴费"),
    OVERDUE("已逾期"),
    CANCELLED("已取消");
}
```

#### FeePayment（支付记录）

```java
@Entity
@Table(name = "fin_fee_payment")
@Data
public class FeePayment {
    @Id
    private UUID id;
    private UUID billId;
    
    private String paymentNo;      // 支付流水号
    private BigDecimal amount;     // 支付金额
    
    // 支付方式
    private PaymentMethod method;  // WECHAT/ALIPAY/CASH/BANK_TRANSFER/CARD
    private String thirdPartyNo;   // 第三方支付流水号
    
    // 支付状态
    private PaymentStatus status;  // PENDING/SUCCESS/FAILED/REFUNDED
    private String failReason;     // 失败原因
    
    // 时间
    private LocalDateTime paymentTime;
    private String remark;
    
    // 操作人
    private UUID operatorId;
    private String operatorName;
    
    // 审计字段
    private LocalDateTime createdAt;
}

// 支付方式枚举
public enum PaymentMethod {
    WECHAT("微信支付"),
    ALIPAY("支付宝"),
    CASH("现金"),
    BANK_TRANSFER("银行转账"),
    CARD("刷卡");
}
```

#### FeeRefund（退款记录）

```java
@Entity
@Table(name = "fin_fee_refund")
@Data
public class FeeRefund {
    @Id
    private UUID id;
    private UUID projectId;
    private UUID billId;
    private UUID paymentId;        // 关联支付记录
    
    private String refundNo;       // 退款单号
    private BigDecimal amount;     // 退款金额
    private String reason;         // 退款原因
    
    // 状态
    private RefundStatus status;   // PENDING/APPROVED/REJECTED/REFUNDED
    
    // 审批
    private UUID approverId;
    private String approverName;
    private LocalDateTime approveTime;
    private String approveRemark;
    
    // 退款执行
    private String thirdPartyRefundNo;
    private LocalDateTime refundTime;
    
    // 审计字段
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
```

---

## 三、费用催缴

### 3.1 定时任务

```java
@Component
public class FeeReminderJob {
    
    @Autowired
    private FeeBillRepository feeBillRepository;
    
    @Autowired
    private NotificationService notificationService;
    
    // 每天9点执行：提醒即将到期
    @Scheduled(cron = "0 0 9 * * ?")
    public void remindUpcomingDue() {
        // 查询3天内到期的账单
        LocalDate targetDate = LocalDate.now().plusDays(3);
        
        List<FeeBill> upcomingBills = feeBillRepository
            .findByDueDateBetweenAndStatus(LocalDate.now(), targetDate, BillStatus.UNPAID);
        
        for (FeeBill bill : upcomingBills) {
            notificationService.sendFeeReminder(bill, "FEE_UPCOMING_DUE");
        }
    }
    
    // 每天9点和18点执行：催缴逾期
    @Scheduled(cron = "0 0 9,18 * * ?")
    public void remindOverdue() {
        // 查询已逾期的账单
        List<FeeBill> overdueBills = feeBillRepository
            .findByOverdueDateBeforeAndStatus(LocalDate.now(), BillStatus.UNPAID);
        
        for (FeeBill bill : overdueBills) {
            // 计算滞纳金
            calculateLateFee(bill);
            notificationService.sendFeeReminder(bill, "FEE_OVERDUE");
        }
    }
    
    // 每周一9点执行：发送汇总
    @Scheduled(cron = "0 0 9 ? * MON")
    public void sendWeeklySummary() {
        // 发送本周待缴费汇总
    }
    
    private void calculateLateFee(FeeBill bill) {
        if (!bill.getEnableLateFee()) {
            return;
        }
        
        // 计算逾期天数
        long overdueDays = ChronoUnit.DAYS.between(bill.getOverdueDate(), LocalDate.now());
        if (overdueDays <= 0) {
            return;
        }
        
        // 计算滞纳金
        BigDecimal lateFee = bill.getAmount()
            .multiply(bill.getLateFeeRate())
            .multiply(BigDecimal.valueOf(overdueDays));
        
        // 不超过上限
        if (bill.getMaxLateFee() != null && lateFee.compareTo(bill.getMaxLateFee()) > 0) {
            lateFee = bill.getMaxLateFee();
        }
        
        bill.setLateFee(lateFee);
        bill.setPayableAmount(bill.getAmount().add(lateFee).subtract(bill.getDiscount()));
        feeBillRepository.save(bill);
    }
}
```

---

## 四、能耗计量管理（已实现，当前在 module-mdm 中）

> **边界说明**: 能耗计量管理当前在 `module-mdm` 中实现，属于基础数据管理（MDM）范畴。能耗数据采集和简单费用计算是已实现功能，但正式的账单生成、支付处理等财务功能属于本领域（module-finance）的待实现功能。两者通过 EnergyConsumption.amount 对接：能耗模块提供用量和计算值，财务模块将其作为按用量计费的账单数据来源。

### 4.1 EnergyMeter（能源计量点）

```java
@Entity
@Table(name = "ops_energy_meter")
@Data
public class EnergyMeter {
    @Id
    private UUID id;
    private UUID projectId;

    private String meterCode;           // 计量点编码（自动生成：EM + yyyyMMddHHmmss）
    private String meterName;           // 计量点名称
    private EnergyType energyType;      // 能源类型枚举
    private UUID spaceNodeId;           // 关联空间节点
    private String installationLocation;// 安装位置
    private BigDecimal ratedCapacity;   // 额定容量
    private BigDecimal unitPrice;       // 单价（精度4位小数，用于费用计算）
    private Status status;              // ACTIVE/INACTIVE

    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
```

**能源类型枚举**:

```java
public enum EnergyType {
    LIGHTING("照明插座用电"),
    HVAC("空调用电"),
    POWER("动力用电"),
    SPECIAL("特殊用电"),
    WATER("给排水"),
    GAS("燃气");
}
```

> **已知问题**: 前后端能源类型枚举不一致。后端为 LIGHTING/HVAC/POWER/SPECIAL/WATER/GAS，前端为 ELECTRICITY/WATER/GAS/CENTRAL_HEATING/CENTRAL_COOLING，需对齐。

### 4.2 EnergyConsumption（能耗记录）

```java
@Entity
@Table(name = "ops_energy_consumption")
@Data
public class EnergyConsumption {
    @Id
    private UUID id;
    private UUID projectId;
    private UUID meterId;               // 关联计量点

    private LocalDate consumptionDate;  // 记录日期
    private BigDecimal previousReading; // 上次读数
    private BigDecimal currentReading;  // 当前读数
    private BigDecimal consumption;     // 消耗量 = current - previous
    private BigDecimal amount;          // 费用 = consumption x unitPrice
    private UUID recordedBy;            // 记录人
    private RecordMethod recordMethod;  // 记录方式
    private String remarks;             // 备注

    private LocalDateTime createdAt;
}
```

**记录方式枚举**:

```java
public enum RecordMethod {
    MANUAL("手动录入"),
    IOT("IoT自动采集");
}
```

### 4.3 能耗业务规则

| 规则 | 说明 |
|------|------|
| 编码自动生成 | 格式：EM + yyyyMMddHHmmss，冲突时追加后缀 |
| 仪表状态校验 | 只能对 ACTIVE 状态的仪表进行抄表 |
| 读数递增校验 | 当前读数不能小于上次读数 |
| 自动获取上次读数 | 从最近一条记录获取 previousReading，首次为 0 |
| 自动计算消耗量 | consumption = currentReading - previousReading |
| 自动计算费用 | amount = consumption x meter.unitPrice（单价为空时为 0） |
| 默认手动录入 | recordMethod 默认设为 MANUAL |
| 软删除 | 删除操作将状态设为 INACTIVE |

### 4.4 能耗 API 接口

```
基础路径: /api/ops/energy

计量点管理:
  POST   /meters              创建计量点
  GET    /meters              查询计量点列表（projectId必填，energyType可选）
  GET    /meters/{id}         获取计量点详情
  PUT    /meters/{id}         更新计量点
  DELETE /meters/{id}         删除计量点（软删除）

能耗记录:
  POST   /consumption         录入能耗数据
  GET    /consumption/{meterId} 查询能耗记录（startDate/endDate可选）

能耗统计:
  GET    /statistics/by-type     按类型统计消耗
  GET    /statistics/unit-consumption 单方能耗
```

> **已知缺陷**: `getConsumptionByType()` 当前将项目总消耗全部归入 LIGHTING 类型，未按 meter.energyType 真正分项汇总，需修复。

### 4.5 能耗与财务的边界

```
EnergyMeter (module-mdm)          FeeItem (module-finance，待实现)
       |                                |
       v                                v
EnergyConsumption --费用计算--> FeeBill (自动/手动生成，待实现)
  (抄表数据)                        |
                                    v
                              FeePayment (支付记录，待实现)
                                    |
                                    v
                              FeeRefund (退款记录，待实现)
```

- **能耗模块（module-mdm）职责**: 计量点管理、抄表录入、消耗量计算、简单费用计算（consumption x unitPrice）
- **财务模块（module-finance，待实现）职责**: 收费项目管理、账单生成、支付处理、催缴通知、滞纳金计算、退款处理

---

## 五、实现状态与差异

### 5.1 实现状态

| 功能模块 | 实现状态 | 备注 |
|---------|---------|------|
| FeeItem | 🔴 未实现 | 无 module-finance 模块，无 fin_* 表 |
| FeeBill | 🔴 未实现 | 无 FeeBill 实体/服务/控制器 |
| FeePayment | 🔴 未实现 | 无 FeePayment 实体/服务/控制器 |
| FeeRefund | 🔴 未实现 | 原设计即标注未实现 |
| 费用催缴 | 🔴 未实现 | 无 FeeReminderJob 定时任务 |
| 滞纳金计算 | 🔴 未实现 | 无计算逻辑 |
| EnergyMeter | 🟢 已实现 | 在 module-mdm 中实现 |
| EnergyConsumption | 🟢 已实现 | 在 module-mdm 中实现，含简单费用计算 |
| 能耗统计分析 | 🟢 已实现(有缺陷) | 按类型统计未真正分项汇总 |
| 退款功能 | 🔴 未实现 | 待开发 |
| 财务报表 | 🔴 未实现 | 待开发 |
| 支付网关 | 🔴 未实现 | 待对接 |

### 5.2 与设计方案的差异

| 设计项 | 设计方案 | 现有实现 | 差异分析 |
|--------|----------|----------|----------|
| **FeeItem** | 完整实体定义 | 不存在 | 完全缺失，需新建 module-finance |
| **FeeBill** | 完整实体定义 | 不存在 | 完全缺失 |
| **FeePayment** | 完整实体定义 | 不存在 | 完全缺失 |
| **FeeRefund** | 完整实体定义 | 不存在 | 完全缺失 |
| **费用催缴** | FeeReminderJob | 不存在 | 完全缺失 |
| **滞纳金** | 自动计算逻辑 | 不存在 | 完全缺失 |
| **能耗管理** | 未在设计文档中 | module-mdm中已实现 | 设计文档遗漏，需补充 |
| **能源类型** | FeeType(8种) | EnergyType(6种) | 实际更细分(按用电分项)，但缺少供暖/物业费 |
| **前后端枚举** | 统一 | 不一致 | 后端LIGHTING/HVAC/POWER/SPECIAL/WATER/GAS vs 前端ELECTRICITY/WATER/GAS/CENTRAL_HEATING/CENTRAL_COOLING |

### 5.3 改进计划

| 优先级 | 改进项 | 说明 |
|--------|--------|------|
| P0 | 创建 module-finance 模块 | 独立财务模块，包含 entity/service/controller/repository |
| P0 | 实现 FeeItem 收费项目 | 含完整字段和 CRUD API |
| P0 | 实现 FeeBill 账单 | 含账单自动生成和状态流转 |
| P0 | 实现 FeePayment 支付记录 | 线下收款登记、支付记录查询 |
| P0 | 能耗数据与账单对接 | 将 EnergyConsumption 的 amount 作为按用量计费的账单数据来源 |
| P1 | 修复能耗按类型统计 | getConsumptionByType() 应按 meter.energyType 真正分项汇总 |
| P1 | 统一前后端能源类型枚举 | 对齐后端和前端的 EnergyType 定义 |
| P1 | 实现费用催缴定时任务 | 到期提醒、逾期催缴、周汇总 |
| P1 | 实现滞纳金自动计算 | 逾期天数 x 日利率，不超过上限 |
| P1 | 实现退款功能 | 退款申请、审批、执行流程 |
| P2 | 对接支付网关 | 集成微信支付、支付宝 SDK |
| P2 | 财务报表 | 收费统计、欠费分析、收入趋势 |

---

## 六、数据库表结构

```sql
-- 收费项目表
CREATE TABLE fin_fee_item (
    id UUID PRIMARY KEY,
    project_id UUID NOT NULL,
    code VARCHAR(50) NOT NULL,
    name VARCHAR(100) NOT NULL,
    type VARCHAR(20) NOT NULL,
    billing_method VARCHAR(20) NOT NULL,
    unit_price NUMERIC(12,2),
    unit VARCHAR(20),
    bill_day INTEGER,
    due_day INTEGER,
    overdue_day INTEGER,
    enable_late_fee BOOLEAN DEFAULT FALSE,
    late_fee_rate NUMERIC(5,4),
    max_late_fee NUMERIC(12,2),
    enabled BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(project_id, code)
);

-- 账单表
CREATE TABLE fin_fee_bill (
    id UUID PRIMARY KEY,
    project_id UUID NOT NULL,
    bill_no VARCHAR(32) NOT NULL,
    fee_item_id UUID NOT NULL,
    space_node_id UUID,
    owner_id UUID,
    bill_period VARCHAR(10) NOT NULL,
    bill_date DATE NOT NULL,
    due_date DATE NOT NULL,
    overdue_date DATE,
    usage_amount NUMERIC(12,2),
    usage_unit VARCHAR(20),
    amount NUMERIC(12,2) NOT NULL,
    late_fee NUMERIC(12,2) DEFAULT 0,
    discount NUMERIC(12,2) DEFAULT 0,
    payable_amount NUMERIC(12,2) NOT NULL,
    paid_amount NUMERIC(12,2) DEFAULT 0,
    status VARCHAR(20) NOT NULL DEFAULT 'UNPAID',
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(project_id, bill_no)
);

-- 支付记录表
CREATE TABLE fin_fee_payment (
    id UUID PRIMARY KEY,
    bill_id UUID NOT NULL,
    payment_no VARCHAR(64) NOT NULL,
    amount NUMERIC(12,2) NOT NULL,
    method VARCHAR(20) NOT NULL,
    third_party_no VARCHAR(100),
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    fail_reason VARCHAR(500),
    payment_time TIMESTAMP,
    remark VARCHAR(500),
    operator_id UUID,
    operator_name VARCHAR(100),
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- 退款记录表
CREATE TABLE fin_fee_refund (
    id UUID PRIMARY KEY,
    project_id UUID NOT NULL,
    bill_id UUID NOT NULL,
    payment_id UUID NOT NULL,
    refund_no VARCHAR(64) NOT NULL,
    amount NUMERIC(12,2) NOT NULL,
    reason VARCHAR(500),
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    approver_id UUID,
    approver_name VARCHAR(100),
    approve_time TIMESTAMP,
    approve_remark VARCHAR(500),
    third_party_refund_no VARCHAR(100),
    refund_time TIMESTAMP,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- 创建索引
CREATE INDEX idx_fee_item_project ON fin_fee_item(project_id);
CREATE INDEX idx_fee_bill_project ON fin_fee_bill(project_id);
CREATE INDEX idx_fee_bill_status ON fin_fee_bill(status);
CREATE INDEX idx_fee_bill_owner ON fin_fee_bill(owner_id);
CREATE INDEX idx_fee_bill_due_date ON fin_fee_bill(due_date);
CREATE INDEX idx_fee_payment_bill ON fin_fee_payment(bill_id);

-- ============================================================
-- 以下为已实现的能耗计量相关表（在 module-mdm 中）
-- ============================================================

-- 能源计量点表（已实现）
CREATE TABLE ops_energy_meter (
    id UUID PRIMARY KEY,
    project_id UUID NOT NULL,
    meter_code VARCHAR(50) NOT NULL,
    meter_name VARCHAR(100) NOT NULL,
    energy_type VARCHAR(20) NOT NULL,
    space_node_id UUID,
    installation_location VARCHAR(255),
    rated_capacity NUMERIC(10,2),
    unit_price NUMERIC(10,4),
    status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(project_id, meter_code)
);

-- 能耗记录表（已实现）
CREATE TABLE ops_energy_consumption (
    id UUID PRIMARY KEY,
    project_id UUID NOT NULL,
    meter_id UUID NOT NULL REFERENCES ops_energy_meter(id),
    consumption_date DATE NOT NULL,
    previous_reading NUMERIC(12,2),
    current_reading NUMERIC(12,2),
    consumption NUMERIC(12,2) NOT NULL,
    amount NUMERIC(10,2),
    recorded_by UUID,
    record_method VARCHAR(20) DEFAULT 'MANUAL',
    remarks VARCHAR(1000),
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- 能耗表索引
CREATE INDEX idx_energy_meter_project ON ops_energy_meter(project_id);
CREATE INDEX idx_energy_meter_status ON ops_energy_meter(status);
CREATE INDEX idx_ec_meter_date ON ops_energy_consumption(meter_id, consumption_date);
CREATE INDEX idx_ec_project_date ON ops_energy_consumption(project_id, consumption_date);
```

---

**文档维护**: 本领域技术方案由 ether-finance 服务负责人维护
